diff --git a/4-seasons.sh b/4-seasons.sh index 58595a0..213361c 100755 --- a/4-seasons.sh +++ b/4-seasons.sh @@ -90,16 +90,32 @@ speed_factor=$(echo "scale=6; $raw_duration / $target_per_clip" | bc) echo "Step 2/2: speed factor $speed_factor..." final_file="$output_dir/${yesterday}_Mvt${MVT_NUM}-Day${DAY_OF_MVT}of${DAYS_IN_MVT}-final.mp4" -ffmpeg -loglevel warning \ +if ! ffmpeg -loglevel warning \ -i "$temp_video" \ -vf "setpts=PTS/$speed_factor" \ -c:v libx264 -pix_fmt yuv420p -crf "$CRF_SEASONS_FINAL" \ -t "$target_per_clip" -an \ - -y "$final_file" + -y "$final_file"; then + rm -f "$final_file" + "$SCRIPT_DIR/notify.sh" "FAILED: $SEASON Mvt$MVT_NUM Day$DAY_OF_MVT speed-adjust ($yesterday)" \ + "ffmpeg speed-adjust failed — re-run 4-seasons.sh $CAM_NAME while JPGs exist" || true + exit 1 +fi adjusted=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_file") echo "Daily clip: $final_file ($adjusted s)" +# Warn if clip duration deviates more than 0.5 s from target (float drift check) +drift=$(echo "scale=3; $adjusted - $target_per_clip" | bc | sed 's/^-//') +if awk "BEGIN{exit !($drift > 0.5)}"; then + echo "WARNING: clip duration ${adjusted}s differs from target ${target_per_clip}s by ${drift}s" + "$SCRIPT_DIR/notify.sh" "WARNING: $SEASON Mvt$MVT_NUM Day$DAY_OF_MVT duration drift ($yesterday)" \ + "Clip ${adjusted}s vs target ${target_per_clip}s (drift ${drift}s)" || true +fi + +"$SCRIPT_DIR/notify.sh" "$SEASON Mvt$MVT_NUM Day$DAY_OF_MVT/$DAYS_IN_MVT saved ($yesterday)" \ + "$(basename "$final_file") — ${adjusted}s" || true + # ── Auto-trigger montage on last day of movement ────────────────────────────── if [ "$IS_LAST_DAY" = "true" ]; then echo "Last day of $SEASON Mvt $MVT_NUM — triggering montage compilation..." diff --git a/daily_sunrise_video.sh b/daily_sunrise_video.sh index 11a2a20..97e0869 100644 --- a/daily_sunrise_video.sh +++ b/daily_sunrise_video.sh @@ -4,6 +4,12 @@ # side, and write the final video. Upload is handled by a separate systemd # service (sky-cam-sunrise-upload) triggered via OnSuccess= so the two jobs # have independent log entries and failure states. +# +# Resilience: the speed-adjusted video is saved permanently before the overlay +# step. If overlay fails, the sped video survives at: +# /-daily-sunrise-sped.mp4 +# Re-run this script once the issue is resolved; the sped file is deleted +# automatically when the overlay step succeeds. set -euo pipefail @@ -22,6 +28,8 @@ mkdir -p "$output_dir" # ── Sunrise time ────────────────────────────────────────────────────────────── sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py") if [[ -z "$sunrise_time" ]]; then + "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \ + "Could not retrieve sunrise time from sunrise.py" || true echo "Error: Failed to retrieve sunrise time." exit 1 fi @@ -40,11 +48,14 @@ echo "Window: $((start_sec/3600)):$(printf '%02d' $(((start_sec%3600)/60))) → # ── Collect images in window ────────────────────────────────────────────────── if [ ! -d "$image_dir" ]; then + "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \ + "Image directory not found: $image_dir" || true echo "Error: image directory not found: $image_dir" exit 1 fi temp_list=$(mktemp --suffix=.txt) +raw_video="" trap 'rm -f "$temp_list" "$raw_video" 2>/dev/null || true' EXIT find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do @@ -56,6 +67,8 @@ done > "$temp_list" num_images=$(wc -l < "$temp_list") if [[ $num_images -lt 1 ]]; then + "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \ + "No images found in sunrise window" || true echo "No images found in sunrise window." exit 1 fi @@ -78,17 +91,39 @@ fi # ── Step 1: Raw video from images ───────────────────────────────────────────── raw_video=$(mktemp --suffix=.mp4) -echo "Step 1/2: encoding raw video..." -ffmpeg -loglevel warning \ +echo "Step 1/3: encoding raw video..." +if ! ffmpeg -loglevel warning \ -f concat -safe 0 -i "$temp_list" \ -c:v libx264 -pix_fmt yuv420p -crf "$CRF_SUNRISE" -vsync 2 -an \ - -y "$raw_video" + -y "$raw_video"; then + "$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 1/3 (encode) $current_date" \ + "ffmpeg raw encode from images failed — check: journalctl -u sky-cam-sunrise.service" || true + exit 1 +fi raw_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$raw_video") speed_factor=$(echo "scale=3; $raw_dur / $SUNRISE_TARGET_SECS" | bc) echo "Raw: ${raw_dur}s speed factor: ${speed_factor}x" -# ── Step 2: Speed-adjust + sunrise time overlay ─────────────────────────────── +# ── Step 2: Speed-adjust — saved permanently so overlay failure is recoverable ─ +# Deleted automatically if step 3 succeeds. +sped_video="$output_dir/$current_date-daily-sunrise-sped.mp4" +echo "Step 2/3: speed-adjust → $sped_video" +if ! ffmpeg -loglevel warning \ + -i "$raw_video" \ + -vf "setpts=PTS/${speed_factor}" \ + -c:v libx264 -pix_fmt yuv420p -crf "$CRF_SUNRISE" -an \ + -y "$sped_video"; then + rm -f "$sped_video" + "$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 2/3 (speed) $current_date" \ + "speed-adjust failed — no video saved" || true + exit 1 +fi + +sped_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$sped_video") +echo "Speed-adjusted: ${sped_dur}s (saved — overlay still pending)" + +# ── Build overlay filter ────────────────────────────────────────────────────── # Time displayed as stacked characters down the right side (e.g. 0/7/:/2/3), # semi-transparent with a soft drop shadow so it reads on any background. SR_TIME=$(echo "$sunrise_time_local" | cut -d'-' -f1,2 | tr '-' ':') @@ -104,14 +139,23 @@ DT="${DT}:line_spacing=4" DT="${DT}:x=w-tw-18:y=(h-th)/2" DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1" +# ── Step 3: Overlay — sunrise time burned in ────────────────────────────────── +# If this fails, $sped_video survives at its permanent path for manual recovery. final_video="$output_dir/$current_date-daily-sunrise.mp4" -echo "Step 2/2: speed-adjust + overlay → $final_video" - -ffmpeg -loglevel warning \ - -i "$raw_video" \ - -vf "setpts=PTS/${speed_factor},${DT}" \ +echo "Step 3/3: overlay → $final_video" +if ! ffmpeg -loglevel warning \ + -i "$sped_video" \ + -vf "${DT}" \ -c:v libx264 -pix_fmt yuv420p -crf "$CRF_SUNRISE" -an \ - -y "$final_video" + -y "$final_video"; then + "$SCRIPT_DIR/notify.sh" "FAILED: sunrise overlay $current_date" \ + "Overlay failed — speed-only video saved: $(basename "$sped_video")" || true + exit 1 +fi actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_video") echo "Done: $final_video (${actual_dur}s, sunrise at ${SR_TIME})" + +rm -f "$sped_video" +"$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \ + "$(basename "$final_video") — ${actual_dur}s, sunrise at ${SR_TIME}" || true diff --git a/montage-mvt.sh b/montage-mvt.sh index c22d6dc..5929ce2 100755 --- a/montage-mvt.sh +++ b/montage-mvt.sh @@ -51,6 +51,8 @@ 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 + "$SCRIPT_DIR/notify.sh" "FAILED: montage $SEASON Mvt$MVT_NUM ($ASTRO_YEAR)" \ + "Music file not found in $music_base_dir" || true echo "ERROR: Music file not found for $SEASON Mvt $MVT_NUM in $music_base_dir" exit 1 fi @@ -60,11 +62,28 @@ 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 + "$SCRIPT_DIR/notify.sh" "FAILED: montage $SEASON Mvt$MVT_NUM ($ASTRO_YEAR)" \ + "No *-final.mp4 clips found in $video_dir" || true echo "ERROR: No *-final.mp4 clips in $video_dir" exit 1 fi echo "Found ${#daily_clips[@]} daily clips" +# ── Validate clip duration sum against music ────────────────────────────────── +total_clip_dur=0 +for clip in "${daily_clips[@]}"; do + d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$clip") + total_clip_dur=$(echo "scale=3; $total_clip_dur + $d" | bc) +done +clip_drift=$(echo "scale=3; $total_clip_dur - $music_duration" | bc | sed 's/^-//') +echo "Sum of clips: ${total_clip_dur}s Music: ${music_duration}s Drift: ${clip_drift}s" +if awk "BEGIN{exit !($clip_drift > 2.0)}"; then + echo "WARNING: clip total differs from music by ${clip_drift}s — speed pass will compensate" + "$SCRIPT_DIR/notify.sh" \ + "WARNING: $SEASON Mvt$MVT_NUM clip drift ${clip_drift}s ($ASTRO_YEAR)" \ + "Clips sum ${total_clip_dur}s vs music ${music_duration}s" || true +fi + # ── 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]}") @@ -105,18 +124,25 @@ 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 ────────────────────── +# ── Step 2: Speed-adjust — saved permanently so music/overlay failure is recoverable ─ +# Deleted automatically if step 3 succeeds. 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 \ +sped_file="$output_dir/${MVT_START}_${SEASON}_Mvt${MVT_NUM}-Sped.mp4" +if ! 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" + -t "$music_duration" -y "$sped_file"; then + rm -f "$sped_file" + "$SCRIPT_DIR/notify.sh" "FAILED: montage step 2/3 (speed) $SEASON Mvt$MVT_NUM ($ASTRO_YEAR)" \ + "speed-adjust failed — concat temp lost; re-run montage-mvt.sh" || true + exit 1 +fi rm "$temp_concat" +echo "Speed-adjusted: $sped_file (saved — music+overlay still pending)" # ── Step 3: Music + fades + attribution overlay ─────────────────────────────── # Attribution overlay: semi-transparent bar across the top for the first @@ -152,16 +178,21 @@ 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" \ +# If this fails, $sped_file survives for manual recovery (re-run step 3 only). +if ! ffmpeg -loglevel warning \ + -i "$sped_file" -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" + -t "$music_duration" -y "$output_file"; then + "$SCRIPT_DIR/notify.sh" "FAILED: montage step 3/3 (music+overlay) $SEASON Mvt$MVT_NUM ($ASTRO_YEAR)" \ + "Music/overlay failed — sped video saved: $(basename "$sped_file")" || true + exit 1 +fi +rm -f "$sped_file" echo "Done: $output_file" # ── Notify ──────────────────────────────────────────────────────────────────── diff --git a/year-end-join.sh b/year-end-join.sh index 4da4746..178fb59 100755 --- a/year-end-join.sh +++ b/year-end-join.sh @@ -32,6 +32,7 @@ concat_list=$(mktemp --suffix=.txt) trap 'rm -f "$concat_list"' EXIT missing=0 +total_expected_dur=0 for season in Winter Spring Summer Autumn; do for mvt in 1 2 3; do mvt_dir="$year_dir/$season/Mvt$mvt/montage" @@ -41,11 +42,14 @@ for season in Winter Spring Summer Autumn; do echo "WARNING: missing montage for $season Mvt$mvt ($mvt_dir)" missing=$((missing + 1)) else - echo " $season Mvt$mvt → $(basename "$f")" + d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f") + total_expected_dur=$(echo "scale=3; $total_expected_dur + $d" | bc) + echo " $season Mvt$mvt → $(basename "$f") (${d}s)" printf "file '%s'\n" "$f" >> "$concat_list" fi done done +echo "Expected total duration: ${total_expected_dur}s ($(echo "scale=1; $total_expected_dur/60" | bc) min)" found=$(wc -l < "$concat_list") echo "$found of 12 movements found ($missing missing)" @@ -63,14 +67,26 @@ fi output_file="$year_dir/${CAM_NAME}-${ASTRO_YEAR}.mp4" echo "Output: $output_file" -ffmpeg -loglevel warning \ +if ! ffmpeg -loglevel warning \ -f concat -safe 0 -i "$concat_list" \ - -c copy -y "$output_file" + -c copy -y "$output_file"; then + "$SCRIPT_DIR/notify.sh" "FAILED: year-end join $CAM_NAME $ASTRO_YEAR" \ + "ffmpeg concat failed — the 12 movement montages are still intact" || true + exit 1 +fi total_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$output_file") total_min=$(echo "scale=1; $total_dur / 60" | bc) echo "Done: $output_file (${total_min} min)" +# Sanity-check actual vs expected duration +dur_drift=$(echo "scale=3; $total_dur - $total_expected_dur" | bc | sed 's/^-//') +if awk "BEGIN{exit !($dur_drift > 2.0)}"; then + echo "WARNING: output ${total_dur}s differs from expected ${total_expected_dur}s by ${dur_drift}s" + "$SCRIPT_DIR/notify.sh" "WARNING: year-end duration drift $CAM_NAME $ASTRO_YEAR" \ + "Output ${total_dur}s vs expected ${total_expected_dur}s (drift ${dur_drift}s)" || true +fi + # ── Notify ──────────────────────────────────────────────────────────────────── "$SCRIPT_DIR/notify.sh" \ "Four Seasons $ASTRO_YEAR complete" \