diff --git a/backend/app/api/v1/routes_jobs.py b/backend/app/api/v1/routes_jobs.py index 4b987f8..acfa5da 100644 --- a/backend/app/api/v1/routes_jobs.py +++ b/backend/app/api/v1/routes_jobs.py @@ -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"] diff --git a/backend/app/models/job.py b/backend/app/models/job.py index 56be357..d7ffa11 100644 --- a/backend/app/models/job.py +++ b/backend/app/models/job.py @@ -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) diff --git a/backend/app/schemas/accessible_video.py b/backend/app/schemas/accessible_video.py index fee9daf..c99fa1d 100644 --- a/backend/app/schemas/accessible_video.py +++ b/backend/app/schemas/accessible_video.py @@ -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)") diff --git a/backend/app/services/video_renderer.py b/backend/app/services/video_renderer.py index 4e5fcc5..6a8832a 100644 --- a/backend/app/services/video_renderer.py +++ b/backend/app/services/video_renderer.py @@ -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 diff --git a/backend/app/tasks/rerender_accessible_video.py b/backend/app/tasks/rerender_accessible_video.py index 1868d5b..268e0d4 100644 --- a/backend/app/tasks/rerender_accessible_video.py +++ b/backend/app/tasks/rerender_accessible_video.py @@ -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({ diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 2f229a3..e9a042d 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -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;