#!/usr/bin/env bash # Turn a video into timestamped contact sheets and scene-change frames for visual analysis. # Optional --window START:DURATION arguments create dense 10-fps sheets around transitions. set -euo pipefail usage() { cat <<'EOF' Usage: scripts/analyze_video_frames.sh VIDEO [OUTPUT_DIR] [--window START:DURATION ...] Examples: scripts/analyze_video_frames.sh chapter.mp4 scripts/analyze_video_frames.sh chapter.mp4 /tmp/chapter-analysis \ --window 5.8:2.2 --window 15.8:2.5 Outputs: overview-001.jpg ... 2-fps timestamped contact sheets scenes/scene-001.jpg ... frames selected by scene-change score scene-timestamps.txt scene-change timestamps from FFmpeg showinfo window-START.jpg 10-fps contact sheet for each requested window Environment overrides: OVERVIEW_FPS=2 SCENE_THRESHOLD=0.18 WINDOW_FPS=10 EOF } if [ "$#" -lt 1 ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then usage exit $([ "$#" -lt 1 ] && echo 1 || echo 0) fi video=$1 shift if [ ! -f "$video" ]; then echo "video not found: $video" >&2 exit 1 fi if [ "$#" -gt 0 ] && [[ $1 != --* ]]; then output_dir=$1 shift else base=$(basename "$video") output_dir="/tmp/${base%.*}-frames" fi overview_fps=${OVERVIEW_FPS:-2} scene_threshold=${SCENE_THRESHOLD:-0.18} window_fps=${WINDOW_FPS:-10} windows=() while [ "$#" -gt 0 ]; do case "$1" in --window) [ "$#" -ge 2 ] || { echo "--window needs START:DURATION" >&2; exit 2; } windows+=("$2") shift 2 ;; *) echo "unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done mkdir -p "$output_dir/scenes" # Six-by-six sheets keep individual frames readable while allowing videos of arbitrary length: # tile emits additional overview-NNN images after every 36 sampled frames. ffmpeg -hide_banner -loglevel error -y -i "$video" \ -vf "fps=${overview_fps},scale=320:-1,drawtext=text='%{pts\\:hms}':x=8:y=8:fontsize=20:fontcolor=yellow:borderw=2,tile=6x6:padding=4:margin=4" \ -fps_mode vfr "$output_dir/overview-%03d.jpg" # Keep stderr because showinfo reports the original timestamps there. ffmpeg -hide_banner -y -i "$video" \ -vf "select='gt(scene,${scene_threshold})',showinfo" -fps_mode vfr \ "$output_dir/scenes/scene-%03d.jpg" 2>"$output_dir/scene-showinfo.log" || true sed -n 's/.*pts_time:\([^ ]*\).*/\1/p' "$output_dir/scene-showinfo.log" \ >"$output_dir/scene-timestamps.txt" for window in "${windows[@]}"; do start=${window%%:*} duration=${window#*:} if [ "$start" = "$window" ] || [ -z "$start" ] || [ -z "$duration" ]; then echo "invalid window '$window'; expected START:DURATION" >&2 exit 2 fi safe_start=${start//./_} # A 5x5 sheet covers 2.5 seconds at the default 10 fps. Longer windows naturally emit more sheets. ffmpeg -hide_banner -loglevel error -y -ss "$start" -t "$duration" -i "$video" \ -vf "fps=${window_fps},scale=320:-1,drawtext=text='%{pts\\:hms}':x=6:y=6:fontsize=18:fontcolor=yellow:borderw=2,tile=5x5:padding=3:margin=3" \ -fps_mode vfr "$output_dir/window-${safe_start}-%03d.jpg" done echo "video analysis frames: $output_dir"