Files
sky-cam/montage-mvt.sh
T
Claude 009f6b4b64 Add seasonal montage with music/fades/attribution and full-day timelapse
4-seasons.sh:
- Fix per-video target duration: divide by days_in_movement (~30) not
  days_in_season (~90), so concatenated clips sum to the movement's
  music duration
- Force consistent 1280x720 output (scale+pad) to prevent resolution
  mismatches when montage-mvt.sh concatenates across days
- Use ffprobe (not ffmpeg stderr parsing) for reliable duration reading
- Add music-dir fallback to repo-local music/ folder
- Remove the 2× music-doubling workaround (root cause fixed above)

montage-mvt.sh — complete rewrite:
- Proper season/movement detection matching 4-seasons.sh logic
- Step 1: concat all *-final.mp4 clips, normalise to 1280x720
- Step 2: speed-adjust concatenated video to exactly match music duration
- Step 3: merge with Vivaldi movement audio + 2 s video/audio fade in/out
- Step 4: generate 7-second black attribution card (drawtext) crediting
  John Harrison / Wichita State University Chamber Players / FMA /
  CC BY-NC-SA 4.0
- Step 5: concat montage + attribution into final file
- Temp files cleaned up via EXIT trap

fullday-video.sh — new script:
- Full-day timelapse from yesterday's images targeting ~30 minutes
- Per-frame duration = 1800 / num_images (clamped 0.017–10 s/frame)
- 1280x720 output, video-only (no audio)
- Auto-deletes fullday videos older than 10 days

https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
2026-04-18 03:16:27 +00:00

172 lines
8.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# montage-mvt.sh — compile daily sunrise clips into a seasonal movement montage.
# Output: daily clips concatenated, speed-adjusted to match music duration,
# with 2-second video/audio fade in/out, followed by a 7-second attribution card.
#
# 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-NC-SA 4.0 — verify at the URL above before redistribution.
set -euo pipefail
# ── Configuration ──────────────────────────────────────────────────────────────
base_dir="/home/motion/drives/local-2tb"
script_name=$(basename "$(dirname "$(realpath "$0")")" | cut -d'-' -f1)
music_base_dir="$base_dir/music/4 Seasons"
# Fallback: use music/ directory next to this script (repo copy)
[ ! -d "$music_base_dir" ] && music_base_dir="$(dirname "$(realpath "$0")")/music"
TARGET_W=1280
TARGET_H=720
FADE_DUR=2.0
ATTR_DUR=7
# ── Season / movement detection ────────────────────────────────────────────────
day_of_year=$(date +%j | sed 's/^0*//') # strip leading zeros for arithmetic
if [ "$day_of_year" -ge 80 ] && [ "$day_of_year" -le 172 ]; then
season="Spring"; season_start=80; days_in_season=93
elif [ "$day_of_year" -ge 173 ] && [ "$day_of_year" -le 264 ]; then
season="Summer"; season_start=173; days_in_season=92
elif [ "$day_of_year" -ge 265 ] && [ "$day_of_year" -le 355 ]; then
season="Autumn"; season_start=265; days_in_season=91
else
season="Winter"; days_in_season=90
# Winter straddles the year boundary (day 35679)
if [ "$day_of_year" -ge 356 ]; then season_start=356; else season_start=0; fi
fi
# Day within the season (1-based)
if [ "$season" = "Winter" ] && [ "$day_of_year" -lt 80 ]; then
day_of_season=$(( day_of_year + 365 - 355 ))
elif [ "$season" = "Winter" ]; then
day_of_season=$(( day_of_year - 355 ))
else
day_of_season=$(( day_of_year - season_start + 1 ))
fi
# Each season split into three movements of ~30 days each
if [ "$day_of_season" -le 30 ]; then movement_num=1; days_in_movement=30
elif [ "$day_of_season" -le 60 ]; then movement_num=2; days_in_movement=30
else movement_num=3; days_in_movement=$(( days_in_season - 60 ))
fi
echo "Season=$season Mvt=$movement_num DayOfSeason=$day_of_season DaysInMvt=$days_in_movement"
# ── Locate input files ─────────────────────────────────────────────────────────
video_dir="$base_dir/movies/$script_name/$season/Mvt$movement_num"
output_dir="$video_dir/montage"
mkdir -p "$output_dir"
music_file=$(find "$music_base_dir" -type f -iname "*${season}*Mvt*${movement_num}*" | sort | head -n 1)
if [ -z "$music_file" ]; then
echo "ERROR: Music file not found for $season Mvt $movement_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 found in $video_dir"
exit 1
fi
echo "Found ${#daily_clips[@]} daily clips"
# ── Temp-file tracking for cleanup ────────────────────────────────────────────
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 to TARGET_W x TARGET_H ────────────────────
temp_concat=$(mktemp --suffix=.mp4); TMPFILES+=("$temp_concat")
echo "Step 1/5: concatenating ${#daily_clips[@]} clips at ${TARGET_W}x${TARGET_H}..."
ffmpeg -loglevel warning \
-f concat -safe 0 -i "$concat_list" \
-vf "scale=${TARGET_W}:${TARGET_H}:force_original_aspect_ratio=decrease,\
pad=${TARGET_W}:${TARGET_H}:(ow-iw)/2:(oh-ih)/2,setsar=1" \
-c:v libx264 -pix_fmt yuv420p -crf 26 -an -y "$temp_concat"
# ── Step 2: Speed-adjust so total video == music duration ─────────────────────
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/5: speed factor $speed_factor (raw=${concat_dur}s target=${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 26 -an \
-t "$music_duration" -y "$temp_sped"
rm "$temp_concat"
# ── Step 3: Merge with music + video/audio fades ──────────────────────────────
fade_out_start=$(echo "scale=3; $music_duration - $FADE_DUR" | bc)
echo "Step 3/5: merging music + fade in/out (fade=${FADE_DUR}s)..."
temp_main=$(mktemp --suffix=.mp4); TMPFILES+=("$temp_main")
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}[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 192k \
-t "$music_duration" -y "$temp_main"
rm "$temp_sped"
# ── Step 4: Create 7-second attribution card ──────────────────────────────────
echo "Step 4/5: building attribution card..."
# Locate a usable font
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"
# Build drawtext chain (colons in text values escaped as \: → \\: in bash string)
DT="drawtext=fontfile='${FONT_BOLD}':text='The Four Seasons - Antonio Vivaldi':fontcolor=white:fontsize=38:x=(w-text_w)/2:y=h*0.18"
DT="${DT},drawtext=fontfile='${FONT}':text='Performed by John Harrison':fontcolor=0xDDDDDD:fontsize=28:x=(w-text_w)/2:y=h*0.34"
DT="${DT},drawtext=fontfile='${FONT}':text='with the Wichita State University Chamber Players':fontcolor=0xDDDDDD:fontsize=22:x=(w-text_w)/2:y=h*0.44"
DT="${DT},drawtext=fontfile='${FONT}':text='Source\\: Free Music Archive':fontcolor=white:fontsize=21:x=(w-text_w)/2:y=h*0.59"
DT="${DT},drawtext=fontfile='${FONT}':text='freemusicarchive.org':fontcolor=0xFFFF88:fontsize=21:x=(w-text_w)/2:y=h*0.68"
DT="${DT},drawtext=fontfile='${FONT}':text='License\\: CC BY-NC-SA 4.0':fontcolor=0xCCCCCC:fontsize=19:x=(w-text_w)/2:y=h*0.78"
DT="${DT},fade=t=in:st=0:d=1,fade=t=out:st=$((ATTR_DUR - 1)):d=1"
temp_attr=$(mktemp --suffix=.mp4); TMPFILES+=("$temp_attr")
ffmpeg -loglevel warning \
-f lavfi -i "color=c=black:s=${TARGET_W}x${TARGET_H}:r=25:d=${ATTR_DUR}" \
-f lavfi -i "anullsrc=r=44100:cl=stereo" \
-vf "$DT" \
-c:v libx264 -pix_fmt yuv420p -c:a aac -b:a 192k \
-t "$ATTR_DUR" -y "$temp_attr"
# ── Step 5: Concatenate montage + attribution ─────────────────────────────────
echo "Step 5/5: appending attribution and writing final file..."
final_list=$(mktemp --suffix=.txt); TMPFILES+=("$final_list")
printf "file '%s'\n" "$temp_main" "$temp_attr" > "$final_list"
output_file="$output_dir/$(date +%Y-%m-%d)_${season}_Mvt${movement_num}-Montage.mp4"
ffmpeg -loglevel warning \
-f concat -safe 0 -i "$final_list" \
-c copy -y "$output_file"
echo "Done: $output_file"