Source code for camtasia.builders.screenplay_builder

"""Build a timeline from a parsed screenplay."""
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Callable

if TYPE_CHECKING:
    from camtasia.project import Project
    from camtasia.screenplay import Screenplay, VOBlock

from camtasia.types import ScreenplayBuildResult


[docs] def build_from_screenplay( project: Project, screenplay: 'Screenplay', audio_dir: str | Path, *, audio_track_name: str = 'Audio', default_pause: float = 1.0, vo_file_resolver: Callable[['VOBlock'], str | Path | None] | None = None, ) -> ScreenplayBuildResult: """Build a timeline from a parsed screenplay. Places voiceover audio clips sequentially with pauses between them. Uses the TimelineBuilder cursor for automatic timing. Note: Pauses are placed after all VO blocks in each section rather than interleaved with individual VO blocks. This is a design limitation of the current section-based iteration. Args: project: Target project. screenplay: Parsed Screenplay object. audio_dir: Directory containing VO audio files. audio_track_name: Name for the audio track. default_pause: Default pause between VO blocks (seconds). vo_file_resolver: Optional callback to resolve VO block to audio file path. If None, looks for files matching the VO ID pattern. Returns: Summary dict with counts. """ from camtasia.builders.timeline_builder import TimelineBuilder builder = TimelineBuilder(project) audio_dir = Path(audio_dir) clips_placed = 0 pauses_added = 0 for section in screenplay.sections: for vo in section.vo_blocks: # Resolve audio file if vo_file_resolver: audio_path = vo_file_resolver(vo) else: # Default: look for files matching VO ID pattern # e.g. VO-1.1 -> try 01-*.wav, VO-1.1.wav, etc. audio_path = _find_audio_file(audio_dir, vo.id) if audio_path and Path(audio_path).exists(): builder.add_audio(audio_path, track_name=audio_track_name) clips_placed += 1 # Add pauses from the section for pause in section.pauses: builder.add_pause(pause.duration_seconds) pauses_added += 1 return { 'clips_placed': clips_placed, 'pauses_added': pauses_added, 'total_duration': builder.cursor, }
def _find_audio_file(audio_dir: Path, vo_id: str) -> Path | None: """Try to find an audio file matching a VO ID.""" # Try exact match: VO-1.1.wav exact = audio_dir / f'{vo_id}.wav' if exact.exists(): return exact # Try numbered prefix using full VO ID: e.g. VO-1.1 -> 01-01-*.wav parts = [p for p in vo_id.split('.') if p] if len(parts) >= 2: prefix = f'{int(parts[0]):02d}-{int(parts[1]):02d}-' for f in sorted(audio_dir.glob(f'{prefix}*.wav')): return f elif parts: # pragma: no cover prefix = f'{int(parts[0]):02d}-' # pragma: no cover for f in sorted(audio_dir.glob(f'{prefix}*.wav')): # pragma: no cover return f # pragma: no cover return None