Short adagio movements (Summer Mvt 2 at 107s, Winter Mvt 2 at 132s, etc.) compress each day to only 3-5 seconds of screen time at 1×. Looping the music N times multiplies the per-day target duration proportionally so the footage is less aggressively sped up and more watchable. - sky-cam.conf: add MONTAGE_MUSIC_LOOPS and per-movement overrides (Summer Mvt2=3×, Winter Mvt2=3×, Autumn Mvt2=2×, Summer Mvt3=2×, Spring Mvt2=2×) - 4-seasons.sh: multiply target_per_clip by loops - montage-mvt.sh: compute effective_duration, use -stream_loop for audio, fix fade_out_start, -t, and post-build duration check - make-finals.sh: multiply target by loops https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
148 lines
4.4 KiB
Bash
Executable File
148 lines
4.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# make-finals.sh — one-off recovery script.
|
|
#
|
|
# Scans ALL *-temp.mp4 files under a backup root, infers the camera name from
|
|
# the path, uses season_info.py to get the correct movement info for each
|
|
# file's date, speed-adjusts to the right target duration, and writes
|
|
# *-final.mp4 files into MOVIES_DIR (from sky-cam.conf / .env).
|
|
#
|
|
# Safe to run multiple times — skips any final that already exists.
|
|
# Never modifies the backup.
|
|
#
|
|
# Usage:
|
|
# ./make-finals.sh <backup_root> [--dry-run]
|
|
#
|
|
# Example:
|
|
# ./make-finals.sh ~/drives/data2-main --dry-run
|
|
# ./make-finals.sh ~/drives/data2-main
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|
source "$SCRIPT_DIR/sky-cam.conf"
|
|
export TIMEZONE
|
|
|
|
BACKUP_ROOT=""
|
|
DRY_RUN=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
-*) echo "Unknown option: $1"; exit 1 ;;
|
|
*) BACKUP_ROOT="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$BACKUP_ROOT" ]; then
|
|
echo "Usage: $0 <backup_root> [--dry-run]"
|
|
echo " e.g. $0 ~/drives/data2-main --dry-run"
|
|
exit 1
|
|
fi
|
|
|
|
BACKUP_ROOT="$(realpath "$BACKUP_ROOT")"
|
|
music_base_dir="$MUSIC_DIR"
|
|
[ ! -d "$music_base_dir" ] && music_base_dir="$SCRIPT_DIR/music"
|
|
|
|
$DRY_RUN && echo "DRY RUN — no files will be written"
|
|
echo "Scanning: $BACKUP_ROOT"
|
|
echo "Output: $MOVIES_DIR"
|
|
echo ""
|
|
|
|
# Known camera names in order of specificity (longer matches first)
|
|
KNOWN_CAMS=(sunrise north south east west)
|
|
|
|
infer_cam() {
|
|
local path="$1"
|
|
for cam in "${KNOWN_CAMS[@]}"; do
|
|
if echo "$path" | grep -qE "(^|/)${cam}(/|$)"; then
|
|
echo "$cam"
|
|
return
|
|
fi
|
|
done
|
|
echo ""
|
|
}
|
|
|
|
processed=0; skipped=0; errors=0
|
|
|
|
while IFS= read -r -d '' temp_file; do
|
|
filename="$(basename "$temp_file")"
|
|
date_str="${filename%%_*}"
|
|
|
|
if ! [[ "$date_str" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
|
|
echo "SKIP (bad name): $temp_file"
|
|
(( skipped++ )) || true
|
|
continue
|
|
fi
|
|
|
|
cam=$(infer_cam "${temp_file#$BACKUP_ROOT/}")
|
|
if [ -z "$cam" ]; then
|
|
echo "SKIP (unknown cam): $temp_file"
|
|
(( skipped++ )) || true
|
|
continue
|
|
fi
|
|
|
|
_info="$(python3 "$SCRIPT_DIR/season_info.py" "$date_str" 2>/dev/null)" || {
|
|
echo "SKIP (season_info failed $date_str): $filename"
|
|
(( skipped++ )) || true
|
|
continue
|
|
}
|
|
eval "$_info"
|
|
|
|
music_file=$(find "$music_base_dir" -type f -iname "*${SEASON}*" \
|
|
| grep -iE "Mvt[^0-9]*${MVT_NUM}[^0-9]" | sort | head -n 1)
|
|
if [ -z "$music_file" ]; then
|
|
echo "SKIP (no music $SEASON Mvt$MVT_NUM): $filename"
|
|
(( skipped++ )) || true
|
|
continue
|
|
fi
|
|
|
|
music_duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$music_file")
|
|
loops_var="MONTAGE_MUSIC_LOOPS_${SEASON}_${MVT_NUM}"
|
|
loops="${!loops_var:-${MONTAGE_MUSIC_LOOPS:-1}}"
|
|
target=$(echo "scale=6; $music_duration * $loops / $DAYS_IN_MVT" | bc)
|
|
|
|
out_dir="$MOVIES_DIR/$cam/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM"
|
|
final="$out_dir/${date_str}_Mvt${MVT_NUM}-Day${DAY_OF_MVT}of${DAYS_IN_MVT}-final.mp4"
|
|
|
|
if [ -f "$final" ]; then
|
|
echo "EXISTS $cam/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM/$(basename "$final")"
|
|
(( skipped++ )) || true
|
|
continue
|
|
fi
|
|
|
|
raw_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$temp_file" 2>/dev/null || true)
|
|
if [ -z "$raw_dur" ] || [ "$raw_dur" = "N/A" ]; then
|
|
echo "SKIP (corrupt/unreadable): $temp_file"
|
|
(( skipped++ )) || true
|
|
continue
|
|
fi
|
|
speed=$(echo "scale=6; $raw_dur / $target" | bc)
|
|
|
|
echo "MAKE $cam/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM/$(basename "$final") [${speed}x]"
|
|
|
|
if $DRY_RUN; then
|
|
(( processed++ )) || true
|
|
continue
|
|
fi
|
|
|
|
mkdir -p "$out_dir"
|
|
if ffmpeg -loglevel warning \
|
|
-i "$temp_file" \
|
|
-vf "setpts=PTS/$speed" \
|
|
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SEASONS_FINAL" \
|
|
-t "$target" -an \
|
|
-y "$final"; then
|
|
(( processed++ )) || true
|
|
else
|
|
rm -f "$final"
|
|
echo " ERROR: ffmpeg failed"
|
|
(( errors++ )) || true
|
|
fi
|
|
|
|
done < <(find "$BACKUP_ROOT" -name "*-temp.mp4" -print0 | sort -z)
|
|
|
|
echo ""
|
|
echo "────────────────────────────────────────"
|
|
echo "Written: $processed Skipped: $skipped Errors: $errors"
|
|
$DRY_RUN && echo "(dry run — re-run without --dry-run to write files)"
|