Py-Shorts-Studio: Viral 2D Vector Explainer Engine
py-shorts-studio is a high-performance Python framework for creating vertical (1080x1920, 9:16), 60 FPS motion graphics explainer Shorts. It bypasses disk I/O by streaming PyCairo vector frames directly to FFmpeg stdin, aligns voiceovers with active-word karaoke captions, synthesizes custom SFX, and mixes multi-track audio.
1. Seamless Audio + Visual Match-Cut Loop (Infinite Retention)
To maximize Average Percentage Viewed (>100% loop retention on YouTube Shorts & Instagram Reels), every generated Short MUST enforce both an audio sentence bridge and a pixel-identical visual match-cut.
A. The Sentence-Bridge Script Rule
When YouTube loops a Short, the end of the audio connects instantaneously to the start. The script must exploit this:
- Opening Hook: MUST open immediately with a sharp Why / How / What question.
- Example:
"Why would Iron Man's suit kill Tony Stark in three seconds?" - Example:
"Why do modern highways crumble in fifteen years while Roman roads last two thousand?"
- Example:
- Ending Lead-In Phrase: The final sentence MUST end on a trailing, anticipatory lead-in phrase rather than a definitive sign-off or subscribe callout:
- Example:
"...which is why people still ask," - Example:
"...so the next time someone asks you," - Example:
"...and that explains,"
- Example:
- Continuous Grammatical Loop: When the video loops from the trailing phrase into the opening question, it forms one seamless grammatical sentence:
"...which is why people still ask — Why would Iron Man's suit kill Tony Stark in three seconds?"
- Zero Trailing Silence:
total_duration = audio_duration + 0.05(NEVER add0.4sor0.75sof trailing dead air).- Any pause at the end breaks the illusion of an infinite loop. The audio loop must hit the first word of the restart with zero perceivable gap.
B. Pixel-Identical Frame-0 Match Cut
The very first frame ($t = 0.0\text{s}$) and the very last frame ($t = \text{total_duration}$) must be visually identical so the viewer cannot detect when the video repeats.
Implementation Pattern: draw_anchor_state
Every project script must define a shared draw_anchor_state(ctx, zoom=1.0, pan=(0, 0)) function containing the primary "Hero" visual and top banner.
def draw_anchor_state(ctx: cairo.Context, zoom: float = 1.0, pan: tuple[float, float] = (0.0, 0.0)) -> None:
"""Renders the master hero diagram and top banner at an arbitrary zoom/pan center.
Used by both Scene 1 (opening ease-out zoom) and the final scene (closing ease-in zoom)
to guarantee a pixel-identical match cut across the loop boundary.
"""
ctx.save()
# Center-origin transform (1080x1920 center is 540, 960)
ctx.translate(540.0 + pan[0], 960.0 + pan[1])
ctx.scale(zoom, zoom)
ctx.translate(-540.0, -960.0)
# 1. Background
draw_dark_bg(ctx, 1080, 1920)
# 2. Top Banner
draw_top_banner(ctx, "HERO TOPIC TITLE", "ENGINEERING EXPLAINER", progress=1.0, theme="dark")
# 3. Hero Visual / Diagram in Action Zone
# (Draw central diagram, cross-section, or apparatus here)
...
ctx.restore()
Match-Cut Zoom Dynamics (0.0s – 1.2s & Last 1.2s)
- Opening ($t = 0.0\text{s} \to 1.2\text{s}$):
- Starts at $t = 0.0\text{s}$ tightly zoomed in on the focal diagram at
zoom = 1.45. - Over $0.0\text{s} \to 1.2\text{s}$, smoothly eases out to wide view (
zoom = 1.0) usingease_out_cubic:if time_sec <= 1.2: zoom_t = ease_out_cubic(time_sec / 1.2) cur_zoom = lerp(1.45, 1.0, zoom_t) draw_anchor_state(ctx, zoom=cur_zoom)
- Starts at $t = 0.0\text{s}$ tightly zoomed in on the focal diagram at
- Closing ($t = \text{total_duration} - 1.2\text{s} \to \text{total_duration}$):
- Smoothly transitions back to that exact same hero diagram.
- Over the final 1.2s, zooms in from
zoom = 1.0back tozoom = 1.45usingease_in_out_cubic:t_closing_start = total_duration - 1.2 if time_sec >= t_closing_start: zoom_t = ease_in_out_cubic((time_sec - t_closing_start) / 1.2) cur_zoom = lerp(1.0, 1.45, zoom_t) draw_anchor_state(ctx, zoom=cur_zoom)
- Result: Frame at $t = 0.0\text{s}$ (
zoom = 1.45) and frame at $t = \text{total_duration}$ (zoom = 1.45) are pixel-identical.
2. The 4-Part High-Retention Script Formula
High-retention Shorts follow this strict narrative arc:
| Part | Timing (45–55s Short) | Timing (25–30s Short) | Purpose & Execution |
|---|---|---|---|
| 1. Paradox Hook | 0.0s – 4.0s | 0.0s – 5.0s | State a stark, counter-intuitive contrast or impossible mystery starting with Why / How / What. Ban fluff, intros, or greetings. |
| 2. Constraint / Escalation | 4.0s – 20.0s | 5.0s – 12.0s | Escalate the problem with concrete numbers, historical stakes, or environmental hazards. Introduce comparative metrics or status HUD bars. |
| 3. The Ingenious Pivot | 20.0s – 45.0s | 12.0s – 22.0s | The architectural or scientific secret revealed. Break down the engineering mechanics using animated diagrams, cutaways, routes, or component layers. |
| 4. Payoff & Trailing Bridge | Last 5.0s | Last 4.0s | Deliver the "aha!" payoff and end with a trailing sentence bridge that loops back into the opening hook with zero trailing silence. |
- Speaking Pacing: 150–170 words per minute (WPM).
- Target Word Counts:
- 45–55s Short: 110–145 words.
- 25–30s Short: 65–75 words.
3. Visual & Layout Rules
- Canvas Specifications:
- Resolution:
1080x1920(9:16 vertical aspect ratio). - Frame Rate:
60 FPSconstant. - Vector Precision: All shapes, icons, and lines drawn via PyCairo for crisp anti-aliasing.
- Resolution:
- Safe Zones (Crucial for Mobile UI):
- Top Safe Zone ($y \ge 130$): Place titles, chapter pills, and top banners between $y = 130$ and $y = 260$ to avoid the channel handle and top UI overlays.
- Center Action Zone ($y = 280$ to $y = 1420$): All focal graphics, cutaways, map routes, and stat badges live here.
- Bottom Caption Safe Zone: Captions are anchored at $y = 1480$–$1540$. Keep background focal elements above $y = 1400$ to prevent overlapping caption text. Avoid essential visual details in the bottom 380px (where title, sound, and subscribe buttons reside on YouTube and TikTok).
- Pacing Rule (3–5 Second State Changes):
- Viewer attention decays rapidly. Change the visual state, camera focus, or trigger an animation every 3 to 5 seconds (e.g. badge spring pop, HUD metric fill, path progression, or layer reveal).
- Exact Word-Synced Sound Design:
- Never guess SFX timings.
- Run
generate_voiceoverfirst to producewords.json. - Inspect
words.jsonfor key trigger words (e.g. numbers, action verbs, scene shifts) and placewhoosh,pop, andthudcues at the exactstart_secof those words.
4. Complete Engine API Reference
All modules are cleanly exported from engine:
from engine import (
# Renderer
VideoRenderer,
# Easing Mathematics
clamp, lerp, ease_out_cubic, ease_in_out_cubic, spring_pop,
cubic_bezier_point, cubic_bezier_tangent, cubic_bezier_angle,
# 2D Vector Primitives
draw_parchment_bg, draw_dark_bg, draw_top_banner, draw_map_route,
draw_hud_bars, draw_stat_badge, draw_rounded_rect, draw_card_shadow,
# Active-Word Karaoke Captions
draw_karaoke_captions, chunk_words,
# Audio Synthesis, Mixing & Muxing
generate_voiceover, generate_voiceover_async,
ensure_default_sfx, get_downloads_dir, mix_final_audio, mux_video_audio,
)
VideoRenderer (engine.renderer)
class VideoRenderer:
def __init__(
self,
output_path: Union[str, Path],
width: int = 1080,
height: int = 1920,
fps: int = 60,
crf: int = 18,
preset: str = "fast",
audio_path: Optional[Union[str, Path]] = None,
ffmpeg_bin: Optional[str] = None,
) -> None: ...
def create_surface(self) -> Tuple[cairo.ImageSurface, cairo.Context]: ...
def write_surface(self, surface: cairo.ImageSurface) -> None: ...
def render(
self,
draw_fn: Callable[[cairo.Context, int, float], None],
total_frames: Optional[int] = None,
duration_sec: Optional[float] = None,
on_progress: Optional[Callable[[int, int], None]] = None,
) -> Path: ...
Easing Helpers (engine.easing)
def clamp(val: float, min_val: float = 0.0, max_val: float = 1.0) -> float: ...
def lerp(a: float, b: float, t: float) -> float: ...
def ease_out_cubic(t: float) -> float: ...
def ease_in_out_cubic(t: float) -> float: ...
def spring_pop(t: float, overshoot: float = 1.2) -> float: ...
def cubic_bezier_point(p0: Point2D | float, p1: Point2D | float, p2: Point2D | float, p3: Point2D | float, t: float) -> Tuple[float, float] | float: ...
def cubic_bezier_tangent(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D, t: float) -> Tuple[float, float]: ...
def cubic_bezier_angle(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D, t: float) -> float: ...
Reusable Vector Primitives (engine.primitives)
def draw_parchment_bg(ctx: cairo.Context, width: int = 1080, height: int = 1920, seed: int = 42) -> None: ...
def draw_dark_bg(ctx: cairo.Context, width: int = 1080, height: int = 1920) -> None: ...
def draw_top_banner(ctx: cairo.Context, title: str, subtitle: Optional[str] = None, progress: float = 1.0, theme: str = "light") -> None: ...
def draw_map_route(ctx: cairo.Context, points: Sequence[Tuple[float, float]], progress: float, color: Tuple[float, float, float] = (0.86, 0.15, 0.15), line_width: float = 8.0, dashed: bool = True, show_arrow: bool = True, pulse_time: float = 0.0) -> None: ...
def draw_hud_bars(ctx: cairo.Context, metrics: List[Dict[str, Any]], x: float, y: float, width: float = 880.0, theme: str = "light") -> None: ...
def draw_stat_badge(ctx: cairo.Context, value_text: str, label_text: str, x: float, y: float, scale: float = 1.0, accent_color: Tuple[float, float, float] = (0.98, 0.80, 0.08), theme: str = "light") -> None: ...
def draw_rounded_rect(ctx: cairo.Context, x: float, y: float, w: float, h: float, r: float) -> None: ...
def draw_card_shadow(ctx: cairo.Context, x: float, y: float, w: float, h: float, r: float, blur_layers: int = 4, offset_y: float = 6.0, base_alpha: float = 0.12) -> None: ...
Active-Word Karaoke Captions (engine.captions)
def chunk_words(words: Sequence[Dict[str, Any]], max_words: int = 3, max_chars: int = 18) -> List[Dict[str, Any]]: ...
def draw_karaoke_captions(ctx: cairo.Context, words: Sequence[Dict[str, Any]], current_time: float, center_x: float = 540.0, y: float = 1480.0, font_path: Optional[Union[str, Path]] = None, font_size: int = 76, stroke_width: int = 8, active_color: Tuple[int, int, int] = (250, 204, 21), inactive_color: Tuple[int, int, int] = (255, 255, 255), active_scale: float = 1.12) -> None: ...
Voiceover, Sound Effects & Mixing (engine.tts_align, engine.audio_mixer)
def generate_voiceover(script_text: str, output_audio_path: Union[str, Path], output_words_json: Optional[Union[str, Path]] = None, voice: str = "en-US-AndrewNeural", rate: str = "+12%") -> float: ...
def ensure_default_sfx(sfx_dir: Union[str, Path] = "assets/sfx") -> Dict[str, Path]: ...
def get_downloads_dir() -> Path: ...
def mix_final_audio(voiceover_path: Union[str, Path], sfx_events: Sequence[Tuple[float, str]], output_audio_path: Union[str, Path], total_duration: float, include_ambient: bool = True, sfx_dir: Union[str, Path] = "assets/sfx", ffmpeg_bin: Optional[str] = None) -> Path: ...
def mux_video_audio(video_path: Union[str, Path], audio_path: Union[str, Path], final_output_path: Optional[Union[str, Path]] = None, filename: Optional[str] = None, ffmpeg_bin: Optional[str] = None) -> str: ...
5. Autonomous Execution Workflow
When tasked with generating a Short on any topic, execute these steps sequentially:
Step 1: Script Composition (Sentence-Bridge Rule)
Write a tight 65–140 word script following the 4-part formula.
- MUST begin with a sharp Why / How / What question.
- MUST end with a trailing sentence bridge (
"...which is why people still ask,"). - Choose a clear topic slug (e.g.
colosseum_floodorvibranium_physics).
Step 2: Voiceover & Timestamp Generation in scratch/
Keep output/ clean by storing all intermediate files in scratch/:
.venv\Scripts\python -c "from engine import generate_voiceover; generate_voiceover('''<script>''', 'scratch/<slug>_vo.mp3', 'scratch/<slug>_words.json')"
Inspect scratch/<slug>_words.json to determine:
- Exact
audio_duration - Key milestone word timestamps (e.g. where the hook ends, where numbers are spoken, where the pivot and final bridge occur).
Step 3: Scripted Scene Pipeline Generation
Create projects/<slug>.py importing from engine:
- Set
total_duration = audio_dur + 0.05(Zero trailing silence to maintain the instantaneous loop). - Write intermediate video/audio files to
scratch/<slug>_silent.mp4andscratch/<slug>_audio.aac. - Define
draw_anchor_state(ctx, zoom, pan)for the hero diagram. - Use
draw_anchor_statewithzoom = 1.45 -> 1.0viaease_out_cubicfor the opening $0.0\text{s} \to 1.2\text{s}$. - Use
draw_anchor_statewithzoom = 1.0 -> 1.45viaease_in_out_cubicfor the final $1.2\text{s}$ ($t_{\text{close}} \to \text{total_duration}$), ensuring a pixel-identical match cut with frame 0. - Define middle scene transitions matching the actual narration pacing from
scratch/<slug>_words.json. - Map SFX events
[(t1, 'whoosh'), (t2, 'pop'), (t3, 'thud')]to the exact word timestamps. - Implement
draw_frame(ctx, frame_idx, time_sec)rendering dynamic vector visuals +draw_karaoke_captions(ctx, words, time_sec). - Stream frames to
scratch/<slug>_silent.mp4viaVideoRenderer. - Mix audio with
mix_final_audio$\to$scratch/<slug>_audio.aac. - Call
mux_video_audio(silent_video, audio_track, filename="<slug>.mp4"). This automatically saves the finished video directly into the user's systemDownloadsfolder on any OS and returns the absolute path string. - Delete all temporary files from
scratch/(<slug>_vo.mp3,<slug>_words.json,<slug>_silent.mp4,<slug>_audio.aac) so no clutter remains.
Step 4: Execution & Verification
Run the project script:
.venv\Scripts\python projects/<slug>.py
Verify the output in Downloads with ffprobe:
ffprobe -v error -show_entries stream=width,height,r_frame_rate,duration -of default=noprint_wrappers=1 "$HOME/Downloads/<slug>.mp4"
Ensure video is 1080x1920, 60fps, with crisp audio, clean active-word gold karaoke captions, zero trailing silence, and a pixel-identical visual match cut across the loop boundary.
Step 5: Generate Upload Pack & Save <topic_slug>_upload.txt
Save a companion file Downloads/<slug>_upload.txt alongside the video containing the title, description, tags, and pinned comment. Always conclude the final assistant response with the complete, copy-paste-ready Upload Pack.
6. YouTube Shorts Upload Pack Standard
Whenever a Short is generated, the agent MUST ALWAYS save Downloads/<topic_slug>_upload.txt and present the complete, copy-paste-ready Upload Pack at the end of the response:
### 🚀 YouTube Shorts Upload Pack
- **Saved Video File**: `<Full Path to Downloads/<topic_slug>.mp4>`
- **Upload Metadata File**: `<Full Path to Downloads/<topic_slug>_upload.txt>`
#### 📌 Viral Video Title (<= 60 chars)
[Curiosity-driven hook title with 1–2 relevant emojis and #shorts at the end]
Example: Why Roman Concrete Still Heals Itself 🏛️⚡ #shorts
#### 📝 SEO Video Description
[2–3 punchy sentences summarizing the core mystery, scientific mechanism, and key takeaway for search indexing, followed by 8–10 relevant hashtags]
Example:
Modern highways crumble after fifteen years, but ancient Roman roads are still standing strong after two thousand. The secret isn't just heavy stones—it's active volcanic pozzolana ash that chemically recrystallizes and self-heals cracks whenever rainwater seeps in. Here is the genius four-layer engineering fortress behind the Roman Empire's immortal infrastructure.
#shorts #youtubeshorts #romanempire #ancientengineering #historyfacts #scienceexplained #civilengineering #stem #education
#### 🏷️ Copy-Paste Hashtags Line
#shorts #youtubeshorts #[tag3] #[tag4] #[tag5] #[tag6] #[tag7] #[tag8] #[tag9] #[tag10]
#### 💬 Pinned Comment Prompt
[1 polarizing or debate-sparking question related to the video to drive comments, debate, and watch-time loops]
Example: "If Rome had this self-healing concrete 2,000 years ago, why aren't modern roads using it today? Drop your theory below 👇"