Adding handoff functionality for reviews

This commit is contained in:
2026-03-13 11:10:32 +01:00
parent fdd275ac0e
commit 3dcd7961c6
35 changed files with 7181 additions and 326 deletions
+58 -11
View File
@@ -442,6 +442,7 @@ def build_render_plan(
audio: Optional[dict[str, AudioDefinition]] = None,
audio_dir: Optional[Path] = None,
slide_range: Optional[tuple[str, Optional[str]]] = None,
proxy: Optional[bool] = False,
) -> tuple[RenderPlan, list[MarkerTiming]]:
"""
Build a complete render plan from manuscript and transcription.
@@ -461,9 +462,15 @@ def build_render_plan(
audio_dir = audio_dir or project_path
# Find the main narration video first (need skip value for timing adjustment)
narration_video_id = config.main_video
narration_video_id = "narration_combined.mov" # Default narration video ID
# Handle legacy list format - use first element
if isinstance(narration_video_id, list):
narration_video_id = narration_video_id[0] if narration_video_id else None
if not (narration_video_id and narration_video_id in videos):
raise ValueError("Main video not specified or not found in videos.")
raise ValueError(
f"Main video '{narration_video_id}' not specified or not found in videos. "
f"Available: {list(videos.keys())}"
)
narration_video = videos[narration_video_id]
# Align markers to transcription timestamps
@@ -495,8 +502,13 @@ def build_render_plan(
narration_video = videos[narration_video_id]
cutout = config.cutouts[narration_video.cutout]
# Track which files are loaded from external cache
cached_files: set[str] = set()
narration_videos: list[tuple[str, VideoSource, CutoutDefinition]] = []
video_path = _resolve_video_path(videos_dir, narration_video, shared_assets_dir)
video_path, is_cached = _resolve_video_path(videos_dir, narration_video, shared_assets_dir, project_path)
if is_cached:
cached_files.add(narration_video_id)
full_duration = get_video_duration(video_path)
# Adjust duration for skip (content starts at skip, so effective duration is less)
effective_duration = full_duration - narration_skip
@@ -536,6 +548,14 @@ def build_render_plan(
time_range=(time_offset, render_end_time) if slide_range else None,
)
# Track cached files for triggered videos
for event in video_events:
_, is_cached = _resolve_video_path(
videos_dir, event.video_source, shared_assets_dir, project_path
)
if is_cached:
cached_files.add(event.video_id)
audio_events = _extract_audio_events(
marker_timings,
audio,
@@ -622,6 +642,8 @@ def build_render_plan(
total_duration,
videos_dir,
shared_assets_dir,
project_path,
cached_files,
)
# Update total duration to include outro
@@ -654,6 +676,7 @@ def build_render_plan(
narration_pauses=narration_pauses,
outro_events=outro_events,
narration_end_time=narration_end_time,
cached_files=cached_files,
)
return plan, marker_timings
@@ -663,8 +686,16 @@ def _resolve_video_path(
videos_dir: Path,
video_source: VideoSource,
shared_assets_dir: Path = None,
) -> Path:
"""Resolve the actual video file path."""
project_path: Path = None,
) -> tuple[Path, bool]:
"""Resolve the actual video file path with cache fallback.
Returns:
Tuple of (resolved_path, is_cached) where is_cached=True if
the file was found in the external cache.
"""
from .cache import resolve_with_cache
if video_source.is_shared and shared_assets_dir:
base_dir = shared_assets_dir
else:
@@ -672,12 +703,24 @@ def _resolve_video_path(
if video_source.output_file:
video_path = base_dir / video_source.output_file
if video_path.exists():
return video_path
if project_path:
resolved, is_cached = resolve_with_cache(video_path, project_path)
if resolved.exists():
return resolved, is_cached
elif video_path.exists():
return video_path, False
webm_path = video_path.with_suffix(".mov")
if webm_path.exists():
return webm_path
return base_dir / video_source.source_file
if project_path:
resolved, is_cached = resolve_with_cache(webm_path, project_path)
if resolved.exists():
return resolved, is_cached
elif webm_path.exists():
return webm_path, False
source_path = base_dir / video_source.source_file
if project_path:
return resolve_with_cache(source_path, project_path)
return source_path, False
def _extract_slide_events(
@@ -932,6 +975,8 @@ def _extract_outro_events(
narration_end_time: float,
videos_dir: Path,
shared_assets_dir: Path = None,
project_path: Path = None,
cached_files: set = None,
) -> list[OutroEvent]:
"""
Extract outro events that play after the narration ends.
@@ -949,7 +994,9 @@ def _extract_outro_events(
video_source = videos[video_id]
# Get the video duration
video_path = _resolve_video_path(videos_dir, video_source, shared_assets_dir)
video_path, is_cached = _resolve_video_path(videos_dir, video_source, shared_assets_dir, project_path)
if is_cached and cached_files is not None:
cached_files.add(video_id)
if video_path.exists():
full_duration = get_video_duration(video_path)
else: