fix: store source video coordinates in pause points for correct re-rendering

The re-render task was using pause point coordinates from the accessible
video timeline (which includes freeze frame durations) instead of the
original source video coordinates. This caused pause points to exceed
the source video duration and get clamped incorrectly.

Changes:
- Add source_ms field to PausePointData model to store source video cut point
- Update video_renderer.py to populate source_ms when building pause points
- Update rerender_accessible_video.py to use source_ms for placement calculations
- Apply user adjustments as relative offsets (delta-based adjustment)
- Update API responses and TypeScript types to include source_ms
- Add backward compatibility fallback for jobs without source_ms

Note: Existing jobs need to be re-processed from initial render to populate
the new source_ms field.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-11 10:48:41 -06:00
parent a59dbb60ac
commit a6cd4cde07
6 changed files with 41 additions and 11 deletions

View file

@ -1532,6 +1532,7 @@ async def get_accessible_video_edit_state(
PausePointResponse(
cue_index=pp.get("cue_index"),
original_ms=pp.get("original_ms"),
source_ms=pp.get("source_ms", pp.get("original_ms")), # Fallback for old data
adjusted_ms=pp.get("adjusted_ms"),
min_bound_ms=pp.get("min_bound_ms"),
max_bound_ms=pp.get("max_bound_ms")
@ -1648,6 +1649,7 @@ async def update_pause_point(
return PausePointResponse(
cue_index=pause_point["cue_index"],
original_ms=pause_point["original_ms"],
source_ms=pause_point.get("source_ms", pause_point["original_ms"]), # Fallback for old data
adjusted_ms=pause_point["adjusted_ms"],
min_bound_ms=pause_point["min_bound_ms"],
max_bound_ms=pause_point["max_bound_ms"]

View file

@ -68,7 +68,8 @@ class RequestedOutputs(BaseModel):
class PausePointData(BaseModel):
"""Pause point timing data for accessible video editing during QC."""
cue_index: int # AD cue index this pause point belongs to
original_ms: float # Original pause point timestamp (ms)
original_ms: float # Rendered timeline position (ms) - for UI display
source_ms: float # Source video cut point (ms) - for re-rendering
adjusted_ms: Optional[float] = None # User-adjusted timestamp (ms), None = use original
min_bound_ms: float # Minimum allowed value (end of previous AD segment)
max_bound_ms: float # Maximum allowed value (start of next AD segment)

View file

@ -130,7 +130,8 @@ class AccessibleVideoProgress(BaseModel):
class PausePointResponse(BaseModel):
"""Pause point timing data for QC editing."""
cue_index: int = Field(..., description="AD cue index this pause point belongs to")
original_ms: float = Field(..., description="Original pause point timestamp (ms)")
original_ms: float = Field(..., description="Rendered timeline position (ms) - for display")
source_ms: float = Field(..., description="Source video cut point (ms) - for re-rendering")
adjusted_ms: Optional[float] = Field(None, description="User-adjusted timestamp (ms)")
min_bound_ms: float = Field(..., description="Minimum allowed value (ms)")
max_bound_ms: float = Field(..., description="Maximum allowed value (ms)")

View file

@ -874,7 +874,8 @@ class VideoRendererService:
pause_point_data_list.append(PausePointData(
cue_index=cue_index,
original_ms=pause_ms,
original_ms=pause_ms, # Rendered timeline position
source_ms=p["pause_point"] * 1000, # Source video cut point
adjusted_ms=None,
min_bound_ms=min_bound_ms,
max_bound_ms=max_bound_ms

View file

@ -418,24 +418,48 @@ def _build_placements_with_adjustments(
"""
Build placement instructions using adjusted pause points from QC edits.
Uses source_ms (source video coordinates) for pause point calculations,
applying user adjustments as relative offsets.
Args:
ad_vtt_content: AD VTT content
cue_durations: TTS durations per cue
pause_points: Pause point data with original and adjusted values
pause_points: Pause point data with source_ms, original_ms, and adjusted values
Returns:
List of placement dicts
"""
cues = VTTParser.parse(ad_vtt_content)
# Build lookup of adjusted pause points by cue index
# Build lookup of pause points by cue index using SOURCE coordinates
adjusted_pause_by_cue = {}
for pp in pause_points:
cue_idx = pp.get("cue_index")
adjusted = pp.get("adjusted_ms")
original = pp.get("original_ms")
# Use adjusted if set, otherwise original (in seconds)
pause_time_s = (adjusted if adjusted is not None else original) / 1000.0
source_ms = pp.get("source_ms")
original_ms = pp.get("original_ms")
adjusted_ms = pp.get("adjusted_ms")
# Fallback for data without source_ms (backward compatibility)
if source_ms is None:
logger.warning(
f"Cue {cue_idx}: No source_ms found, falling back to original_ms. "
"Job may need to be re-processed from initial render."
)
source_ms = original_ms
# Apply user adjustment as relative offset
if adjusted_ms is not None and original_ms is not None:
# User adjusted in rendered timeline - apply same delta to source
adjustment_delta = adjusted_ms - original_ms
source_ms = source_ms + adjustment_delta
logger.info(
f"Cue {cue_idx}: Applying adjustment delta {adjustment_delta:.1f}ms "
f"(rendered: {original_ms:.1f} -> {adjusted_ms:.1f}, "
f"source: {source_ms - adjustment_delta:.1f} -> {source_ms:.1f})"
)
# Convert to seconds for placement
pause_time_s = source_ms / 1000.0
adjusted_pause_by_cue[cue_idx] = pause_time_s
placements = []
@ -443,7 +467,7 @@ def _build_placements_with_adjustments(
if i >= len(cue_durations):
break
# Get pause point: use adjusted value if available
# Get pause point: use source-based value if available, otherwise fall back to VTT
pause_point = adjusted_pause_by_cue.get(i, cue.start_time)
placements.append({

View file

@ -336,7 +336,8 @@ export interface ReviewNotesListResponse {
export interface PausePointData {
cue_index: number;
original_ms: number;
original_ms: number; // Rendered timeline position (for display)
source_ms: number; // Source video cut point (for re-rendering)
adjusted_ms: number | null;
min_bound_ms: number;
max_bound_ms: number;