perfect-postcode/video/review.sh
2026-05-26 19:45:13 +01:00

117 lines
3.8 KiB
Bash
Executable file

#!/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"