Files
memby/server/internal/credits/testdata/gen_corpus.sh
T
2026-08-18 08:41:48 +12:00

149 lines
5.0 KiB
Bash

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