Initial commit: GnommoEditor video pipeline
A code-first, declarative video editing system that compiles text documents into rendered video via FFmpeg. Uses a compiler-style ETL pipeline: Extract (parse inputs) → Validate → Transform (build timeline) → Render (FFmpeg). Features: - Text-based project definition (manuscript, transcript, JSON configs) - Slide markers [S1], [S2] in transcript map to timed overlays - Strict validation with fail-fast error reporting - FFmpeg filter_complex generation with time-based enables - CLI with validate/render/dry-run modes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Transform stage: resolve timings and build render plan."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .models import (
|
||||
ProjectConfig,
|
||||
RenderPlan,
|
||||
SlideDefinition,
|
||||
SlideEvent,
|
||||
TimedWord,
|
||||
VideoSource,
|
||||
)
|
||||
from .parser import get_video_duration
|
||||
|
||||
|
||||
def build_render_plan(
|
||||
project_path: Path,
|
||||
config: ProjectConfig,
|
||||
slides: dict[str, SlideDefinition],
|
||||
videos: dict[str, VideoSource],
|
||||
transcript: list[TimedWord],
|
||||
) -> RenderPlan:
|
||||
"""
|
||||
Build a complete render plan from parsed and validated data.
|
||||
|
||||
This transforms transcript markers into timed slide events and
|
||||
assembles all information needed for the render stage.
|
||||
"""
|
||||
# For POC: use the first video as the talking head
|
||||
talking_head_id = next(iter(videos.keys()))
|
||||
talking_head = videos[talking_head_id]
|
||||
|
||||
# Get video duration for end time calculations
|
||||
video_path = project_path / talking_head.file
|
||||
total_duration = get_video_duration(video_path)
|
||||
|
||||
# Build slide events from transcript markers
|
||||
slide_events = _extract_slide_events(transcript, slides, total_duration)
|
||||
|
||||
return RenderPlan(
|
||||
project_path=project_path,
|
||||
config=config,
|
||||
talking_head=talking_head,
|
||||
slide_events=slide_events,
|
||||
total_duration=total_duration,
|
||||
slides=slides,
|
||||
)
|
||||
|
||||
|
||||
def _extract_slide_events(
|
||||
transcript: list[TimedWord],
|
||||
slides: dict[str, SlideDefinition],
|
||||
total_duration: float,
|
||||
) -> list[SlideEvent]:
|
||||
"""
|
||||
Extract slide events from transcript markers.
|
||||
|
||||
Each marker like [S1] in the transcript becomes a SlideEvent with:
|
||||
- start_time: timestamp of the marker
|
||||
- end_time: timestamp of next marker, or end of video
|
||||
"""
|
||||
# Find all markers in transcript
|
||||
marker_times: list[tuple[float, str]] = []
|
||||
|
||||
for timed_word in transcript:
|
||||
if timed_word.is_marker:
|
||||
marker_id = timed_word.marker_id
|
||||
if marker_id and marker_id in slides:
|
||||
marker_times.append((timed_word.time, marker_id))
|
||||
|
||||
# Convert markers to slide events
|
||||
events: list[SlideEvent] = []
|
||||
|
||||
for i, (start_time, marker_id) in enumerate(marker_times):
|
||||
# End time is start of next marker, or end of video
|
||||
if i + 1 < len(marker_times):
|
||||
end_time = marker_times[i + 1][0]
|
||||
else:
|
||||
end_time = total_duration
|
||||
|
||||
events.append(SlideEvent(
|
||||
slide_id=marker_id,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
slide_def=slides[marker_id],
|
||||
))
|
||||
|
||||
return events
|
||||
Reference in New Issue
Block a user