Adds six CRF vars, AUDIO_BITRATE, and three MONTAGE_* overlay durations. Values match previous hardcoded defaults so behavior is unchanged. The same CRF appeared in several scripts but with intentionally different values per purpose (28 for draft intermediates, 26 for polished output). Each script's CRF is now its own variable in conf, so changing one never silently changes another. https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
178 lines
8.9 KiB
Bash
Executable File
178 lines
8.9 KiB
Bash
Executable File
#!/bin/bash
|
|
# montage-mvt.sh — compile one Vivaldi movement's daily clips into a montage.
|
|
#
|
|
# Duration matches the movement's music exactly. Attribution is overlaid as a
|
|
# translucent bar across the top of the video for the first ATTR_DUR seconds
|
|
# (fade in/out) — no separate card appended, so year-end joining is a plain
|
|
# concat with no duplicate attribution concerns.
|
|
#
|
|
# Called automatically by 4-seasons.sh on the last day of each movement.
|
|
# Can also be run manually for a mid-season preview.
|
|
#
|
|
# On the last day of Autumn Mvt 3, automatically triggers year-end-join.sh.
|
|
#
|
|
# Music: "The Four Seasons" by Antonio Vivaldi
|
|
# Performed by John Harrison with the Wichita State University Chamber Players
|
|
# Source: Free Music Archive — freemusicarchive.org
|
|
# License: CC BY-SA 3.0
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|
source "$SCRIPT_DIR/sky-cam.conf"
|
|
export TIMEZONE # make it visible to season_info.py subprocess
|
|
|
|
# Args: [date] [camera-name]
|
|
# Camera name: second argument overrides conf (passed through from 4-seasons.sh).
|
|
CAM_NAME="${2:-$CAM_NAME}"
|
|
|
|
# ── Configuration ──────────────────────────────────────────────────────────────
|
|
base_dir="$BASE_DIR"
|
|
music_base_dir="$MUSIC_DIR"
|
|
[ ! -d "$music_base_dir" ] && music_base_dir="$SCRIPT_DIR/music"
|
|
|
|
# Fade/overlay durations come from sky-cam.conf (MONTAGE_FADE_DUR,
|
|
# MONTAGE_ATTR_DUR, MONTAGE_ATTR_FADE). Alias for shorter local use.
|
|
FADE_DUR="$MONTAGE_FADE_DUR"
|
|
ATTR_DUR="$MONTAGE_ATTR_DUR"
|
|
ATTR_FADE="$MONTAGE_ATTR_FADE"
|
|
|
|
# ── Season / movement info ────────────────────────────────────────────────────
|
|
DATE_ARG="${1:-}"
|
|
eval "$(python3 "$SCRIPT_DIR/season_info.py" ${DATE_ARG:+"$DATE_ARG"})"
|
|
# Provides: SEASON MVT_NUM DAY_OF_MVT DAYS_IN_MVT MVT_START MVT_END
|
|
# IS_LAST_DAY ASTRO_YEAR
|
|
echo "Season=$SEASON Mvt=$MVT_NUM Year=$ASTRO_YEAR (ref: ${DATE_ARG:-today})"
|
|
|
|
# ── Locate files ──────────────────────────────────────────────────────────────
|
|
video_dir="$MOVIES_DIR/$CAM_NAME/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM"
|
|
output_dir="$video_dir/montage"
|
|
mkdir -p "$output_dir"
|
|
|
|
music_file=$(find "$music_base_dir" -type f -iname "*${SEASON}*Mvt*${MVT_NUM}*" | sort | head -n 1)
|
|
if [ -z "$music_file" ]; then
|
|
echo "ERROR: Music file not found for $SEASON Mvt $MVT_NUM in $music_base_dir"
|
|
exit 1
|
|
fi
|
|
music_duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$music_file")
|
|
echo "Music: $(basename "$music_file") (${music_duration}s)"
|
|
|
|
# ── Collect sorted daily clips ────────────────────────────────────────────────
|
|
mapfile -d '' daily_clips < <(find "$video_dir" -maxdepth 1 -name "*-final.mp4" -print0 | sort -z)
|
|
if [ "${#daily_clips[@]}" -eq 0 ]; then
|
|
echo "ERROR: No *-final.mp4 clips in $video_dir"
|
|
exit 1
|
|
fi
|
|
echo "Found ${#daily_clips[@]} daily clips"
|
|
|
|
# ── Source resolution ─────────────────────────────────────────────────────────
|
|
VIDEO_W=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "${daily_clips[0]}")
|
|
VIDEO_H=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of csv=p=0 "${daily_clips[0]}")
|
|
echo "Source resolution: ${VIDEO_W}x${VIDEO_H}"
|
|
|
|
# ── Font detection ────────────────────────────────────────────────────────────
|
|
FONT=""; FONT_BOLD=""
|
|
if command -v fc-match &>/dev/null; then
|
|
FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true)
|
|
FONT_BOLD=$(fc-match "DejaVu Sans:style=Bold" --format="%{file}" 2>/dev/null || true)
|
|
fi
|
|
if [ -z "$FONT" ]; then
|
|
for f in \
|
|
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf \
|
|
/usr/share/fonts/dejavu/DejaVuSans.ttf \
|
|
/usr/share/fonts/TTF/DejaVuSans.ttf \
|
|
/usr/share/fonts/truetype/freefont/FreeSans.ttf; do
|
|
[ -f "$f" ] && { FONT="$f"; break; }
|
|
done
|
|
fi
|
|
[ -z "$FONT_BOLD" ] && FONT_BOLD="$FONT"
|
|
|
|
# ── Temp files ────────────────────────────────────────────────────────────────
|
|
TMPFILES=()
|
|
cleanup() { rm -f "${TMPFILES[@]}" 2>/dev/null || true; }
|
|
trap cleanup EXIT
|
|
|
|
concat_list=$(mktemp --suffix=.txt); TMPFILES+=("$concat_list")
|
|
printf "file '%s'\n" "${daily_clips[@]}" > "$concat_list"
|
|
|
|
# ── Step 1: Concatenate + normalise resolution ────────────────────────────────
|
|
temp_concat=$(mktemp --suffix=.mp4); TMPFILES+=("$temp_concat")
|
|
echo "Step 1/3: concatenating ${#daily_clips[@]} clips..."
|
|
ffmpeg -loglevel warning \
|
|
-f concat -safe 0 -i "$concat_list" \
|
|
-vf "scale=${VIDEO_W}:${VIDEO_H}:force_original_aspect_ratio=decrease,\
|
|
pad=${VIDEO_W}:${VIDEO_H}:(ow-iw)/2:(oh-ih)/2,setsar=1" \
|
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_MONTAGE" -an \
|
|
-y "$temp_concat"
|
|
|
|
# ── Step 2: Speed-adjust to match music duration exactly ──────────────────────
|
|
concat_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$temp_concat")
|
|
speed_factor=$(echo "scale=6; $concat_dur / $music_duration" | bc)
|
|
echo "Step 2/3: speed $speed_factor (raw=${concat_dur}s → music=${music_duration}s)"
|
|
|
|
temp_sped=$(mktemp --suffix=.mp4); TMPFILES+=("$temp_sped")
|
|
ffmpeg -loglevel warning \
|
|
-i "$temp_concat" \
|
|
-vf "setpts=PTS/$speed_factor" \
|
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_MONTAGE" -an \
|
|
-t "$music_duration" -y "$temp_sped"
|
|
rm "$temp_concat"
|
|
|
|
# ── Step 3: Music + fades + attribution overlay ───────────────────────────────
|
|
# Attribution overlay: semi-transparent bar across the top for the first
|
|
# ATTR_DUR seconds, text fades in over ATTR_FADE s and out over ATTR_FADE s.
|
|
# alpha expression: 0→1 over first ATTR_FADE s, 1 in the middle, 1→0 over
|
|
# the last ATTR_FADE s before ATTR_DUR.
|
|
ATTR_FADE_END=$(echo "$ATTR_DUR - $ATTR_FADE" | bc)
|
|
ALPHA="if(lt(t,${ATTR_FADE}),t/${ATTR_FADE},if(gt(t,${ATTR_FADE_END}),(${ATTR_DUR}-t)/${ATTR_FADE},1))"
|
|
ENABLE="between(t,0,${ATTR_DUR})"
|
|
|
|
fade_out_start=$(echo "scale=3; $music_duration - $FADE_DUR" | bc)
|
|
echo "Step 3/3: music + fades + attribution overlay..."
|
|
|
|
# Build attribution drawtext filters (box=1 gives a per-line background).
|
|
# Colons inside text values are escaped as \: (\\: in bash double-quoted strings).
|
|
DT="drawtext=fontfile='${FONT_BOLD}'"
|
|
DT="${DT}:text='The Four Seasons \: Antonio Vivaldi'"
|
|
DT="${DT}:fontcolor=white:fontsize=h/22:x=(w-text_w)/2:y=h*0.012"
|
|
DT="${DT}:box=1:boxcolor=black@0.55:boxborderw=8"
|
|
DT="${DT}:alpha='${ALPHA}':enable='${ENABLE}'"
|
|
|
|
DT="${DT},drawtext=fontfile='${FONT}'"
|
|
DT="${DT}:text='John Harrison / Wichita State University Chamber Players'"
|
|
DT="${DT}:fontcolor=0xEEEEEE:fontsize=h/30:x=(w-text_w)/2:y=h*0.068"
|
|
DT="${DT}:box=1:boxcolor=black@0.55:boxborderw=6"
|
|
DT="${DT}:alpha='${ALPHA}':enable='${ENABLE}'"
|
|
|
|
DT="${DT},drawtext=fontfile='${FONT}'"
|
|
DT="${DT}:text='Free Music Archive \: freemusicarchive.org \: CC BY-SA 3.0'"
|
|
DT="${DT}:fontcolor=0xCCCCCC:fontsize=h/36:x=(w-text_w)/2:y=h*0.113"
|
|
DT="${DT}:box=1:boxcolor=black@0.55:boxborderw=5"
|
|
DT="${DT}:alpha='${ALPHA}':enable='${ENABLE}'"
|
|
|
|
output_file="$output_dir/${MVT_START}_${SEASON}_Mvt${MVT_NUM}-Montage.mp4"
|
|
|
|
ffmpeg -loglevel warning \
|
|
-i "$temp_sped" -i "$music_file" \
|
|
-filter_complex \
|
|
"[0:v]fade=t=in:st=0:d=${FADE_DUR},fade=t=out:st=${fade_out_start}:d=${FADE_DUR},${DT}[vout];\
|
|
[1:a]afade=t=in:st=0:d=${FADE_DUR},afade=t=out:st=${fade_out_start}:d=${FADE_DUR}[aout]" \
|
|
-map "[vout]" -map "[aout]" \
|
|
-c:v libx264 -pix_fmt yuv420p -c:a aac -b:a "$AUDIO_BITRATE" \
|
|
-t "$music_duration" -y "$output_file"
|
|
rm "$temp_sped"
|
|
|
|
echo "Done: $output_file"
|
|
|
|
# ── Notify ────────────────────────────────────────────────────────────────────
|
|
"$SCRIPT_DIR/notify.sh" \
|
|
"Montage ready: $SEASON Mvt $MVT_NUM ($ASTRO_YEAR)" \
|
|
"$(basename "$output_file") — ${music_duration}s" \
|
|
|| true # notifications are best-effort
|
|
|
|
# ── Trigger year-end join on last Autumn movement ────────────────────────────
|
|
if [ "$SEASON" = "Autumn" ] && [ "$MVT_NUM" = "3" ]; then
|
|
echo "Last movement of astronomical year $ASTRO_YEAR — triggering year-end join..."
|
|
"$SCRIPT_DIR/year-end-join.sh" "$ASTRO_YEAR" "$CAM_NAME"
|
|
fi
|