#!/usr/bin/env bash # Synthetic credits-detection corpus with exact ground truth. # # Every clip is 25fps, 640x360, with audio, and every positive puts credits_start # at exactly frame 1534 = 61.360s. That figure is deliberately not round: at a # 750ms fine-pass interval anchored to the window start, a boundary at 60.000s is # itself a sample point, so a detector sampling on that grid scores a perfect zero # for reasons that have nothing to do with how well it found anything. Off-grid # truth is what makes the reported error the real error. # # Segments are encoded separately and concatenated, so the boundary is a genuine # frame boundary rather than something a filter approximated. set -euo pipefail cd "$(dirname "$0")" OUT=corpus rm -rf "$OUT" parts mkdir -p "$OUT" parts FONT="C\\:/Windows/Fonts/arial.ttf" ENC="-c:v libx264 -preset veryfast -pix_fmt yuv420p -g 50 -r 25 -c:a aac -b:a 96k -ar 48000 -ac 2" Q="-hide_banner -loglevel error -y" # Frame-exact durations. 1534 frames at 25fps is 61.360s. D_PROG=61.36 # programme before the credits D_PROG_A=46.36 # programme before a dark closing scene D_DARK=15.00 # the dark closing scene D_CRED=30.00 D_CRED_SHORT=20.00 D_POST=10.00 D_DARKTAIL=30.00 TRUTH_MS=61360 cat > parts/credits.txt <<'EOF' DIRECTED BY ALEX MERRIWEATHER WRITTEN BY JORDAN HALE PRODUCED BY SAM OKONKWO CAST RILEY BRENNAN DANA VOSS KIT ARMITAGE MARGO SEELEY DIRECTOR OF PHOTOGRAPHY NOOR HADDAD EDITED BY TOBY LINDQVIST MUSIC BY PRIYA RAGHAVAN PRODUCTION DESIGNER ELLIOT SHAW COSTUME DESIGNER FRANCES ADEYEMI CASTING BY WES TANAKA UNIT PRODUCTION MANAGER HANNAH DELACROIX FIRST ASSISTANT DIRECTOR OSCAR BRENNAN EOF # Audio beds. Programme is broadband and non-stationary the way speech is; # credits are a steady two-note pad. The distinction a detector can find here is # stationarity and spectral shape, which is the same distinction real end-credit # music offers against dialogue — synthetic, but not a different mechanism. A_PROG="anoisesrc=color=brown:amplitude=0.35:r=48000,tremolo=f=3.5:d=0.8" A_CRED="sine=frequency=220:r=48000,volume=0.3" # $1 out $2 duration $3 video lavfi $4 audio lavfi [$5 extra -vf] seg() { local extra="${5:-null}" ffmpeg $Q -f lavfi -i "$3" -f lavfi -i "$4" -t "$2" -vf "$extra" $ENC "parts/$1.mp4" } # scrolling credits over a supplied background. $1 out $2 dur $3 vsrc $4 fontcolour credits_over() { seg "$1" "$2" "$3" "$A_CRED" \ "drawtext=fontfile='$FONT':textfile=parts/credits.txt:fontcolor=$4:fontsize=15:line_spacing=10:x=(w-tw)/2:y=h-35*t" } join() { # $1 out name, rest: part names local out="$1"; shift : > parts/list.txt # Relative to the list file's own directory: a POSIX $(pwd) from Git Bash is # not a path native ffmpeg can open. for p in "$@"; do echo "file '$p.mp4'" >> parts/list.txt; done ffmpeg $Q -f concat -safe 0 -i parts/list.txt -c copy "$OUT/$out.mp4" } PROG="testsrc2=s=640x360:r=25" DARKPROG="color=c=#0d0d10:s=640x360:r=25" BLACK="color=c=black:s=640x360:r=25" WHITE="color=c=white:s=640x360:r=25" echo "building parts..." seg prog $D_PROG "$PROG" "$A_PROG" seg progA $D_PROG_A "$PROG" "$A_PROG" seg dark15 $D_DARK "$DARKPROG" "$A_PROG" seg darktail $D_DARKTAIL "$DARKPROG" "$A_PROG" seg post10 $D_POST "$PROG" "$A_PROG" seg progfade $D_PROG "$PROG" "$A_PROG" "fade=t=out:st=59.86:d=1.5" credits_over cred_black $D_CRED "$BLACK" white credits_over cred_short $D_CRED_SHORT "$BLACK" white credits_over cred_over $D_CRED "$PROG" white credits_over cred_white $D_CRED "$WHITE" black # static centred card credits (no scroll) seg cred_static $D_CRED "$BLACK" "$A_CRED" \ "drawtext=fontfile='$FONT':textfile=parts/credits.txt:fontcolor=white:fontsize=13:line_spacing=6:x=(w-tw)/2:y=(h-th)/2" echo "assembling clips..." join hard-cut-black prog cred_black join fade-to-black progfade cred_black join credits-over-footage prog cred_over join bright-credits prog cred_white join static-card-credits prog cred_static join dark-scene-then-credits progA dark15 cred_black join short-credits-postcred prog cred_short post10 join negative-dark-ending progA dark15 darktail # Ground truth travels with the corpus rather than being written into the # harness: a truth table kept apart from the media it describes is one that # silently stops matching when a clip is regenerated. { echo '[' first=1 for f in "$OUT"/*.mp4; do n=$(basename "$f" .mp4) [ $first -eq 1 ] || echo ',' first=0 if [ "$n" = "negative-dark-ending" ]; then printf ' {"clip":"%s","creditsStartMs":-1}' "$n" else printf ' {"clip":"%s","creditsStartMs":%s}' "$n" "$TRUTH_MS" fi done echo; echo ']' } > "$OUT/truth.json" echo printf '%-28s %-9s %s\n' CLIP DURATION TRUTH for f in "$OUT"/*.mp4; do n=$(basename "$f" .mp4) d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f") if [ "$n" = "negative-dark-ending" ]; then t="none"; else t="61.360"; fi printf '%-28s %-9.2f %s\n' "$n" "$d" "$t" done