Files
manga-recap-pipeline/HANDOFF.md
T
kami 18b49c43bd 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>
2026-08-12 21:51:36 +04:00

8.6 KiB

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 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.

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.

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

Vision is non-deterministic, so detection counts move between runs. Each cycle is ~6 minutes: vision ~3m50s, identity ~1m25s, reconcile ~50s.

First, a correction the session started with

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.

Run A: the four staged changes, verified

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:

Panel 7, the worked example:

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

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.

person_1's box now frames the lead. The old "empty window mullion" note was written against pre-fix boxes and is retired.

Run A's new defect: a roster hint named the wrong man

Verified against the art, three real people:

  • 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.

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

/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

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.

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.

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.