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:
+55
-127
@@ -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.
|
||||
|
||||
+58
@@ -586,3 +586,61 @@ 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.
|
||||
|
||||
## 2026-08-12, sixth session — the ground truth is for the wrong manga
|
||||
|
||||
No GPU work. Two things, both cheap.
|
||||
|
||||
**`_bbox_to_pixels` orders its corners.** Each coordinate pair is sorted after clamping. 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. `worker_vision.py` self-check passes.
|
||||
The vision worker is now STALE and needs a restart before the next cycle.
|
||||
|
||||
**Item 1 of the plan cannot run as written.** All 145 rows of `identity_labels` belong to chapter
|
||||
`8ca8249b`, a different manga with cast "Rico" and "Ikekin", spread over 81 panels. Chapter `7c944dd4`
|
||||
has none, so `/review/identity?job_id=778297bc...` returns `labeled: 0, correct: 0, accuracy: null`.
|
||||
|
||||
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.
|
||||
|
||||
So the eval path is proven end to end and the ground truth is absent. A precision number for the current
|
||||
registry needs a hand-labelling pass over `7c944dd4` through `POST /review/identity/label`, keyed by bbox
|
||||
rather than by `local_id`, because vision reassigns `local_id` every run.
|
||||
|
||||
## 2026-08-12 18:07-18:13 UTC, third GPU cycle
|
||||
|
||||
Restarted vision (window 3) and identity (`/characters/reset` returned `restart_identity_worker: true`),
|
||||
both by `send-keys C-c` then re-sending the launch line, never by `pgrep`. `check_stale.sh` exit 0 before
|
||||
the run. Reset dropped 20 characters, 59 assignments, 69 sources, 168 S3 objects.
|
||||
|
||||
vision 116/116 in 3m59s, identity 116/116 in 1m24s, reconcile 18/18 in 50s.
|
||||
|
||||
| metric | 17:38 run | 18:07 run |
|
||||
| --- | --- | --- |
|
||||
| detections | 117 | 119 |
|
||||
| assignments | 59 | 68 |
|
||||
| coverage | 50% | 57% |
|
||||
| degenerate boxes | 1 | **0** |
|
||||
| `has_face` true / gated | 72 / 45 | 71 / 48 |
|
||||
| assigned among face-bearing | 59/72 = 82% | **68/71 = 96%** |
|
||||
| assigned among gated | -- | 0 |
|
||||
| registry rows / merged | 20 / 7 | 18 / 9 |
|
||||
| `merged_from` stamps | 9 | 22 |
|
||||
| chains deeper than one hop | 1 | **0** |
|
||||
|
||||
The corner-ordering fix works: 0 degenerate boxes. The merge-chain fix holds under a heavier merge load,
|
||||
9 merges and no chain.
|
||||
|
||||
**Coverage is now the gate and nothing else.** Every assignment landed on a face-bearing detection and none
|
||||
on a gated one. Recall among face-bearing detections is 96%. So the 43% with no assignment is the 40% the
|
||||
gate drops plus 3 detections, and the resolver is not losing cast. That closes the part of
|
||||
`#coverage-trend` that suspected the resolver.
|
||||
|
||||
**The open question moved to precision.** The lead holds 36 of 68 assignments, 53%, against 25 of 59
|
||||
before, and `audit_registry.py` flags it as absorbing. The merge count rose from 7 to 9 over the same
|
||||
interval. Whether 36 is a protagonist in half the panels or an over-merge cannot be told apart without
|
||||
labels, which is item 1.
|
||||
|
||||
Panel 7 got worse, not better: 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, item 4.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NEXT
|
||||
|
||||
Updated 2026-08-12 (fifth session). What the fourth session did is in `HANDOFF.md`, the run is in
|
||||
Updated 2026-08-12 (sixth session). What the fifth session did is in `HANDOFF.md`, the runs are in
|
||||
`JOURNAL.md`.
|
||||
|
||||
## State
|
||||
@@ -9,38 +9,50 @@ The chapter runs end to end. The A/V sync defect is fixed and `chapter.mp4` is r
|
||||
against audio 364.122s at `25/1`.
|
||||
|
||||
Job `778297bc-e7ce-439d-91b5-8a027060d17f`, chapter `7c944dd4-e972-42c7-ba60-9f6939548e80`, 116 panels.
|
||||
Two GPU cycles ran on 2026-08-12, the last 17:38-17:44 UTC on a reset registry. The job is `running` and
|
||||
parked at `dialogue waiting`. Everything from `dialogue` down is cleared and stale.
|
||||
Three GPU cycles ran on 2026-08-12, the last 18:07-18:13 UTC on a reset registry. The job is `running` and
|
||||
parked at `dialogue waiting`. Everything from `dialogue` down is cleared and stale. All 9 workers are
|
||||
current.
|
||||
|
||||
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:
|
||||
Eight changes are now proven on real panels. The fourth session contributed four. The fifth added the
|
||||
roster-hint removal, merge-chain flattening and `name_claims` reset. This session added bbox corner
|
||||
ordering. The registry after the 18:07 run holds 18 rows, 9 live and 9 carrying `merged_into`:
|
||||
|
||||
- `character_713ea2ce` "LIM SEONHO", 25 assignments, the lead, one row, named from the p010 caption.
|
||||
- `character_4fb94c15`, 18 assignments, the woman, correct and unnamed.
|
||||
- `character_023ba5a3` "Seonho", 1 assignment, still the wrong man, down from 15.
|
||||
- `LIM SEONHO`, 36 assignments, the lead, one row, named from the p010 caption.
|
||||
- `character_b112d4`, 23 assignments, the woman, correct and unnamed.
|
||||
- `Seonho`, still the wrong man, still named off in-panel text.
|
||||
|
||||
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`.
|
||||
Coverage is 57%, from 50%, 61% and 70% before it, and it is now the `has_face` gate and nothing else. All 68
|
||||
assignments landed on face-bearing detections and none on a gated one. Recall among face-bearing detections
|
||||
is 68 of 71, or 96%, up from 82%. Degenerate boxes went from 1 in 117 to 0 in 119, and the merge
|
||||
chain is flat with 22 assignments stamped `merged_from`.
|
||||
|
||||
The open question moved. The lead holds 36 of 68 assignments, 53%, which `audit_registry.py` flags as
|
||||
absorbing. Nothing separates a protagonist in half the panels from an over-merge
|
||||
(`caveats/audit-open.md#coverage-trend`).
|
||||
|
||||
## Next
|
||||
|
||||
1. **Settle coverage before trusting the registry.** 70% -> 61% -> 50% over three runs. `has_face` gates a
|
||||
steady 39%, and against face-bearing detections alone the last run assigned 59 of 72 (82%). Nothing
|
||||
separates correct abstention from lost cast. `identity_labels` already holds 145 rows of human ground
|
||||
truth, and `eval_identity.py` already scores against it. Measure precision and recall instead of
|
||||
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,
|
||||
1 in 117. It crops to nothing, so that detection is silently lost
|
||||
(`caveats/audit-open.md#degenerate-bbox`). Two `min`/`max` pairs. It is a worker change, so it needs a
|
||||
vision restart and a GPU cycle to prove.
|
||||
1. **Decide whether the lead absorbing 53% of assignments is real.** It needs labels this chapter lacks.
|
||||
The resolver is cleared: recall among face-bearing detections is 96%. What is unmeasured is precision.
|
||||
The lead went from 25 of 59 to 36 of 68 as the merge count rose from 7 to 9.
|
||||
|
||||
The previous plan pointed at `identity_labels` and its 145 rows. Measured this session: every one of
|
||||
those rows belongs to chapter `8ca8249b`, a different manga. Chapter `7c944dd4` has zero labels, so
|
||||
`/review/identity` reports `labeled: 0, accuracy: null`. Scoring `8ca8249b` gives 7/138 on an identity
|
||||
run that predates all eight fixes. The eval plumbing works and the ground truth is missing.
|
||||
Labelling `7c944dd4` by hand is the only path to a precision number
|
||||
(`caveats/audit-open.md#coverage-trend`).
|
||||
2. ~~**Order the corners in `_bbox_to_pixels`.**~~ **Done 2026-08-12, run and verified on a GPU.** 0
|
||||
degenerate boxes over 119 detections (`decisions/identity-bbox.md#bbox-corners-ordered`).
|
||||
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
|
||||
4. **Decide what a back-turned character costs.** This is now the whole of the coverage number. `has_face`
|
||||
gates 40% of detections. On the 18:07 run panel 7 lost 5 of its 6 detections to the gate, and its one
|
||||
face-bearing detection went unassigned. 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.
|
||||
|
||||
@@ -68,9 +80,9 @@ Coverage is 50%, from 61% and 70% before it. That trend is the open question and
|
||||
`_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
|
||||
audit can see what was gated.
|
||||
d. ~~Merge and split.~~ **Safety net done and now exercised.** The rerun's `reconcile` merged 7 rows
|
||||
and stamped 9 assignments `merged_from:`, so the non-destructive path is proven on real data. It also
|
||||
produced the chain in item 3, which is the first thing to fix in it.
|
||||
d. ~~Merge and split.~~ **Safety net done and now exercised.** The 18:07 run merged 9 rows and stamped
|
||||
22 assignments `merged_from:`, with no chain deeper than one hop. The non-destructive path and the
|
||||
flattening fix are both proven on real data.
|
||||
|
||||
A merge no
|
||||
longer deletes the losing row: it sets `merged_into`, and stamps every repointed assignment with
|
||||
|
||||
@@ -42,5 +42,4 @@ 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 |
|
||||
| [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 |
|
||||
| [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 |
|
||||
|
||||
+26
-21
@@ -182,28 +182,33 @@ 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.
|
||||
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%.
|
||||
70% -> 61% -> 50% -> 57% across the four 2026-08-12 runs. The gate is stable at 39 to 40% of detections.
|
||||
|
||||
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.
|
||||
**The 18:07 run resolves most of this.** Coverage is now the gate and nothing else. All 68 assignments went
|
||||
to face-bearing detections and none to a gated one. Recall among face-bearing detections is 68 of 71, or
|
||||
96%, up from 82%. The resolver is not losing cast. The 43% that carries no assignment is the 40% the gate
|
||||
drops plus 3 detections.
|
||||
|
||||
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.
|
||||
So the coverage number no longer measures the resolver. What it measures is the cost of requiring a face,
|
||||
which is item 4 in `NEXT.md`. Panel 7 is the case: 5 of its 6 detections carry `has_face = False`, and the
|
||||
one that does is unassigned.
|
||||
|
||||
The open question moved. The lead now holds 36 of 68 assignments, or 53%, against 25 of 59 before, and
|
||||
`audit_registry.py` flags that as absorbing. Whether 36 is the protagonist appearing in half the panels or
|
||||
an over-merge cannot be told apart without labels.
|
||||
|
||||
Revisit trigger: before trusting the registry for a downstream run.
|
||||
|
||||
**The 145 ground-truth labels do not cover this chapter.** Measured 2026-08-12. All 145 rows in
|
||||
`identity_labels` key to chapter `8ca8249b`, a different manga with cast "Rico" and "Ikekin", over 81
|
||||
distinct panels. Chapter `7c944dd4` has zero labels, so `/review/identity?job_id=778297bc...` returns
|
||||
`labeled: 0, accuracy: null`. Scoring 8ca instead gives 7/138, because its identity run is stale: 44
|
||||
assignments over 246 panels, predating all seven fixes. That number measures the old code on the wrong
|
||||
chapter and settles nothing.
|
||||
|
||||
The plumbing works, the labels are missing. Precision and recall on the current registry need a hand
|
||||
pass over `7c944dd4` first, via `POST /review/identity/label` per detection. Label against the bbox, not
|
||||
the `local_id`. Vision is non-deterministic and reassigns `local_id` top-to-bottom every run. A label
|
||||
taken before a vision rerun then points at whoever now occupies that slot.
|
||||
|
||||
@@ -47,3 +47,4 @@ still live belongs in `caveats/`.
|
||||
| [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 |
|
||||
| [`_bbox_to_pixels` orders the corners, because the model sometimes swaps them](identity-bbox.md#bbox-corners-ordered) | closed |
|
||||
|
||||
@@ -221,3 +221,25 @@ Two rules, one per direction, and both are needed:
|
||||
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.
|
||||
|
||||
## `_bbox_to_pixels` orders the corners, because the model sometimes swaps them {#bbox-corners-ordered}
|
||||
|
||||
**Closed, 2026-08-12.**
|
||||
|
||||
The 17:38 run returned `p007 person_1` as `[226, 417, 130, 551]`, x1 greater than x2. One detection in 117.
|
||||
Clamping each coordinate into the panel kept the swap, so the box stayed a negative-width region. It
|
||||
cropped to nothing, so that detection could not enroll, embed or match, and nothing reported the loss.
|
||||
|
||||
`_bbox_to_pixels` now sorts each pair after clamping:
|
||||
|
||||
```python
|
||||
xs = sorted((clamped_x1, clamped_x2))
|
||||
ys = sorted((clamped_y1, clamped_y2))
|
||||
c["bbox"] = [xs[0], ys[0], xs[1], ys[1]]
|
||||
```
|
||||
|
||||
Sorting is enough. A zero-area box still crops to nothing, and no consumer needs a minimum size that it
|
||||
does not already enforce. The self-check feeds the real swapped box in and asserts `[117, 688, 203, 909]`,
|
||||
which fails without the sort.
|
||||
|
||||
Proven on the 18:07 run: 0 degenerate boxes over 119 detections.
|
||||
|
||||
+11
-5
@@ -323,10 +323,13 @@ def _bbox_to_pixels(chars: list, w: int, h: int) -> list:
|
||||
b = c.get("bbox")
|
||||
if not (isinstance(b, list) and len(b) == 4 and all(isinstance(v, (int, float)) for v in b)):
|
||||
continue
|
||||
c["bbox"] = [min(w, max(0, round(b[0] * w / BBOX_GRID))),
|
||||
min(h, max(0, round(b[1] * h / BBOX_GRID))),
|
||||
min(w, max(0, round(b[2] * w / BBOX_GRID))),
|
||||
min(h, max(0, round(b[3] * h / BBOX_GRID)))]
|
||||
xs = sorted((min(w, max(0, round(b[0] * w / BBOX_GRID))),
|
||||
min(w, max(0, round(b[2] * w / BBOX_GRID)))))
|
||||
ys = sorted((min(h, max(0, round(b[1] * h / BBOX_GRID))),
|
||||
min(h, max(0, round(b[3] * h / BBOX_GRID)))))
|
||||
# ponytail: the model swapped corners on 1 of 117 boxes (p007 person_1, x1 > x2).
|
||||
# Ordering here is enough because a zero-area box still crops to nothing downstream.
|
||||
c["bbox"] = [xs[0], ys[0], xs[1], ys[1]]
|
||||
return chars
|
||||
|
||||
|
||||
@@ -1216,8 +1219,11 @@ if __name__ == "__main__":
|
||||
# the top sixth of the panel, inside a speech balloon, which is what identity embedded.
|
||||
_ch = [{"local_id": "person_5", "bbox": [222, 405, 654, 1000]},
|
||||
{"local_id": "edge", "bbox": [0, 0, 1000, 1000]},
|
||||
{"local_id": "junk", "bbox": "nope"}]
|
||||
{"local_id": "junk", "bbox": "nope"},
|
||||
# p007 person_1 of run B: x1 > x2, so the crop came out empty and the detection was lost
|
||||
{"local_id": "swapped", "bbox": [226, 417, 130, 551]}]
|
||||
_bbox_to_pixels(_ch, 900, 1650)
|
||||
assert _ch[3]["bbox"] == [117, 688, 203, 909], _ch[3]["bbox"] # corners ordered, non-empty crop
|
||||
assert _ch[0]["bbox"] == [200, 668, 589, 1650], _ch[0]["bbox"]
|
||||
assert _ch[1]["bbox"] == [0, 0, 900, 1650], _ch[1]["bbox"] # a clamped box spans the whole panel
|
||||
assert _ch[2]["bbox"] == "nope", _ch[2]["bbox"] # unparseable is left alone, not crashed
|
||||
|
||||
Reference in New Issue
Block a user