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:
@@ -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
|
||||
Reference in New Issue
Block a user