Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
161 lines
5.1 KiB
Bash
Executable File
161 lines
5.1 KiB
Bash
Executable File
#!/bin/bash
|
||
# render.sh — Render a pr-walkthrough video from a manifest JSON file using
|
||
# hyperframes. The pipeline:
|
||
# 1. Copy referenced audio/image files into ./assets/
|
||
# 2. Run whisper transcription on each audio file → ./transcripts/
|
||
# 3. Run build.mjs to generate index.html
|
||
# 4. Lint + render via npx hyperframes (1080p/30fps)
|
||
# 5. Downscale + recompress to 1280×720 with ffmpeg → final MP4
|
||
#
|
||
# Usage:
|
||
# ./render.sh <manifest.json> <output.mp4>
|
||
#
|
||
set -euo pipefail
|
||
|
||
MANIFEST="${1:?Usage: render.sh <manifest.json> <output.mp4>}"
|
||
OUTPUT="${2:?Usage: render.sh <manifest.json> <output.mp4>}"
|
||
|
||
# Resolve relative paths
|
||
[[ "$MANIFEST" != /* ]] && MANIFEST="$(pwd)/$MANIFEST"
|
||
[[ "$OUTPUT" != /* ]] && OUTPUT="$(pwd)/$OUTPUT"
|
||
|
||
if [ ! -f "$MANIFEST" ]; then
|
||
echo "Error: Manifest not found: $MANIFEST" >&2
|
||
exit 1
|
||
fi
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
ASSETS_DIR="$SCRIPT_DIR/assets"
|
||
TRANSCRIPTS_DIR="$SCRIPT_DIR/transcripts"
|
||
RENDERS_DIR="$SCRIPT_DIR/renders"
|
||
MANIFEST_DIR="$(dirname "$MANIFEST")"
|
||
|
||
mkdir -p "$ASSETS_DIR" "$TRANSCRIPTS_DIR" "$RENDERS_DIR"
|
||
mkdir -p "$(dirname "$OUTPUT")"
|
||
|
||
echo "=== Rendering walkthrough video ==="
|
||
echo " Manifest: $MANIFEST"
|
||
echo " Output: $OUTPUT"
|
||
echo ""
|
||
|
||
# --- 1. Extract referenced audio/image filenames from the manifest ---------
|
||
REFERENCED_FILES=$(python3 -c "
|
||
import json, sys
|
||
with open(sys.argv[1]) as f:
|
||
m = json.load(f)
|
||
files = set()
|
||
for s in m['slides']:
|
||
if 'audio' in s and s['audio']: files.add(s['audio'])
|
||
if 'src' in s and s['src']: files.add(s['src'])
|
||
print('\n'.join(sorted(files)))
|
||
" "$MANIFEST")
|
||
|
||
# --- 2. Copy referenced files into ./assets/ -------------------------------
|
||
echo " [1/5] Copying assets..."
|
||
# Clear assets to avoid stale files from previous runs.
|
||
rm -rf "$ASSETS_DIR"
|
||
mkdir -p "$ASSETS_DIR"
|
||
for FILE in $REFERENCED_FILES; do
|
||
SRC="$MANIFEST_DIR/$FILE"
|
||
if [ -f "$SRC" ]; then
|
||
cp "$SRC" "$ASSETS_DIR/$FILE"
|
||
else
|
||
echo " Warning: Referenced file not found: $SRC" >&2
|
||
fi
|
||
done
|
||
|
||
# --- 3. Whisper transcribe each audio file (idempotent per file) ----------
|
||
echo " [2/5] Transcribing audio (whisper)..."
|
||
for WAV in "$ASSETS_DIR"/*.wav; do
|
||
[ -f "$WAV" ] || continue
|
||
BASE=$(basename "$WAV" .wav)
|
||
OUT_JSON="$TRANSCRIPTS_DIR/$BASE.json"
|
||
# Skip if already transcribed and source is older
|
||
if [ -f "$OUT_JSON" ] && [ "$OUT_JSON" -nt "$WAV" ]; then
|
||
continue
|
||
fi
|
||
echo " transcribing $BASE..."
|
||
(cd "$SCRIPT_DIR" && npx --yes hyperframes transcribe "assets/$BASE.wav" --json >/dev/null)
|
||
# hyperframes writes to ./transcript.json — move to per-audio location
|
||
if [ -f "$SCRIPT_DIR/transcript.json" ]; then
|
||
mv "$SCRIPT_DIR/transcript.json" "$OUT_JSON"
|
||
fi
|
||
done
|
||
|
||
# --- 4. Generate index.html ------------------------------------------------
|
||
echo " [3/5] Building composition..."
|
||
(cd "$SCRIPT_DIR" && node build.mjs "$MANIFEST")
|
||
|
||
# --- 5. Lint (warn but don't fail) ------------------------------------------
|
||
(cd "$SCRIPT_DIR" && npx --yes hyperframes lint) || {
|
||
echo " Warning: lint reported issues (continuing)" >&2
|
||
}
|
||
|
||
# --- 6. Render at 1080p/30fps with hyperframes -----------------------------
|
||
# hyperframes can hang on telemetry/cleanup after writing the MP4. Run it in
|
||
# the background, wait for the file to appear and stop growing, then kill the
|
||
# process and continue.
|
||
echo " [4/5] Rendering 1080p frames..."
|
||
RENDER_NAME="walkthrough-$$"
|
||
TEMP_RENDER="$RENDERS_DIR/$RENDER_NAME.mp4"
|
||
rm -f "$TEMP_RENDER"
|
||
(cd "$SCRIPT_DIR" && npx --yes hyperframes render \
|
||
-q draft --crf 30 \
|
||
-o "renders/$RENDER_NAME.mp4") >/dev/null 2>&1 &
|
||
RENDER_PID=$!
|
||
|
||
# Poll until the MP4 exists and its size is stable for 3 consecutive checks
|
||
# (file fully written), then kill the parent npx and any child processes.
|
||
PREV_SIZE=-1
|
||
STABLE=0
|
||
while kill -0 "$RENDER_PID" 2>/dev/null; do
|
||
if [ -f "$TEMP_RENDER" ]; then
|
||
SIZE=$(stat -f '%z' "$TEMP_RENDER" 2>/dev/null || echo 0)
|
||
if [ "$SIZE" -gt 0 ] && [ "$SIZE" -eq "$PREV_SIZE" ]; then
|
||
STABLE=$((STABLE + 1))
|
||
if [ "$STABLE" -ge 3 ]; then break; fi
|
||
else
|
||
STABLE=0
|
||
fi
|
||
PREV_SIZE=$SIZE
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
# Reap any lingering hyperframes/node children, then the npx wrapper.
|
||
pkill -P "$RENDER_PID" 2>/dev/null || true
|
||
kill "$RENDER_PID" 2>/dev/null || true
|
||
wait "$RENDER_PID" 2>/dev/null || true
|
||
|
||
if [ ! -f "$TEMP_RENDER" ]; then
|
||
echo "Error: hyperframes did not produce $TEMP_RENDER" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# --- 7. Downscale 1080p → 720p, recompress for smaller file ---------------
|
||
echo " [5/5] Downscaling to 720p / 30fps..."
|
||
ffmpeg -y -i "$TEMP_RENDER" \
|
||
-vf "scale=1280:720:flags=lanczos,fps=30" \
|
||
-c:v libx264 -preset slow -crf 26 -pix_fmt yuv420p \
|
||
-c:a aac -b:a 96k -ar 48000 \
|
||
-movflags +faststart \
|
||
"$OUTPUT" 2>/dev/null
|
||
|
||
rm -f "$TEMP_RENDER"
|
||
|
||
# --- Report ------------------------------------------------------------------
|
||
if [ -f "$OUTPUT" ]; then
|
||
FINAL_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUTPUT" 2>/dev/null || echo "?")
|
||
FINAL_SIZE=$(ls -lh "$OUTPUT" | awk '{print $5}')
|
||
echo ""
|
||
echo "=== Done ==="
|
||
echo " Output: $OUTPUT"
|
||
echo " Resolution: 1280×720"
|
||
echo " FPS: 30"
|
||
echo " Duration: ${FINAL_DUR}s"
|
||
echo " Size: $FINAL_SIZE"
|
||
else
|
||
echo "Error: render failed — output file not created" >&2
|
||
exit 1
|
||
fi
|