Skill: nissan/scene-video-pipeline (version 1.0.0)

--- name: scene-video-pipeline version: 1.0.0 description: Produce modular scene-based videos from slide deck source and per-scene narration audio (text manifest + Kokoro). Includes Playwright slide rendering, per-scene clip generation, and final stitch via editable order file for easy reordering/splicing without full rerender. metadata: { "openclaw": { "emoji": "🎬", "requires": { "bins": ["ffmpeg", "ffprobe", "node"], "env": [] }, "primaryEnv": "/Users/loki/.kokoro-venv/bin/python3", "network": { "outbound": true, "reason": "Only needed on first Kokoro run if model weights are not cached. Rendering/stitching is local." } } } --- # Scene Video Pipeline Use this skill when you need movie-style modular production: - per-scene narration - per-scene rendered clips - final assembly from an order file This allows quick cut/splice/reorder/insert changes without regenerating the whole video. ## Directory Contract ``` <module_dir>/ slides/deck.html slides/01.png ... tts/ manifest.csv audio/01.mp3 ... dist/ scenes/01.mp4 ... scene-order.txt module-first-cut.mp4 ``` ## Commands ### 1) Render slides from deck source (Playwright) ```bash # project helper wrapper (solana-academy) projects/solana-academy/content-production/scripts/render-slides.sh <module_dir> ``` ### 2) Generate narration audio (Kokoro local) ```bash scripts/generate_tts_kokoro.sh <module_dir> [voice] [speed] ``` ### 3) Build per-scene clips ```bash scripts/build_scenes.sh <module_dir> ``` ### 4) Stitch final from ordered scene list ```bash scripts/stitch_scenes.sh <module_dir> [order_file] ``` ## Workflow 1. Draft deck source (`slides/deck.html`) and scripts. 2. Render slide PNGs via Playwright. 3. Generate audio from manifest. 4. Build scene clips. 5. QA scene-by-scene. 6. Adjust `scene-order.txt` for narrative edits. 7. Stitch final cut. ## Notes - Keep scene filenames zero-padded (`01.mp4`, `02.mp4`) to preserve stable sorting. - For reorder-only edits, rerun stitch only. - For a single content fix, rerender only the affected scene clip and restitch. ## Files - `scripts/generate_tts_kokoro.sh` - `scripts/build_scenes.sh` - `scripts/stitch_scenes.sh`

path: scripts/build_scenes.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Usage:
#   build_scenes.sh <module_dir>
# Expects:
#   <module_dir>/slides/NN.png
#   <module_dir>/tts/audio/NN.mp3
# Produces:
#   <module_dir>/dist/scenes/NN.mp4
#   <module_dir>/dist/scene-order.txt (if missing)

MODULE_DIR="${1:?module_dir required}"
SLIDES_DIR="$MODULE_DIR/slides"
AUDIO_DIR="$MODULE_DIR/tts/audio"
DIST_DIR="$MODULE_DIR/dist"
SCENES_DIR="$DIST_DIR/scenes"
ORDER_FILE="$DIST_DIR/scene-order.txt"

mkdir -p "$SCENES_DIR"

for audio in "$AUDIO_DIR"/*.mp3; do
  [ -e "$audio" ] || { echo "No audio files found in $AUDIO_DIR"; exit 1; }
  base="$(basename "$audio" .mp3)"
  slide="$SLIDES_DIR/${base}.png"
  out="$SCENES_DIR/${base}.mp4"

  [ -f "$slide" ] || { echo "Missing slide image: $slide"; exit 1; }

  duration="$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$audio")"

  ffmpeg -y \
    -loop 1 -framerate 30 -t "$duration" -i "$slide" \
    -i "$audio" \
    -c:v libx264 -pix_fmt yuv420p -r 30 \
    -c:a aac -b:a 192k -shortest \
    "$out" >/dev/null 2>&1

  echo "Built scene: $out"
done

if [ ! -f "$ORDER_FILE" ]; then
  ls "$SCENES_DIR"/*.mp4 | sort | xargs -n1 basename > "$ORDER_FILE"
  echo "Created default scene order: $ORDER_FILE"
fi

echo "Scene build complete: $SCENES_DIR"


path: scripts/generate_tts_kokoro.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Usage:
#   generate_tts_kokoro.sh <module_dir> [voice] [speed]
# Expects:
#   <module_dir>/tts/manifest.csv with columns: slide,audio_file,text
# Produces:
#   <module_dir>/tts/audio/*.mp3

MODULE_DIR="${1:?module_dir required}"
VOICE="${2:-af_sarah}"
SPEED="${3:-1.0}"
MANIFEST="$MODULE_DIR/tts/manifest.csv"
KOKORO_PY="/Users/loki/.kokoro-venv/bin/python3"

if [ ! -x "$KOKORO_PY" ]; then
  echo "Kokoro runtime missing: $KOKORO_PY"
  exit 1
fi

"$KOKORO_PY" - "$MANIFEST" "$MODULE_DIR" "$VOICE" "$SPEED" <<'PY'
import csv
import subprocess
import sys
from pathlib import Path

import numpy as np
import soundfile as sf
from kokoro import KPipeline

manifest = Path(sys.argv[1])
module_dir = Path(sys.argv[2])
voice = sys.argv[3]
speed = float(sys.argv[4])

if not manifest.exists():
    raise SystemExit(f"Manifest not found: {manifest}")

lang_code = voice[0] if voice and voice[0] in 'abefhijpz' else 'a'
pipeline = KPipeline(lang_code=lang_code, repo_id='hexgrad/Kokoro-82M')

with manifest.open(newline='', encoding='utf-8') as f:
    rows = list(csv.DictReader(f))

for row in rows:
    slide = row['slide'].strip()
    rel_out = row['audio_file'].strip()
    text = row['text'].strip()

    mp3_out = module_dir / 'tts' / rel_out
    wav_out = mp3_out.with_suffix('.wav')
    mp3_out.parent.mkdir(parents=True, exist_ok=True)

    print(f"Generating slide {slide} -> {mp3_out}")

    chunks = []
    for _, _, audio in pipeline(text, voice=voice, speed=speed):
        chunks.append(audio)

    if not chunks:
        raise RuntimeError(f"No audio generated for slide {slide}")

    combined = np.concatenate(chunks)
    sf.write(str(wav_out), combined, 24000)

    subprocess.run([
        'ffmpeg', '-y', '-i', str(wav_out), '-codec:a', 'libmp3lame', '-b:a', '192k', str(mp3_out)
    ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    wav_out.unlink(missing_ok=True)

print(f"TTS generation complete: {module_dir / 'tts' / 'audio'}")

# Avoid intermittent torch teardown SIGBUS after successful generation.
import os
os._exit(0)
PY


path: scripts/stitch_scenes.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Usage:
#   stitch_scenes.sh <module_dir> [order_file]
# Expects:
#   <module_dir>/dist/scenes/*.mp4
#   order file lines: NN.mp4
# Produces:
#   <module_dir>/dist/module-first-cut.mp4

MODULE_DIR="${1:?module_dir required}"
DIST_DIR="$MODULE_DIR/dist"
SCENES_DIR="$DIST_DIR/scenes"
ORDER_FILE="${2:-$DIST_DIR/scene-order.txt}"
CONCAT_FILE="$DIST_DIR/concat-scenes.txt"
OUT_FILE="$DIST_DIR/module-first-cut.mp4"

[ -d "$SCENES_DIR" ] || { echo "Missing scenes dir: $SCENES_DIR"; exit 1; }
[ -f "$ORDER_FILE" ] || { echo "Missing scene order file: $ORDER_FILE"; exit 1; }

: > "$CONCAT_FILE"
while IFS= read -r line; do
  line="${line%%#*}"
  line="$(echo "$line" | xargs)"
  [ -z "$line" ] && continue

  scene="$SCENES_DIR/$line"
  [ -f "$scene" ] || { echo "Scene listed but missing: $scene"; exit 1; }
  echo "file '$scene'" >> "$CONCAT_FILE"
done < "$ORDER_FILE"

ffmpeg -y -f concat -safe 0 -i "$CONCAT_FILE" -c copy "$OUT_FILE" >/dev/null 2>&1

echo "Built final: $OUT_FILE"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)