Order bbox corners, and find the ground truth is for another manga

`_bbox_to_pixels` sorts each coordinate pair after clamping. Run B returned
`p007 person_1` as `[226, 417, 130, 551]`, x1 > x2, which cropped to nothing
and lost that detection silently. The third GPU cycle came back with 0
degenerate boxes over 119 detections.

The plan's first item cannot run as written. All 145 rows of `identity_labels`
belong to chapter 8ca8249b, a different manga. Chapter 7c944dd4 has none, so
`/review/identity` reports `accuracy: null`. Scoring 8ca8249b gives 7/138 on
an identity run that predates every fix.

The cycle also settled the coverage question. All 68 assignments landed on
face-bearing detections and none on a gated one, so recall among face-bearing
detections is 96%, up from 82%. Coverage is the `has_face` gate and nothing
else. What is now open is precision: the lead holds 36 of 68 assignments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 22:21:52 +04:00
parent 18b49c43bd
commit 54bd126cd7
8 changed files with 211 additions and 180 deletions
+55 -127
View File
@@ -1,160 +1,88 @@
# HANDOFF, 2026-08-12 (fifth session)
# HANDOFF, 2026-08-12 (sixth session)
Live state is in `NEXT.md`. This file is only what this session did. The previous handoff is in
`JOURNAL.md`.
## Asked
"go ahead" on the GPU cycle the fourth session left staged. Then "what do we do now". Then go ahead on
the fixes its evidence asked for, and run it again.
The two files, then "go" on the third GPU cycle.
## Result
Two full GPU cycles ran. All four of the fourth session's identity changes are now proven on real panels.
Four more fixes were written on top, all orchestrator-side, all deployed. Coverage fell twice and that is
the open question.
One worker fix, one measurement that killed the plan's first item, one GPU cycle. The cycle proved the new
fix and the fifth session's merge fix. Coverage rose for the first time, 50% to 57%. It is now explained
entirely by the `has_face` gate.
| cycle | detections | assignments | coverage | registry | named |
| --- | --- | --- | --- | --- | --- |
| baseline (13:11, pre-change) | 110 | 77 | 70% | 8 | 1 |
| run A (16:39-16:45) | 110 | 67 | 61% | 16 | 2 |
| run B (17:38-17:44) | 117 | 59 | 50% | 20 | 2 |
| metric | 17:38 run | 18:07 run |
| --- | --- | --- |
| detections | 117 | 119 |
| assignments | 59 | 68 |
| coverage | 50% | 57% |
| degenerate boxes | 1 | 0 |
| assigned among face-bearing | 59/72 = 82% | 68/71 = 96% |
| assigned among gated | -- | 0 |
| chains deeper than one hop | 1 | 0 |
| `merged_from` stamps | 9 | 22 |
Vision is non-deterministic, so detection counts move between runs. Each cycle is ~6 minutes:
vision ~3m50s, identity ~1m25s, reconcile ~50s.
## The corner-ordering fix
## First, a correction the session started with
`_bbox_to_pixels` sorts each coordinate pair after clamping. Three lines
(`decisions/identity-bbox.md#bbox-corners-ordered`). The self-check feeds it run B's real
`[226, 417, 130, 551]` and asserts `[117, 688, 203, 909]`. Unsorted the call returns `[203, 688, 117, 909]`,
so the assert fails without the fix.
The vision/identity/reconcile timestamps in `/job/status` are UTC. The git log is local, UTC+4. The run
that looked like a completed rerun was the pre-change baseline: it finished 13:17 local, and
`_mark_has_face` was not committed until 19:07. The fourth session's handoff was right that nothing had
run.
The run returned 0 degenerate boxes over 119 detections. The caveat is deleted and the decision is indexed.
## Run A: the four staged changes, verified
## The measurement that killed item 1
| change | verdict |
| --- | --- |
| `has_face` stamp | on all 110 detections, 67 true / 43 gated (39%) |
| extras gate | panel 7's two wrong bindings gone |
| NONE mints | 16 characters where 8 existed |
| non-destructive merge | 7 rows carry `merged_into`, 9 assignments stamped `merged_from:` |
The plan said to score precision with `eval_identity.py` against the 145 rows of `identity_labels`. Every
one of those rows belongs to chapter `8ca8249b`, a different manga with cast "Rico" and "Ikekin", spread
over 81 panels. Chapter `7c944dd4` has zero labels, so
`/review/identity?job_id=778297bc...` returns `labeled: 0, correct: 0, accuracy: null`.
Panel 7, the worked example:
Scoring `8ca8249b` anyway gives 7/138, with 113 rows labelled as a real person and left unassigned. That
chapter's identity run is stale: 44 assignments over 246 panels, predating every fix. The number measures
old code on the wrong chapter.
```
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
```
So the eval path is proven end to end and the ground truth is absent. A precision number needs a hand pass
over `7c944dd4` through `POST /review/identity/label`, keyed by bbox rather than `local_id`.
`person_1` going unassigned is correct, and this was measured, not assumed. Ran `face_detect.detect_faces`
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.
## What the cycle settled
`person_1`'s box now frames the lead. The old "empty window mullion" note was written against pre-fix
boxes and is retired.
**Coverage is the gate and nothing else.** All 68 assignments landed on face-bearing detections and none on
a gated one. Recall among face-bearing detections is 96%, up from 82%. The resolver is not losing cast, so
the suspicion in `caveats/audit-open.md#coverage-trend` is closed.
## Run A's new defect: a roster hint named the wrong man
**The merge-chain fix holds under load.** 9 merges this run against 7 last run, and 22 `merged_from`
stamps. No chain is deeper than one hop.
Verified against the art, three real people:
## What the cycle opened
- `character_92129ac7` "Lim Seonho", 22 assignments. The p010 caption reads `LIM SEONHO (29)`, yellow
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.
**The lead may be absorbing.** 36 of 68 assignments, 53%, against 25 of 59 before. `audit_registry.py`
flags it. A protagonist in half the panels looks identical to an over-merge without labels.
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.
**Panel 7 got worse.** 5 of its 6 detections carry `has_face = False`, and `person_6`, the one that does,
went unassigned. The previous run bound its colleague. That is the cost of requiring a face, `NEXT.md`
item 4.
## 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.
- Nothing downstream re-ran. The job is still parked at `dialogue waiting` with every stage below cleared.
- Nothing is committed. All edits are in the working tree.
## Checks
```bash
/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
./check_stale.sh # exit 0, all 9 workers current
.venv/bin/python worker_vision.py # self-check ok, including the swapped-corner assert
./check_stale.sh # exit 0 before the run and after it
```
Both new merge tests were confirmed to fail with their fix disabled, then the fix was restored and the
suite re-run. The deployed container was verified by parsing its source, not grepping it. The first
attempt gave a false negative, matching `_roster_char_hints` inside the comment that explains its removal.
## Traps confirmed
## Next command
The third cycle, after adding the corner-ordering guard to `_bbox_to_pixels` and restarting the vision
worker. Watch coverage against 50% and the degenerate box count against 1.
```bash
cd /home/kami/Programs/n8n-worker && ./check_stale.sh # must exit 0
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/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
/usr/bin/ssh kami@192.168.1.104 "docker exec manga-orchestrator python3 /app/audit_registry.py 7c944dd4-e972-42c7-ba60-9f6939548e80 6"
```
## Traps confirmed or found
- **`kill $(pgrep -f "worker_identity:app")` kills the shell running it.** The pattern matches the calling
command line. It killed this session's own script mid-way, so the `send-keys` relaunch never fired and
the identity worker sat dead. Restart it with the two `tmux send-keys` lines only, then poll `/health`.
- **Verify deployed code by parsing it, not by grepping it.** A comment explaining a removal contains the
name of the thing removed.
- **A test that cannot fail proves nothing.** Both merge tests were run with their fix disabled first.
- 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.
- Restart a worker with `tmux send-keys -t manga-workers:<window> C-c`, then re-send the launch line from
`start_workers.sh` with the `MIOPEN_ENV` prefix. `pgrep`-based kills match the calling shell.
- `POST /characters/reset` returns `restart_identity_worker: true`. Honour it, the worker caches the
registry.
- `identity_assignments` has no `method` column. The `merged_from:` stamps live in
`identity_assignment_sources`.
- `chapters` has no `title` column.