1457556ce3
_xfade_chain positioned every transition using _audio_dur, which probes format=duration, which is max(video, audio). A clip's audio outlasts its video by about a frame, so the offset accumulator crept ahead of the real picture timeline. Once the creep exceeded the transition width, xfade emitted the transition and silently discarded the second input and every clip downstream, exiting 0 with nothing on stderr. That is the whole of the shipped chapter's 436.39s of video over 363.67s of audio. Offsets now come from min(video, audio). Every input is floored to a whole frame count and trimmed on both streams, so the accumulator tracks the real timeline instead of estimating it. _check_assembled verifies each encode against the predicted length and against its own audio, because both assembly branches drop stream time without failing. Verified over the 49 real clips of chapter 7c944dd4: the round that turned 359s of video into 100s now loses 0.85s, and the chapter comes out 358.76s video against 358.76s audio. The single-item passthrough was not the cause. Two round-0 groups of 8 fresh clips collapse without one, recorded void in decisions/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
9.9 KiB
Markdown
156 lines
9.9 KiB
Markdown
# NEXT
|
|
|
|
Updated 2026-08-12. What this session did is in `HANDOFF.md`.
|
|
|
|
## State
|
|
|
|
The chapter runs end to end and the output is **not watchable**. That is now measured, not guessed.
|
|
Assembly is fixed and verified offline. The shipped `chapter.mp4` has not been rebuilt yet.
|
|
|
|
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
|
|
`chapter.mp4`. The user watched it and read out 19 defects. They are grouped by cause in `HANDOFF.md`.
|
|
|
|
Two numbers set the agenda:
|
|
|
|
- The shipped `chapter.mp4` is video 436.39s over audio 363.67s. Cause found and fixed, see below.
|
|
- Panel 7 checked against the art has **zero correct identity bindings** out of two, and the one
|
|
character who matters is unbound. `HANDOFF.md#panel-7-walked-against-the-art` has the table.
|
|
|
|
## Next
|
|
|
|
1. **Rebuild `chapter.mp4`.** Assembly is fixed in `worker_render.py`. Verified over the 49 real clips of
|
|
this chapter: video 358.76s against audio 358.76s, agreeing to the frame. The cause was `_xfade_chain`
|
|
taking offsets from `format=duration`, which is `max(video, audio)`. The accumulator drifted past the
|
|
end of its input, and ffmpeg silently discarded whole clips at `rc 0`
|
|
(`decisions/chapter-assembly.md#offsets-from-min-stream`). The single-item passthrough was innocent
|
|
and the one-path rewrite is not needed (`decisions/chapter-assembly.md#passthrough-innocent`).
|
|
|
|
What is left is to clear the `assemble` stage and resume, then watch the result. That is CPU-only
|
|
ffmpeg, no GPU, but it needs the user's go-ahead.
|
|
|
|
Smaller follow-on: nine other `_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.
|
|
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
|
|
`HANDOFF.md#panel-7-walked-against-the-art` carries the evidence. Do not start at the registry.
|
|
|
|
a. **Settle the `bbox` coordinate space.** Consumed as pixels, all six boxes on panel 7 land in the
|
|
top third of the panel, two inside a speech balloon. Divided by 1000 they mostly land on their
|
|
subjects. `worker_vision.py:271` and `worker_identity.py:91` both assert pixels, and the art says
|
|
otherwise. Identity embeds `_crop_bbox(img, ch["bbox"])` at `worker_identity.py:200`, so today it
|
|
matches faces against crops of balloons and window frames. Nothing downstream can be judged until
|
|
this is right. Rescaling alone is **not** the fix: after scaling, one box still sits on an empty
|
|
window frame and another clips its subject.
|
|
b. **Let identity abstain and stay abstained.** The colleague has no name in the story and was
|
|
labelled `Choi Haeseon` at 0.9. An unnamed recurring person needs a stable anonymous identity so
|
|
narration says "the colleague" every time. `match()` already returns `None` below threshold. Check
|
|
whether the Tier-2 gemma resolver can answer "none of these"; that was not verified.
|
|
c. **Separate extra from cast.** Four of the six detections on panel 7 are background extras or
|
|
nothing at all, and all six reach identity as equal candidates.
|
|
d. Only then merge `Seonho` into `Lim Seonho` and split `character_afa7623b`, which still needs the
|
|
reversible-merge design (`caveats/audit-open.md#destructive-reconcile`), not a patch.
|
|
|
|
**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
|
|
`embedding_uri`, and all 53 rows have both populated. The mechanism is not missing, it is enrolled
|
|
from the wrong crops, so today it stores references to balloon edges and window frames. Step (a) is
|
|
what makes it work. 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
|
|
- nothing re-enrolls a reference set once it is written, so the wrong crops persist
|
|
|
|
**The visual "is this them?" check is already built. Do not write it again.** `/vision/resolve` at
|
|
`worker_vision.py:963` sends the query crop plus up to 3 labelled reference images per candidate.
|
|
`build_resolve_prompt` tells the model to judge face shape first, to treat hair and outfit as
|
|
secondary, that two people sharing a hair colour are not the same, and to answer `0` for NONE when
|
|
unsure. `choice: 0` becomes a new character, an out-of-range index becomes `unresolved`, and
|
|
`ref_image_uris` is republished as `reference_image_uris` at `worker_identity.py:152` and `:161`. The
|
|
mechanism, the prompt and the abstain path are all correct. They are fed crops of the wrong region,
|
|
which is step (a).
|
|
- no human gate to name, merge or split the clusters. The user wants this as a minor adjustment on
|
|
top, not as the mechanism. The `gates` table and the review gates from [#136] are the place to hang
|
|
it
|
|
|
|
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
|
|
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
|
|
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
|
|
a drink" at 1:35. A colleague pointing into the distance became "pointing towards the screen" at
|
|
1:59.
|
|
5. **`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`).
|
|
6. **Clear the stale job error.** The completed job still carries `error: "partial: 112/116 completed"`
|
|
(`caveats/audit-open.md#stale-job-error`).
|
|
7. 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,
|
|
geometry buys nothing.
|
|
8. 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.
|
|
9. Start Phase 2 from `ROADMAP.md`. Set SQLite `busy_timeout` before any concurrency work
|
|
(`caveats/audit-open.md#sqlite-locking`).
|
|
|
|
## Lesson worth keeping
|
|
|
|
Every metric recorded before this session said the pipeline was fine or nearly fine. `script` 116/116,
|
|
"9 named speech lines", `layers` 116/116, `assemble` 1/1. Watching two and a half minutes of output
|
|
found a 20% sync failure, a cast that is 84% anonymous, invented narration, and a stage that writes
|
|
nothing while reporting success. Stage counters measure whether code ran. They say nothing about whether
|
|
the result is correct. Watch the output before trusting a number.
|
|
|
|
## Running the pieces
|
|
|
|
```bash
|
|
./start_workers.sh # session_manager + 9 workers, each a uvicorn in a tmux window
|
|
tmux attach -t manga-workers # per-worker logs
|
|
.venv/bin/python worker_render.py # self-check, runs real ffmpeg, about 4 minutes
|
|
```
|
|
|
|
Read the state, or clear a stage and resume:
|
|
|
|
```bash
|
|
/usr/bin/ssh kami@192.168.1.104 "curl -s 'http://127.0.0.1:9090/job/status?job_id=778297bc-e7ce-439d-91b5-8a027060d17f'"
|
|
/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\":\"778297bc-e7ce-439d-91b5-8a027060d17f\",\"stage\":\"<stage>\"}'"
|
|
/usr/bin/ssh kami@192.168.1.104 "curl -s -X POST 'http://127.0.0.1:9090/job/resume?job_id=778297bc-e7ce-439d-91b5-8a027060d17f'"
|
|
```
|
|
|
|
Traps: plain `ssh` is the kitty ssh kitten and refuses non-interactive stdin, so use `/usr/bin/ssh`.
|
|
`mc` aliases on homesrv are `homesrv` and `mio`. `local` returns Access Denied and `rfs` is the empty
|
|
rustfs. `cp` is aliased to `cp -i` and hangs on overwrite, so use `/usr/bin/cp -f`.
|
|
|
|
Re-fixing assembly needs the real clips, which the session scratchpad no longer holds:
|
|
|
|
```bash
|
|
/usr/bin/ssh kami@192.168.1.104 'P=homesrv/video/ef105a86-4b7e-4ac4-b45c-b7d83b8f5b5e/7c944dd4-e972-42c7-ba60-9f6939548e80; mc cp -q -r $P/clips/ /tmp/rclips/; cd /tmp/rclips && tar cf - .' | tar xf - -C clips/
|
|
```
|
|
|
|
## Storage and viewer, tasks #116/#117
|
|
|
|
[#117] is done. `stowage` serves the manga buckets. It was never a MinIO problem: the container had been
|
|
dead since 2026-07-19 on an arm64 digest pin.
|
|
|
|
[#116] is closer but not cut over. Artifacts split one bucket per class
|
|
(`decisions/storage-layout.md#bucket-per-artifact`), and both MinIO and `rustfs` hold all six buckets.
|
|
`rustfs` on `127.0.0.1:9010/9011` is still empty and nothing is repointed, so MinIO serves every read
|
|
and write. Remaining: `mc mirror` the live buckets, verify counts and sizes, then decide on cutover
|
|
(`decisions/storage-layout.md#rustfs-staged`).
|
|
|
|
Two containers on homesrv had been dead for two weeks and now run. `manga-fetch` is the one
|
|
`/job/create` needs. `manga-web` is what `manga.kvmx.ru` proxies to on 8083. Nothing watches them, and
|
|
nothing watches the workers.
|
|
|
|
## Open questions
|
|
|
|
Four Phase 1 items have no Vikunja task, because writing to the tracker was not asked for: the speaker
|
|
contract fix, the verifier rules, the tracklet constraints, and the flag resolution path. Only [#203]
|
|
existed and is closed by `decisions/audit-phase1.md#unlocked-model-load`.
|
|
|
|
Three audit items are deliberately not done and are recorded as caveats rather than silently dropped:
|
|
honest stage clearing, ComfyUI under the session mutex, and reversible identity merges. Each needs a
|
|
design decision, not a patch.
|
|
|
|
Carried over from the reconstruction: `.venv` needs the ROCm torch wheel reinstalled, and `dots.tts/`,
|
|
`legacy/`, `RESUME_SPEC.md`, `pipeline-design-notes.md`, `spec-v2.md` are unrecoverable.
|