#!/usr/bin/env bash # # Extract visual and audio snippets from rendered homepage videos. # # Usage: # ./review.sh # recording + recording-mobile # ./review.sh recording ad-01-foo # explicit storyboard slugs # # Outputs land under output/review/current by default. Override REVIEW_DIR # if you want to keep multiple passes side by side. set -euo pipefail cd "$(dirname "$0")" REVIEW_DIR="${REVIEW_DIR:-output/review/current}" mkdir -p "$REVIEW_DIR" if [ "$#" -gt 0 ]; then STORYBOARDS=("$@") else STORYBOARDS=(recording recording-mobile) fi for sb in "${STORYBOARDS[@]}"; do src="output/$sb/recording.mp4" if [ ! -s "$src" ]; then echo "[review] missing rendered video: $src" >&2 exit 1 fi width="$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "$src")" height="$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "$src")" scale=360 poster_t=16 if [ "$height" -gt "$width" ]; then scale=240 poster_t=12 fi ffprobe -v error \ -select_streams v:0 \ -show_entries stream=codec_name,width,height,avg_frame_rate \ -show_entries format=duration,size \ -of default=noprint_wrappers=1 \ "$src" > "$REVIEW_DIR/$sb-ffprobe.txt" ffmpeg -nostdin -y -loglevel warning -i "$src" \ -vf "fps=1/4,scale=${scale}:-1:flags=lanczos,tile=5x3:padding=12:margin=12:color=white" \ -frames:v 1 -update 1 -q:v 2 "$REVIEW_DIR/$sb-contact.jpg" ffmpeg -nostdin -y -loglevel warning -i "$src" -ss "$poster_t" \ -frames:v 1 -update 1 -q:v 2 "$REVIEW_DIR/$sb-postercheck-t${poster_t}.jpg" ffmpeg -nostdin -y -loglevel warning -i "$src" -t 12 \ -vn -ac 1 -ar 24000 "$REVIEW_DIR/$sb-audio-first12.wav" done while IFS=$'\t' read -r sb idx clip_start clip_dur midpoint; do src="output/$sb/recording.mp4" cue="$(printf '%02d' "$idx")" ffmpeg -nostdin -y -loglevel warning -i "$src" -ss "$midpoint" \ -frames:v 1 -update 1 -q:v 2 "$REVIEW_DIR/$sb-cue-$cue-mid.jpg" ffmpeg -nostdin -y -loglevel warning -ss "$clip_start" -i "$src" -t "$clip_dur" \ -c:v libx264 -pix_fmt yuv420p -crf 18 -preset veryfast \ -c:a aac -b:a 128k -movflags +faststart \ "$REVIEW_DIR/$sb-cue-$cue.mp4" ffmpeg -nostdin -y -loglevel warning -ss "$clip_start" -i "$src" -t "$clip_dur" \ -vn -ac 1 -ar 24000 "$REVIEW_DIR/$sb-cue-$cue.wav" done < <(node - "${STORYBOARDS[@]}" <<'NODE' const fs = require('fs'); const storyboards = process.argv.slice(2); const review = process.env.REVIEW_DIR || 'output/review/current'; for (const sb of storyboards) { const narration = JSON.parse(fs.readFileSync(`output/${sb}/narration.json`, 'utf8')); const audioPath = `output/${sb}/audio/index.json`; const audio = fs.existsSync(audioPath) ? JSON.parse(fs.readFileSync(audioPath, 'utf8')) : { items: [] }; const byCue = new Map((audio.items || []).map((item) => [Number(item.cueIndex), item])); const rows = ['cueIndex\tstartS\tendS\tdurationS\tgapBeforeMs\twav\ttext']; narration.cues.forEach((cue, i) => { const item = byCue.get(i) || {}; const startMs = Number(cue.videoTimeMs); const durationMs = Number(item.durationMs || cue.durationMs); const endMs = startMs + durationMs; rows.push([ i, (startMs / 1000).toFixed(3), (endMs / 1000).toFixed(3), (durationMs / 1000).toFixed(3), item.gapBeforeMs ?? '', item.wav ?? '', cue.text, ].join('\t')); console.log([ sb, i, (Math.max(0, startMs - 250) / 1000).toFixed(3), ((durationMs + 500) / 1000).toFixed(3), ((startMs + durationMs / 2) / 1000).toFixed(3), ].join('\t')); }); fs.writeFileSync(`${review}/${sb}-timing.tsv`, rows.join('\n') + '\n'); } NODE ) echo "[review] wrote snippets to $REVIEW_DIR"