Fix the 72.7s A/V gap: xfade offsets ran off the end of their input
_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>
This commit is contained in:
@@ -5,6 +5,7 @@ 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
|
||||
@@ -12,23 +13,25 @@ Job `778297bc-e7ce-439d-91b5-8a027060d17f`, chapter `7c944dd4-e972-42c7-ba60-9f6
|
||||
|
||||
Two numbers set the agenda:
|
||||
|
||||
- `chapter.mp4` is video 436.39s over audio 363.67s. The narration finishes 72.7s before the picture.
|
||||
- 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.
|
||||
|
||||
Uncommitted work sits in the tree: `worker_render.py` has an `FPS = 25` constant, fps normalization in
|
||||
the xfade branch, a new `_stream_dur`, and a self-check that compares video against audio. The
|
||||
self-check passes. It does **not** yet fix the chapter. Details and one dead end in `HANDOFF.md`.
|
||||
|
||||
## Next
|
||||
|
||||
1. **Fix chapter assembly.** `_assemble_batched` turns 359s of video into 100s while the audio survives.
|
||||
It reproduces offline in two minutes, no GPU. Round 0 is correct and round 1 collapses. Round 1 is
|
||||
the only round holding a raw clip that skipped encoding, so suspect the single-item passthrough
|
||||
first. The recommended shape is one path, not three: normalize every input, then xfade every
|
||||
boundary, treating `cut` as a 0.05s fade. `acrossfade` and `xfade` shorten audio and video equally,
|
||||
so the streams stay locked. `HANDOFF.md` holds the per-round table, the filtergraph, and the repro
|
||||
commands. Nothing downstream is worth judging until this lands.
|
||||
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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user