Each stage can now fail independently, giving four upload paths: audio+overlay, audio-only, overlay-only, or speed-only video. Step 3a copies the video stream (no re-encode) when mixing audio, so step 3b failure still leaves a clean audio-mixed file to promote. Overlay failure notification now reports which quality was uploaded. https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
230 lines
10 KiB
Bash
230 lines
10 KiB
Bash
#!/bin/bash
|
|
# daily_sunrise_video.sh — collect today's sunrise images, speed-adjust to
|
|
# SUNRISE_TARGET_SECS, burn the local sunrise time vertically on the right
|
|
# 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
|
|
|
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|
source "$SCRIPT_DIR/sky-cam.conf"
|
|
export TZ="$TIMEZONE"
|
|
|
|
# ── Date / paths ──────────────────────────────────────────────────────────────
|
|
current_date=$(date +%Y-%m-%d)
|
|
output_date=$(date +%Y-%m)
|
|
|
|
image_dir="$BASE_DIR/$CAM_NAME/$current_date"
|
|
output_dir="$BASE_DIR/movies/$CAM_NAME/$output_date/sunrise-only"
|
|
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
|
|
echo "Sunrise (UTC): $sunrise_time"
|
|
|
|
sunrise_time_local=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S")
|
|
echo "Sunrise (local): $sunrise_time_local"
|
|
|
|
# ── Capture window ────────────────────────────────────────────────────────────
|
|
time_to_seconds() { IFS='-' read -r h m s <<< "$1"; echo $((10#$h * 3600 + 10#$m * 60 + 10#$s)); }
|
|
|
|
sunrise_sec=$(time_to_seconds "$sunrise_time_local")
|
|
start_sec=$((sunrise_sec - SUNRISE_PRE_MIN * 60))
|
|
end_sec=$((sunrise_sec + SUNRISE_POST_MIN * 60))
|
|
echo "Window: $((start_sec/3600)):$(printf '%02d' $(((start_sec%3600)/60))) → $((end_sec/3600)):$(printf '%02d' $(((end_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=""
|
|
temp_audio=""
|
|
trap 'rm -f "$temp_list" "$raw_video" "$temp_audio" 2>/dev/null || true' EXIT
|
|
|
|
find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do
|
|
img_sec=$(time_to_seconds "$(basename "$img" .jpg)")
|
|
if [[ $img_sec -ge $start_sec && $img_sec -le $end_sec ]]; then
|
|
echo "file '$img'"
|
|
fi
|
|
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
|
|
echo "Images in window: $num_images"
|
|
|
|
# ── Font detection (same fallback chain as montage-mvt.sh) ───────────────────
|
|
FONT=""
|
|
if command -v fc-match &>/dev/null; then
|
|
FONT=$(fc-match "DejaVu Sans:style=Regular" --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
|
|
|
|
# ── Step 1: Raw video from images ─────────────────────────────────────────────
|
|
raw_video=$(mktemp --suffix=.mp4)
|
|
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"; 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 — 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 '-' ':')
|
|
# Build "H\nH\n:\nM\nM" for vertical stacking in drawtext
|
|
SR_VERT=$(echo "$SR_TIME" | awk 'BEGIN{FS=""}{for(i=1;i<=NF;i++){printf "%s",$i; if(i<NF)printf "\\n"}}')
|
|
|
|
DT="drawtext"
|
|
[ -n "$FONT" ] && DT="${DT}=fontfile='${FONT}'" || DT="${DT}"
|
|
DT="${DT}:text='${SR_VERT}'"
|
|
DT="${DT}:fontcolor=white@${SUNRISE_OVERLAY_OPACITY}"
|
|
DT="${DT}:fontsize=h/22"
|
|
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: Audio mix + overlay — each independently optional/fallible ────────
|
|
# Step 3a: mixes audio into a temp copy of the sped video (video stream is
|
|
# copied, not re-encoded — fast, and lets step 3b fail independently).
|
|
# Step 3b: burns the overlay onto whatever 3a produced.
|
|
# Failure matrix → upload always fires:
|
|
# audio ✓ overlay ✓ → final has audio + overlay
|
|
# audio ✓ overlay ✗ → final has audio, no overlay
|
|
# audio ✗ overlay ✓ → final has overlay, no audio
|
|
# audio ✗ overlay ✗ → sped video promoted (no audio, no overlay)
|
|
final_video="$output_dir/$current_date-daily-sunrise.mp4"
|
|
fade_out=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc)
|
|
|
|
# ── Pick audio source ─────────────────────────────────────────────────────────
|
|
audio_src=""
|
|
audio_offset="0"
|
|
|
|
cam_audio="$image_dir/sunrise-audio.m4a"
|
|
if [ "${AUDIO_ENABLED:-false}" = "true" ]; then
|
|
if [ -f "$cam_audio" ]; then
|
|
buffer_sec=$(( ${AUDIO_PRE_BUFFER_MIN:-2} * 60 ))
|
|
audio_offset=$(echo "scale=1; $SUNRISE_PRE_MIN * 60 + $buffer_sec - $SUNRISE_TARGET_SECS / 2" | bc)
|
|
audio_src="$cam_audio"
|
|
echo "Audio: camera recording offset=${audio_offset}s"
|
|
else
|
|
library_dir="$SCRIPT_DIR/sunrise-sounds"
|
|
if [ -d "$library_dir" ]; then
|
|
random_file=$(find "$library_dir" -name "*.mp3" | shuf -n 1 2>/dev/null || true)
|
|
if [ -n "$random_file" ]; then
|
|
audio_src="$random_file"
|
|
echo "Audio: library fallback $(basename "$audio_src")"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ── Step 3a: mix audio (video copied, not re-encoded) ────────────────────────
|
|
work_video="$sped_video"
|
|
has_audio=false
|
|
|
|
if [ -n "$audio_src" ]; then
|
|
temp_audio=$(mktemp --suffix=.mp4)
|
|
echo "Step 3a/3: mixing audio → temp"
|
|
if ffmpeg -loglevel warning \
|
|
-i "$sped_video" \
|
|
-ss "$audio_offset" -t "$SUNRISE_TARGET_SECS" -i "$audio_src" \
|
|
-filter_complex "[1:a]afade=t=in:st=0:d=0.5,afade=t=out:st=${fade_out}:d=0.5[aout]" \
|
|
-map "0:v" -map "[aout]" \
|
|
-c:v copy -c:a aac -b:a 128k \
|
|
-y "$temp_audio"; then
|
|
work_video="$temp_audio"
|
|
has_audio=true
|
|
echo "Audio: mixed OK"
|
|
else
|
|
rm -f "$temp_audio"; temp_audio=""
|
|
echo "Audio mix failed — step 3b will be overlay-only"
|
|
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise audio mix failed $current_date" \
|
|
"Audio could not be mixed — continuing with overlay only" || true
|
|
[ "$audio_src" = "$cam_audio" ] && rm -f "$cam_audio"
|
|
fi
|
|
fi
|
|
|
|
# ── Step 3b: burn overlay ─────────────────────────────────────────────────────
|
|
echo "Step 3b/3: overlay → $final_video"
|
|
if $has_audio; then audio_out_flags=(-c:a copy); else audio_out_flags=(-an); fi
|
|
|
|
if ffmpeg -loglevel warning \
|
|
-i "$work_video" \
|
|
-vf "${DT}" \
|
|
-c:v libx264 -pix_fmt yuv420p -crf "$CRF_SUNRISE" \
|
|
"${audio_out_flags[@]}" \
|
|
-y "$final_video"; then
|
|
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" "$temp_audio"; temp_audio=""
|
|
[ -f "$cam_audio" ] && rm -f "$cam_audio"
|
|
"$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \
|
|
"$(basename "$final_video") — ${actual_dur}s, sunrise at ${SR_TIME}" || true
|
|
else
|
|
if $has_audio; then promote_label="audio-mixed"; else promote_label="speed-only"; fi
|
|
mv "$work_video" "$final_video"
|
|
[ "$work_video" != "$sped_video" ] && rm -f "$sped_video"
|
|
temp_audio=""
|
|
[ -f "$cam_audio" ] && rm -f "$cam_audio"
|
|
echo "Overlay failed — promoting ${promote_label} video as upload target"
|
|
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise overlay failed $current_date" \
|
|
"Overlay failed — uploading ${promote_label} video (no timestamp)" || true
|
|
fi
|