Initial release — OpenMontage: the first open-source agentic video production system

11 production pipelines, 47 tools, 124 agent skills.
Supports cloud APIs (fal.ai, OpenAI, ElevenLabs, Suno, HeyGen, Runway) and
free local providers (diffusers, Piper TTS, WAN 2.1, Hunyuan, CogVideo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
calesthio
2026-03-29 08:25:17 -07:00
commit a3e735cc7a
1147 changed files with 240221 additions and 0 deletions
View File
+210
View File
@@ -0,0 +1,210 @@
"""Checkpoint writer/reader for pipeline state persistence.
Each stage writes a checkpoint after completion. The orchestrator uses
checkpoints to resume pipelines and to present state at human checkpoints.
"""
from __future__ import annotations
import json
from functools import lru_cache
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
import jsonschema
from schemas.artifacts import ARTIFACT_NAMES, validate_artifact
STAGES = ["research", "proposal", "idea", "script", "scene_plan", "assets", "edit", "compose", "publish"]
CANONICAL_STAGE_ARTIFACTS = {
"research": "research_brief",
"proposal": "proposal_packet",
"idea": "brief",
"script": "script",
"scene_plan": "scene_plan",
"assets": "asset_manifest",
"edit": "edit_decisions",
"compose": "render_report",
"publish": "publish_log",
}
CHECKPOINT_SCHEMA_PATH = (
Path(__file__).resolve().parent.parent
/ "schemas"
/ "checkpoints"
/ "checkpoint.schema.json"
)
class CheckpointValidationError(ValueError):
"""Raised when a checkpoint or its canonical artifacts are invalid."""
@lru_cache(maxsize=1)
def _load_checkpoint_schema() -> dict[str, Any]:
with open(CHECKPOINT_SCHEMA_PATH) as f:
return json.load(f)
def _validate_artifacts_for_stage(
stage: str,
status: str,
artifacts: dict[str, Any],
) -> None:
required_artifact = CANONICAL_STAGE_ARTIFACTS[stage]
if status in {"completed", "awaiting_human"} and required_artifact not in artifacts:
raise CheckpointValidationError(
f"Stage {stage!r} with status {status!r} must include "
f"canonical artifact {required_artifact!r}"
)
for artifact_name, artifact_data in artifacts.items():
if artifact_name not in ARTIFACT_NAMES:
continue
if not isinstance(artifact_data, dict):
raise CheckpointValidationError(
f"Artifact {artifact_name!r} must be a JSON object matching its schema"
)
try:
validate_artifact(artifact_name, artifact_data)
except Exception as exc:
raise CheckpointValidationError(
f"Artifact {artifact_name!r} failed schema validation: {exc}"
) from exc
def validate_checkpoint(checkpoint: dict[str, Any]) -> None:
"""Validate checkpoint structure and canonical artifact payloads."""
stage = checkpoint.get("stage")
status = checkpoint.get("status")
artifacts = checkpoint.get("artifacts")
if not isinstance(stage, str) or stage not in STAGES:
raise CheckpointValidationError(f"Invalid stage: {stage!r}")
if not isinstance(status, str):
raise CheckpointValidationError(f"Invalid status: {status!r}")
if not isinstance(artifacts, dict):
raise CheckpointValidationError("Checkpoint artifacts must be a dictionary")
_validate_artifacts_for_stage(stage, status, artifacts)
try:
jsonschema.validate(instance=checkpoint, schema=_load_checkpoint_schema())
except jsonschema.ValidationError as exc:
raise CheckpointValidationError(f"Checkpoint failed schema validation: {exc.message}") from exc
def _checkpoint_path(pipeline_dir: Path, project_id: str, stage: str) -> Path:
return pipeline_dir / project_id / f"checkpoint_{stage}.json"
def write_checkpoint(
pipeline_dir: Path,
project_id: str,
stage: str,
status: str,
artifacts: dict[str, Any],
*,
pipeline_type: Optional[str] = None,
style_playbook: Optional[str] = None,
checkpoint_policy: str = "guided",
human_approval_required: bool = False,
human_approved: bool = False,
review: Optional[dict] = None,
cost_snapshot: Optional[dict] = None,
error: Optional[str] = None,
metadata: Optional[dict] = None,
) -> Path:
"""Write a checkpoint file for a pipeline stage."""
if stage not in STAGES:
raise ValueError(f"Invalid stage: {stage!r}. Must be one of {STAGES}")
checkpoint = {
"version": "1.0",
"project_id": project_id,
"stage": stage,
"status": status,
"timestamp": datetime.now(timezone.utc).isoformat(),
"checkpoint_policy": checkpoint_policy,
"human_approval_required": human_approval_required,
"human_approved": human_approved,
"artifacts": artifacts,
}
if pipeline_type is not None:
checkpoint["pipeline_type"] = pipeline_type
if style_playbook is not None:
checkpoint["style_playbook"] = style_playbook
if review is not None:
checkpoint["review"] = review
if cost_snapshot is not None:
checkpoint["cost_snapshot"] = cost_snapshot
if error is not None:
checkpoint["error"] = error
if metadata is not None:
checkpoint["metadata"] = metadata
validate_checkpoint(checkpoint)
path = _checkpoint_path(pipeline_dir, project_id, stage)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
json.dump(checkpoint, f, indent=2)
return path
def read_checkpoint(
pipeline_dir: Path, project_id: str, stage: str
) -> Optional[dict[str, Any]]:
"""Read a checkpoint file. Returns None if not found."""
path = _checkpoint_path(pipeline_dir, project_id, stage)
if not path.exists():
return None
with open(path) as f:
checkpoint = json.load(f)
validate_checkpoint(checkpoint)
return checkpoint
def get_latest_checkpoint(
pipeline_dir: Path, project_id: str
) -> Optional[dict[str, Any]]:
"""Find the most recent checkpoint for a project (by file mtime)."""
project_dir = pipeline_dir / project_id
if not project_dir.exists():
return None
checkpoints = sorted(
project_dir.glob("checkpoint_*.json"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
if not checkpoints:
return None
with open(checkpoints[0]) as f:
checkpoint = json.load(f)
validate_checkpoint(checkpoint)
return checkpoint
def get_completed_stages(pipeline_dir: Path, project_id: str) -> list[str]:
"""Return list of stages that have a completed checkpoint."""
completed = []
for stage in STAGES:
cp = read_checkpoint(pipeline_dir, project_id, stage)
if cp and cp.get("status") == "completed":
completed.append(stage)
return completed
def get_next_stage(pipeline_dir: Path, project_id: str) -> Optional[str]:
"""Determine the next stage to run based on completed checkpoints."""
completed = set(get_completed_stages(pipeline_dir, project_id))
for stage in STAGES:
if stage not in completed:
return stage
return None
+92
View File
@@ -0,0 +1,92 @@
"""Runtime configuration model for OpenMontage.
Loads config.yaml, merges with env overrides, and provides typed access.
"""
from __future__ import annotations
from enum import Enum
from pathlib import Path
from typing import Optional
import yaml
from pydantic import BaseModel, Field
class BudgetMode(str, Enum):
OBSERVE = "observe"
WARN = "warn"
CAP = "cap"
class CheckpointPolicy(str, Enum):
GUIDED = "guided"
MANUAL_ALL = "manual_all"
AUTO_NONCREATIVE = "auto_noncreative"
class LLMConfig(BaseModel):
provider: str = "anthropic"
model: Optional[str] = None
temperature: float = 0.7
max_tokens: int = 4096
class BudgetConfig(BaseModel):
mode: BudgetMode = BudgetMode.WARN
total_usd: float = 10.0
reserve_pct: float = 0.10
single_action_approval_usd: float = 0.50
require_approval_for_new_paid_tool: bool = True
class CheckpointConfig(BaseModel):
policy: CheckpointPolicy = CheckpointPolicy.GUIDED
storage_dir: str = "pipeline"
class OutputConfig(BaseModel):
default_format: str = "mp4"
default_codec: str = "libx264"
default_audio_codec: str = "aac"
default_resolution: str = "1920x1080"
default_fps: int = 30
default_crf: int = 23
class PathsConfig(BaseModel):
pipeline_dir: str = "pipeline"
library_dir: str = "library"
styles_dir: str = "styles"
skills_dir: str = "skills"
output_dir: str = "output"
class OpenMontageConfig(BaseModel):
"""Top-level runtime configuration."""
llm: LLMConfig = Field(default_factory=LLMConfig)
budget: BudgetConfig = Field(default_factory=BudgetConfig)
checkpoint: CheckpointConfig = Field(default_factory=CheckpointConfig)
output: OutputConfig = Field(default_factory=OutputConfig)
paths: PathsConfig = Field(default_factory=PathsConfig)
@classmethod
def load(cls, config_path: Optional[Path] = None) -> "OpenMontageConfig":
"""Load config from YAML file. Falls back to defaults if file missing."""
if config_path is None:
config_path = Path(__file__).resolve().parent.parent / "config.yaml"
if config_path.exists():
with open(config_path) as f:
raw = yaml.safe_load(f) or {}
return cls.model_validate(raw)
return cls()
def resolve_path(self, key: str, project_root: Optional[Path] = None) -> Path:
"""Resolve a relative path from PathsConfig against project root."""
if project_root is None:
project_root = Path(__file__).resolve().parent.parent
value = getattr(self.paths, key)
return (project_root / value).resolve()
+34
View File
@@ -0,0 +1,34 @@
"""Environment variable loader for OpenMontage.
Loads .env file and provides typed access to environment configuration.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Optional
from dotenv import load_dotenv
def load_env(project_root: Optional[Path] = None) -> None:
"""Load .env file from project root."""
if project_root is None:
project_root = Path(__file__).resolve().parent.parent
env_path = project_root / ".env"
if env_path.exists():
load_dotenv(env_path)
def get_env(key: str, default: Optional[str] = None) -> Optional[str]:
"""Get an environment variable with optional default."""
return os.environ.get(key, default)
def require_env(key: str) -> str:
"""Get a required environment variable. Raises if missing."""
value = os.environ.get(key)
if value is None:
raise EnvironmentError(f"Required environment variable {key!r} is not set")
return value
+165
View File
@@ -0,0 +1,165 @@
"""Internal media profile constants and render-profile helpers.
Defines platform-specific media profiles (resolution, aspect ratio, codec, etc.)
so the composer and publisher agents can format output correctly.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class AspectRatio(str, Enum):
LANDSCAPE_16_9 = "16:9"
PORTRAIT_9_16 = "9:16"
SQUARE_1_1 = "1:1"
CINEMATIC_21_9 = "21:9"
STANDARD_4_3 = "4:3"
@dataclass(frozen=True)
class MediaProfile:
"""A named render profile for a target platform/format."""
name: str
width: int
height: int
aspect_ratio: AspectRatio
fps: int
codec: str
audio_codec: str
crf: int
pixel_format: str = "yuv420p"
max_file_size_mb: Optional[float] = None
max_duration_seconds: Optional[float] = None
caption_format: str = "srt"
notes: str = ""
# ---- Platform profiles ----
YOUTUBE_LANDSCAPE = MediaProfile(
name="youtube_landscape",
width=1920, height=1080,
aspect_ratio=AspectRatio.LANDSCAPE_16_9,
fps=30, codec="libx264", audio_codec="aac", crf=18,
caption_format="srt",
notes="YouTube standard HD upload",
)
YOUTUBE_4K = MediaProfile(
name="youtube_4k",
width=3840, height=2160,
aspect_ratio=AspectRatio.LANDSCAPE_16_9,
fps=30, codec="libx264", audio_codec="aac", crf=18,
caption_format="srt",
notes="YouTube 4K upload",
)
YOUTUBE_SHORTS = MediaProfile(
name="youtube_shorts",
width=1080, height=1920,
aspect_ratio=AspectRatio.PORTRAIT_9_16,
fps=30, codec="libx264", audio_codec="aac", crf=20,
max_duration_seconds=60,
caption_format="srt",
notes="YouTube Shorts (max 60s, vertical)",
)
INSTAGRAM_REELS = MediaProfile(
name="instagram_reels",
width=1080, height=1920,
aspect_ratio=AspectRatio.PORTRAIT_9_16,
fps=30, codec="libx264", audio_codec="aac", crf=20,
max_file_size_mb=250,
max_duration_seconds=90,
caption_format="srt",
notes="Instagram Reels (max 90s, vertical)",
)
INSTAGRAM_FEED = MediaProfile(
name="instagram_feed",
width=1080, height=1080,
aspect_ratio=AspectRatio.SQUARE_1_1,
fps=30, codec="libx264", audio_codec="aac", crf=20,
max_file_size_mb=250,
max_duration_seconds=60,
notes="Instagram feed video (square)",
)
TIKTOK = MediaProfile(
name="tiktok",
width=1080, height=1920,
aspect_ratio=AspectRatio.PORTRAIT_9_16,
fps=30, codec="libx264", audio_codec="aac", crf=20,
max_file_size_mb=287,
max_duration_seconds=600,
caption_format="srt",
notes="TikTok (max 10min, vertical preferred)",
)
LINKEDIN = MediaProfile(
name="linkedin",
width=1920, height=1080,
aspect_ratio=AspectRatio.LANDSCAPE_16_9,
fps=30, codec="libx264", audio_codec="aac", crf=20,
max_file_size_mb=5120,
max_duration_seconds=600,
caption_format="srt",
notes="LinkedIn video (landscape preferred, max 10min)",
)
CINEMATIC = MediaProfile(
name="cinematic",
width=2560, height=1080,
aspect_ratio=AspectRatio.CINEMATIC_21_9,
fps=24, codec="libx264", audio_codec="aac", crf=16,
notes="Cinematic ultra-wide format",
)
GENERIC_HD = MediaProfile(
name="generic_hd",
width=1920, height=1080,
aspect_ratio=AspectRatio.LANDSCAPE_16_9,
fps=30, codec="libx264", audio_codec="aac", crf=23,
caption_format="srt",
notes="Generic HD output (no platform-specific constraints)",
)
# ---- Profile registry ----
ALL_PROFILES: dict[str, MediaProfile] = {
p.name: p for p in [
YOUTUBE_LANDSCAPE, YOUTUBE_4K, YOUTUBE_SHORTS,
INSTAGRAM_REELS, INSTAGRAM_FEED,
TIKTOK, LINKEDIN, CINEMATIC, GENERIC_HD,
]
}
def get_profile(name: str) -> MediaProfile:
"""Get a media profile by name."""
if name not in ALL_PROFILES:
available = ", ".join(ALL_PROFILES.keys())
raise ValueError(f"Unknown profile {name!r}. Available: {available}")
return ALL_PROFILES[name]
def get_profiles_for_platform(platform: str) -> list[MediaProfile]:
"""Get all profiles matching a platform prefix."""
return [p for name, p in ALL_PROFILES.items() if name.startswith(platform)]
def ffmpeg_output_args(profile: MediaProfile) -> list[str]:
"""Generate FFmpeg output arguments for a media profile."""
args = [
"-c:v", profile.codec,
"-c:a", profile.audio_codec,
"-crf", str(profile.crf),
"-pix_fmt", profile.pixel_format,
"-r", str(profile.fps),
"-vf", f"scale={profile.width}:{profile.height}",
]
return args
+87
View File
@@ -0,0 +1,87 @@
"""Pipeline manifest loader.
Loads and validates pipeline YAML manifests from pipeline_defs/.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Optional
import yaml
import jsonschema
PIPELINE_DEFS_DIR = Path(__file__).resolve().parent.parent / "pipeline_defs"
SCHEMA_PATH = (
Path(__file__).resolve().parent.parent
/ "schemas"
/ "pipelines"
/ "pipeline_manifest.schema.json"
)
def _load_manifest_schema() -> dict:
with open(SCHEMA_PATH) as f:
return json.load(f)
def load_pipeline(name: str, defs_dir: Optional[Path] = None) -> dict[str, Any]:
"""Load and validate a pipeline manifest by name.
Args:
name: Pipeline name (without .yaml extension).
defs_dir: Override directory for pipeline definitions.
Returns:
Validated pipeline manifest dict.
"""
defs_dir = defs_dir or PIPELINE_DEFS_DIR
path = defs_dir / f"{name}.yaml"
if not path.exists():
raise FileNotFoundError(f"Pipeline manifest not found: {path}")
with open(path) as f:
manifest = yaml.safe_load(f)
schema = _load_manifest_schema()
jsonschema.validate(instance=manifest, schema=schema)
return manifest
def list_pipelines(defs_dir: Optional[Path] = None) -> list[str]:
"""List all available pipeline manifest names."""
defs_dir = defs_dir or PIPELINE_DEFS_DIR
return [p.stem for p in defs_dir.glob("*.yaml")]
def get_stage_order(manifest: dict) -> list[str]:
"""Extract the ordered list of stage names from a manifest."""
return [stage["name"] for stage in manifest["stages"]]
def get_required_tools(manifest: dict) -> set[str]:
"""Collect all preferred + fallback + available tools across all stages."""
tools: set[str] = set()
for stage in manifest["stages"]:
tools.update(stage.get("preferred_tools", []))
tools.update(stage.get("fallback_tools", []))
tools.update(stage.get("tools_available", []))
return tools
def get_stage_skill(manifest: dict, stage_name: str) -> Optional[str]:
"""Get the skill path for an instruction-driven stage."""
for stage in manifest["stages"]:
if stage["name"] == stage_name:
return stage.get("skill")
return None
def get_stage_review_focus(manifest: dict, stage_name: str) -> list[str]:
"""Get the review focus items for a stage."""
for stage in manifest["stages"]:
if stage["name"] == stage_name:
return stage.get("review_focus", [])
return []
View File