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:
2026-01-12 11:19:38 +01:00
co-authored by Claude Opus 4.5
commit d5a8d38c9c
15 changed files with 967 additions and 0 deletions
+88
View File
@@ -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