← back to Restaurant Directory

videos/build-before-after.sh

116 lines

#!/usr/bin/env bash
#
# lacountyeats — before/after video builder.
#
# Renders a 1280x720 MP4 with:
#   - Title card  (3s) — "lacountyeats — before & after"
#   - Light-mode comparison (8s)  — current production | proposed mockup, side-by-side, labelled
#   - Dark-mode comparison  (8s)  — same in dark mode
#   - Findings card (5s) — bullet-list of changes (CSP fix, NEW openings, dark toggle)
#   - Outro card (2s) — "Loop continues — every 25-35 min"
# Steve avatar (talking states) animates in the bottom-right throughout.
# Narration via macOS `say` → AIFF → re-encoded by ffmpeg.
#
# Inputs (defaults; override via env):
#   ITER_DIR  = iter dir holding the playwright screenshots
#   OUT_DIR   = where to write the final mp4
#   AVATAR_FRONT, AVATAR_TALK1, AVATAR_TALK2 = avatar PNGs (square, ≤256px)
#
set -euo pipefail
cd "$(dirname "$0")"

ITER_DIR="${ITER_DIR:-/Users/macstudio3/Projects/restaurant-directory/iterations/20260503T215435Z-detail}"
OUT_DIR="${OUT_DIR:-/Users/macstudio3/Projects/restaurant-directory/videos}"
TS=$(date -u +%Y%m%dT%H%M%SZ)
WORK="$OUT_DIR/_build_$TS"
mkdir -p "$WORK"

AVATAR_FRONT="${AVATAR_FRONT:-$OUT_DIR/steve-avatar-front.png}"
AVATAR_TALK1="${AVATAR_TALK1:-$OUT_DIR/steve-avatar-talking-1.png}"
AVATAR_TALK2="${AVATAR_TALK2:-$OUT_DIR/steve-avatar-talking-2.png}"

# Verify required inputs
for f in \
  "$ITER_DIR/home-live-light.png" \
  "$ITER_DIR/home-live-dark.png" \
  "$ITER_DIR/home-mockup-iter1-light.png" \
  "$ITER_DIR/home-mockup-iter1-dark.png" \
  "$AVATAR_FRONT" "$AVATAR_TALK1" "$AVATAR_TALK2"; do
  [ -f "$f" ] || { echo "MISSING: $f" >&2; exit 2; }
done

# ---- 1. Narration script -----------------------------------------------------
NARRATION="$WORK/narration.txt"
cat > "$NARRATION" <<'EOF'
lacountyeats dot com. Before, and after.

The current production site, in light mode and dark mode. Cream on ink, every restaurant in LA County.

Now the proposed iteration. Same brand floor, but with a working dark mode toggle, a quote stack hero, and a newest-in-LA section that surfaces brand-new openings the moment they hit the press.

Three deploys queued. The dark toggle. Four newly-opened restaurants from this weekend's Eater LA coverage. And a real production bug fix — the live map page was blocking Leaflet's stylesheet at the content security policy. Now patched.

The loop continues. Every twenty-five to thirty-five minutes, a new mockup variant gets generated, scored, and tested against the live site. This is build number one.
EOF

# Generate audio per line for visual sync; macOS `say` writes AIFF.
SAY_VOICE="${SAY_VOICE:-Daniel}"   # crisp UK voice; swap to Samantha/Alex if preferred
say -v "$SAY_VOICE" -r 175 -f "$NARRATION" -o "$WORK/narration.aiff"
ffmpeg -hide_banner -loglevel error -y -i "$WORK/narration.aiff" -ac 2 -ar 48000 -b:a 192k "$WORK/narration.m4a"
DURATION=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$WORK/narration.m4a")
DURATION_INT=$(printf "%.0f" "$DURATION")
echo "narration: ${DURATION_INT}s"

# ---- 2. ALL cards via Playwright (ffmpeg drawtext NA in 8.1) ----
( cd /Users/macstudio3/Projects/restaurant-directory && node videos/render-cards.mjs "$WORK" "$ITER_DIR" )

# ---- 4. Stitch the cards into a silent video, then mux narration + avatar overlay ----
# Card durations (sec): title 3, sxs-light 8, sxs-dark 8, findings 5, outro 2  =  26s
# narration may be shorter; we set -shortest at the end.
CARDS=( "$WORK/title.png:3" "$WORK/sxs-light.png:8" "$WORK/sxs-dark.png:8" "$WORK/findings.png:5" "$WORK/outro.png:2" )
CONCAT_LIST="$WORK/concat.txt"
> "$CONCAT_LIST"
for c in "${CARDS[@]}"; do
  IFS=':' read -r img dur <<< "$c"
  echo "file '$img'" >> "$CONCAT_LIST"
  echo "duration $dur" >> "$CONCAT_LIST"
done
# concat demuxer requires last image repeated without duration
# (use ${arr[${#arr[@]}-1]} not ${arr[-1]} — macOS bash 3.2 lacks negative indices)
LAST_CARD="${CARDS[${#CARDS[@]}-1]}"
echo "file '${LAST_CARD%:*}'" >> "$CONCAT_LIST"

ffmpeg -hide_banner -loglevel error -y -f concat -safe 0 -i "$CONCAT_LIST" \
  -fps_mode cfr -r 30 -pix_fmt yuv420p -c:v libx264 -preset medium -crf 20 \
  "$WORK/silent.mp4"

# ---- 5. Animate avatar (alternate front/talk1/talk2 every 0.4s) -------------
# Build a short looping avatar video at 25fps that we'll overlay.
ffmpeg -hide_banner -loglevel error -y \
  -loop 1 -t 0.4 -i "$AVATAR_FRONT" \
  -loop 1 -t 0.4 -i "$AVATAR_TALK1" \
  -loop 1 -t 0.4 -i "$AVATAR_TALK2" \
  -filter_complex "[0:v][1:v][2:v]concat=n=3:v=1:a=0[v]" -map "[v]" -r 25 -pix_fmt yuva420p "$WORK/avatar-loop.mov"

# Composite avatar in bottom-right at 110px, gated by time (HIDDEN during sxs panels
# so it doesn't occlude the grade-badge column). Cards run: title 0-3s, sxs-light 3-11s,
# sxs-dark 11-19s, findings 19-24s, outro 24-26s. Avatar shown only during title/findings/outro.
# Post-build review (33/60) flagged 170px avatar covering grade badges — fix per review.
ffmpeg -hide_banner -loglevel error -y \
  -i "$WORK/silent.mp4" \
  -stream_loop -1 -i "$WORK/avatar-loop.mov" \
  -i "$WORK/narration.m4a" \
  -filter_complex "[1:v]scale=110:110[av]; [0:v][av]overlay=W-w-20:H-h-20:enable='lt(t,3)+gte(t,19)':shortest=1[outv]" \
  -map "[outv]" -map 2:a:0 \
  -c:v libx264 -preset medium -crf 20 -pix_fmt yuv420p \
  -c:a aac -b:a 192k -shortest \
  "$OUT_DIR/lacountyeats-before-after-${TS}.mp4"

FINAL="$OUT_DIR/lacountyeats-before-after-${TS}.mp4"
SIZE=$(stat -f %z "$FINAL")
DUR=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$FINAL")
echo ""
echo "DONE: $FINAL"
echo "size: $((SIZE/1024)) KB · duration: ${DUR}s"
ls -la "$FINAL"