Add per-step resilience and notifications across all video pipeline scripts

Each pipeline now saves intermediate files permanently before the step most
likely to fail, so a partial failure leaves a recoverable artifact:
- daily_sunrise_video.sh: speed-adjusted video (*-sped.mp4) survives if the
  drawtext overlay step fails; deleted automatically on success
- montage-mvt.sh: speed-adjusted video (*-Sped.mp4) survives if the
  music-mux + attribution overlay step fails; deleted on success
- 4-seasons.sh / montage-mvt.sh / year-end-join.sh: explicit ffmpeg error
  handling with targeted notify.sh calls naming the failed step and the
  surviving artifact path
- montage-mvt.sh / 4-seasons.sh / year-end-join.sh: clip-duration-sum vs
  music-duration drift check warns when cumulative float error exceeds 2 s

https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
This commit is contained in:
Claude
2026-04-19 17:35:26 +00:00
parent 62c787fc0b
commit 9c929f28d8
4 changed files with 130 additions and 23 deletions
+54 -10
View File
@@ -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:
# <output_dir>/<date>-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