agentsclimarketplace

Longform video

Skill vdk888/bubble-skills/longform-video

The Bubble Shop — free, open-source Claude Code skills & plugins by Bubble Invest. One-command install. We're not developers — we built these to run our own family office. Help us improve them.

Install
npx -y skills add vdk888/bubble-skills --skill longform-video

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Produce 1-5 min landscape (1920x1080) video essays from an article, using a fully local Remotion project — React scene components, local TTS voice cloning, Pexels b-roll, kinetic typography, burned-in subtitles, chapter transitions, ambient music. Use when asked to turn an article into a narrated explainer video.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

11.3 KB, as published. Nobody here has run it

SKILL.md — Long-Form Video (Remotion + Local TTS)

Produces 1-5 min landscape (1920×1080) video essays from an article using a fully local, self-contained Remotion project with React scene components, local TTS (VoxCPM2) voice cloning, Pexels b-roll, kinetic typography, burned-in subtitles, chapter transitions, and ambient background music.

This skill creates one standalone Remotion project per video for maximum flexibility, scene-component composition, and zero external API dependencies for voice (TTS runs locally — see the companion local-tts skill).

Adapt the visual tokens (colors, fonts, logo) in src/brand.ts to your own brand before rendering.


When to use

  • Turn a long-form article into a narrated landscape (1920×1080) video essay
  • A video with data viz + b-roll + kinetic typography
  • Clone a narrator's voice for the voiceover (local, offline)
  • A Kurzgesagt / video-essay-style explainer

Do NOT use for vertical short-form (9:16) — this is a landscape template.


Project structure

<project>/
├── package.json              # Remotion 4.x + transitions + google-fonts
├── tsconfig.json             # include src + public/scene_timings.json + script.json
├── remotion.config.ts        # entry point ./src/index.ts
├── script.json               # narration + per-scene specs (see Schema below)
├── batch_tts.py              # single-process VoxCPM generator (model loaded once)
├── build_voice.py            # ffmpeg timeline assembly with mix A+B wpm normalization
├── fetch_broll.py            # Pexels b-roll downloader (reads PEXELS_API_KEY from env)
├── normalize_broll.sh        # Re-encodes b-roll to H.264 baseline, no B-frames, 30fps
├── audio/
│   ├── _ref.wav              # Voice reference (ffmpeg'd from a short narrator clip)
│   ├── <scene>.wav           # Per-scene raw TTS output
│   └── _proc_<scene>.wav     # Speed-adjusted + padded clips (build artifact)
├── public/
│   ├── voice.wav             # Final assembled voice timeline (Remotion via staticFile)
│   ├── music.mp3             # Background ambient track
│   ├── scene_timings.json    # AUDIO-DRIVEN timing data, consumed by Root.tsx
│   └── broll/
│       ├── _raw/             # Pristine Pexels downloads (don't touch)
│       └── <scene>.mp4       # Normalized 1920×1080 @ 30fps H.264 baseline
├── src/
│   ├── index.ts              # registerRoot(RemotionRoot)
│   ├── Root.tsx              # Composition registration — imports scene_timings.json
│   ├── Main.tsx              # TransitionSeries orchestration: scenes + audio + music
│   ├── Backdrop.tsx          # Background wash + dot grid
│   ├── Logo.tsx              # Your logo SVG
│   ├── LogoBadge.tsx         # Corner watermark
│   ├── SubtitleLayer.tsx     # Burned-in pill captions, chunked & kinetic
│   ├── brand.ts              # Brand tokens (colors, fonts) — EDIT THIS
│   └── scenes/
│       ├── TitleScene.tsx
│       ├── StatCounterScene.tsx
│       ├── KineticQuoteScene.tsx
│       ├── BarChartScene.tsx
│       ├── ComparisonScene.tsx
│       ├── ScenarioScene.tsx
│       ├── OutroScene.tsx
│       └── BrollScene.tsx    # Full-bleed video with Ken Burns + chapter banner
└── out/
    └── video.mp4             # Final render

script.json schema

{
  "title": "Main title shown on intro",
  "subtitle": "Intro subtitle",
  "voice_style": "(enthusiastic, passionate)",
  "voice_speed": 1.0,
  "scenes": [
    { "id": "intro", "type": "title", "duration": 6,
      "title": "Multi-line\ntitle",
      "subtitle": "Tagline shown beneath",
      "narration": "Voiced over this scene." },

    { "id": "stat-94", "type": "stat-counter", "duration": 7,
      "value": 94, "suffix": "%",
      "label": "Multi-line\nlabel",
      "source": "Source, 2026",
      "narration": "..." },

    { "id": "wrong-movie", "type": "broll", "duration": 11,
      "broll_query": "factory robotic arm",
      "subtitle": "Chapter banner text",
      "narration": "..." },

    { "id": "chart", "type": "bar-chart", "duration": 14,
      "title": "Chart title",
      "data": [{ "label": "Row", "value": 74.5 }],
      "narration": "..." },

    { "id": "comparison", "type": "comparison", "duration": 12,
      "left":  { "label": "Exposed",  "items": ["Master's", "+47% salary"] },
      "right": { "label": "Protected", "items": ["Cooks", "Mechanics"] },
      "narration": "..." },

    { "id": "quote", "type": "kinetic-quote", "duration": 9,
      "lines": ["Line 1,", "line 2,", "final punch line."],
      "narration": "..." },

    { "id": "scenario", "type": "scenario", "duration": 14,
      "title": "Scenario title",
      "before": { "label": "Today", "value": "10 consultants", "detail": "..." },
      "after":  { "label": "2 years", "value": "3 consultants",  "detail": "..." },
      "punch": "Italic punch line",
      "narration": "..." },

    { "id": "outro", "type": "outro", "duration": 9,
      "title": "Your brand",
      "subtitle": "Your tagline",
      "url": "yoursite.example",
      "narration": "..." }
  ]
}

Workflow

1. Adapt your article into script.json

Pacing rule:

  • 3-4 spoken words per second of scene at voice_speed 1.0
  • Keep each scene's narration within budget (e.g. a 6s scene ≈ 20-24 words max)
  • Pick scene types: data-anim for numbers/comparisons, broll for narrative beats, kinetic-quote for impact lines

2. Prepare the voice reference

ffmpeg -i /path/to/narrator_sample.mp3 -ar 48000 -ac 1 -y audio/_ref.wav

3. Batch-generate voice clips

~/.venvs/voxcpm/bin/python batch_tts.py

Loads the VoxCPM2 model once, generates all clips with cloning + the voice_style prefix, cfg=2.5, 12 steps. RTF ~3x on Apple Silicon.

4. Fetch b-roll from Pexels

export PEXELS_API_KEY="..."   # free key: https://www.pexels.com/api/
python3 fetch_broll.py

Downloads via curl with a User-Agent (Python urllib gets 403 on the Pexels CDN).

5. Normalize b-roll (CRITICAL)

./normalize_broll.sh

Pexels clips come in a wild mix of fps (24, 25, 30, 60, 100) and resolutions (720p to 4K). Remotion's Video component struggles with mismatched fps and H.264 B-frames. This script re-encodes everything to 1920×1080 @ 30fps H.264 baseline, no B-frames, GOP 30 — eliminates stutter. Do not skip this step.

6. Build the voice timeline

python3 build_voice.py
  • Measures per-clip wpm
  • Applies mix A+B normalization: clips in the 150-200 wpm band are untouched (natural variability kept), outliers are brought back to the nearest band edge via ffmpeg atempo (clamped 0.85-1.35x)
  • Adds 0.3s lead silence + 0.5s trailing silence per clip
  • Concatenates into public/voice.wav
  • Writes public/scene_timings.json — this drives Remotion's per-scene durationInFrames (audio-first architecture)

7. Render

npx remotion render Main out/video.mp4 --codec h264 --pixel-format yuv420p --crf 18 --concurrency 4

~5 min for 2 min of 1080p at concurrency=4 on an M-series Mac.


Architecture principles (learned the hard way)

Audio drives timing, not the reverse

build_voice.py generates clips → measures actual durations → writes scene_timings.jsonRoot.tsx imports this to set each scene's durationInFrames. The visual adapts to the audio, not the other way. This avoids per-clip speed-compensation that creates uneven pacing.

Mix A+B voice pacing

VoxCPM2 produces wildly variable tempi (150-315 wpm on identical ref voice, depending on content). Don't force uniform speed — it either butchers the slow clips or leaves the fast ones untouched. Instead:

  • Clips in the 150-200 wpm band: don't touch
  • Outliers: normalize to the nearest band edge, capped at 0.85-1.35x atempo

Short Voice Design prefixes only

Long prefixes (e.g. a 7-adjective style description) cause VoxCPM to generate 2-3x longer audio than expected. Keep prefixes to 2-4 descriptors: (enthusiastic, passionate).

B-roll must be normalized before use

Pexels videos have B-frames, variable fps, and varied resolutions. Remotion's Video tag produces glitchy/stuttery playback on anything not conforming to the composition's fps + H.264 baseline. Always run normalize_broll.sh after fetch_broll.py.

Chapter vs fade transitions

Fade everywhere (10 frames) for smoothness, slide spring (16 frames) at semantic chapter changes. Scene IDs in CHAPTER_BOUNDARIES set in both Root.tsx and Main.tsx trigger slide — alternate direction (from-right/left/bottom) for rhythm.

Typography

Pick one display family and use weight contrast (400 body vs 800 titles vs 900 stats) and aggressive negative letter-spacing (-1 to -14 depending on size) for punchy display text. Define these tokens in src/brand.ts.


Scene component API

Every scene component receives { scene: any } props and reads fields from script.json. Common patterns:

  • Entry animations: spring({ frame, fps, config: { damping: 14, stiffness: 90 } }) + interpolate(s, [0,1], [startY, 0]) for translateY-fade-in
  • Stat number ramp: interpolate(frame, [10, 10+rampFrames], [0,1]) cubic-eased, then post-ramp pulse via Math.sin
  • Stagger: frame - (i * spacing + offset) offsets each element's animation start
  • Ken Burns: scale(1.02 + t * 0.04) where t = frame / durationInFrames

Dependencies

  • Node.js 18+, npm
  • Remotion 4.x suite (cli, transitions, google-fonts, media-utils, animation-utils)
  • Python venv with VoxCPM2 + soundfile (see the local-tts skill)
  • ffmpeg, ffprobe
  • Pexels API key (free; set PEXELS_API_KEY in the env)
  • A short voice-reference clip for cloning
  • A background ambient music track at public/music.mp3

Gotchas cheat sheet

SymptomCauseFix
B-roll stuttersSource fps != 30, B-framesnormalize_broll.sh
Intro voice way faster than restLong Voice Design prefix bloated audioShorten to 2-4 descriptors
One scene feels super rapidVoxCPM produced 250+ wpm rawRegenerate, or let mix A+B clamp it
Uneven voice pacingYou tried to force uniform speedSwitch to mix A+B approach in build_voice.py
Fonts look genericUsing default weight 400Push to 800-900 with LS -1 to -14 for display
Scene too clutteredTrying to animate everythingData/stats = anim; narrative = broll

Delivery checklist

  • Brand tokens set in src/brand.ts
  • B-roll normalized (fps probe = 30, no B-frames)
  • Voice pacing uniform-feeling (no outlier > 205 wpm after build_voice.py)
  • Music ducked appropriately
  • Logo visible on every scene (corner watermark or prominent on key scenes)
  • Subtitles on non-text scenes (stat, comparison, scenario)
  • Transitions: fade default, slide at chapter changes

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.