Spaces:
Running on Zero
Running on Zero
| """Matching a capture against a farm's enrolled animals, and refusing to name one. | |
| Directive §6.4, and the counterpart to `app/counting.py`: that module turns boxes | |
| into a claim, this one turns a nearest neighbour into a candidate. Both exist | |
| because the hard part is not the model — it is being honest about the cases the | |
| model cannot carry. | |
| **The measurement this runner ships against is the open-set one, not the | |
| closed-set one.** Run `8db9e0bd1b30` | |
| (`experiments/cattle_identity/metrics.json`) enrolled 169 animals from the | |
| muzzle268 database and measured, on 789 probes of animals that *were* enrolled: | |
| top-1 0.9772, top-3 0.9937, MRR 0.9853, against a chance rate of 0.005917. That | |
| is the number worth quoting and it is not the number that decides the product. | |
| On 416 probes of animals nobody enrolled, **the unthresholded false-accept rate | |
| is 1.000**. Every unenrolled animal comes back as somebody, because every query | |
| has a nearest neighbour and nothing about a nearest neighbour knows the right | |
| answer was absent. The two similarity distributions overlap badly — enrolled | |
| probes median 0.9707, unenrolled median 0.9042 with an unenrolled *maximum* of | |
| 0.9776 — so the cutoff that admits no impostor at all sits at 0.98 and accepts | |
| **24.08%** of the correct matches. | |
| `MEASURED_POLICY` is that operating point, and choosing it is the whole design: | |
| - Three quarters of the time an enrolled animal is photographed, this runner | |
| claims `no_confident_match` **and still returns the ranked candidates**, so a | |
| person sees the names and picks one. That is a worse headline and the same | |
| information. | |
| - The alternative — no threshold, always name the top candidate — is the one | |
| that reads better and puts a neighbour's cow in a farm's records 100% of the | |
| time an unenrolled animal is photographed. | |
| `§6.4`'s confirm step is therefore load-bearing rather than decorative. | |
| **What carries that requirement to a device, precisely.** This paragraph used to | |
| say `IdentityResult.to_json` states `requires_confirmation: True` on every path. | |
| It does — and this runner never calls it. The served result is | |
| `schemas.InferenceResult`, which has no such field, so **the flag reaches | |
| nobody** and citing it here described a guarantee that was not on the wire. | |
| What actually crosses is the capability row. `Requirement.HUMAN_CONFIRMATION` is | |
| on the registry entry, travels verbatim to the API, and is published as | |
| `requirements: ["guided_capture", "human_confirmation"]` by `GET /capabilities`, | |
| where a database constraint forces §6.4's four buttons to travel beside it. That | |
| is a real mechanism and it is a **different endpoint from the result**: a client | |
| that renders a result without having read `/capabilities` has nothing in the | |
| payload telling it to ask. Closing that gap means a field on `InferenceResult`, | |
| which is a contract change for every capability and is not made here. | |
| What this runner does guarantee on every path is narrower and worth stating | |
| exactly: no result carries an interpretation, an `observation_confidence`, or a | |
| non-`None` confidence on any observation; `identity_candidate` is emitted only | |
| when the measured policy accepted; and `_result` raises on | |
| `identity_without_confirmation`. A result from here cannot *look* settled. It | |
| relies on the client having read the capability to know it must ask. | |
| **What is not measured here.** The database is US beef breeds — Angus, Angus x | |
| Hereford, Continental x British crosses. No Nigerian and no zebu animal has been | |
| through this, and a White Fulani is white all over, so the coat-pattern signal | |
| the published re-identification literature leans on is absent for the herds this | |
| product is for. The registry carries `UNVALIDATED_GEOGRAPHY`, which is what stops | |
| `may_be_promoted_to_production` mechanically. | |
| ## The cold-gallery cost, which is a real limit and not a solved problem | |
| Measured on 2026-08-23 over this service's own HTTP path by | |
| `scripts/verify_identity_wiring.py`, on an Apple M-series laptop, single process, | |
| enrolling five muzzle photographs per animal: | |
| | Register | Enrolment photographs | Cold request | Per photograph | Warm median | | |
| |---|---|---|---|---| | |
| | 20 animals | 100 | **11.13 s** | 0.11 s | **0.059 s** | | |
| | 60 animals | 300 | **60.35 s** | 0.20 s | **0.07 s** | | |
| `_VECTOR_CACHE` is what separates the cold column from the warm one, and it only | |
| helps the warm one. **A farm's first identification pays for embedding its whole | |
| register.** | |
| **Both per-photograph rates are recorded because they disagree**, and the honest | |
| reading is that this is a loaded shared laptop rather than a controlled | |
| measurement — the 0.20 s run had a test suite beside it. Extrapolate on the | |
| slower one; a latency figure from a shared machine is not a latency figure, which | |
| is a rule this repository already writes down in | |
| `experiments/cattle_respiratory/README.md`. At 0.20 s a 169-animal register is | |
| about **169 seconds** cold. | |
| Neither rate is the 59 ms in `backbones.py`. That is the embed call alone on a | |
| frame somebody already decoded; this path also reads the blob and decodes a JPEG. | |
| An earlier version of this paragraph extrapolated on 59 ms and so put a | |
| 169-animal farm at "about a minute" — roughly three times better than measured, | |
| in a paragraph whose entire purpose is to be honest about a limit. | |
| **Even the small register does not fit the inline job model.** `app/main.py` runs | |
| jobs synchronously, and this service declined to wire an eighteen-second | |
| capability inline at all; eleven seconds for twenty animals is the same order and | |
| a hundred and sixty-nine seconds is not close. So the honest statement is not | |
| "wired for the farm sizes Animap has" — it is that **every** cold request is slow | |
| and only the warm path is fast, so a deployment has to keep the cache warm rather | |
| than treat the cold case as an edge. | |
| The fix is not a bigger cache. It is for the API to store each enrolment vector | |
| beside its `MediaAsset` and send vectors instead of media ids, which moves the | |
| cost to enrolment time where a person is already waiting. That is a change to the | |
| API's schema and is not made here. | |
| **Where these numbers come from, since they cite no run record.** They are a | |
| session measurement, not a benchmark, and the distinction matters in a repository | |
| whose whole discipline is that a figure names a run id. The script is | |
| `scripts/verify_identity_wiring.py`; it is reproducible and it has not been | |
| through the experiment harness, so nothing here may be quoted as a measured | |
| accuracy. What it does establish is that the wiring reproduces the benchmark | |
| rather than merely citing it: over 122 held-out probes against a 60-animal | |
| gallery, top-1 similarity ran min 0.8336, median 0.9764, max 0.9930 against the | |
| run record's min 0.8336, median 0.9707, max 0.9930, and rank-1 was right 121 | |
| times out of 122. The accept rate was 41% against the benchmark's 24.08%, which | |
| is the direction a 60-animal gallery moves it against 169. | |
| """ | |
| from __future__ import annotations | |
| import threading | |
| from collections import OrderedDict | |
| from uuid import UUID, uuid4 | |
| import numpy as np | |
| from PIL import Image | |
| from app.adapters.embedding import DINOV3_SPEC, OnnxEmbeddingAdapter | |
| from app.adapters.embedding.identity import ( | |
| ENROLMENT_VIEWS, | |
| PRIMARY_VIEW, | |
| Embedding, | |
| IdentityIndex, | |
| IndexMismatch, | |
| OpenSetPolicy, | |
| ) | |
| from app.capabilities import FORBIDDEN_CLAIMS, Capability | |
| from app.media import MediaRef, MediaStore | |
| from app.providers import ModelArtefact | |
| from app.quality import assess | |
| from app.schemas import ( | |
| EnrolledAnimal, | |
| InferenceLocation, | |
| InferenceRequest, | |
| InferenceResult, | |
| Observation, | |
| QualityCheck, | |
| ) | |
| #: The operating point measured in run `8db9e0bd1b30`, and the only policy this | |
| #: runner ships. | |
| #: | |
| #: `accept_similarity` 0.98 is the lowest cutoff in the published sweep whose | |
| #: false-accept rate is 0.0. `accept_margin` 0.0 is the measured companion and it | |
| #: never rejects anything — a margin can only be non-negative — which is | |
| #: deliberate rather than an oversight: `open_set_margin_sweep` in the same run | |
| #: shows the margin rule buys nothing here. At margin 0.07 it holds the | |
| #: false-accept rate to 0.0072 while accepting 23.45% of correct matches, which | |
| #: is *worse on both axes* than the similarity rule alone. The rule stays wired | |
| #: because a future backbone may separate the two distributions differently, and | |
| #: it is set where the evidence puts it. | |
| #: | |
| #: **Both numbers cite the run that produced them and `OpenSetPolicy` will not | |
| #: accept them otherwise.** Its `__post_init__` raises `UnmeasuredThreshold` on a | |
| #: threshold with no run id, which is what stops a cutoff being tuned by whoever | |
| #: is looking at a demo that morning. | |
| MEASURED_POLICY = OpenSetPolicy.measured( | |
| accept_similarity=0.98, | |
| accept_margin=0.0, | |
| run_id="8db9e0bd1b30", | |
| measured_on="muzzle268-169enrolled", | |
| ) | |
| #: Said on every run, whatever the result, in the shape `CountingProfile` | |
| #: established. §6.4's product statement, cut to what a person reads. | |
| STANDING_WARNING = ( | |
| "A suggestion, not a record. Animap proposes an animal and you confirm it — " | |
| "an unconfirmed match is never written to this animal's history." | |
| ) | |
| #: Said whenever candidates are returned at all, because the open-set result is | |
| #: not something a reader can infer from a ranked list. | |
| UNENROLLED_WARNING = ( | |
| "An animal that has never been enrolled will still produce a nearest match. " | |
| "Measured on 416 photographs of unenrolled cattle, every one of them came " | |
| "back as somebody. If this animal is new, register it rather than picking " | |
| "the closest name." | |
| ) | |
| #: How many candidates a result carries. §6.4 shows a ranked list rather than one | |
| #: answer, and `evidence_correction.selected_interpretation` is the reason: a | |
| #: farmer picking the second name records *"rank 2 was right"*, which its own | |
| #: docstring calls the highest-value training signal in the table. Three, because | |
| #: `closed_set_top3_accuracy` is 0.9937 and a fourth carries no measurement. | |
| TOP_K = 3 | |
| #: Enrolment vectors held between requests, keyed by media id and artefact. | |
| #: | |
| #: **Without this the design does not fit the inline job model.** The gallery | |
| #: travels on the request so the service holds no farm state | |
| #: (`schemas.EnrolledAnimal` says why), and the cost of that is re-embedding | |
| #: every enrolled photograph on every identification. At the **measured 0.20 s** | |
| #: per enrolment photograph — end to end, including decode, not the 59 ms embed | |
| #: call `backbones.py` reports on a frame somebody already decoded — a farm with | |
| #: a hundred animals and the five-shot enrolment the accuracy was measured on is | |
| #: 500 photographs, or **about a hundred seconds a request**. That is far past | |
| #: the inline ceiling `app/main.py` describes and far worse than the | |
| #: eighteen-second capability this service declined to wire inline. | |
| #: | |
| #: **A media id is immutable, which is what makes caching it correct rather than | |
| #: merely fast.** `MediaAsset` rows are write-once and the blob behind one is | |
| #: never rewritten, so the same id is the same bytes forever and its vector | |
| #: cannot go stale. The artefact digest is in the key because a re-export with | |
| #: different pooling produces a vector of the same width and a different meaning | |
| #: — the case `IdentityIndex` pins its backbone id to catch — so vectors from two | |
| #: artefacts must never collide here. | |
| #: | |
| #: **The farm id is in the key, and it was not for one commit.** The argument for | |
| #: leaving it out was that nothing can be read from the cache without already | |
| #: holding the media id. A watchdog showed that is not quite the property that | |
| #: matters: `AzureBlobMediaStore` scopes an unpathed lookup to `farm/{farm_id}/`, | |
| #: so *without* a cache a request naming another farm's media id fails to resolve | |
| #: — and *with* one it would hit and score. What leaked was a similarity against | |
| #: a foreign photograph rather than the photograph or the name, and it needed the | |
| #: service token plus a known foreign UUID, so it was narrow. It was also free to | |
| #: close, and a cache must not be the reason a farm boundary that the media store | |
| #: enforces stops being enforced. | |
| _VECTOR_CACHE: OrderedDict[tuple[str, str, str], np.ndarray] = OrderedDict() | |
| #: Entries kept. 768 float32 values is 3 KB, so this is about 12 MB — small | |
| #: against the container's 4 GiB, and enough for a few hundred animals at five | |
| #: shots each. Least-recently-used is evicted, so the farms being worked today | |
| #: stay warm. | |
| _CACHE_LIMIT = 4096 | |
| #: `submit()` runs in FastAPI's threadpool, so two captures can be scored at | |
| #: once and an `OrderedDict` is not safe under that on its own. | |
| _CACHE_LOCK = threading.Lock() | |
| def cache_clear() -> None: | |
| """Empty the vector cache. For tests, and for a deployment that swaps an | |
| artefact without a restart.""" | |
| with _CACHE_LOCK: | |
| _VECTOR_CACHE.clear() | |
| def _cached_vector(key: tuple[str, str, str]) -> np.ndarray | None: | |
| with _CACHE_LOCK: | |
| vector = _VECTOR_CACHE.get(key) | |
| if vector is not None: | |
| _VECTOR_CACHE.move_to_end(key) | |
| return vector | |
| def _remember(key: tuple[str, str, str], vector: np.ndarray) -> None: | |
| with _CACHE_LOCK: | |
| _VECTOR_CACHE[key] = vector | |
| _VECTOR_CACHE.move_to_end(key) | |
| while len(_VECTOR_CACHE) > _CACHE_LIMIT: | |
| _VECTOR_CACHE.popitem(last=False) | |
| #: Muzzle photographs per animal in the protocol the accuracy was measured under. | |
| #: | |
| #: Run `8db9e0bd1b30` enrolled each animal from five, all under the `muzzle` view | |
| #: name, and `IdentityIndex.candidates` scores an animal by its **best** view — | |
| #: so the five are five chances to match rather than five samples averaged | |
| #: together. An animal enrolled from one photograph is being asked a harder | |
| #: question than the benchmark asked, and measured that way on twelve animals, | |
| #: **0 of 23 held-out queries cleared the 0.98 cutoff**. | |
| #: | |
| #: Used only to report a thin enrolment, never to refuse one. | |
| MEASURED_ENROLMENT_SHOTS = 5 | |
| class IdentityRunner: | |
| """Runs `cattle_identity` against one capture and one farm's register.""" | |
| #: The view a single capture is compared against. §6.4 leads with the muzzle | |
| #: and the re-identification measurement is entirely muzzle-to-muzzle, so | |
| #: comparing a muzzle print against a side-body vector would contribute | |
| #: nothing but a chance of a spurious high score. Restricting the pool is | |
| #: what keeps the runtime behaviour inside the measured one. | |
| #: | |
| #: **Nothing checks that the capture actually is a muzzle, and a watchdog was | |
| #: right to call that a gap.** The registry declares `muzzle_not_visible` in | |
| #: `reject_if`, but `app/quality.py` implements resolution, illumination and | |
| #: motion blur and nothing that looks for an anatomical part — so the | |
| #: condition is declared and never evaluated. A caller that sends the five | |
| #: enrolment views in the order the registry lists them sends `front_face` | |
| #: first, and it would be scored against a gallery of muzzles. | |
| #: | |
| #: What that costs is bounded in the safe direction: a front-face photograph | |
| #: is *less* similar to every enrolled muzzle, so it drives the top score down | |
| #: and towards `no_confident_match`. It degrades to a refusal rather than to a | |
| #: wrong name. But it is a refusal for a reason nobody is told, and the fix is | |
| #: a real one — either the capture flow states which view it took, or | |
| #: something evaluates `muzzle_not_visible`. Neither exists today. | |
| query_view = PRIMARY_VIEW | |
| def run( | |
| self, | |
| *, | |
| request: InferenceRequest, | |
| capability: Capability, | |
| artefact: ModelArtefact, | |
| store: MediaStore, | |
| request_id: UUID | None = None, | |
| ) -> InferenceResult: | |
| request_id = request_id or uuid4() | |
| warnings: list[str] = [STANDING_WARNING] | |
| if len(request.media_ids) > capability.frames_required: | |
| warnings.append( | |
| f"{len(request.media_ids)} frames were supplied; this capability " | |
| f"reads {capability.frames_required}." | |
| ) | |
| image = store.open_image(self._ref(request, request.media_ids[0])) | |
| verdict = assess(image) | |
| checks = list(verdict.checks) | |
| if verdict.blocked: | |
| failure = verdict.first_failure | |
| warnings.append( | |
| failure.detail if failure and failure.detail | |
| else "The capture was not usable." | |
| ) | |
| return self._result( | |
| request=request, capability=capability, artefact=artefact, | |
| request_id=request_id, observations=[], checks=checks, | |
| warnings=warnings, recapture=True, | |
| ) | |
| adapter = OnnxEmbeddingAdapter( | |
| artefact, DINOV3_SPEC, | |
| input_size=artefact.input_size or 224, | |
| mean=artefact.image_mean or (0.485, 0.456, 0.406), | |
| std=artefact.image_std or (0.229, 0.224, 0.225), | |
| dimensions=artefact.embedding_dimensions or 768, | |
| ).load() | |
| index = IdentityIndex( | |
| farm_id=str(request.farm_id), | |
| backbone_id=DINOV3_SPEC.adapter_id, | |
| dimensions=adapter.dimensions, | |
| artefact_sha256=artefact.sha256, | |
| ) | |
| names: dict[str, str] = {} | |
| enrolled_views, skipped = self._enrol( | |
| index, adapter, artefact, request, store, names, | |
| ) | |
| warnings.extend(skipped) | |
| query = self._embed(adapter, artefact, image) | |
| result = index.match( | |
| query, | |
| policy=MEASURED_POLICY, | |
| top_k=TOP_K, | |
| # Compared against muzzles only. See `query_view`. | |
| restrict_to_views=(self.query_view,), | |
| names=names, | |
| ) | |
| checks.append(self._enrolment_check(index, enrolled_views)) | |
| observations: list[Observation] = [ | |
| # Emitted on every path, refusals included, for the reason | |
| # `counting.py` emits its grid: a threshold that has to be re-derived | |
| # later is re-derived from stored results or not at all. | |
| Observation( | |
| type="enrolled_animals", | |
| value=float(result.enrolled_animals), | |
| unit="animals", | |
| confidence=None, | |
| ), | |
| Observation( | |
| type="open_set_verified", | |
| value=1.0 if result.open_set_verified else 0.0, | |
| confidence=None, | |
| ), | |
| ] | |
| top = result.top | |
| if top is not None: | |
| observations.append(Observation( | |
| # **Uncalibrated, and named so.** Nothing mapped cosine | |
| # similarity onto a probability that the animal is Kofi, so this | |
| # is a diagnostic and the release declares no sentence for it — | |
| # exactly as `counting_grid` carries none. §37's whole complaint | |
| # is an uncalibrated model score reaching a person as a promise. | |
| type="top_candidate_similarity", | |
| value=round(top.similarity, 4), | |
| confidence=None, | |
| )) | |
| accepted = result.claim == "identity_candidate" | |
| if not accepted: | |
| observations.append(Observation( | |
| type="no_confident_match", value=None, confidence=None, | |
| )) | |
| # **The ranked list is emitted on both paths, under different names.** | |
| # `IdentityResult` returns candidates whether or not the policy accepted | |
| # the top one, and dropping them on the refusal path would throw away the | |
| # thing §6.4 is shaped around: `evidence_correction.selected_ | |
| # interpretation` records *"rank 2 was right"*, and its own docstring | |
| # calls that the highest-value training signal in the table. At the | |
| # measured operating point three quarters of correct matches are refused, | |
| # so the refusal path is where most of that signal lives — emitting | |
| # nothing there would collect it almost nowhere. | |
| # | |
| # The type differs because the claim differs. `identity_candidate` is | |
| # §6.4's *"This looks like Kofi"*; `closest_candidate` is *"not a | |
| # confident match, and these are the nearest on your register"*. One | |
| # observation type carrying both would leave the device deciding which | |
| # sentence to show from a field that does not say, and the release | |
| # declares a separate sentence for each. | |
| # | |
| # ## Exactly one settled name, ever | |
| # | |
| # An accepted run used to publish `identity_candidate` for **all three** | |
| # ranked candidates, so `deploy/semantics` rendered *"This looks like | |
| # Cow 0100"*, *"This looks like Cow 0200"* and *"This looks like Cow | |
| # 0300"* — three settled names for one animal, on one `captured_at`, | |
| # which no ordering on the device can pick between. The policy accepted | |
| # **the top candidate**; it said nothing whatever about ranks 2 and 3. | |
| # | |
| # So rank 1 is the proposal and the rest are what §6.4's screen calls | |
| # *"Also considered"* — the same list, under the type that does not | |
| # assert. Picking one of them is `choose_another_animal`, which is a | |
| # correction and the signal the ledger is built to keep. | |
| for position, candidate in enumerate(result.candidates): | |
| settled = accepted and position == 0 | |
| observations.append(Observation( | |
| type="identity_candidate" if settled else "closest_candidate", | |
| # The animal's own name. `identity_candidate` is one of three | |
| # claims in the whole registry that carries free text, and | |
| # `app/capabilities.py` says why: a registry cannot enumerate a | |
| # farm's animals. The value comes from the farm's own register | |
| # via the request, never from this service. | |
| value=candidate.display_name, | |
| # **`confidence` stays None on both paths.** | |
| # `Candidate.confidence` is documented as never set by the | |
| # matcher, and promoting a similarity into it here would defeat | |
| # that in one line. | |
| confidence=None, | |
| )) | |
| if result.candidates: | |
| warnings.append(UNENROLLED_WARNING) | |
| warnings.extend(result.warnings) | |
| if index.unverified_queries: | |
| # Should be zero on this path: every vector is built as an | |
| # `Embedding` below. Reported rather than asserted, because the count | |
| # existing at all is the gap between what the index checks and what | |
| # it would like to. | |
| warnings.append( | |
| f"{index.unverified_queries} vectors were compared without " | |
| f"provenance. Every vector this service builds carries it, so " | |
| f"this is a defect rather than a capture problem." | |
| ) | |
| return self._result( | |
| request=request, capability=capability, artefact=artefact, | |
| request_id=request_id, observations=observations, checks=checks, | |
| warnings=warnings, | |
| # A refusal here is not a bad photograph. The animal may simply not | |
| # be enrolled, and asking for a recapture would send a worker back to | |
| # a pen to re-photograph a cow the system has never seen. | |
| recapture=verdict.degraded, | |
| ) | |
| def _ref(self, request: InferenceRequest, media_id: UUID, | |
| object_path: str | None = None) -> MediaRef: | |
| return MediaRef( | |
| media_id=media_id, | |
| farm_id=request.farm_id, | |
| captured_at=request.captured_at, | |
| object_path=object_path or request.path_for(media_id), | |
| ) | |
| def _embed( | |
| self, adapter: OnnxEmbeddingAdapter, artefact: ModelArtefact, | |
| image: Image.Image, | |
| ) -> Embedding: | |
| """A vector that can prove where it came from. | |
| Always an `Embedding`, never a bare array. `IdentityIndex._accept` can | |
| only check the *width* of an array, and a watchdog scored a foreign | |
| vector against a `dinov3-vits16` index at similarity 1.0 through exactly | |
| that gap. | |
| """ | |
| return Embedding( | |
| vector=adapter.embed(image), | |
| backbone_id=DINOV3_SPEC.adapter_id, | |
| artefact_sha256=artefact.sha256, | |
| ) | |
| def _enrol( | |
| self, | |
| index: IdentityIndex, | |
| adapter: OnnxEmbeddingAdapter, | |
| artefact: ModelArtefact, | |
| request: InferenceRequest, | |
| store: MediaStore, | |
| names: dict[str, str], | |
| ) -> tuple[int, list[str]]: | |
| """Build this farm's gallery from the request. Returns views enrolled. | |
| A gallery entry that cannot be read is **skipped with a warning rather | |
| than failing the job**: one unreadable enrolment photograph out of two | |
| hundred should not stop a farmer identifying an animal, and the animals | |
| that did load are still a gallery. The count that reaches the result is | |
| the count that actually enrolled, so a farm cannot be told it was | |
| compared against more animals than it was. | |
| """ | |
| enrolled_views = 0 | |
| skipped: list[str] = [] | |
| for entry in request.enrolled: | |
| usable = self._views_for(entry, skipped) | |
| if not usable: | |
| continue | |
| who = entry.display_name or entry.animal_id | |
| stored = 0 | |
| for view, media_ids in usable.items(): | |
| for position, media_id in enumerate(media_ids): | |
| key = (str(request.farm_id), str(media_id), artefact.sha256) | |
| cached = _cached_vector(key) | |
| if cached is not None: | |
| vector = Embedding( | |
| vector=cached, | |
| backbone_id=DINOV3_SPEC.adapter_id, | |
| artefact_sha256=artefact.sha256, | |
| ) | |
| else: | |
| try: | |
| image = store.open_image(self._ref( | |
| request, media_id, | |
| entry.path_for_view(view, position), | |
| )) | |
| except Exception as exc: # MediaError, or any store error | |
| skipped.append( | |
| f"An enrolled {view} photograph of {who} could " | |
| f"not be read ({exc}), so it was left out of " | |
| f"the comparison." | |
| ) | |
| continue | |
| vector = self._embed(adapter, artefact, image) | |
| _remember(key, vector.vector) | |
| try: | |
| # **One call per photograph, under the one view name.** | |
| # `enrol` takes a mapping of view to vector, so a single | |
| # call can only hold one shot per view — and the measured | |
| # protocol is five muzzles per animal, scored by maximum. | |
| # Calling it per photograph is how those five reach the | |
| # index, and it is what run 8db9e0bd1b30 did. | |
| index.enrol( | |
| entry.animal_id, {view: vector}, | |
| media_ids={view: str(media_id)}, | |
| ) | |
| except (IndexMismatch, ValueError) as exc: | |
| skipped.append( | |
| f"A {view} photograph of {who} was left out of the " | |
| f"comparison: {exc}" | |
| ) | |
| continue | |
| stored += 1 | |
| if not stored: | |
| continue | |
| names[entry.animal_id] = who | |
| enrolled_views += stored | |
| return enrolled_views, skipped | |
| def _views_for( | |
| self, entry: EnrolledAnimal, skipped: list[str] | |
| ) -> dict[str, list[UUID]]: | |
| """The views of one animal this runner will compare against. | |
| Unknown view names are dropped here with a warning rather than left for | |
| `enrol` to raise on, because one typo in one animal's record should cost | |
| that animal's view and not the whole request. | |
| """ | |
| usable: dict[str, list[UUID]] = {} | |
| for view, media_ids in entry.views.items(): | |
| if view not in ENROLMENT_VIEWS: | |
| skipped.append( | |
| f"{entry.display_name or entry.animal_id} has a view called " | |
| f"{view!r}, which is not one of the five §6.4 enrols " | |
| f"({', '.join(ENROLMENT_VIEWS)}). It was ignored." | |
| ) | |
| continue | |
| if media_ids: | |
| usable[view] = list(media_ids) | |
| return usable | |
| def _enrolment_check( | |
| self, index: IdentityIndex, enrolled_views: int | |
| ) -> QualityCheck: | |
| """Whether this farm had anything to compare the capture against. | |
| A quality check rather than a warning, because the answer is about the | |
| *farm's register* rather than about the photograph, and a capture flow | |
| that can distinguish the two can say *"register this animal"* instead of | |
| *"take it again"*. | |
| """ | |
| muzzles = sum(1 for v in index.views if v.view == self.query_view) | |
| animals = len(index.animal_ids) | |
| if muzzles: | |
| detail = ( | |
| f"{animals} animals enrolled, {enrolled_views} views, " | |
| f"{muzzles} of them muzzles." | |
| ) | |
| thin = [ | |
| a for a in index.animal_ids | |
| if sum(1 for v in index.views | |
| if v.animal_id == a and v.view == self.query_view) | |
| < MEASURED_ENROLMENT_SHOTS | |
| ] | |
| if thin: | |
| # **Reported, never a refusal.** A thin enrolment still matches, | |
| # and one good muzzle photograph is worth more than a refused | |
| # enrolment — `IdentityIndex.enrol` takes that position and this | |
| # agrees with it. What it is not is the protocol the accuracy was | |
| # measured under, and a farm whose animals never quite match | |
| # deserves to know the reason is its register rather than its | |
| # camera. | |
| detail += ( | |
| f" {len(thin)} of them carry fewer than " | |
| f"{MEASURED_ENROLMENT_SHOTS} muzzle photographs, which is " | |
| f"the enrolment the accuracy was measured on." | |
| ) | |
| return QualityCheck(check="gallery", passed=True, detail=detail) | |
| return QualityCheck( | |
| check="gallery", | |
| passed=False, | |
| detail=( | |
| f"No enrolled {self.query_view} to compare against. " | |
| f"{len(index.animal_ids)} animals were sent and none carries " | |
| f"the view this capability matches on." | |
| ), | |
| ) | |
| def _result( | |
| self, *, request, capability, artefact, request_id, observations, | |
| checks, warnings, recapture, | |
| ) -> InferenceResult: | |
| forbidden = [o.type for o in observations if o.type in FORBIDDEN_CLAIMS] | |
| if forbidden: | |
| raise ValueError( | |
| f"{capability.key} tried to emit a forbidden claim: {forbidden}" | |
| ) | |
| banned = set(capability.acquisition.forbidden_claims) | |
| offending = [o.type for o in observations if o.type in banned] | |
| if offending: | |
| # `identity_without_confirmation` is the one thing §6.4 forbids, and | |
| # it is checked here as well as in the registry because this runner | |
| # is the only thing that could emit it. | |
| raise ValueError( | |
| f"{capability.key} tried to emit {offending}, which its own " | |
| f"registry entry forbids by name." | |
| ) | |
| return InferenceResult( | |
| request_id=request_id, | |
| capability_key=capability.key, | |
| model_id=artefact.model_id, | |
| model_version=artefact.version, | |
| inference_location=InferenceLocation.REMOTE, | |
| subject_type=request.subject_type, | |
| subject_id=request.subject_id, | |
| observations=observations, | |
| # **No interpretation, and none is possible.** A name is a fact about | |
| # the register, not a judgement about the animal, and there is | |
| # nothing for a vet to review. `observation_confidence` is likewise | |
| # None: the three-level label would be read as a confidence in the | |
| # name, and nothing calibrated one. | |
| interpretations=[], | |
| observation_confidence=None, | |
| interpretation_confidence=None, | |
| quality_checks=checks, | |
| warnings=warnings, | |
| recommended_recapture=recapture, | |
| ) | |
| #: Capabilities with an implemented adapter, in the shape `app/counting.py` | |
| #: publishes. `app/main.py` merges the two: a capability with a validated | |
| #: artefact and no entry in either returns 501 rather than a plausible result. | |
| RUNNERS: dict[str, IdentityRunner] = { | |
| "cattle_identity": IdentityRunner(), | |
| } | |