render: clamp composite output to the base video duration - #123
render: clamp composite output to the base video duration#123eldermoraes wants to merge 1 commit into
Conversation
In build_final_composite(), overlay's framesync extends the output whenever an overlay input outlasts the base video: the last base frame is frozen and repeated until the overlay ends. An overlay layer that is even slightly longer (e.g. a caption layer with a trailing hold on its last page) silently stretches the final video with frozen frames. Probe the base duration and pass -t to the composite command. Falls back to previous behavior when the probe fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="helpers/render.py">
<violation number="1" location="helpers/render.py:567">
P3: The broad `except Exception: pass` silently swallows every probe failure, so if ffprobe is unavailable, returns no video-stream duration, or `-select_streams v:0` matches nothing, the clamp silently reverts to the old frozen-frame-extension behavior with zero indication. Since this whole feature depends on that probe succeeding, a warning log (e.g. `warnings.warn` / print) would make regressions much more diagnosable. Consider narrowing the catch to expected failures and logging the fallback.</violation>
<violation number="2" location="helpers/render.py:579">
P2: The new `-t base_dur` clamps the whole output, not just the video. Because the audio is copied (`-c:a copy`), it is truncated at packet boundaries rather than exactly at `base_dur`, so the copied audio can end a few ms before/after the clamped video (small end-of-file A/V duration mismatch). And since the clamp uses the base VIDEO stream duration only, any audio that extends beyond the video stream (e.g. a short trailing silence or AAC priming tail) is silently cut off. Consider clamping only the video stream so the copied audio isn't truncated away, or verify/round the clamp so audio packets are not dropped.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| "-c:v", "libx264", "-preset", "fast", "-crf", "18", | ||
| "-pix_fmt", "yuv420p", | ||
| "-c:a", "copy", | ||
| *(["-t", f"{base_dur:.3f}"] if base_dur else []), |
There was a problem hiding this comment.
P2: The new -t base_dur clamps the whole output, not just the video. Because the audio is copied (-c:a copy), it is truncated at packet boundaries rather than exactly at base_dur, so the copied audio can end a few ms before/after the clamped video (small end-of-file A/V duration mismatch). And since the clamp uses the base VIDEO stream duration only, any audio that extends beyond the video stream (e.g. a short trailing silence or AAC priming tail) is silently cut off. Consider clamping only the video stream so the copied audio isn't truncated away, or verify/round the clamp so audio packets are not dropped.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 579:
<comment>The new `-t base_dur` clamps the whole output, not just the video. Because the audio is copied (`-c:a copy`), it is truncated at packet boundaries rather than exactly at `base_dur`, so the copied audio can end a few ms before/after the clamped video (small end-of-file A/V duration mismatch). And since the clamp uses the base VIDEO stream duration only, any audio that extends beyond the video stream (e.g. a short trailing silence or AAC priming tail) is silently cut off. Consider clamping only the video stream so the copied audio isn't truncated away, or verify/round the clamp so audio packets are not dropped.</comment>
<file context>
@@ -561,6 +576,7 @@ def build_final_composite(
"-c:v", "libx264", "-preset", "fast", "-crf", "18",
"-pix_fmt", "yuv420p",
"-c:a", "copy",
+ *(["-t", f"{base_dur:.3f}"] if base_dur else []),
"-movflags", "+faststart",
str(out_path),
</file context>
| capture_output=True, text=True, check=True, | ||
| ) | ||
| base_dur = float(out.stdout.strip().splitlines()[0]) | ||
| except Exception: |
There was a problem hiding this comment.
P3: The broad except Exception: pass silently swallows every probe failure, so if ffprobe is unavailable, returns no video-stream duration, or -select_streams v:0 matches nothing, the clamp silently reverts to the old frozen-frame-extension behavior with zero indication. Since this whole feature depends on that probe succeeding, a warning log (e.g. warnings.warn / print) would make regressions much more diagnosable. Consider narrowing the catch to expected failures and logging the fallback.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/render.py, line 567:
<comment>The broad `except Exception: pass` silently swallows every probe failure, so if ffprobe is unavailable, returns no video-stream duration, or `-select_streams v:0` matches nothing, the clamp silently reverts to the old frozen-frame-extension behavior with zero indication. Since this whole feature depends on that probe succeeding, a warning log (e.g. `warnings.warn` / print) would make regressions much more diagnosable. Consider narrowing the catch to expected failures and logging the fallback.</comment>
<file context>
@@ -552,6 +552,21 @@ def build_final_composite(
+ capture_output=True, text=True, check=True,
+ )
+ base_dur = float(out.stdout.strip().splitlines()[0])
+ except Exception:
+ pass
+
</file context>
Problem
In
build_final_composite(), overlay's framesync extends the output whenever an overlay input outlasts the base video: the last base frame is frozen and repeated until the overlay ends. An overlay even slightly longer than the base — e.g. a caption layer with a trailing hold on its last page — silently stretches the final video with frozen frames at the end.Change
Probe the base duration with ffprobe and pass
-t <base_dur>to the composite command. Falls back to previous behavior if the probe fails, so the failure mode is the status quo.How it was found
Compositing a caption overlay (68.98s, rendered with a 300ms hold after the last word) over a 68.73s base: the output came out 250ms longer than the EDL's
total_duration_s, with the last frame frozen.🤖 Generated with Claude Code
https://claude.ai/code/session_01HTd52WxWgWPYeakcJAiAnR
Summary by cubic
Clamp the final composite to the base video’s duration to prevent frozen-frame tails when an overlay outlasts the base. We probe the base duration with
ffprobeand pass-ttoffmpeg; if probing fails, we keep the previous behavior.Written for commit 44be066. Summary will update on new commits.