Merge pull request #32 from outis1one/clDemoe/seasonal-sunrise-montage-nn268

ClDemoe/seasonal sunrise montage nn268
This commit is contained in:
Outis
2026-04-21 14:25:32 -04:00
committed by GitHub
3 changed files with 230 additions and 141 deletions
+2 -1
View File
@@ -15,6 +15,7 @@ set -euo pipefail
SCRIPT_DIR="$(dirname "$(realpath "$0")")" SCRIPT_DIR="$(dirname "$(realpath "$0")")"
source "$SCRIPT_DIR/sky-cam.conf" source "$SCRIPT_DIR/sky-cam.conf"
source "$SCRIPT_DIR/.env" 2>/dev/null || true
export TZ="$TIMEZONE" export TZ="$TIMEZONE"
CAM="${1:-}" CAM="${1:-}"
@@ -26,7 +27,7 @@ fi
rtsp_var="CAM_RTSP_${CAM}" rtsp_var="CAM_RTSP_${CAM}"
RTSP_URL="${!rtsp_var:-}" RTSP_URL="${!rtsp_var:-}"
if [ -z "$RTSP_URL" ]; then if [ -z "$RTSP_URL" ]; then
echo "ERROR: CAM_RTSP_${CAM} not set in sky-cam.conf" echo "ERROR: CAM_RTSP_${CAM} not set in .env"
exit 1 exit 1
fi fi
+197 -112
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# daily_sunrise_video.sh — collect today's sunrise images, speed-adjust to # daily_sunrise_video.sh — collect today's sunrise images, speed-adjust to
# SUNRISE_TARGET_SECS, burn the local sunrise time vertically on the right # SUNRISE_TARGET_SECS, burn the local sunrise time vertically in the lower-right
# side, and write the final video. Upload is handled by a separate systemd # corner, and write the final video. Upload is handled by a separate systemd
# service (sky-cam-sunrise-upload) triggered via OnSuccess= so the two jobs # service (sky-cam-sunrise-upload) triggered via OnSuccess= so the two jobs
# have independent log entries and failure states. # have independent log entries and failure states.
# #
@@ -10,60 +10,76 @@
# <output_dir>/<date>-daily-sunrise-sped.mp4 # <output_dir>/<date>-daily-sunrise-sped.mp4
# Re-run this script once the issue is resolved; the sped file is deleted # Re-run this script once the issue is resolved; the sped file is deleted
# automatically when the overlay step succeeds. # automatically when the overlay step succeeds.
#
# Usage:
# ./daily_sunrise_video.sh [cam_name]
# ./daily_sunrise_video.sh [cam_name] --test [MINUTES]
#
# --test MINUTES skips sunrise.py and the capture-window wait; uses the last
# MINUTES minutes of captured frames (default 2), grabs live
# RTSP audio, writes <date>-daily-sunrise-test.mp4.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(dirname "$(realpath "$0")")" SCRIPT_DIR="$(dirname "$(realpath "$0")")"
source "$SCRIPT_DIR/sky-cam.conf" source "$SCRIPT_DIR/sky-cam.conf"
source "$SCRIPT_DIR/.env" 2>/dev/null || true
export TZ="$TIMEZONE" export TZ="$TIMEZONE"
CAM_NAME="${1:-$SUNRISE_CAM}" CAM_NAME="${1:-$SUNRISE_CAM}"
TEST_MODE=false
TEST_MINUTES=2
if [[ "${2:-}" == "--test" ]]; then
TEST_MODE=true
TEST_MINUTES="${3:-2}"
fi
time_to_seconds() { IFS='-' read -r h m s <<< "$1"; echo $((10#$h * 3600 + 10#$m * 60 + 10#$s)); }
# ── Date / paths ────────────────────────────────────────────────────────────── # ── Date / paths ──────────────────────────────────────────────────────────────
current_date=$(date +%Y-%m-%d) current_date=$(date +%Y-%m-%d)
output_date=$(date +%Y-%m) output_date=$(date +%Y-%m)
image_dir="$BASE_DIR/$CAM_NAME/$current_date" image_dir="$BASE_DIR/$CAM_NAME/$current_date"
output_dir="$MOVIES_DIR/$CAM_NAME/$output_date/sunrise-only" output_dir="$MOVIES_DIR/$CAM_NAME/$output_date/sunrise-only"
mkdir -p "$output_dir" mkdir -p "$output_dir"
# ── Sunrise time ────────────────────────────────────────────────────────────── # ── Sunrise time (skipped in test mode) ───────────────────────────────────────
sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py") SR_TIME=""
if [[ -z "$sunrise_time" ]]; then sunrise_time_local=""
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \ if ! $TEST_MODE; then
"Could not retrieve sunrise time from sunrise.py" || true sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py")
echo "Error: Failed to retrieve sunrise time." if [[ -z "$sunrise_time" ]]; then
exit 1 "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
fi "Could not retrieve sunrise time from sunrise.py" || true
echo "Sunrise (UTC): $sunrise_time" echo "Error: Failed to retrieve sunrise time."
exit 1
sunrise_time_local=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S") fi
echo "Sunrise (local): $sunrise_time_local" echo "Sunrise (UTC): $sunrise_time"
sunrise_time_local=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S")
# ── Capture window ──────────────────────────────────────────────────────────── echo "Sunrise (local): $sunrise_time_local"
time_to_seconds() { IFS='-' read -r h m s <<< "$1"; echo $((10#$h * 3600 + 10#$m * 60 + 10#$s)); } SR_TIME=$(echo "$sunrise_time_local" | cut -d'-' -f1,2 | tr '-' ':')
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)))"
# ── Wait for capture window to finish ────────────────────────────────────────
# The timer fires early (default 03:00); we sleep internally so images from
# the full window are available before we start encoding.
midnight=$(date -d "today 00:00:00" +%s)
now_day_sec=$(( $(date +%s) - midnight ))
if [ "$now_day_sec" -lt "$end_sec" ]; then
wait_sec=$(( end_sec - now_day_sec + 30 ))
echo "Waiting ${wait_sec}s for capture window to finish (ends $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S'))..."
sleep "$wait_sec"
else
echo "Capture window already closed at $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S') — processing available images"
fi fi
# ── Collect images in window ────────────────────────────────────────────────── # ── Capture window (skipped in test mode) ─────────────────────────────────────
if ! $TEST_MODE; then
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)))"
midnight=$(date -d "today 00:00:00" +%s)
now_day_sec=$(( $(date +%s) - midnight ))
if [ "$now_day_sec" -lt "$end_sec" ]; then
wait_sec=$(( end_sec - now_day_sec + 30 ))
echo "Waiting ${wait_sec}s for capture window to finish (ends $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S'))..."
sleep "$wait_sec"
else
echo "Capture window already closed at $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S') — processing available images"
fi
fi
# ── Collect images ────────────────────────────────────────────────────────────
if [ ! -d "$image_dir" ]; then if [ ! -d "$image_dir" ]; then
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \ $TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
"Image directory not found: $image_dir" || true "Image directory not found: $image_dir" || true
echo "Error: image directory not found: $image_dir" echo "Error: image directory not found: $image_dir"
exit 1 exit 1
@@ -72,25 +88,48 @@ fi
temp_list=$(mktemp --suffix=.txt) temp_list=$(mktemp --suffix=.txt)
raw_video="" raw_video=""
temp_audio="" temp_audio=""
trap 'rm -f "$temp_list" "$raw_video" "$temp_audio" 2>/dev/null || true' EXIT temp_text=""
temp_live_audio=""
trap 'rm -f "$temp_list" "$raw_video" "$temp_audio" "$temp_text" "$temp_live_audio" 2>/dev/null || true' EXIT
find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do if $TEST_MODE; then
img_sec=$(time_to_seconds "$(basename "$img" .jpg)") test_frames=$(( TEST_MINUTES * 60 / ${CAPTURE_INTERVAL:-10} ))
if [[ $img_sec -ge $start_sec && $img_sec -le $end_sec ]]; then mapfile -t images < <(find "$image_dir" -name "*.jpg" | sort | tail -n "$test_frames")
echo "file '$img'" echo "Test mode: last ${TEST_MINUTES} min (${#images[@]} frames)"
fi else
done > "$temp_list" mapfile -t images < <(
find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do
img_sec=$(time_to_seconds "$(basename "$img" .jpg)")
[[ $img_sec -ge $start_sec && $img_sec -le $end_sec ]] && echo "$img"
done
)
fi
num_images=$(wc -l < "$temp_list") if [ "${#images[@]}" -lt 1 ]; then
if [[ $num_images -lt 1 ]]; then $TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
"No images found in sunrise window" || true "No images found in sunrise window" || true
echo "No images found in sunrise window." echo "No images found."
exit 1 exit 1
fi fi
echo "Images in window: $num_images" echo "Images: ${#images[@]}"
# ── Font detection (same fallback chain as montage-mvt.sh) ─────────────────── # Concat list with explicit frame duration so each frame represents real elapsed
# time. Without this, all frames collapse to timestamp 0 → one-frame video.
# The final entry needs a duplicate without duration (ffmpeg concat requirement).
for img in "${images[@]}"; do
printf "file '%s'\nduration %s\n" "$img" "${CAPTURE_INTERVAL:-10}"
done > "$temp_list"
printf "file '%s'\n" "${images[-1]}" >> "$temp_list"
# ── Set overlay time ──────────────────────────────────────────────────────────
if $TEST_MODE; then
mid_idx=$(( ${#images[@]} / 2 ))
mid_hms=$(basename "${images[$mid_idx]}" .jpg)
SR_TIME=$(echo "$mid_hms" | cut -c1-5 | tr '-' ':')
echo "Mid-point frame: $mid_hms → overlay time: $SR_TIME"
fi
# ── Font detection ────────────────────────────────────────────────────────────
FONT="" FONT=""
if command -v fc-match &>/dev/null; then if command -v fc-match &>/dev/null; then
FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true) FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true)
@@ -107,12 +146,12 @@ fi
# ── Step 1: Raw video from images ───────────────────────────────────────────── # ── Step 1: Raw video from images ─────────────────────────────────────────────
raw_video=$(mktemp --suffix=.mp4) raw_video=$(mktemp --suffix=.mp4)
echo "Step 1/3: encoding raw video..." echo "Step 1/3: encoding raw video from ${#images[@]} frames..."
if ! ffmpeg -loglevel warning \ if ! ffmpeg -loglevel warning \
-f concat -safe 0 -i "$temp_list" \ -f concat -safe 0 -i "$temp_list" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -vsync 2 -an \ -c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -vsync 2 -an \
-y "$raw_video"; then -y "$raw_video"; then
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 1/3 (encode) $current_date" \ $TEST_MODE || "$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 "ffmpeg raw encode from images failed — check: journalctl -u sky-cam-sunrise.service" || true
exit 1 exit 1
fi fi
@@ -121,72 +160,111 @@ 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) speed_factor=$(echo "scale=3; $raw_dur / $SUNRISE_TARGET_SECS" | bc)
echo "Raw: ${raw_dur}s speed factor: ${speed_factor}x" echo "Raw: ${raw_dur}s speed factor: ${speed_factor}x"
# ── Step 2: Speed-adjust — saved permanently so overlay failure is recoverable # ── Step 2: Speed-adjust ─────────────────────────────────────────────────────
# Deleted automatically if step 3 succeeds. # Production: saved permanently so overlay failure is recoverable.
sped_video="$output_dir/$current_date-daily-sunrise-sped.mp4" # Test: temporary file, deleted on exit.
echo "Step 2/3: speed-adjust → $sped_video" # fps=25 forces proper frame rate so all source frames appear in the output.
if $TEST_MODE; then
sped_video=$(mktemp --suffix=-sped.mp4)
echo "Step 2/3: speed-adjust (test)..."
else
sped_video="$output_dir/$current_date-daily-sunrise-sped.mp4"
echo "Step 2/3: speed-adjust → $sped_video"
fi
if ! ffmpeg -loglevel warning \ if ! ffmpeg -loglevel warning \
-i "$raw_video" \ -i "$raw_video" \
-vf "setpts=PTS/${speed_factor}" \ -vf "setpts=PTS/${speed_factor},fps=25" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \ -c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
-t "$SUNRISE_TARGET_SECS" \
-y "$sped_video"; then -y "$sped_video"; then
rm -f "$sped_video" rm -f "$sped_video"
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 2/3 (speed) $current_date" \ $TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 2/3 (speed) $current_date" \
"speed-adjust failed — no video saved" || true "speed-adjust failed — no video saved" || true
exit 1 exit 1
fi fi
sped_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$sped_video") 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)" $TEST_MODE || echo "Speed-adjusted: ${sped_dur}s (saved — overlay still pending)"
$TEST_MODE && echo "Speed-adjusted: ${sped_dur}s"
# ── Build overlay filter (skipped if SUNRISE_OVERLAY_ENABLED=false) ────────── # ── Build overlay filter ──────────────────────────────────────────────────────
SR_TIME=$(echo "$sunrise_time_local" | cut -d'-' -f1,2 | tr '-' ':')
DT="" DT=""
if [ "${SUNRISE_OVERLAY_ENABLED:-true}" = "true" ]; then if [ "${SUNRISE_OVERLAY_ENABLED:-true}" = "true" ]; then
SR_VERT="${SR_TIME:0:1}\\\\n${SR_TIME:1:1}\\\\n${SR_TIME:2:1}\\\\n${SR_TIME:3:1}\\\\n${SR_TIME:4:1}" temp_text=$(mktemp --suffix=.txt)
DT="drawtext" printf '%s\n%s\n%s\n%s\n%s' \
[ -n "$FONT" ] && DT="${DT}=fontfile='${FONT}'" || DT="${DT}" "${SR_TIME:0:1}" "${SR_TIME:1:1}" "${SR_TIME:2:1}" "${SR_TIME:3:1}" "${SR_TIME:4:1}" \
DT="${DT}:text='${SR_VERT}'" > "$temp_text"
DT="${DT}:fontcolor=white@${SUNRISE_OVERLAY_OPACITY}" if [ -n "$FONT" ]; then
DT="${DT}:fontsize=h/22" DT="drawtext=fontfile='${FONT}':textfile='${temp_text}'"
DT="${DT}:line_spacing=4" else
DT="${DT}:x=w-tw-18:y=(h-th)/2" DT="drawtext=textfile='${temp_text}'"
fi
DT="${DT}:fontcolor=yellow@${SUNRISE_OVERLAY_OPACITY}"
DT="${DT}:fontsize=h/22:line_spacing=4"
DT="${DT}:x=w-tw-18:y=h-th-18"
DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1" DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1"
fi fi
# ── Pick audio source ─────────────────────────────────────────────────────────
audio_src=""
audio_offset="0"
if $TEST_MODE; then
# Test: grab live RTSP audio now
rtsp_var="CAM_RTSP_${CAM_NAME}"
RTSP_URL="${!rtsp_var:-}"
if [ -n "$RTSP_URL" ]; then
temp_live_audio=$(mktemp --suffix=.m4a)
echo "Test: recording ${SUNRISE_TARGET_SECS}s of live audio..."
if ffmpeg -loglevel warning \
-rtsp_transport tcp \
-i "$RTSP_URL" \
-t "$SUNRISE_TARGET_SECS" \
-vn -c:a aac -b:a 64k \
-y "$temp_live_audio"; then
audio_src="$temp_live_audio"
echo "Audio: live RTSP captured OK"
else
echo "Audio: live RTSP failed — continuing without audio"
rm -f "$temp_live_audio"; temp_live_audio=""
fi
else
echo "Audio: CAM_RTSP_${CAM_NAME} not set — skipping"
fi
else
cam_audio="$image_dir/sunrise-audio.m4a"
if [ "${AUDIO_ENABLED:-false}" = "true" ]; then
if [ -f "$cam_audio" ]; then
audio_src="$cam_audio"
echo "Audio: camera recording (centred on sunrise)"
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
fi
# ── Step 3: Audio mix + overlay — each independently optional/fallible ──────── # ── Step 3: Audio mix + overlay — each independently optional/fallible ────────
# Step 3a: mixes audio into a temp copy of the sped video (video stream is # 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). # copied, not re-encoded — fast, and lets step 3b fail independently).
# Step 3b: burns the overlay onto whatever 3a produced. # Step 3b: burns the overlay onto whatever 3a produced.
# Failure matrix → upload always fires: # Failure matrix → upload always fires (production); test just continues:
# audio ✓ overlay ✓ → final has audio + overlay # audio ✓ overlay ✓ → final has audio + overlay
# audio ✓ overlay ✗ → final has audio, no overlay # audio ✓ overlay ✗ → final has audio, no overlay
# audio ✗ overlay ✓ → final has overlay, no audio # audio ✗ overlay ✓ → final has overlay, no audio
# audio ✗ overlay ✗ → sped video promoted (no audio, no overlay) # audio ✗ overlay ✗ → sped video promoted (no audio, no overlay)
final_video="$output_dir/$current_date-daily-sunrise.mp4" if $TEST_MODE; then
fade_out=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc) final_video="$output_dir/$current_date-daily-sunrise-test.mp4"
else
# ── Pick audio source ───────────────────────────────────────────────────────── final_video="$output_dir/$current_date-daily-sunrise.mp4"
audio_src=""
audio_offset="0" # cam audio is pre-centred on sunrise; library files are trimmed to fit
cam_audio="$image_dir/sunrise-audio.m4a"
if [ "${AUDIO_ENABLED:-false}" = "true" ]; then
if [ -f "$cam_audio" ]; then
# Already exactly SUNRISE_TARGET_SECS, centred on sunrise — no offset needed
audio_src="$cam_audio"
echo "Audio: camera recording (centred on sunrise)"
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 fi
fade_out=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc)
# ── Step 3a: mix audio (video copied, not re-encoded) ──────────────────────── # ── Step 3a: mix audio (video copied, not re-encoded) ────────────────────────
work_video="$sped_video" work_video="$sped_video"
@@ -208,9 +286,11 @@ if [ -n "$audio_src" ]; then
else else
rm -f "$temp_audio"; temp_audio="" rm -f "$temp_audio"; temp_audio=""
echo "Audio mix failed — step 3b will be overlay-only" echo "Audio mix failed — step 3b will be overlay-only"
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise audio mix failed $current_date" \ if ! $TEST_MODE; then
"Audio could not be mixed — continuing with overlay only" || true "$SCRIPT_DIR/notify.sh" "WARNING: sunrise audio mix failed $current_date" \
[ "$audio_src" = "$cam_audio" ] && rm -f "$cam_audio" "Audio could not be mixed — continuing with overlay only" || true
[ "$audio_src" = "${cam_audio:-}" ] && rm -f "${cam_audio:-}"
fi
fi fi
fi fi
@@ -232,10 +312,12 @@ if [ -n "$DT" ]; then
mv "$work_video" "$final_video" mv "$work_video" "$final_video"
[ "$work_video" != "$sped_video" ] && rm -f "$sped_video" [ "$work_video" != "$sped_video" ] && rm -f "$sped_video"
temp_audio="" temp_audio=""
[ -f "$cam_audio" ] && rm -f "$cam_audio" if ! $TEST_MODE; then
echo "Overlay failed — promoting ${promote_label} video as upload target" [ -f "${cam_audio:-}" ] && rm -f "${cam_audio:-}"
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise overlay failed $current_date" \ echo "Overlay failed — promoting ${promote_label} video as upload target"
"Overlay failed — uploading ${promote_label} video (no timestamp)" || true "$SCRIPT_DIR/notify.sh" "WARNING: sunrise overlay failed $current_date" \
"Overlay failed — uploading ${promote_label} video (no timestamp)" || true
fi
step3b_ok=true # degraded but recoverable — upload still fires step3b_ok=true # degraded but recoverable — upload still fires
fi fi
else else
@@ -248,19 +330,22 @@ fi
if $step3b_ok; then if $step3b_ok; then
actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_video") 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})" echo "Done: $final_video (${actual_dur}s, time: ${SR_TIME})"
rm -f "$sped_video" "$temp_audio" 2>/dev/null || true; temp_audio="" rm -f "$sped_video" "$temp_audio" 2>/dev/null || true; 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
# ── Rolling retention: delete sunrise videos older than SUNRISE_RETENTION_DAYS ─ if ! $TEST_MODE; then
retain="${SUNRISE_RETENTION_DAYS:-10}" [ -f "${cam_audio:-}" ] && rm -f "${cam_audio:-}"
if [ "$retain" -gt 0 ]; then "$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \
deleted_old=$(find "$MOVIES_DIR/$CAM_NAME" \ "$(basename "$final_video")${actual_dur}s, sunrise at ${SR_TIME}" || true
-path "*/sunrise-only/*-daily-sunrise.mp4" \
-mtime +"$retain" -print -delete 2>/dev/null | wc -l) # ── Rolling retention ─────────────────────────────────────────────────
[ "$deleted_old" -gt 0 ] && \ retain="${SUNRISE_RETENTION_DAYS:-10}"
echo "Retention: deleted $deleted_old sunrise video(s) older than ${retain} days" if [ "$retain" -gt 0 ]; then
deleted_old=$(find "$MOVIES_DIR/$CAM_NAME" \
-path "*/sunrise-only/*-daily-sunrise.mp4" \
-mtime +"$retain" -print -delete 2>/dev/null | wc -l)
[ "$deleted_old" -gt 0 ] && \
echo "Retention: deleted $deleted_old sunrise video(s) older than ${retain} days"
fi
fi fi
fi fi
+31 -28
View File
@@ -1,15 +1,16 @@
#!/bin/bash #!/bin/bash
# test-sunrise.sh — smoke-test the sunrise pipeline using recent captured frames. # test-sunrise.sh — smoke-test the sunrise pipeline using recent captured frames.
# #
# Uses the same steps as daily_sunrise_video.sh: # Uses the last ~10 minutes of JPEGs from SUNRISE_CAM then runs the same
# 1. raw video from the most recent ~2 min of JPEGs (12 frames @ 10s interval) # 4-step pipeline as daily_sunrise_video.sh:
# 1. raw video from captured JPEGs
# 2. speed-adjust to SUNRISE_TARGET_SECS # 2. speed-adjust to SUNRISE_TARGET_SECS
# 3. grab 10s of live RTSP audio centred on the mid-point frame # 3. grab SUNRISE_TARGET_SECS of live RTSP audio
# 4. mix audio + burn vertical time overlay # 4. mix audio + burn vertical time overlay
# #
# Usage: # Usage:
# ./test-sunrise.sh [output_path] # ./test-sunrise.sh [output_path]
# ./test-sunrise.sh ~/drives/data2-main/test2.mp4 # ./test-sunrise.sh ~/drives/data2-main/test3.mp4
set -euo pipefail set -euo pipefail
@@ -26,7 +27,7 @@ OUTPUT="${1:-/tmp/sunrise-test.mp4}"
CAM="$SUNRISE_CAM" CAM="$SUNRISE_CAM"
TODAY=$(date +%Y-%m-%d) TODAY=$(date +%Y-%m-%d)
image_dir="$BASE_DIR/$CAM/$TODAY" image_dir="$BASE_DIR/$CAM/$TODAY"
NUM_FRAMES=12 # ~2 minutes at 10s interval NUM_FRAMES=60 # ~10 minutes at 10s interval
# ── Collect frames ──────────────────────────────────────────────────────────── # ── Collect frames ────────────────────────────────────────────────────────────
if [ ! -d "$image_dir" ]; then if [ ! -d "$image_dir" ]; then
@@ -49,11 +50,20 @@ mid_hms=$(basename "$mid_frame" .jpg) # HH-MM-SS
MID_TIME=$(echo "$mid_hms" | cut -c1-5 | tr '-' ':') # HH:MM MID_TIME=$(echo "$mid_hms" | cut -c1-5 | tr '-' ':') # HH:MM
echo "Mid-point frame: $mid_hms → overlay time: $MID_TIME" echo "Mid-point frame: $mid_hms → overlay time: $MID_TIME"
# Vertical text: each character on its own line. # ── Temp files ────────────────────────────────────────────────────────────────
# Inside ffmpeg single-quoted text values, \ is an escape prefix so \n → n. TMP_LIST=$(mktemp --suffix=.txt)
# We need \\n so ffmpeg parses it as \n which drawtext renders as a newline. TMP_RAW=$(mktemp --suffix=.mp4)
# In bash double-quotes \\\\n → \\n (the value stored in the variable). TMP_SPED=$(mktemp --suffix=.mp4)
MID_VERT="${MID_TIME:0:1}\\\\n${MID_TIME:1:1}\\\\n${MID_TIME:2:1}\\\\n${MID_TIME:3:1}\\\\n${MID_TIME:4:1}" TMP_AUDIO=$(mktemp --suffix=.m4a)
TMP_TEXT=$(mktemp --suffix=.txt)
cleanup() { rm -f "$TMP_LIST" "$TMP_RAW" "$TMP_SPED" "$TMP_AUDIO" "$TMP_TEXT" 2>/dev/null || true; }
trap cleanup EXIT
# Write each character on its own line for drawtext's textfile option.
# Avoids \n escape ambiguity across ffmpeg filter-string escaping levels.
printf '%s\n%s\n%s\n%s\n%s' \
"${MID_TIME:0:1}" "${MID_TIME:1:1}" "${MID_TIME:2:1}" "${MID_TIME:3:1}" "${MID_TIME:4:1}" \
> "$TMP_TEXT"
# ── Font detection ──────────────────────────────────────────────────────────── # ── Font detection ────────────────────────────────────────────────────────────
FONT="" FONT=""
@@ -69,16 +79,8 @@ if [ -z "$FONT" ]; then
done done
fi fi
# ── Temp files ──────────────────────────────────────────────────────────────── # ── Write concat list ─────────────────────────────────────────────────────────
TMP_LIST=$(mktemp --suffix=.txt) # Explicit duration per frame so raw video represents real elapsed time.
TMP_RAW=$(mktemp --suffix=.mp4)
TMP_SPED=$(mktemp --suffix=.mp4)
TMP_AUDIO=$(mktemp --suffix=.m4a)
cleanup() { rm -f "$TMP_LIST" "$TMP_RAW" "$TMP_SPED" "$TMP_AUDIO" 2>/dev/null || true; }
trap cleanup EXIT
# Write concat list with explicit duration per frame so raw video represents
# real elapsed time: 12 frames × CAPTURE_INTERVAL seconds = ~2 minutes.
# The final entry needs a duplicate without duration (ffmpeg concat requirement). # The final entry needs a duplicate without duration (ffmpeg concat requirement).
for f in "${frames[@]}"; do for f in "${frames[@]}"; do
printf "file '%s'\nduration %s\n" "$f" "${CAPTURE_INTERVAL:-10}" printf "file '%s'\nduration %s\n" "$f" "${CAPTURE_INTERVAL:-10}"
@@ -101,14 +103,12 @@ echo " Raw: ${raw_dur}s speed: ${speed}x"
echo "Step 2/4: speed-adjust to ${SUNRISE_TARGET_SECS}s..." echo "Step 2/4: speed-adjust to ${SUNRISE_TARGET_SECS}s..."
ffmpeg -loglevel warning \ ffmpeg -loglevel warning \
-i "$TMP_RAW" \ -i "$TMP_RAW" \
-vf "setpts=PTS/$speed" \ -vf "setpts=PTS/$speed,fps=25" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \ -c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
-t "$SUNRISE_TARGET_SECS" \ -t "$SUNRISE_TARGET_SECS" \
-y "$TMP_SPED" -y "$TMP_SPED"
# ── Step 3: grab 10s of live audio centred on mid-point ────────────────────── # ── Step 3: grab audio ────────────────────────────────────────────────────────
# Connects to the RTSP stream briefly — audio from "right now" stands in for
# the real sunrise-audio.m4a that capture.sh would record at sunrise.
echo "Step 3/4: recording ${SUNRISE_TARGET_SECS}s of live audio from camera..." echo "Step 3/4: recording ${SUNRISE_TARGET_SECS}s of live audio from camera..."
rtsp_var="CAM_RTSP_${CAM}" rtsp_var="CAM_RTSP_${CAM}"
RTSP_URL="${!rtsp_var:-}" RTSP_URL="${!rtsp_var:-}"
@@ -135,11 +135,14 @@ echo "Step 4/4: mix audio + overlay → $OUTPUT"
mkdir -p "$(dirname "$OUTPUT")" mkdir -p "$(dirname "$OUTPUT")"
FADE_OUT=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc) FADE_OUT=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc)
DT="drawtext=fontfile='${FONT}'" if [ -n "$FONT" ]; then
DT="${DT}:text='${MID_VERT}'" DT="drawtext=fontfile='${FONT}':textfile='${TMP_TEXT}'"
DT="${DT}:fontcolor=white@${SUNRISE_OVERLAY_OPACITY}" else
DT="drawtext=textfile='${TMP_TEXT}'"
fi
DT="${DT}:fontcolor=yellow@${SUNRISE_OVERLAY_OPACITY}"
DT="${DT}:fontsize=h/22:line_spacing=4" DT="${DT}:fontsize=h/22:line_spacing=4"
DT="${DT}:x=w-tw-18:y=(h-th)/2" DT="${DT}:x=w-tw-18:y=h-th-18"
DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1" DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1"
if $HAS_AUDIO; then if $HAS_AUDIO; then