| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
| from typing import Any |
|
|
|
|
| @dataclass(frozen=True) |
| class PlatformProfile: |
| key: str |
| label: str |
| width: int |
| height: int |
| fps: int = 30 |
| video_codec: str = "libx264" |
| audio_codec: str = "aac" |
| audio_bitrate: str = "128k" |
| audio_sample_rate: int = 48000 |
| crf: int = 23 |
| maxrate: str | None = None |
| bufsize: str | None = None |
| max_duration_seconds: int | None = None |
| recommended_duration_seconds: tuple[int, int] | None = None |
| safe_zones: dict[str, int] = field(default_factory=dict) |
| notes: tuple[str, ...] = () |
|
|
| @property |
| def aspect_ratio(self) -> str: |
| return f"{self.width}:{self.height}" |
|
|
| def metadata(self) -> dict[str, Any]: |
| return { |
| "platform": self.key, |
| "label": self.label, |
| "width": self.width, |
| "height": self.height, |
| "fps": self.fps, |
| "aspect_ratio": self.aspect_ratio, |
| "video_codec": self.video_codec, |
| "audio_codec": self.audio_codec, |
| "audio_bitrate": self.audio_bitrate, |
| "max_duration_seconds": self.max_duration_seconds, |
| "recommended_duration_seconds": self.recommended_duration_seconds, |
| "safe_zones": self.safe_zones, |
| "notes": list(self.notes), |
| } |
|
|
|
|
| COMMON_VERTICAL_SAFE_ZONES = { |
| "top_px": 220, |
| "bottom_px": 340, |
| "left_px": 80, |
| "right_px": 80, |
| } |
|
|
|
|
| PLATFORM_PROFILES: dict[str, PlatformProfile] = { |
| "tiktok": PlatformProfile( |
| key="tiktok", |
| label="TikTok vertical", |
| width=1080, |
| height=1920, |
| fps=30, |
| maxrate="8M", |
| bufsize="16M", |
| max_duration_seconds=600, |
| recommended_duration_seconds=(12, 60), |
| safe_zones=COMMON_VERTICAL_SAFE_ZONES, |
| notes=( |
| "9:16 vertical MP4 keeps the frame full-screen in the For You feed.", |
| "Use licensed or original music to avoid muted audio or takedowns.", |
| ), |
| ), |
| "instagram_reels": PlatformProfile( |
| key="instagram_reels", |
| label="Instagram Reels", |
| width=1080, |
| height=1920, |
| fps=30, |
| maxrate="8M", |
| bufsize="16M", |
| max_duration_seconds=180, |
| recommended_duration_seconds=(7, 90), |
| safe_zones=COMMON_VERTICAL_SAFE_ZONES, |
| notes=( |
| "9:16 is the safest Reels export to avoid cropping or blank space.", |
| "Keep captions and logos away from top and bottom app chrome.", |
| ), |
| ), |
| "facebook_reels": PlatformProfile( |
| key="facebook_reels", |
| label="Facebook Reels", |
| width=1080, |
| height=1920, |
| fps=30, |
| maxrate="8M", |
| bufsize="16M", |
| max_duration_seconds=180, |
| recommended_duration_seconds=(7, 90), |
| safe_zones=COMMON_VERTICAL_SAFE_ZONES, |
| ), |
| "youtube_shorts": PlatformProfile( |
| key="youtube_shorts", |
| label="YouTube Shorts", |
| width=1080, |
| height=1920, |
| fps=30, |
| audio_bitrate="192k", |
| maxrate="10M", |
| bufsize="20M", |
| max_duration_seconds=180, |
| recommended_duration_seconds=(15, 60), |
| safe_zones={"top_px": 180, "bottom_px": 300, "left_px": 80, "right_px": 80}, |
| notes=( |
| "YouTube categorizes square or vertical videos up to 3 minutes as Shorts.", |
| "Avoid Content ID-claimed music in Shorts longer than 60 seconds.", |
| ), |
| ), |
| "youtube_1080p": PlatformProfile( |
| key="youtube_1080p", |
| label="YouTube 1080p landscape", |
| width=1920, |
| height=1080, |
| fps=30, |
| audio_bitrate="192k", |
| maxrate="12M", |
| bufsize="24M", |
| recommended_duration_seconds=(60, 600), |
| notes=("16:9 H.264/AAC output for standard YouTube uploads.",), |
| ), |
| "instagram_feed_square": PlatformProfile( |
| key="instagram_feed_square", |
| label="Instagram feed square", |
| width=1080, |
| height=1080, |
| fps=30, |
| maxrate="8M", |
| bufsize="16M", |
| max_duration_seconds=3600, |
| recommended_duration_seconds=(5, 60), |
| safe_zones={"top_px": 80, "bottom_px": 120, "left_px": 80, "right_px": 80}, |
| ), |
| "instagram_feed_portrait": PlatformProfile( |
| key="instagram_feed_portrait", |
| label="Instagram feed portrait", |
| width=1080, |
| height=1350, |
| fps=30, |
| maxrate="8M", |
| bufsize="16M", |
| max_duration_seconds=3600, |
| recommended_duration_seconds=(5, 60), |
| safe_zones={"top_px": 80, "bottom_px": 140, "left_px": 80, "right_px": 80}, |
| ), |
| "snapchat_spotlight": PlatformProfile( |
| key="snapchat_spotlight", |
| label="Snapchat Spotlight", |
| width=1080, |
| height=1920, |
| fps=30, |
| maxrate="8M", |
| bufsize="16M", |
| max_duration_seconds=60, |
| recommended_duration_seconds=(5, 30), |
| safe_zones=COMMON_VERTICAL_SAFE_ZONES, |
| notes=("Vertical, fast-paced edits perform best in Spotlight.",), |
| ), |
| "pinterest_idea_pins": PlatformProfile( |
| key="pinterest_idea_pins", |
| label="Pinterest Idea Pins", |
| width=1080, |
| height=1920, |
| fps=30, |
| maxrate="8M", |
| bufsize="16M", |
| max_duration_seconds=300, |
| recommended_duration_seconds=(6, 45), |
| safe_zones={"top_px": 160, "bottom_px": 240, "left_px": 80, "right_px": 80}, |
| notes=("Use clear text overlays and evergreen discovery keywords.",), |
| ), |
| "linkedin_video": PlatformProfile( |
| key="linkedin_video", |
| label="LinkedIn video", |
| width=1920, |
| height=1080, |
| fps=30, |
| audio_bitrate="192k", |
| maxrate="10M", |
| bufsize="20M", |
| max_duration_seconds=600, |
| recommended_duration_seconds=(30, 180), |
| safe_zones={"top_px": 80, "bottom_px": 100, "left_px": 80, "right_px": 80}, |
| notes=("Landscape explainers and square clips both work; captions are strongly recommended.",), |
| ), |
| } |
|
|
|
|
| def get_platform_profile(key: str | None) -> PlatformProfile: |
| if not key: |
| return PLATFORM_PROFILES["tiktok"] |
| return PLATFORM_PROFILES.get(key, PLATFORM_PROFILES["tiktok"]) |
|
|
|
|
| def list_platform_profiles() -> list[str]: |
| return sorted(PLATFORM_PROFILES) |
|
|
|
|
| def platform_profile_metadata() -> dict[str, dict[str, Any]]: |
| return {key: profile.metadata() for key, profile in sorted(PLATFORM_PROFILES.items())} |
|
|