fix: validate pause points and frame extraction

- Get video duration BEFORE the render loop (not after)
- Clamp pause_point to 100ms before video end if it exceeds duration
- Add validation in _extract_frame() to verify frame was created
- Add debug logging for frame extraction timestamps

This prevents "Frame file not found" errors when pause points
calculated by Whisper refinement exceed the source video duration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2025-12-29 09:32:13 -06:00
parent 40ece78652
commit 37f5e8d1b0

View file

@ -307,7 +307,8 @@ class VideoRendererService:
# Get detailed video AND audio properties for uniform encoding
video_props = await self._get_video_properties(source_video_path)
logger.info(f"Source Properties: {video_props}")
source_duration = await self._get_video_duration(source_video_path)
logger.info(f"Source Properties: {video_props}, Duration: {source_duration:.2f}s")
segment_files = []
current_time = 0.0
@ -320,6 +321,18 @@ class VideoRendererService:
cue_index = placement["ad_cue_index"]
ad_duration = placement["ad_duration"]
resume_from = placement.get("resume_from", pause_point) # Fallback for backwards compat
# Validate pause_point is within video bounds
if pause_point >= source_duration:
logger.warning(
f"Cue {cue_index}: pause_point {pause_point:.2f}s exceeds video duration "
f"{source_duration:.2f}s, clamping to {source_duration - 0.1:.2f}s"
)
pause_point = max(0, source_duration - 0.1) # Clamp to 100ms before end
if resume_from >= source_duration:
resume_from = pause_point
overlap = max(0, pause_point - resume_from)
# Get the AD audio for this cue
@ -401,7 +414,6 @@ class VideoRendererService:
current_time = pause_point
# 4. Add final segment from last pause point to end (re-encoded for uniformity)
source_duration = await self._get_video_duration(source_video_path)
logger.info(
f"Final segment check: current_time={current_time:.2f}s, "
f"source_duration={source_duration:.2f}s, "
@ -550,6 +562,8 @@ class VideoRendererService:
async def _extract_frame(self, video_path: str, time_point: float, output_path: str):
"""Extract a single frame as PNG using ffmpeg."""
logger.debug(f"Extracting frame at {time_point:.2f}s from {video_path}")
cmd = [
self.ffmpeg_path,
"-y",
@ -561,6 +575,14 @@ class VideoRendererService:
]
await self._run_ffmpeg(cmd)
# Verify frame was actually created (FFmpeg can "succeed" but produce nothing
# if time_point is beyond video duration)
if not os.path.exists(output_path):
raise FileNotFoundError(
f"Frame extraction failed: no frame created at {time_point:.2f}s "
f"(time may be beyond video duration)"
)
async def _create_freeze_segment(
self,
frame_path: str,