Skip to content

Instantly share code, notes, and snippets.

@tcaddy
Last active May 7, 2026 04:21
Show Gist options
  • Select an option

  • Save tcaddy/5cd025fd2711773355408b99a18beb3b to your computer and use it in GitHub Desktop.

Select an option

Save tcaddy/5cd025fd2711773355408b99a18beb3b to your computer and use it in GitHub Desktop.

Infrastructure Videos

Repository for animated infrastructure explainer videos, built using HyperFrames.

Overview

Videos are authored as HTML/CSS/GSAP compositions and rendered to MP4 via a containerized pipeline.

Prerequisites

  • Docker + Docker Compose: All dependencies (Node, Chromium, FFmpeg, Kokoro TTS) are bundled in the provided Dockerfiles.

Compositions

Directory Description Status
proof-of-concept Initial technical validation video. Legacy
renovate-story Explainer for self-hosted Renovate automation. Active

Rendering Pipeline

Execute the following from the videos/ directory:

1. Audio Generation

If the composition includes narration.yaml, generate the TTS assets first:

COMPOSITION=<name> docker compose run --rm generate-audio

2. Timeline Validation

Verify audio durations to synchronize HTML clip timings:

COMPOSITION=<name> docker compose run --rm audio-timeline

3. Rendering

Generate the final MP4 output:

COMPOSITION=<name> docker compose run --rm render

Output artifacts are saved to videos/output/<composition>.mp4.

Development Workflow

When creating or updating a composition:

  1. Define Narration: Update narration.yaml with scene text and voice settings.
  2. Generate Assets: Run the audio generation task to create local .wav files.
  3. Sync Timing: Use the audio-timeline output to set data-start and data-duration on HTML .clip elements.
  4. Animate: Script GSAP timelines in index.html to align with the synced timestamps.
  5. Validate: Run the lint task to ensure HyperFrames compliance before rendering.
#!/usr/bin/env python3
"""
Audio Timeline Calculator for HyperFrames
Managed by Gemini CLI
"""
import math
import yaml
import sys
import soundfile as sf
from pathlib import Path
from typing import Dict, Any
def calculate_timeline() -> None:
config_path = Path("/app/narration.yaml")
if not config_path.exists():
print(f"Error: {config_path} not found.")
sys.exit(1)
config: Dict[str, Any] = yaml.safe_load(config_path.read_text())
gap: float = config.get("gap", 1.0)
offset: float = config.get("offset", 0.5)
print(f"{'Scene File':<35} {'Duration':>9} {'Start':>8} {'End':>8}")
print("-" * 65)
cursor: float = offset
for scene in config.get("scenes", []):
file_path = Path("/app") / scene["file"]
if not file_path.exists():
print(f" {scene['file']:<33} {'MISSING':>9}")
continue
data, sr = sf.read(str(file_path))
duration = round(len(data) / sr, 1)
end = round(cursor + duration, 1)
print(f" {scene['file']:<33} {duration:>8.1f}s {cursor:>7.1f}s {end:>7.1f}s")
# Calculate next start point with gap and rounding
cursor = math.ceil(end) + gap
total_duration = cursor - gap
print("-" * 65)
print(f" {'Total Composition':.<33} {total_duration:>27.1f}s")
print(f"\nRecommended data-duration for root div: {math.ceil(total_duration)}")
if __name__ == "__main__":
calculate_timeline()
services:
preview:
image: hyperframes-render:22.14.0-bookworm
ports:
- "3000:3000"
volumes:
- ./${COMPOSITION:-proof-of-concept}:/app
entrypoint: ["npx", "--yes", "hyperframes@0.5.0"]
command: ["preview", "--host", "0.0.0.0"]
render:
image: hyperframes-render:22.14.0-bookworm
volumes:
- ./${COMPOSITION:-proof-of-concept}:/app
- ./output:/app/output
entrypoint: ["npx", "--yes", "hyperframes@0.5.0"]
command: ["render", "--output", "output/${COMPOSITION:-proof-of-concept}.mp4", "--quality", "high", "--fps", "30"]
lint:
image: hyperframes-render:22.14.0-bookworm
volumes:
- ./${COMPOSITION:-proof-of-concept}:/app
entrypoint: ["npx", "--yes", "hyperframes@0.5.0"]
command: ["lint", "--verbose"]
tts:
image: hyperframes-tts:22.14.0-bookworm
volumes:
- ./${COMPOSITION:-proof-of-concept}:/app
- tts-cache:/root/.cache/hyperframes
entrypoint: ["npx", "--yes", "hyperframes@0.5.0"]
command: ["tts"]
generate-audio:
image: hyperframes-tts:22.14.0-bookworm
volumes:
- ./${COMPOSITION:-proof-of-concept}:/app
- ./generate-audio.py:/generate-audio.py:ro
- tts-cache:/root/.cache/hyperframes
entrypoint: ["python3", "/generate-audio.py"]
audio-timeline:
image: hyperframes-tts:22.14.0-bookworm
volumes:
- ./${COMPOSITION:-proof-of-concept}:/app
- ./audio-timeline.py:/audio-timeline.py:ro
entrypoint: ["python3", "/audio-timeline.py"]
volumes:
tts-cache:
FROM node:22.14.0-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
chromium \
fonts-liberation \
libnss3 \
libatk-bridge2.0-0 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
libgbm1 \
libasound2 \
libpangocairo-1.0-0 \
libgtk-3-0 \
&& rm -rf /var/lib/apt/lists/*
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV PUPPETEER_SKIP_DOWNLOAD=true
RUN npx --yes hyperframes@0.5.0 --help > /dev/null 2>&1 || true
WORKDIR /app
ENTRYPOINT ["npx", "--yes", "hyperframes@0.5.0"]
CMD ["render"]
FROM node:22.14.0-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --break-system-packages kokoro-onnx soundfile pyyaml
RUN npx --yes hyperframes@0.5.0 --help > /dev/null 2>&1 || true
RUN npx --yes hyperframes@0.5.0 tts "warmup" --voice bf_emma --output /tmp/warmup.wav \
&& rm /tmp/warmup.wav
WORKDIR /app
ENTRYPOINT ["npx", "--yes", "hyperframes@0.5.0"]
CMD ["tts"]
#!/usr/bin/env python3
"""
Infrastructure Video Audio Generator
Managed by Gemini CLI
"""
import subprocess
import sys
import yaml
from pathlib import Path
from typing import Dict, List, Any
def generate_audio() -> None:
config_path = Path("/app/narration.yaml")
if not config_path.exists():
print(f"Error: Configuration not found at {config_path}")
sys.exit(1)
config: Dict[str, Any] = yaml.safe_load(config_path.read_text())
voice: str = config.get("voice", "bf_emma")
speed: str = str(config.get("speed", 1.0))
scenes: List[Dict[str, str]] = config.get("scenes", [])
print(f"Starting audio generation using voice: {voice} (speed: {speed})")
for scene in scenes:
output: str = scene["file"]
text: str = scene["text"].strip()
print(f"\nProcessing: {output}")
try:
subprocess.run(
[
"npx", "--yes", "hyperframes@0.5.0", "tts", text,
"--voice", voice, "--speed", speed, "--output", output
],
check=True,
capture_output=True,
text=True
)
except subprocess.CalledProcessError as e:
print(f"Failed to generate {output}: {e.stderr}")
sys.exit(1)
print("\nBatch generation complete.")
if __name__ == "__main__":
generate_audio()
@tcaddy

tcaddy commented May 7, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment