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
+104
View File
@@ -0,0 +1,104 @@
"""Validation stage: fail-fast checks on parsed data."""
from pathlib import Path
from .errors import ValidationError, ValidationIssue
from .models import ProjectConfig, SlideDefinition, VideoSource, SLIDE_LAYOUTS
def validate_project(
project_path: Path,
manuscript_markers: list[str],
config: ProjectConfig,
slides: dict[str, SlideDefinition],
videos: dict[str, VideoSource],
) -> None:
"""
Validate all parsed project data. Raises ValidationError if any issues found.
Checks:
- All slide markers in manuscript exist in slides.json
- All slide images exist on disk
- All video files exist on disk
- Background video exists (if specified)
- Slide types are valid
"""
issues: list[ValidationIssue] = []
# Check all manuscript markers have corresponding slides
for marker in manuscript_markers:
if marker not in slides:
issues.append(ValidationIssue(
f"Slide marker [{marker}] referenced in manuscript but not defined in slides.json",
project_path / "manuscript.txt"
))
# Check all slide images exist
media_path = project_path / "media"
slides_path = media_path / "slides"
for slide_id, slide_def in slides.items():
image_path = slides_path / slide_def.image
if not image_path.exists():
issues.append(ValidationIssue(
f"Slide image not found: {slide_def.image}",
project_path / "slides.json"
))
# Check slide type is valid
if slide_def.type not in SLIDE_LAYOUTS:
issues.append(ValidationIssue(
f"Unknown slide type '{slide_def.type}' for slide {slide_id}. "
f"Valid types: {list(SLIDE_LAYOUTS.keys())}",
project_path / "slides.json"
))
# Check all video files exist
for video_id, video_source in videos.items():
video_path = project_path / video_source.file
if not video_path.exists():
issues.append(ValidationIssue(
f"Video file not found: {video_source.file}",
project_path / "videos.json"
))
# Check background video exists (if specified)
if config.background_video:
bg_path = project_path / config.background_video
if not bg_path.exists():
issues.append(ValidationIssue(
f"Background video not found: {config.background_video}",
project_path / "project.json"
))
# Check we have at least one video source
if not videos:
issues.append(ValidationIssue(
"No video sources defined in videos.json",
project_path / "videos.json"
))
# Check resolution is reasonable
width, height = config.resolution
if width < 100 or height < 100:
issues.append(ValidationIssue(
f"Resolution too small: {width}x{height}",
project_path / "project.json"
))
if width > 7680 or height > 4320:
issues.append(ValidationIssue(
f"Resolution too large: {width}x{height} (max 8K)",
project_path / "project.json"
))
# Check FPS is reasonable
if config.fps < 1 or config.fps > 120:
issues.append(ValidationIssue(
f"Invalid FPS: {config.fps} (must be 1-120)",
project_path / "project.json"
))
# If any issues, raise ValidationError
if issues:
raise ValidationError(issues)