diff --git a/frontend/ui-core/src/annotator/AnnotationPage.tsx b/frontend/ui-core/src/annotator/AnnotationPage.tsx index 274e146a..73b94c4d 100644 --- a/frontend/ui-core/src/annotator/AnnotationPage.tsx +++ b/frontend/ui-core/src/annotator/AnnotationPage.tsx @@ -372,10 +372,11 @@ export interface AnnotationPageProps { * Where somebody goes to set up a model connection, if the app has such a * screen (#424, D6). * - * Optional, and it is expected to be absent for now: the Inference surface - * waits on #421's open rail question, and `ui-core` imports no router. Absent, - * the suggest tool's panel still says what is missing and simply renders no - * control — a host that cannot honour one renders none rather than a dead one. + * Optional because `ui-core` imports no router and cannot know whether its + * host has such a screen — the app does, and wires this to the Inference + * section. Absent, the suggest tool's panel still says what is missing and + * simply renders no control: a host that cannot honour one renders none + * rather than a dead one. */ readonly onConfigureInference?: () => void; } diff --git a/frontend/ui-core/src/data/inferenceQueries.ts b/frontend/ui-core/src/data/inferenceQueries.ts index e45f2173..7a258a18 100644 --- a/frontend/ui-core/src/data/inferenceQueries.ts +++ b/frontend/ui-core/src/data/inferenceQueries.ts @@ -93,7 +93,6 @@ export interface SuggestionOut { readonly region?: SuggestedRegion | null; } -export type ConnectionAction = components["schemas"]["ConnectionAction"]; export type ConnectionType = components["schemas"]["ConnectionType"]; export type ConnectionSetupState = components["schemas"]["ConnectionSetupState"]; export type DownloadSizeOut = components["schemas"]["DownloadSizeOut"]; diff --git a/pyproject.toml b/pyproject.toml index 51c882b6..7a6ffbb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,8 +63,12 @@ dependencies = [ # # The canonical spelling is `local-inference`; it supersedes the earlier # `autolabel` framing. Version floors rather than pins: this is a user's -# environment and their CUDA build decides most of it. `uv.lock` does not carry -# extras, so nothing here is resolved by a `uv sync` in this repository. +# environment and their CUDA build decides most of it. `uv.lock` does record the +# extra — once as a member list, once per member under `requires-dist` — so +# editing these lines without re-running `uv lock` leaves it stale and +# `uv lock --check` fails on the result. What a plain `uv sync` does not do is +# *install* it: that takes `--extra local-inference`, which is how two gigabytes +# of CUDA wheels stay out of a developer's environment by default. # # `torchvision` is here because SAM 2's image processor imports it, and nothing # else in this list brings it: `Sam2ImageProcessor` refuses to construct without diff --git a/src/visionset/inference/__init__.py b/src/visionset/inference/__init__.py index a8749923..df08f750 100644 --- a/src/visionset/inference/__init__.py +++ b/src/visionset/inference/__init__.py @@ -14,11 +14,10 @@ **Importing this package imports nothing heavy.** Every reference to torch, transformers, accelerate and huggingface_hub is inside a function, and torchvision — which nothing here names, and ``transformers`` imports for us — is -held to the same line — see -``_extra`` for why that is load-bearing rather than tidy — so a base install -starts a server, runs a worker and imports this module without the optional -runtime present. ``tests/architecture/test_optional_runtime.py`` proves it in a -fresh interpreter. +held to the same line. See ``_extra`` for why that is load-bearing rather than +tidy: a base install starts a server, runs a worker and imports this module +without the optional runtime present, and +``tests/architecture/test_optional_runtime.py`` proves it in a fresh interpreter. **There is no plugin registry, and resolution happens in two steps.** #418's recorded decision is that adapters are instantiated from user-created model diff --git a/src/visionset/inference/sam_provider.py b/src/visionset/inference/sam_provider.py index 571e2ea9..5a6012b4 100644 --- a/src/visionset/inference/sam_provider.py +++ b/src/visionset/inference/sam_provider.py @@ -87,7 +87,7 @@ def points_and_labels(prompt: PointPrompt) -> tuple[list[list[float]], list[int] return points, labels -def best_of(iou_scores: list[float], masks: list[Any]) -> tuple[int, float]: +def best_of(iou_scores: list[float]) -> tuple[int, float]: """Which of the multi-mask answers to offer, and how sure it is. A segmenter of this family answers a single click with several masks at @@ -256,7 +256,7 @@ def _regions( outputs.pred_masks, original_sizes=[list(size)], binarize=True )[0] scores = [float(value) for value in outputs.iou_scores.flatten().tolist()] - chosen, confidence = best_of(scores, lifted) + chosen, confidence = best_of(scores) if confidence < minimum_confidence: return () mask = lifted.reshape(-1, *lifted.shape[-2:])[chosen] diff --git a/tests/architecture/test_optional_runtime.py b/tests/architecture/test_optional_runtime.py index e48945de..2d12b5d5 100644 --- a/tests/architecture/test_optional_runtime.py +++ b/tests/architecture/test_optional_runtime.py @@ -4,10 +4,9 @@ huggingface_hub in the `local-inference` extra, and `visionset/inference/_extra.py` reaches every one of them from inside a function — torchvision by never naming it at all, since it is `transformers` that imports it. Neither of those is -self-enforcing: a single -module-level `import torch` anywhere under `visionset.inference` would leave the -metadata unchanged and make roughly two gigabytes of CUDA wheels a condition of -starting the server. +self-enforcing: a single module-level `import torch` anywhere under +`visionset.inference` would leave the metadata unchanged and make roughly two +gigabytes of CUDA wheels a condition of starting the server. It is a condition of *starting* rather than of running a model because `visionset.jobs` imports its handler modules at package import to populate the diff --git a/tests/inference/test_provider.py b/tests/inference/test_provider.py index 42113df9..9a6e1cdf 100644 --- a/tests/inference/test_provider.py +++ b/tests/inference/test_provider.py @@ -1,9 +1,12 @@ -"""The composition root's refusals, and the adapter's tensor-to-domain half. +"""What the runtime's presence decides, and the adapter's tensor-to-domain half. -Two subjects, both reachable without the optional runtime and without a GPU: +Two subjects, both reachable without a GPU: -- `provider_for`, which answers "can this connection predict here?" and must - refuse with a sentence rather than a `None` or a stack trace; +- the half of `provider_for` that turns on whether the optional runtime is + installed here — the refusal that names the install command when it is not, + and structural conformance to the port when it is. The refusals that turn on + the *connection* instead are `test_providers.py`'s, beside the resolution + they belong to; - `regions_from`, the conversion the adapter does after a forward — the one part of running a model that can be wrong in a way no weights are needed to see. @@ -35,8 +38,6 @@ TextPrompt, ) from visionset.kernel.errors import ( - InferenceConnectionNotRunnable, - InferenceConnectionNotSetUp, LocalInferenceUnavailable, UnsupportedPrompt, ) @@ -59,58 +60,9 @@ def local( ) -def remote() -> InferenceConnection: - return InferenceConnection( - name="remote", - connection_type=ConnectionType.HTTP, - model_id="some/model", - model_revision="abc123", - endpoint_url="https://example.invalid/predict", - setup_state=ConnectionSetupState.READY, - ) - - # --- resolving a connection to something that can answer ---------------------- -def test_a_local_connection_without_weights_is_refused_by_state(tmp_path: Path) -> None: - """And the message names the action that fixes it. - - "Not set up" alone tells an operator what they already knew; the remedy is - the point, and it is a real one — the identical call succeeds after the - download. - """ - with pytest.raises(InferenceConnectionNotSetUp) as raised: - provider_for(local(), workspace_root=tmp_path) - assert "download_weights" in str(raised.value) - - -def test_an_http_connection_is_refused_because_this_build_has_no_adapter( - tmp_path: Path, -) -> None: - """A different refusal from the one above, deliberately. - - No state change fixes this and no wait helps — the adapter that would speak - to an endpoint is a later slice — so it must not be the error whose whole - meaning is "change the state and resubmit". - """ - with pytest.raises(InferenceConnectionNotRunnable) as raised: - provider_for(remote(), workspace_root=tmp_path) - assert "http" in str(raised.value) - - -def test_the_connections_own_state_is_reported_before_the_machines(tmp_path: Path) -> None: - """Order of the two checks, and it matters to whoever reads the answer. - - A not-set-up connection on a machine with no extra has two problems. Telling - somebody to run a `pip install` when what they needed was a download sends - them to the wrong place; the state is the one they can act on from where they - are standing. - """ - with pytest.raises(InferenceConnectionNotSetUp): - provider_for(local(), workspace_root=tmp_path) - - @pytest.mark.skipif(EXTRA_INSTALLED, reason="the local runtime is installed here") def test_a_ready_connection_without_the_runtime_names_the_install_command( tmp_path: Path, diff --git a/tests/inference/test_sam_provider.py b/tests/inference/test_sam_provider.py index ddf19556..5156ebab 100644 --- a/tests/inference/test_sam_provider.py +++ b/tests/inference/test_sam_provider.py @@ -94,11 +94,11 @@ def test_a_prompt_with_no_negatives_still_labels_every_point() -> None: def test_the_highest_scoring_mask_is_the_one_offered() -> None: """A click is ambiguous about scale; the model's own IoU head is what resolves it.""" - assert best_of([0.2, 0.91, 0.5], [[], [], []]) == (1, 0.91) + assert best_of([0.2, 0.91, 0.5]) == (1, 0.91) def test_a_score_outside_the_domains_bounds_is_clamped_rather_than_refused() -> None: - assert best_of([1.0000001], [[]])[1] == 1.0 + assert best_of([1.0000001])[1] == 1.0 # --- what it refuses ----------------------------------------------------------