Fix the real A/V gap: a stream copy across mixed frame rates
The rebuild after 1457556 came out byte-identical to the broken file,
which proved the xfade fix never runs for this chapter. An all-cut chapter
goes down the concat demuxer with -c copy, which writes the output in the
FIRST input's time_base and reinterprets every later packet in it. 14 of
49 clips are 30/1 at 1/15360 against 35 at 25/1 at 1/12800, so those 14
play 15360/12800 = 1.2 too long with their audio untouched. collage_cmd
hardcoded -r 30 and yesterday's FPS sweep missed it.
collage_cmd now emits -r FPS, and assemble probes r_frame_rate across the
clips and routes mixed rates through the re-encoding tree. Rebuilt
chapter.mp4 is 364.120s video against 364.122s audio at 25/1, from
436.392 over 363.675.
Also settle the bbox coordinate space, measured over all 113 detections:
47 boxes have x2 past the 900px panel width, none has y2 past 1000 on
panels up to 2307px tall, and the range is exactly [0, 1000]. It is
gemma's normalized grid, not pixels, whatever the prompt asks for.
/vision converts before returning, which fixes identity's crop, the gated
face pairing that was comparing pixel face boxes against grid boxes, the
set-of-mark boxes and the review UI at once. Checked by eye on panel 7:
five of six boxes now land on their subject, including the foreground
character who had no identity.
The registry still holds boxes and embeddings enrolled from the wrong
space. vision and identity have to re-run, which is GPU work and was not
started.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,8 @@ still live belongs in `caveats/`.
|
||||
| [An interjection is not a name and not a misquote](speaker-attribution.md#interjection-false-positive) | closed |
|
||||
| [Cast names enter the verifier tokenized](speaker-attribution.md#multiword-cast-names) | closed |
|
||||
| [Clearing a stage strips the vision blob it wrote](storage-layout.md#clear-vision-blob) | closed |
|
||||
| [The shipped 72.7s gap was a stream copy across mixed frame rates](chapter-assembly.md#mixed-rate-stream-copy) | closed |
|
||||
| [xfade offsets come from `min(video, audio)`, never `format=duration`](chapter-assembly.md#offsets-from-min-stream) | closed |
|
||||
| [A vision `bbox` is gemma's 0-1000 grid, converted to pixels at `/vision`](identity-bbox.md#bbox-is-normalized) | closed |
|
||||
| [Assembly verifies its own output instead of trusting ffmpeg's exit code](chapter-assembly.md#check-assembled) | closed |
|
||||
| [The single-item passthrough is not the assembly bug](chapter-assembly.md#passthrough-innocent) | void |
|
||||
|
||||
@@ -2,10 +2,40 @@
|
||||
|
||||
Settled questions about `_assemble_batched` / `_assemble_once` / `_xfade_chain` in `worker_render.py`.
|
||||
|
||||
## xfade offsets are computed from `min(video, audio)`, never `format=duration` {#offsets-from-min-stream}
|
||||
## The shipped 72.7s gap was a stream copy across mixed frame rates {#mixed-rate-stream-copy}
|
||||
|
||||
**Closed, 2026-08-12.**
|
||||
|
||||
`assemble` routes an all-`cut` chapter to a `concat` demuxer with `-c copy`. That path writes the output
|
||||
with the **first** input's `time_base` and reinterprets every later packet in it.
|
||||
|
||||
14 of this chapter's 49 clips came off `collage_cmd`, which hardcoded `-r 30`. They carry
|
||||
`r_frame_rate=30/1` and `time_base=1/15360`. The other 35 are `25/1` at `1/12800`. Copied into the first
|
||||
clip's timebase, those 14 play `15360/12800 = 1.2` times too long while their audio is untouched. That is
|
||||
the 1.2001 ratio, and the whole of video 436.39s over audio 363.67s.
|
||||
|
||||
Reproduced offline by running the same `-c copy` concat over the 49 real clips. Duration 436.392031 and
|
||||
`nb_frames` 9902, identical to the shipped file. Seconds to run, no GPU.
|
||||
|
||||
Two changes hold it closed:
|
||||
|
||||
* `collage_cmd` emits `-r FPS` like every other clip path, and the `__main__` self-check asserts
|
||||
`_fps_of(clip) == "25/1"` on a real collage encode.
|
||||
* `assemble` probes `r_frame_rate` across the clips and sends mixed rates through `_assemble_batched`,
|
||||
whose branches both normalize with `fps={FPS}`. Only a single shared rate keeps the stream copy.
|
||||
|
||||
Verified end to end. The rebuilt `chapter.mp4` is video 364.120s against audio 364.122s at `25/1`.
|
||||
|
||||
**An earlier version of this file, and commit `1457556`, blamed `#offsets-from-min-stream` below for the
|
||||
shipped gap. That was wrong.** The rebuild came out byte-identical to the broken file, which proved the
|
||||
xfade tree never ran for this chapter. The entry below is a real defect and stays closed on its own
|
||||
evidence. It was not this one.
|
||||
|
||||
## xfade offsets are computed from `min(video, audio)`, never `format=duration` {#offsets-from-min-stream}
|
||||
|
||||
**Closed, 2026-08-12.** A real latent defect on the transition path. Not the cause of the shipped gap,
|
||||
see `#mixed-rate-stream-copy` above.
|
||||
|
||||
`_xfade_chain` accumulates `cum += dur[i] - td` and hands each boundary `offset=cum-td`. That offset is
|
||||
an assertion about where input `i-1` still has frames. It fed on `_audio_dur`, which probes
|
||||
`format=duration`, which is `max(video, audio)`. A rendered clip's audio outlasts its video by about a
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# identity-bbox
|
||||
|
||||
The coordinate space of a vision character box, and what reads it.
|
||||
|
||||
## A vision `bbox` arrives on gemma's 0-1000 grid, and `/vision` converts it to pixels {#bbox-is-normalized}
|
||||
|
||||
**Closed, 2026-08-12.**
|
||||
|
||||
`build_detect_prompt` asks for a "pixel bounding box". The model answers on its own normalized grid
|
||||
regardless. Measured over all 113 detections of job `778297bc`, read straight from `/review/identity`:
|
||||
|
||||
| test | result |
|
||||
| --- | --- |
|
||||
| boxes with `x2` past the 900px panel width | **47 of 113** |
|
||||
| boxes with `y2` past 1000, on panels 1257 to 2307px tall | **0 of 113** |
|
||||
| boxes clamped at exactly 1000 | 21 in x, 5 in y |
|
||||
| coordinate range over every box | `[0, 1000]` |
|
||||
|
||||
Pixels cannot behave that way. A person standing in the lower half of a 2307px panel needs `y2` near
|
||||
2000, and it never once exceeds 1000.
|
||||
|
||||
Consumed as pixels the boxes collapse into the top-left corner of the panel. Four consumers were reading
|
||||
them:
|
||||
|
||||
* `worker_identity._crop_bbox` at `worker_identity.py:200`, which embeds the crop. This is why a crop of
|
||||
a speech balloon's edge matched `Choi Haeseon` at 0.9.
|
||||
* `_pair_faces_to_present` in `worker_vision.py`, which compares real detector face boxes, in pixels,
|
||||
against these. The gate could almost never pass, which is the mechanism behind the 7 `unknown` results
|
||||
out of 7 `som_face` lines already recorded at `worker_vision.py:169`.
|
||||
* the set-of-mark boxes drawn for attribution.
|
||||
* the review UI, which crops client-side off the panel PNG.
|
||||
|
||||
`/vision` now calls `_bbox_to_pixels(characters, w, h)` before returning, so all four see pixels and no
|
||||
consumer needs to know the grid existed. Verified by drawing the converted boxes on panel 7. Five of six
|
||||
land on their subject, including `person_5`, who is Seonho in the foreground and had no identity.
|
||||
`person_1` still frames a window mullion with nobody in it, which is `#extras-as-cast`, not this.
|
||||
|
||||
The prompt text still says "pixel bounding box". Rewording it changes what the model emits and needs a
|
||||
GPU run to re-verify, so the boundary converts instead. The `ponytail:` note on `_bbox_to_pixels` records
|
||||
that. It also records the trap: a model that really answered in pixels would be scaled down here.
|
||||
|
||||
**Consequence: every assignment in the registry came from a wrong crop.** The existing embeddings and
|
||||
`ref_image_uris` are enrolled on balloons and window frames. Re-running identity
|
||||
is what makes the registry mean anything. The anonymous-identity and extra-versus-cast work cannot be
|
||||
judged until that rerun happens.
|
||||
Reference in New Issue
Block a user