Ship each crop's embedding so a resolver NONE can mint

Two identity fixes, neither yet run on a GPU.

has_face, the extras gate: fixing the bbox coordinate space made extras worse,
because crops finally landed on their subjects and a background extra bound to
the lead at 1.00. /vision stamps has_face per character using face_detect and
_pair_faces_to_present, so containment and its margin match the speaker path.
Identity skips has_face is False before it crops or embeds. Fails open on a
missing or raising detector, and gates on `is False` so an older vision blob
behaves as before.

The NONE mint: /identity/resolve now writes each crop's embedding to the crop's
key with a .npy suffix and returns emb_uri. The orchestrator cannot compute an
embedding of its own (siglip and gemma cannot both be resident), which is why it
was clearing a deliberate NONE instead of minting an anonymous character.

Also: audit_registry.py, and the tmux respawn-window trap, which
leaves a bare shell rather than re-running the window command.

decisions/identity-bbox.md#face-gates-enrollment
decisions/identity-bbox.md#none-mints-an-anonymous-character

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:07:56 +04:00
parent 7f25dc43e4
commit 491dad1c67
9 changed files with 476 additions and 109 deletions
+90
View File
@@ -306,3 +306,93 @@ subject, including `person_5`, who is Seonho in the foreground with headphones a
Not done: `vision` and `identity` have not re-run, so every box, embedding and `ref_image_uris` in the
registry is still from the wrong space. That rerun is GPU work and was not started.
## 2026-08-12, third session — the rerun, and the run that did not count
Asked: move on from the chapter rebuild. Scope chosen mid-session: reset the registry, rerun vision,
identity and reconcile only, and inspect before spending the ~80 minutes downstream.
**The registry had to be wiped first, and nothing could do it.** `clear_stage_data` maps `identity` to
`identity_assignments` only and spares `characters` on purpose (`db.py:790`), because the registry is
cross-run. So a rerun would have matched fresh crops against 19 stale grid-space embeddings and kept the
duplicate `Lim Seonho` / `Seonho` rows that raise `ambiguous-speaker`. Added `db.reset_registry` and
`POST /characters/reset` (`confirm=true` required), which also clears `identity_assignment_sources`
untouched by any stage clear, and a leftover `manual` row there makes `assign_identity` refuse the next
model assignment (`db.py:663`). Covered by `test_db.py:TestResetRegistry`; 115 orchestrator tests pass.
The orchestrator image bakes its source, so it needed `docker compose up -d --build orchestrator`.
**The first full cycle was void: the worker was serving pre-fix code.** vision + identity + reconcile
completed 116/116/20 and reproduced the defect exactly — 46 of 110 boxes past the 900px panel width,
coordinates clamping at 1000. The vision worker started 12:00:09, `worker_vision.py` changed 12:11:35,
and the commit carrying `_bbox_to_pixels` landed 12:16:22. Python had already bound the old module.
Nothing in the stage output said so (`decisions/identity-bbox.md#stale-worker-invalidates`). Added
`check_stale.sh`, which compares every worker's process start against its module mtime and exits
non-zero; mutation-tested by touching `worker_tts.py`.
**The rerun against restarted workers.** 8 minutes for all three stages.
```
before stale run after
x2 past panel width 47/113 46/110 0/110
coords exactly on 1000 21 27 1
max y2 1000 1000 2307
characters 19 8 8
coverage 26/113 = 23% 77/110 = 70% 77/110 = 70%
top character's share 96% Haeseon 47% 47%
```
`Choi Haeseon`, which had absorbed 25 of 26 assignments, no longer exists in the registry. On panel 7
Seonho is bound for the first time, and the unnamed colleague took an anonymous id instead of being
called `Choi Haeseon` at 0.9.
**Fixing the boxes made the extras problem worse.** With crops finally landing on their subjects, a
background extra bound to `Seonho` at confidence 1.00, putting an extra into the lead's reference set.
`/vision` now stamps `has_face` per character via `face_detect` + `_pair_faces_to_present`, and identity
skips `has_face is False` before it crops or embeds
(`decisions/identity-bbox.md#face-gates-enrollment`). Fails open on a missing or raising detector.
Self-checked in both workers. **Not yet proven on a GPU run** — that is the next command.
Deleted along the way and not recoverable: the rebuilt `chapter.mp4` and all 49 clips, by the cascade
from `/stage/clear vision`. The user chose not to keep a copy.
## 2026-08-12, fourth session — identity 2b, the resolver NONE branch
Asked: "how much will `has_face` help with character and identity problems?", then "fix 2b first".
Answered the first honestly: `has_face` reaches 2 of 6 detections on panel 7 and nothing else. It does not
touch naming or merging, and it cannot touch the chibi at 1:35, because an anime face detector detects a
chibi face. Also flagged its real cost: the model is face-only by design, so back-turned cast lose
enrollment along with the extras, and coverage is where that shows up first.
Measured before writing anything, read-only, no GPU:
```
registry: 8 characters, 1 named -> ['Seonho']
detections: 110 assignments: 77 = 70% coverage
spread: Seonho 36, character_565c88 24, character_759e23 9, character_f7a4fd 3,
character_25f682 3, character_d72710 1, character_823aba 1
```
That killed the assumed cause. Anonymous ids already recur, so the identity worker's own
pending-promote path gives stable anonymous identities. The defect was elsewhere.
`/vision/resolve` can answer "none of these" and always could: `worker_vision.py:1071` returns
`state="new"` for `choice: 0` and `state="unresolved"` for an out-of-range index. `service.py` read only
`character_id` and unassigned every crop of the tracklet for either. The stale `ponytail:` comment above
that block named the real blocker and was right: minting needs an `embedding_uri` the orchestrator cannot
compute, since siglip and gemma cannot both be resident.
Fixed by carrying the embedding, not by adding a GPU pass. `/identity/resolve` writes each crop's
embedding to the crop's key with a `.npy` suffix and returns `emb_uri`. `tracklets.resolve_outcome` holds
the three-way decision as a pure function. `service.py` mints via the existing `create_character` and
falls into the existing assign loop. `decisions/identity-bbox.md#none-mints-an-anonymous-character`.
Checks: `worker_identity self-check ok`, `tracklets self-check ok`, 115 passed on homesrv.
Deployed: image rebuilt, `resolve_outcome` verified inside the running container, `audit_registry.py`
re-copied after the recreate. Vision and identity restarted, `./check_stale.sh` exits 0.
New trap: `tmux respawn-window -k` does not re-run the window command. It leaves a bare shell and the
worker down. Both workers were dead for two minutes before `/health` caught it.
Not run: the GPU cycle. `has_face` and the NONE mint are both unproven on real panels and now land in the
same run.