Record two GPU cycles: the gate works, coverage is the open question
No worker code changed. This is the evidence from the 2026-08-12 16:39 and 17:38 runs, and where each finding now lives. The fourth session's four identity changes all work on real panels. Panel 7's two wrong bindings are gone. The lead going unassigned there is correct and was measured, not assumed: face_detect finds one face on the whole panel at conf 0.599, nothing else above 0.056 even at a 0.04 threshold, and the crop shows him drawn from behind. Two decisions, both closed: a roster name is a guess so it never reaches detection, and merged_into is exactly one hop deep. Two caveats, both open: detection can order a bbox backwards (1 in 117), and identity coverage has fallen on every run since the gate landed (70 -> 61 -> 50). Coverage is the thing to settle next, and not by reading the number. identity_labels already holds 145 rows of ground truth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+117
-81
@@ -1,124 +1,160 @@
|
|||||||
# HANDOFF, 2026-08-12 (fourth session of the day)
|
# HANDOFF, 2026-08-12 (fifth session)
|
||||||
|
|
||||||
Live state is in `NEXT.md`. This file is only what this session did. The previous handoff is in
|
Live state is in `NEXT.md`. This file is only what this session did. The previous handoff is in
|
||||||
`JOURNAL.md`.
|
`JOURNAL.md`.
|
||||||
|
|
||||||
## Asked
|
## Asked
|
||||||
|
|
||||||
"How much will `has_face` help with character and identity problems?", then in order: fix 2b, fix
|
"go ahead" on the GPU cycle the fourth session left staged. Then "what do we do now". Then go ahead on
|
||||||
extra-versus-cast, 2d.
|
the fixes its evidence asked for, and run it again.
|
||||||
|
|
||||||
## Result
|
## Result
|
||||||
|
|
||||||
Identity item 2 in `NEXT.md` is now done as far as it can go without a run. Four changes are written,
|
Two full GPU cycles ran. All four of the fourth session's identity changes are now proven on real panels.
|
||||||
self-checked and deployed. **None has run on a GPU.** No pipeline stage was executed this session.
|
Four more fixes were written on top, all orchestrator-side, all deployed. Coverage fell twice and that is
|
||||||
|
the open question.
|
||||||
|
|
||||||
| item | was | now |
|
| cycle | detections | assignments | coverage | registry | named |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2a bbox space | done last session | unchanged |
|
| baseline (13:11, pre-change) | 110 | 77 | 70% | 8 | 1 |
|
||||||
| 2b abstain and stay abstained | resolver blamed, unverified | fixed in the orchestrator, `f6804e7` + `ffda3df` |
|
| run A (16:39-16:45) | 110 | 67 | 61% | 16 | 2 |
|
||||||
| 2c extra versus cast | open | two consumers gated, `ca46617` + `8b27aec` |
|
| run B (17:38-17:44) | 117 | 59 | 50% | 20 | 2 |
|
||||||
| 2d merge and split | open | examples gone; merge made non-destructive, `00096cc` |
|
|
||||||
|
|
||||||
## What each change was, and where the cause turned out to be
|
Vision is non-deterministic, so detection counts move between runs. Each cycle is ~6 minutes:
|
||||||
|
vision ~3m50s, identity ~1m25s, reconcile ~50s.
|
||||||
|
|
||||||
**2b was never the resolver.** `/vision/resolve` at `worker_vision.py:1071` already returns
|
## First, a correction the session started with
|
||||||
`state="new"` for `choice: 0` and `state="unresolved"` for an out-of-range index. `service.py` read only
|
|
||||||
`v.get("character_id")` and ran `unassign_identity` on every crop of the tracklet for either one. A
|
|
||||||
deliberate NONE produced nothing, so an unnamed recurring person was `unknown` on every panel.
|
|
||||||
|
|
||||||
The old `ponytail:` comment above that block named the real blocker and was right. Minting needs an
|
The vision/identity/reconcile timestamps in `/job/status` are UTC. The git log is local, UTC+4. The run
|
||||||
`embedding_uri` the orchestrator cannot compute. siglip is resident in the identity worker and gemma in
|
that looked like a completed rerun was the pre-change baseline: it finished 13:17 local, and
|
||||||
the vision worker. Fixed by carrying the embedding, not by adding a GPU pass.
|
`_mark_has_face` was not committed until 19:07. The fourth session's handoff was right that nothing had
|
||||||
`/identity/resolve` writes each crop's embedding to the crop's key with a `.npy` suffix and returns
|
run.
|
||||||
`emb_uri`. `tracklets.resolve_outcome` holds the three-way decision as a pure function.
|
|
||||||
`decisions/identity-bbox.md#none-mints-an-anonymous-character`.
|
|
||||||
|
|
||||||
**2c: `build_scene` already dropped extras from the cast list.** `worker_scene.py:63` skips an unassigned
|
## Run A: the four staged changes, verified
|
||||||
detection, so extras never reached `characters` or `present`. Their **actions** did. `actions` was built
|
|
||||||
from every detection. That list is the script prompt's content and the verifier's evidence. So a background
|
|
||||||
extra's "standing at the window" arrived as a fact with no character attached, and the verifier confirmed
|
|
||||||
it. Gated `worker_scene`'s `actions` and `service._beat` on `has_face is False`. Left
|
|
||||||
`service._present_characters` ungated on purpose, reasoned out in
|
|
||||||
`decisions/identity-bbox.md#extras-gate-consumers`.
|
|
||||||
|
|
||||||
**2d's worked examples no longer exist.** The registry reset deleted `Lim Seonho` and
|
| change | verdict |
|
||||||
`character_afa7623b`. The registry is 8 rows, one named. Built the safety net for the coming rerun
|
| --- | --- |
|
||||||
instead, since `reconcile` runs inside it: `merge_characters` sets `merged_into = keeper` rather than
|
| `has_face` stamp | on all 110 detections, 67 true / 43 gated (39%) |
|
||||||
deleting, and stamps every repointed assignment `method = merged_from:<loser_id>`. Roster readers filter
|
| extras gate | panel 7's two wrong bindings gone |
|
||||||
`merged_into IS NULL`, lookup by id does not.
|
| NONE mints | 16 characters where 8 existed |
|
||||||
`caveats/audit-open.md#destructive-reconcile` is half-closed.
|
| non-destructive merge | 7 rows carry `merged_into`, 9 assignments stamped `merged_from:` |
|
||||||
|
|
||||||
## Measured, read-only, before writing anything
|
Panel 7, the worked example:
|
||||||
|
|
||||||
```
|
```
|
||||||
registry: 8 characters, 1 named -> ['Seonho']
|
person_1 Seonho 1.00 -> -- none -- has_face=False
|
||||||
detections: 110 assignments: 77 = 70% coverage
|
person_2 character_f7a4fd 0.00 -> character_519d2b 0.00 has_face=True
|
||||||
spread: Seonho 36, character_565c88 24, character_759e23 9, character_f7a4fd 3,
|
person_4 character_d72710 0.94 -> -- none -- WRONG binding removed
|
||||||
character_25f682 3, character_d72710 1, character_823aba 1
|
person_5 Seonho 1.00 -> -- none -- WRONG binding removed
|
||||||
bbox space: 77/110 exceed 1000, 1 on 1000 -> PIXELS
|
|
||||||
```
|
```
|
||||||
|
|
||||||
That killed the assumed cause of 2b. Anonymous ids already recur, so the identity worker's own
|
`person_1` going unassigned is correct, and this was measured, not assumed. Ran `face_detect.detect_faces`
|
||||||
pending-promote path gives stable anonymous identities. Only the gemma NONE branch was discarding people.
|
on `p006.png` directly: one face on the whole panel, conf 0.599, inside `person_2`. Swept the threshold to
|
||||||
|
0.04 and nothing else appears above 0.056. The crop shows the lead drawn from behind at his desk, back of
|
||||||
|
the head and headphones. Detector right, gate right, lead unidentifiable in that panel.
|
||||||
|
|
||||||
## Deliberately not built
|
`person_1`'s box now frames the lead. The old "empty window mullion" note was written against pre-fix
|
||||||
|
boxes and is retired.
|
||||||
|
|
||||||
- **The unmerge path and the split.** No wrong merge has been seen since the crops were fixed. Undoing
|
## Run A's new defect: a roster hint named the wrong man
|
||||||
one today is a hand-written SQL walk of the two records above. Revisit trigger is in the caveat.
|
|
||||||
- **`service._present_characters` gating.** An extra picked as speaker already resolves to unknown. A
|
Verified against the art, three real people:
|
||||||
character drawn from behind has no face box. Gating would delete a real speaker from the only list that
|
|
||||||
can attribute their line.
|
- `character_92129ac7` "Lim Seonho", 22 assignments. The p010 caption reads `LIM SEONHO (29)`, yellow
|
||||||
- **Vision still emits extras into the blob.** Deliberate, so the audit can see what was gated.
|
plaid shirt, headphones, matching the roster's description. Correct.
|
||||||
|
- `character_556aef60`, 25 assignments, unnamed. The woman with short black hair and pearls, the roster's
|
||||||
|
second character, whom the roster itself calls "Unknown". Correct.
|
||||||
|
- `character_dbadfff7` "Seonho", 15 assignments. A different man, glasses, dark clothing, wearing the
|
||||||
|
lead's roster name.
|
||||||
|
|
||||||
|
p020 assigned `Seonho` to `person_1` and `Lim Seonho` to `person_3`, two people in one panel.
|
||||||
|
|
||||||
|
## What was changed, and where the cause was
|
||||||
|
|
||||||
|
**Roster hints no longer seed detection.** Deleted the two lines at `service.py:882` that appended
|
||||||
|
`_roster_char_hints` to `known_characters`. `build_detect_prompt` drops any hint without a name. Passing a
|
||||||
|
nameless hint would have contributed nothing, so removing the seeding was the only real option.
|
||||||
|
Names now reach detection from registry rows only, which carry embeddings and were named from an in-panel
|
||||||
|
caption or address. The roster still feeds `roster_cast` in `run_stage_dialogue`, where names match
|
||||||
|
against speech rather than faces.
|
||||||
|
|
||||||
|
**`merge_characters` keeps `merged_into` one hop deep.** Two halves, one per direction, and the second was
|
||||||
|
missed on the first attempt:
|
||||||
|
|
||||||
|
- resolve the keeper to its chain root before merging (a bounded 64-step walk, the cap only so a cycle
|
||||||
|
cannot hang reconcile)
|
||||||
|
- repoint the loser's own dependents to the keeper when the loser is retired
|
||||||
|
|
||||||
|
The keeper walk alone does not work. Run B still produced `477c1894 -> a92d9df4 -> 4fb94c15`. At merge
|
||||||
|
time that pair was fine. The chain formed later, when a row that was already somebody's keeper was itself
|
||||||
|
retired.
|
||||||
|
|
||||||
|
**`reset_registry` deletes `name_claims`** for the manga's panels and reports the count. Confirmed live:
|
||||||
|
run B's reset reported `name_claims: 5`, the orphans that had pointed at `character_afa7623b` since two
|
||||||
|
resets earlier.
|
||||||
|
|
||||||
|
**`audit_registry.py` is in the repo.** It had only ever been `docker cp`'d, so every rebuild dropped it.
|
||||||
|
`Dockerfile` has `COPY . .`, so it is baked now and that trap is gone.
|
||||||
|
|
||||||
|
## Run B: what the fixes did
|
||||||
|
|
||||||
|
The registry split is fixed. "Lim Seonho" came back as ONE row holding 25 assignments. That answers the
|
||||||
|
open risk from before the run: dropping the roster hint did not split the lead across the panels before
|
||||||
|
his caption. "Seonho" fell from 15 assignments to 1.
|
||||||
|
|
||||||
|
Two things run B surfaced:
|
||||||
|
|
||||||
|
- **A second naming mechanism, untouched by the fix.** Even with no roster hint, p011 and p026 emit
|
||||||
|
`name: "Seonho"` on `person_2` from in-panel text. Detection reads a name off the panel and attaches it
|
||||||
|
to the wrong body. That is what keeps the glasses man named, now at 1 assignment instead of 15.
|
||||||
|
- **One degenerate bbox.** `p007 person_1` is `[226, 417, 130, 551]`, x1 > x2. One in 117. It crops to
|
||||||
|
nothing, so that detection can never enroll or match. `_bbox_to_pixels` clamps each coordinate but never
|
||||||
|
orders the corners.
|
||||||
|
|
||||||
|
## Not done
|
||||||
|
|
||||||
|
- **The degenerate-box guard.** It is a worker change (`worker_vision.py:_bbox_to_pixels`), so it needs a
|
||||||
|
vision worker restart and a third GPU cycle to prove. Not started, nothing half-edited.
|
||||||
|
- **The coverage trend.** 70% -> 61% -> 50%. Part is the `has_face` gate, which is stable at ~39% of
|
||||||
|
detections across both runs. Framed against face-bearing detections only, run B assigned 59 of 72, or
|
||||||
|
82%. Nothing establishes whether the rest is correct abstention or lost cast. Settle that number before
|
||||||
|
trusting the registry.
|
||||||
|
- **Nothing downstream re-ran.** The job is parked at `dialogue waiting` with every stage below it
|
||||||
|
cleared. No clip or chapter has been rebuilt against the new cast.
|
||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/python worker_identity.py # ok
|
/usr/bin/ssh kami@192.168.1.104 "cd /home/kami/docker-apps/manga-infra/orchestrator && python3 -m pytest -q --ignore=test_api.py" # 118 passed, was 116
|
||||||
.venv/bin/python worker_scene.py # ok, 3 new cases on the actions gate
|
./check_stale.sh # exit 0, all 9 workers current
|
||||||
./check_stale.sh # exit 0, all 9 workers current
|
|
||||||
/usr/bin/ssh kami@192.168.1.104 "cd /home/kami/docker-apps/manga-infra/orchestrator && python3 -m pytest -q --ignore=test_api.py" # 116 passed
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Deployed and verified inside the running container: `resolve_outcome` returns `mint`, `_beat` drops a
|
Both new merge tests were confirmed to fail with their fix disabled, then the fix was restored and the
|
||||||
faceless detection, `merged_into` exists on the live database with 0 rows merged. Vision, identity and
|
suite re-run. The deployed container was verified by parsing its source, not grepping it. The first
|
||||||
scene workers restarted.
|
attempt gave a false negative, matching `_roster_char_hints` inside the comment that explains its removal.
|
||||||
|
|
||||||
## Next command
|
## Next command
|
||||||
|
|
||||||
Four changes ride one GPU cycle. Coverage is 70% and is the number to beat. A gate that abstains too hard
|
The third cycle, after adding the corner-ordering guard to `_bbox_to_pixels` and restarting the vision
|
||||||
shows up there before it shows up on panel 7. Watch the identity log line for `minted N anonymous`.
|
worker. Watch coverage against 50% and the degenerate box count against 1.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/kami/Programs/n8n-worker && ./check_stale.sh # must exit 0
|
cd /home/kami/Programs/n8n-worker && ./check_stale.sh # must exit 0
|
||||||
J=778297bc-e7ce-439d-91b5-8a027060d17f; M=ef105a86-4b7e-4ac4-b45c-b7d83b8f5b5e
|
J=778297bc-e7ce-439d-91b5-8a027060d17f; M=ef105a86-4b7e-4ac4-b45c-b7d83b8f5b5e
|
||||||
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST http://127.0.0.1:9090/characters/reset -H 'Content-Type: application/json' -d '{\"manga_id\":\"$M\",\"confirm\":true}'"
|
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST http://127.0.0.1:9090/characters/reset -H 'Content-Type: application/json' -d '{\"manga_id\":\"$M\",\"confirm\":true}'"
|
||||||
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST http://127.0.0.1:9090/stage/clear -H 'Content-Type: application/json' -d '{\"job_id\":\"$J\",\"stage\":\"vision\"}'"
|
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST http://127.0.0.1:9090/stage/clear -H 'Content-Type: application/json' -d '{\"job_id\":\"$J\",\"stage\":\"vision\"}'"
|
||||||
|
# restart the identity worker here, see the trap below
|
||||||
for S in vision identity reconcile; do /usr/bin/ssh kami@192.168.1.104 "curl -s --max-time 5400 -X POST http://127.0.0.1:9090/stage/run -H 'Content-Type: application/json' -d '{\"job_id\":\"$J\",\"stage\":\"$S\"}'"; done
|
for S in vision identity reconcile; do /usr/bin/ssh kami@192.168.1.104 "curl -s --max-time 5400 -X POST http://127.0.0.1:9090/stage/run -H 'Content-Type: application/json' -d '{\"job_id\":\"$J\",\"stage\":\"$S\"}'"; done
|
||||||
/usr/bin/ssh kami@192.168.1.104 "docker exec manga-orchestrator python3 /app/audit_registry.py 7c944dd4-e972-42c7-ba60-9f6939548e80 6"
|
/usr/bin/ssh kami@192.168.1.104 "docker exec manga-orchestrator python3 /app/audit_registry.py 7c944dd4-e972-42c7-ba60-9f6939548e80 6"
|
||||||
```
|
```
|
||||||
|
|
||||||
Panel 7 (`panel_index 6`) is the worked example. Before this session's changes:
|
|
||||||
|
|
||||||
```
|
|
||||||
person_1 [457, 657, 642, 937] Seonho 0.00 foreground, correct
|
|
||||||
person_2 [669, 591, 763, 822] character_f7a4fd 0.00 the unnamed colleague, correct
|
|
||||||
person_3 [428, 386, 496, 526] -- none -- extra
|
|
||||||
person_4 [498, 386, 568, 533] character_d72710 0.94 extra, WRONG
|
|
||||||
person_5 [31, 554, 94, 728] Seonho 1.00 extra bound to the lead, WRONG
|
|
||||||
person_6 [34, 414, 122, 564] -- none -- extra
|
|
||||||
```
|
|
||||||
|
|
||||||
`person_4` and `person_5` are what `has_face` must remove without taking `person_1` or `person_2` with
|
|
||||||
them. Restart the identity worker after every reset: it caches the known list in-process and only
|
|
||||||
invalidates on enrollment.
|
|
||||||
|
|
||||||
## Traps confirmed or found
|
## Traps confirmed or found
|
||||||
|
|
||||||
- **`tmux respawn-window -k` does not re-run the window command.** It leaves a bare shell. Both vision and
|
- **`kill $(pgrep -f "worker_identity:app")` kills the shell running it.** The pattern matches the calling
|
||||||
identity sat dead for two minutes before an empty `/health` caught it. Now in `AGENTS.md`.
|
command line. It killed this session's own script mid-way, so the `send-keys` relaunch never fired and
|
||||||
- `docker compose up --build orchestrator` recreates the container and drops any `docker cp`'d file, so
|
the identity worker sat dead. Restart it with the two `tmux send-keys` lines only, then poll `/health`.
|
||||||
`audit_registry.py` needs re-copying after every rebuild.
|
- **Verify deployed code by parsing it, not by grepping it.** A comment explaining a removal contains the
|
||||||
- Two test assertions asserted the old destructive merge (`test_db.py:235`, `test_merge_refs.py:37`). They
|
name of the thing removed.
|
||||||
were rewritten, not deleted: the invariant changed on purpose.
|
- **A test that cannot fail proves nothing.** Both merge tests were run with their fix disabled first.
|
||||||
- The orchestrator image bakes its source. Editing the repo on homesrv does nothing until the rebuild.
|
- Panel-7 line numbers move between runs, because vision is non-deterministic and `local_id` is assigned
|
||||||
|
top-to-bottom per run. In run B the lead is `person_5`, not `person_1`. Compare by bbox, not by id.
|
||||||
|
|||||||
+127
@@ -459,3 +459,130 @@ block, verified on the live database (`merged_into` present, 0 rows merged). Com
|
|||||||
Deliberately not built: the unmerge path and the split. No wrong merge has been observed since the crops
|
Deliberately not built: the unmerge path and the split. No wrong merge has been observed since the crops
|
||||||
were fixed, so the consumer of these records waits for one. The forward case is partly covered by 2b, since
|
were fixed, so the consumer of these records waits for one. The forward case is partly covered by 2b, since
|
||||||
a resolver NONE now mints instead of folding a stranger into the nearest match.
|
a resolver NONE now mints instead of folding a stranger into the nearest match.
|
||||||
|
|
||||||
|
## 2026-08-12, fifth session — the GPU cycle all four changes were waiting for
|
||||||
|
|
||||||
|
Asked: "go ahead" on the rerun.
|
||||||
|
|
||||||
|
First correction: the vision/identity/reconcile timestamps in `/job/status` are UTC and the git log is
|
||||||
|
local (UTC+4), so the run that looked like a rerun was the pre-change baseline. `_mark_has_face` landed at
|
||||||
|
19:07 local, the run finished 13:17 local. The handoff was right that nothing had run.
|
||||||
|
|
||||||
|
Reset the registry for `ef105a86` (8 characters, 77 assignments, 101 sources, 142 S3 objects), cleared
|
||||||
|
`vision` and everything downstream (116 vision results), restarted the identity worker for its in-process
|
||||||
|
known-character cache, then ran vision, identity and reconcile. 6 minutes wall: vision 3m48s, identity
|
||||||
|
1m25s, reconcile 48s. All three `completed`, 116/116/16.
|
||||||
|
|
||||||
|
### What the four changes did
|
||||||
|
|
||||||
|
| change | verdict |
|
||||||
|
| --- | --- |
|
||||||
|
| `has_face` stamp | works, present on all 110 detections, 67 true / 43 gated (39%) |
|
||||||
|
| extras gate | works, panel 7's two wrong bindings are gone |
|
||||||
|
| NONE mints | works, 16 characters minted where 8 existed |
|
||||||
|
| non-destructive merge | works, 7 rows carry `merged_into`, 9 assignments stamped `merged_from:` |
|
||||||
|
|
||||||
|
Coverage went 70% -> 61% (77 -> 67 assignments over the same 110 detections). That is the gate's price and
|
||||||
|
it is mostly correct, see below.
|
||||||
|
|
||||||
|
### Panel 7, the worked example, before and after
|
||||||
|
|
||||||
|
```
|
||||||
|
person_1 Seonho 1.00 -> -- none -- has_face=False
|
||||||
|
person_2 character_f7a4fd 0.00 -> character_519d2b 0.00 has_face=True
|
||||||
|
person_4 character_d72710 0.94 -> -- none -- WRONG binding removed
|
||||||
|
person_5 Seonho 1.00 -> -- none -- WRONG binding removed
|
||||||
|
```
|
||||||
|
|
||||||
|
The two wrong bindings the gate existed to kill are dead. It also took `person_1`, and that is correct:
|
||||||
|
ran `face_detect.detect_faces` on p006.png directly and it finds exactly one face on the whole panel, conf
|
||||||
|
0.599, inside `person_2`. Swept the threshold to 0.04 and nothing else appears above 0.056. Looked at the
|
||||||
|
crop: `person_1` is the lead drawn from behind at his desk, back of the head and headphones, no face in
|
||||||
|
frame. The detector is right and the gate is right. The cost is that a back-turned character cannot be
|
||||||
|
identified from that panel at all.
|
||||||
|
|
||||||
|
Also worth recording: `person_1`'s new box frames the lead correctly. The old note that it framed an empty
|
||||||
|
window mullion was written against the pre-fix boxes.
|
||||||
|
|
||||||
|
### The new defect: a roster hint named the wrong man
|
||||||
|
|
||||||
|
The registry holds three real people and one wrong name.
|
||||||
|
|
||||||
|
- `character_92129ac7` "Lim Seonho", 22 assignments. Verified against the art: the p010 introduction panel
|
||||||
|
captioned `LIM SEONHO (29)`, yellow plaid shirt and headphones, which is the roster's description for
|
||||||
|
"Seonho". This is the lead and the name is right.
|
||||||
|
- `character_556aef60`, 25 assignments, unnamed. Verified: the woman with short black hair and pearls, the
|
||||||
|
roster's second character, whom the roster itself calls "Unknown".
|
||||||
|
- `character_dbadfff7` "Seonho", 15 assignments. Verified: a different man, glasses, dark clothing, in
|
||||||
|
profile. He carries the lead's roster name.
|
||||||
|
|
||||||
|
p020 assigns `Seonho` to `person_1` and `Lim Seonho` to `person_3`, so the pipeline holds them as two
|
||||||
|
people in one panel. inference: the roster hint injected at `service.py:882` puts a name in front of
|
||||||
|
detection, and detection attached it to the wrong face before the caption panel could mint the real one.
|
||||||
|
This is not the alias-merge case and merging the two rows would be wrong. It needs name binding to require
|
||||||
|
evidence, the way `name_claims` already does for captions and address.
|
||||||
|
|
||||||
|
### Two smaller things the run exposed
|
||||||
|
|
||||||
|
- **Merge chains.** `character_e1ab7776 -> character_521c301f -> character_556aef60`. `merged_into` points
|
||||||
|
at a row that is itself merged, so a single-hop resolve lands on a retired character. Roster readers are
|
||||||
|
fine because they filter `merged_into IS NULL`. Anything that follows one hop is not.
|
||||||
|
- **Orphan `name_claims`.** All 5 rows point at `character_afa7623b`, which the reset deleted. The reset
|
||||||
|
clears characters and assignments but not claims.
|
||||||
|
|
||||||
|
Not run: dialogue and everything downstream. The job sits at `dialogue waiting`.
|
||||||
|
|
||||||
|
### Same session — the three fixes the run's evidence asked for
|
||||||
|
|
||||||
|
All three are orchestrator-side, so no worker changed and `check_stale.sh` is not in play.
|
||||||
|
|
||||||
|
- **Roster hints no longer seed detection.** Deleted the two lines at `service.py:882` that appended
|
||||||
|
`_roster_char_hints` to `known_characters`. `build_detect_prompt` drops any hint without a name, so a
|
||||||
|
nameless hint would have contributed nothing anyway. Names now reach detection only from registry rows,
|
||||||
|
which are embedding-backed and were themselves named from an in-panel caption or address. The roster
|
||||||
|
still feeds `roster_cast` in `run_stage_dialogue`, where names are matched against speech.
|
||||||
|
- **`merge_characters` resolves the keeper to its chain root** before merging, so `merged_into` stays one
|
||||||
|
hop deep. Bounded 64-step walk, the cap only exists so a cycle cannot hang reconcile.
|
||||||
|
- **`reset_registry` deletes `name_claims`** for the manga's panels and reports the count.
|
||||||
|
|
||||||
|
Checks: 117 passed on homesrv, up from 116. The new chain test was confirmed to fail with the walk
|
||||||
|
disabled, then the walk was restored and the suite re-run. Deployed by rebuilding the image; verified
|
||||||
|
inside the running container by parsing the deployed source, not by grepping it, because the first check
|
||||||
|
matched the word `_roster_char_hints` inside the comment that explains its removal.
|
||||||
|
|
||||||
|
Also copied `audit_registry.py` into the repo before the rebuild. It had only ever been `docker cp`'d, so
|
||||||
|
every rebuild dropped it. `Dockerfile` has `COPY . .`, so it is baked now and the trap is gone.
|
||||||
|
|
||||||
|
Open risk on the next run: without the roster hint the lead is unnamed until the p010 caption, so panels 1
|
||||||
|
to 9 may mint him as an anonymous character that reconcile then has to merge. Watch whether "Lim Seonho"
|
||||||
|
comes back as one row or two.
|
||||||
|
|
||||||
|
### Same session — run B, 17:38-17:44
|
||||||
|
|
||||||
|
Reset (16 characters, 67 assignments, 67 sources, **5 name_claims**, 156 S3 objects), cleared `vision`,
|
||||||
|
restarted the identity worker, ran the three stages. The `name_claims: 5` line is the orphan fix confirmed
|
||||||
|
on live data.
|
||||||
|
|
||||||
|
117 detections, 59 assignments, 50% coverage, 20 characters, 72 with `has_face` (45 gated).
|
||||||
|
|
||||||
|
The registry split is fixed. "Lim Seonho" is one row with 25 assignments, "Seonho" fell from 15 to 1. So
|
||||||
|
dropping the roster hint did not split the lead across the panels before his caption, which was the risk
|
||||||
|
recorded before the run.
|
||||||
|
|
||||||
|
Two findings, both now filed:
|
||||||
|
|
||||||
|
- detection still names from in-panel text and can attach the name to the wrong body (p011, p026), which
|
||||||
|
is `decisions/identity-bbox.md#roster-does-not-name`'s "not covered" paragraph
|
||||||
|
- one degenerate bbox, `caveats/audit-open.md#degenerate-bbox`
|
||||||
|
|
||||||
|
And one fix of my own that was incomplete: the merge chain came back as
|
||||||
|
`477c1894 -> a92d9df4 -> 4fb94c15` with the keeper walk deployed. The walk only sees the pair being merged
|
||||||
|
now. Retiring a row that is already somebody's keeper needed the second half, a repoint of the loser's
|
||||||
|
dependents. Both halves and both tests are in `decisions/identity-bbox.md#merge-chains-flatten`.
|
||||||
|
|
||||||
|
Checks: 118 passed, was 116. Each new merge test was run with its fix disabled and confirmed to fail
|
||||||
|
first. Deployed by rebuild and verified by parsing the container's source.
|
||||||
|
|
||||||
|
Coverage is now 70 -> 61 -> 50 across three runs and is the open question, recorded as
|
||||||
|
`caveats/audit-open.md#coverage-trend`. `identity_labels` already holds 145 rows of ground truth, so the
|
||||||
|
next move is to score with `eval_identity.py` rather than to keep reading the coverage number.
|
||||||
|
|||||||
@@ -1,51 +1,66 @@
|
|||||||
# NEXT
|
# NEXT
|
||||||
|
|
||||||
Updated 2026-08-12. What this session did is in `HANDOFF.md`.
|
Updated 2026-08-12 (fifth session). What the fourth session did is in `HANDOFF.md`, the run is in
|
||||||
|
`JOURNAL.md`.
|
||||||
|
|
||||||
## State
|
## State
|
||||||
|
|
||||||
The chapter runs end to end. The A/V sync defect is fixed and `chapter.mp4` is rebuilt: video 364.120s
|
The chapter runs end to end. The A/V sync defect is fixed and `chapter.mp4` is rebuilt: video 364.120s
|
||||||
against audio 364.122s at `25/1`. The identity defects are still in the output.
|
against audio 364.122s at `25/1`.
|
||||||
|
|
||||||
Job `778297bc-e7ce-439d-91b5-8a027060d17f`, chapter `7c944dd4-e972-42c7-ba60-9f6939548e80`, 116 panels,
|
Job `778297bc-e7ce-439d-91b5-8a027060d17f`, chapter `7c944dd4-e972-42c7-ba60-9f6939548e80`, 116 panels.
|
||||||
`status=completed`, finished 2026-08-11T20:08:16Z. `s3://video/` holds 49 clips and a 50MiB
|
Two GPU cycles ran on 2026-08-12, the last 17:38-17:44 UTC on a reset registry. The job is `running` and
|
||||||
`chapter.mp4`. The user watched it and read out 19 defects. They are grouped by cause in `HANDOFF.md`.
|
parked at `dialogue waiting`. Everything from `dialogue` down is cleared and stale.
|
||||||
|
|
||||||
One number sets the agenda:
|
Seven changes are now proven on real panels: the fourth session's four, plus this session's roster-hint
|
||||||
|
removal, merge-chain flattening and `name_claims` reset. The registry after the last run:
|
||||||
|
|
||||||
- Panel 7 checked against the art has **zero correct identity bindings** out of two, and the one
|
- `character_713ea2ce` "LIM SEONHO", 25 assignments, the lead, one row, named from the p010 caption.
|
||||||
character who matters is unbound. `HANDOFF.md#panel-7-walked-against-the-art` has the table. The
|
- `character_4fb94c15`, 18 assignments, the woman, correct and unnamed.
|
||||||
coordinate cause is fixed. The registry built on it is not.
|
- `character_023ba5a3` "Seonho", 1 assignment, still the wrong man, down from 15.
|
||||||
|
|
||||||
|
Coverage is 50%, from 61% and 70% before it. That trend is the open question and is filed as
|
||||||
|
`caveats/audit-open.md#coverage-trend`.
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
1. **Re-run vision and identity.** The `bbox` space is settled and converted at `/vision`
|
1. **Settle coverage before trusting the registry.** 70% -> 61% -> 50% over three runs. `has_face` gates a
|
||||||
(`decisions/identity-bbox.md#bbox-is-normalized`). Every stored box, embedding and `ref_image_uris` in
|
steady 39%, and against face-bearing detections alone the last run assigned 59 of 72 (82%). Nothing
|
||||||
the registry came from the wrong space. The fix changes nothing until those stages run again.
|
separates correct abstention from lost cast. `identity_labels` already holds 145 rows of human ground
|
||||||
This is GPU work and needs the user's go-ahead. Clear `vision` and everything downstream of it, or
|
truth, and `eval_identity.py` already scores against it. Measure precision and recall instead of
|
||||||
accept that the boxes in the database stay normalized while new ones are pixels.
|
reading the coverage number (`caveats/audit-open.md#coverage-trend`).
|
||||||
|
2. **Order the corners in `_bbox_to_pixels`.** `p007 person_1` came back `[226, 417, 130, 551]`, x1 > x2,
|
||||||
Watch two things on the rerun. Whether `som_face` still returns `unknown` on every face, since gated
|
1 in 117. It crops to nothing, so that detection is silently lost
|
||||||
pairing was comparing pixel face boxes against 0-1000 character boxes. And whether `Choi Haeseon` still
|
(`caveats/audit-open.md#degenerate-bbox`). Two `min`/`max` pairs. It is a worker change, so it needs a
|
||||||
absorbs every unnamed woman, which is item (b) below and independent of the crops.
|
vision restart and a GPU cycle to prove.
|
||||||
|
3. **Detection still names from in-panel text and can hit the wrong body.** p011 and p026 emit
|
||||||
|
`name: "Seonho"` on `person_2` with the roster hint gone. That is the residue of the naming defect and
|
||||||
|
the reason the glasses man is named at all
|
||||||
|
(`decisions/identity-bbox.md#roster-does-not-name`, "not covered").
|
||||||
|
4. **Decide what a back-turned character costs.** `has_face` gates 39% of detections. Panel 7's lead is at
|
||||||
|
his desk from behind. `face_detect` finds one face on the whole panel at conf 0.599, and nothing else
|
||||||
|
above 0.056 even at a 0.04 threshold. The gate is right and the detector is right. The lead is still
|
||||||
|
unidentifiable there. Options are a body or head detector alongside the face one, or letting the
|
||||||
|
tracklet carry identity across a back-turned panel. Do not lower `FACE_CONF`, the sweep shows nothing
|
||||||
|
to find.
|
||||||
|
|
||||||
Smaller follow-on: nine `_audio_dur` calls in `worker_render.py` measure finished clips with
|
Smaller follow-on: nine `_audio_dur` calls in `worker_render.py` measure finished clips with
|
||||||
`format=duration`. So the durations reported to the orchestrator are blind to per-clip drift.
|
`format=duration`. So the durations reported to the orchestrator are blind to per-clip drift.
|
||||||
They position no filter, so invariant 9 does not cover them. Worth converting to `_stream_dur`.
|
They position no filter, so invariant 9 does not cover them. Worth converting to `_stream_dur`.
|
||||||
2. **Fix identity, in this order.** Panel 7 is the worked example and
|
5. **Fix identity, in this order.** All of 2a-2d below are done and now proven on a GPU. Kept for the
|
||||||
`HANDOFF.md#panel-7-walked-against-the-art` carries the evidence. Do not start at the registry.
|
evidence trail.
|
||||||
|
|
||||||
a. ~~Settle the `bbox` coordinate space.~~ **Done 2026-08-12**, proven over all 113 detections and
|
a. ~~Settle the `bbox` coordinate space.~~ **Done 2026-08-12**, proven over all 113 detections and
|
||||||
checked by eye on panel 7, where five of six converted boxes land on their subject
|
checked by eye on panel 7, where five of six converted boxes land on their subject
|
||||||
(`decisions/identity-bbox.md#bbox-is-normalized`). `person_1` still frames an empty window mullion,
|
(`decisions/identity-bbox.md#bbox-is-normalized`). The rerun settles the last doubt: `person_1` now
|
||||||
which is (c).
|
frames the lead at his desk, not the window mullion the pre-fix box caught.
|
||||||
b. ~~Let identity abstain and stay abstained.~~ **Done 2026-08-12, not yet run on a GPU**
|
b. ~~Let identity abstain and stay abstained.~~ **Done 2026-08-12, run and verified on a GPU**
|
||||||
(`decisions/identity-bbox.md#none-mints-an-anonymous-character`). The resolver could always answer
|
(`decisions/identity-bbox.md#none-mints-an-anonymous-character`). The resolver could always answer
|
||||||
"none of these". The orchestrator was discarding the answer: it read only `character_id`, so a
|
"none of these". The orchestrator was discarding the answer: it read only `character_id`, so a
|
||||||
deliberate NONE and a hallucinated index both unassigned every crop of the tracklet. A NONE now
|
deliberate NONE and a hallucinated index both unassigned every crop of the tracklet. A NONE now
|
||||||
mints an anonymous character from the crop, using the embedding `/identity/resolve` ships beside
|
mints an anonymous character from the crop, using the embedding `/identity/resolve` ships beside
|
||||||
it as `emb_uri`. Deployed: image rebuilt, `resolve_outcome` verified inside the container.
|
it as `emb_uri`. Deployed: image rebuilt, `resolve_outcome` verified inside the container.
|
||||||
c. ~~Separate extra from cast.~~ **Done 2026-08-12, not yet run on a GPU**
|
c. ~~Separate extra from cast.~~ **Done 2026-08-12, run and verified on a GPU**
|
||||||
(`decisions/identity-bbox.md#face-gates-enrollment`,
|
(`decisions/identity-bbox.md#face-gates-enrollment`,
|
||||||
`decisions/identity-bbox.md#extras-gate-consumers`). `has_face` stops a faceless detection
|
`decisions/identity-bbox.md#extras-gate-consumers`). `has_face` stops a faceless detection
|
||||||
enrolling, and two more consumers now skip it: `worker_scene`'s `actions`, which is the script
|
enrolling, and two more consumers now skip it: `worker_scene`'s `actions`, which is the script
|
||||||
@@ -53,11 +68,11 @@ One number sets the agenda:
|
|||||||
`_present_characters` stays ungated on purpose, reasoned out in the decision.
|
`_present_characters` stays ungated on purpose, reasoned out in the decision.
|
||||||
The remaining gap is that vision still emits extras into the blob, which is deliberate so the
|
The remaining gap is that vision still emits extras into the blob, which is deliberate so the
|
||||||
audit can see what was gated.
|
audit can see what was gated.
|
||||||
d. **The worked examples are gone.** The registry reset deleted `Lim Seonho` and
|
d. ~~Merge and split.~~ **Safety net done and now exercised.** The rerun's `reconcile` merged 7 rows
|
||||||
`character_afa7623b`. The current registry is 8 rows, one named (`Seonho`), so there is nothing to
|
and stamped 9 assignments `merged_from:`, so the non-destructive path is proven on real data. It also
|
||||||
merge or split until the rerun mints a new set.
|
produced the chain in item 3, which is the first thing to fix in it.
|
||||||
|
|
||||||
What was done instead is the safety net for that rerun, since `reconcile` runs inside it. A merge no
|
A merge no
|
||||||
longer deletes the losing row: it sets `merged_into`, and stamps every repointed assignment with
|
longer deletes the losing row: it sets `merged_into`, and stamps every repointed assignment with
|
||||||
`method = merged_from:<loser_id>`. A wrong merge now costs a hand-written SQL walk, not a full
|
`method = merged_from:<loser_id>`. A wrong merge now costs a hand-written SQL walk, not a full
|
||||||
rebaseline (`caveats/audit-open.md#destructive-reconcile`).
|
rebaseline (`caveats/audit-open.md#destructive-reconcile`).
|
||||||
@@ -69,9 +84,9 @@ One number sets the agenda:
|
|||||||
|
|
||||||
**Cast profiles already exist. Do not rebuild them.** The user asked whether the main cast could get a
|
**Cast profiles already exist. Do not rebuild them.** The user asked whether the main cast could get a
|
||||||
profile built from reference frames and reused. `characters` already carries `ref_image_uris` and
|
profile built from reference frames and reused. `characters` already carries `ref_image_uris` and
|
||||||
`embedding_uri`, and all 53 rows have both populated. The mechanism is not missing, it is enrolled
|
`embedding_uri`, and every row has both populated (16 rows after the rerun, 53 before it). The
|
||||||
from the wrong crops, so today it stores references to balloon edges and window frames. Step (a) is
|
mechanism was never missing. It was enrolled from the wrong crops, so it stored balloon edges and
|
||||||
what makes it work. Three things are genuinely absent and are the smaller follow-on:
|
window frames. Step (a) fixed that, and the audit now reports 0 characters missing a ref crop. Three things are genuinely absent and are the smaller follow-on:
|
||||||
|
|
||||||
- no quality gate on enrollment, so nothing checks that a reference crop holds a face at all
|
- no quality gate on enrollment, so nothing checks that a reference crop holds a face at all
|
||||||
- nothing re-enrolls a reference set once it is written, so the wrong crops persist
|
- nothing re-enrolls a reference set once it is written, so the wrong crops persist
|
||||||
@@ -91,21 +106,21 @@ One number sets the agenda:
|
|||||||
The chibi at 1:35 will survive all of this. He genuinely is brown hair plus a yellow shirt, so a
|
The chibi at 1:35 will survive all of this. He genuinely is brown hair plus a yellow shirt, so a
|
||||||
profile match is correct on appearance and wrong on reality. That needs item 4 below, plus requiring
|
profile match is correct on appearance and wrong on reality. That needs item 4 below, plus requiring
|
||||||
a real face before a crop can enroll.
|
a real face before a crop can enroll.
|
||||||
3. **Stop the narration inventing facts.** 0:43, 2:03, 2:05 and 2:15 assert things no panel shows. The
|
6. **Stop the narration inventing facts.** 0:43, 2:03, 2:05 and 2:15 assert things no panel shows. The
|
||||||
correctness verifier passed 116/116 because it checks quotes and names, never invented claims.
|
correctness verifier passed 116/116 because it checks quotes and names, never invented claims.
|
||||||
4. **Teach vision that art inside a panel is not the scene.** A chibi on a monitor became "a man holding
|
7. **Teach vision that art inside a panel is not the scene.** A chibi on a monitor became "a man holding
|
||||||
a drink" at 1:35. A colleague pointing into the distance became "pointing towards the screen" at
|
a drink" at 1:35. A colleague pointing into the distance became "pointing towards the screen" at
|
||||||
1:59.
|
1:59.
|
||||||
5. **`layers` writes nothing** and reports `completed 116/116`, so no clip has parallax and a still
|
8. **`layers` writes nothing** and reports `completed 116/116`, so no clip has parallax and a still
|
||||||
holds for 28s from 2:24 (`caveats/audit-open.md#layers-writes-nothing`).
|
holds for 28s from 2:24 (`caveats/audit-open.md#layers-writes-nothing`).
|
||||||
6. **Clear the stale job error.** The completed job still carries `error: "partial: 112/116 completed"`
|
9. **Clear the stale job error.** The completed job still carries `error: "partial: 112/116 completed"`
|
||||||
(`caveats/audit-open.md#stale-job-error`).
|
(`caveats/audit-open.md#stale-job-error`).
|
||||||
7. Balloon-to-speaker geometry via the unused `det`/`seg` heads
|
10. Balloon-to-speaker geometry via the unused `det`/`seg` heads
|
||||||
(`caveats/speaker-attribution.md#tail-is-not-geometry`) is now behind item 2. With no name to attach,
|
(`caveats/speaker-attribution.md#tail-is-not-geometry`) is now behind item 5. With no name to attach,
|
||||||
geometry buys nothing.
|
geometry buys nothing.
|
||||||
8. Resolve a speaker answer across the whole dialogue window, not just the answering panel. The last 3
|
11. Resolve a speaker answer across the whole dialogue window, not just the answering panel. The last 3
|
||||||
unresolved refs describe a neighbouring panel in the same 8-panel call.
|
unresolved refs describe a neighbouring panel in the same 8-panel call.
|
||||||
9. Start Phase 2 from `ROADMAP.md`. Set SQLite `busy_timeout` before any concurrency work
|
12. Start Phase 2 from `ROADMAP.md`. Set SQLite `busy_timeout` before any concurrency work
|
||||||
(`caveats/audit-open.md#sqlite-locking`).
|
(`caveats/audit-open.md#sqlite-locking`).
|
||||||
|
|
||||||
## Lesson worth keeping
|
## Lesson worth keeping
|
||||||
|
|||||||
@@ -42,3 +42,5 @@ a complaint, so give it one or drop it.
|
|||||||
| [Identity cannot say "a person with no name"](speaker-attribution.md#no-anonymous-identity) | 2026-08-12 panel 7 |
|
| [Identity cannot say "a person with no name"](speaker-attribution.md#no-anonymous-identity) | 2026-08-12 panel 7 |
|
||||||
| [Vision does not separate a background extra from cast](speaker-attribution.md#extras-as-cast) | 2026-08-12 panel 7 |
|
| [Vision does not separate a background extra from cast](speaker-attribution.md#extras-as-cast) | 2026-08-12 panel 7 |
|
||||||
| [Cast reference profiles are enrolled from wrong crops](speaker-attribution.md#poisoned-reference-set) | 2026-08-12 panel 7 |
|
| [Cast reference profiles are enrolled from wrong crops](speaker-attribution.md#poisoned-reference-set) | 2026-08-12 panel 7 |
|
||||||
|
| [Detection can order a bbox backwards](audit-open.md#degenerate-bbox) | next vision run |
|
||||||
|
| [Identity coverage has fallen on every run since the gate landed](audit-open.md#coverage-trend) | before the next downstream run |
|
||||||
|
|||||||
@@ -181,3 +181,29 @@ Every clip in that run therefore has no parallax. The stage is a sibling of
|
|||||||
|
|
||||||
Costs: a silent quality regression that no status field reveals.
|
Costs: a silent quality regression that no status field reveals.
|
||||||
Revisit when: parallax matters for a deliverable, or before quoting this run as a full-pipeline pass.
|
Revisit when: parallax matters for a deliverable, or before quoting this run as a full-pipeline pass.
|
||||||
|
|
||||||
|
## Detection can order a bbox backwards {#degenerate-bbox}
|
||||||
|
|
||||||
|
`p007 person_1` came back as `[226, 417, 130, 551]` on the 2026-08-12 17:38 run: x1 greater than x2. One
|
||||||
|
detection in 117. `_bbox_to_pixels` clamps every coordinate into the panel but never orders the corners,
|
||||||
|
so the box survives as a zero-or-negative-width region. It crops to nothing, so that detection can never
|
||||||
|
enroll, embed or match, and it is silently lost rather than reported.
|
||||||
|
|
||||||
|
The guard is two `min`/`max` pairs in `_bbox_to_pixels`. It was not written this session because it is a
|
||||||
|
worker change and needs a vision worker restart plus a GPU cycle to prove.
|
||||||
|
|
||||||
|
Revisit trigger: the next vision run. Count degenerate boxes against 1 in 117.
|
||||||
|
|
||||||
|
## Identity coverage has fallen on every run since the gate landed {#coverage-trend}
|
||||||
|
|
||||||
|
70% -> 61% -> 50% across the 13:11 baseline, the 16:39 run and the 17:38 run. The `has_face` gate explains
|
||||||
|
part of it and is stable, gating 39% of detections on both post-change runs. Against face-bearing
|
||||||
|
detections only, the 17:38 run assigned 59 of 72, or 82%.
|
||||||
|
|
||||||
|
Nothing yet separates correct abstention from lost cast, and both fixes that could have caused the second
|
||||||
|
drop landed together. A back-turned lead is a correct abstention. A real character the resolver refused is
|
||||||
|
not, and the two are indistinguishable in the coverage number alone.
|
||||||
|
|
||||||
|
Revisit trigger: before trusting the registry for a downstream run. `identity_labels` already exists for
|
||||||
|
exactly this and holds 145 rows of human ground truth, so `eval_identity.py` can score precision against
|
||||||
|
recall instead of counting assignments.
|
||||||
|
|||||||
@@ -45,3 +45,5 @@ still live belongs in `caveats/`.
|
|||||||
| [A detection with no detected face never enrolls or binds](identity-bbox.md#face-gates-enrollment) | closed |
|
| [A detection with no detected face never enrolls or binds](identity-bbox.md#face-gates-enrollment) | closed |
|
||||||
| [A resolver NONE mints an anonymous character, it does not clear the crop](identity-bbox.md#none-mints-an-anonymous-character) | closed |
|
| [A resolver NONE mints an anonymous character, it does not clear the crop](identity-bbox.md#none-mints-an-anonymous-character) | closed |
|
||||||
| [The extras gate runs at enrollment and at narration, not at the speaker prompt](identity-bbox.md#extras-gate-consumers) | closed |
|
| [The extras gate runs at enrollment and at narration, not at the speaker prompt](identity-bbox.md#extras-gate-consumers) | closed |
|
||||||
|
| [A roster name is a guess, so it never reaches detection](identity-bbox.md#roster-does-not-name) | closed |
|
||||||
|
| [`merged_into` is exactly one hop deep](identity-bbox.md#merge-chains-flatten) | closed |
|
||||||
|
|||||||
@@ -183,3 +183,41 @@ previous behaviour.
|
|||||||
|
|
||||||
Forbids: adding a fourth consumer of `vision["characters"]` without deciding which side of this line it is
|
Forbids: adding a fourth consumer of `vision["characters"]` without deciding which side of this line it is
|
||||||
on. The blob keeps every detection on purpose, so the audit can still see what was gated.
|
on. The blob keeps every detection on purpose, so the audit can still see what was gated.
|
||||||
|
|
||||||
|
## A roster name is a guess, so it never reaches detection {#roster-does-not-name}
|
||||||
|
|
||||||
|
`service.py` seeded `known_characters` with `_roster_char_hints` before every detect call, so the chapter
|
||||||
|
roster's names were in front of gemma before any panel had identified anyone. On the 2026-08-12 16:39 run
|
||||||
|
that put "Seonho (short brown hair, yellow plaid shirt)" on a different man wearing glasses, who then held
|
||||||
|
15 assignments under the lead's name. The real lead was minted separately from the p010 caption as
|
||||||
|
"Lim Seonho", and p020 held both as two people in one panel.
|
||||||
|
|
||||||
|
The seeding is removed. A name now reaches detection only from a registry row, and a registry row is named
|
||||||
|
from an in-panel caption or address through `name_claims`. The distinction is evidence: a roster name is a
|
||||||
|
claim about art nobody has looked at yet, and a registry row carries an embedding plus the panel that
|
||||||
|
named it.
|
||||||
|
|
||||||
|
The roster still feeds `roster_cast` in `run_stage_dialogue`. There the names are matched against speech,
|
||||||
|
not against faces, so a wrong guess costs an unresolved speaker rather than a wrong face binding.
|
||||||
|
|
||||||
|
Confirmed on the 17:38 run: "Lim Seonho" came back as one row with 25 assignments, so dropping the hint
|
||||||
|
did not split the lead across the panels before his caption. "Seonho" fell to 1 assignment.
|
||||||
|
|
||||||
|
Not covered: detection still reads a name off the panel and can attach it to the wrong body. p011 and p026
|
||||||
|
emit `name: "Seonho"` on `person_2` with no roster hint present. That is a separate mechanism and it is
|
||||||
|
what keeps the glasses man named at all.
|
||||||
|
|
||||||
|
## `merged_into` is exactly one hop deep {#merge-chains-flatten}
|
||||||
|
|
||||||
|
A merge chain means `merged_into` points at a row that is itself merged, so anything resolving one hop
|
||||||
|
lands on a retired character. Roster readers filter `merged_into IS NULL` and were never affected.
|
||||||
|
|
||||||
|
Two rules, one per direction, and both are needed:
|
||||||
|
|
||||||
|
- `merge_characters` resolves the keeper to its chain root before merging. Bounded 64-step walk, the cap
|
||||||
|
only exists so a cycle cannot hang reconcile.
|
||||||
|
- retiring a loser repoints every row whose `merged_into` was that loser.
|
||||||
|
|
||||||
|
The keeper walk alone is not enough, which the 17:38 run proved by producing
|
||||||
|
`477c1894 -> a92d9df4 -> 4fb94c15` with the walk deployed. At merge time that pair was fine. The chain
|
||||||
|
formed later, when a row that was already somebody's keeper was itself retired.
|
||||||
|
|||||||
Reference in New Issue
Block a user