From 3f5b89e5dad34b38bf0f156da04dfa7559487f0d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 22:31:06 +0000 Subject: [PATCH 1/4] README: document backfilling multiple dates with 4-seasons.sh loop https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index e5e2fda..e5734bf 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,23 @@ Attribution data for every file is written to `sunrise-sounds/manifest.json`. ./4-seasons.sh east ``` +**Re-run a daily seasons clip** for a specific past date: +```bash +./4-seasons.sh east 2026-04-19 +``` + +**Backfill multiple past dates** (useful after a camera outage or a fresh install with existing images): +```bash +for d in 2026-04-15 2026-04-16 2026-04-17 2026-04-18 2026-04-19; do + ./4-seasons.sh east "$d" +done +``` + +Replace the date list with whatever range you need. Each run produces one `*-final.mp4` in the movement's output directory. Once all days for a movement are present you can build the montage manually: +```bash +./montage-mvt.sh east 2026-04-19 # date of the last day of that movement +``` + **Rebuild a movement montage** (e.g. to retry audio after a failure): ```bash ./montage-mvt.sh east # uses today's date From a9faa63cd9342a22136c781afc87a62fe8cf8f1d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 01:52:38 +0000 Subject: [PATCH 2/4] =?UTF-8?q?4-seasons:=20remove=20per-frame=20ffprobe?= =?UTF-8?q?=20validation=20=E2=80=94=20too=20slow=20at=20scale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 43k images × ffprobe process launch = 30-90 min before encoding starts. Every corrupt file seen in practice has been 0-byte (caught instantly by [ ! -s ]). Non-zero corrupt files are rare; if ffmpeg hits one the existing drift warning flags the short output. Removing ffprobe restores the fast path: one stat() call per image. https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ --- 4-seasons.sh | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/4-seasons.sh b/4-seasons.sh index 48a2582..d051a55 100755 --- a/4-seasons.sh +++ b/4-seasons.sh @@ -102,18 +102,11 @@ GREY_MIN="${SEASONS_GREY_STDDEV_MIN:-0}" GREY_DARK_FLOOR="${SEASONS_GREY_DARK_FLOOR:-30}" # frames darker than this are never skipped images=(); bad=0; grey=0 for img in "${all_images[@]}"; do - # Empty or unreadable + # Skip empty or unreadable files (0-byte writes from camera reconnects) if [ ! -r "$img" ] || [ ! -s "$img" ]; then echo "WARNING: skipping empty/missing frame: $(basename "$img")" ((bad++)) || true; continue fi - # Corrupt JPEG — ffprobe can't identify any video stream - if ! ffprobe -v quiet -select_streams v:0 \ - -show_entries stream=codec_name -of csv=p=0 "$img" 2>/dev/null \ - | grep -q .; then - echo "WARNING: skipping corrupt frame: $(basename "$img")" - ((bad++)) || true; continue - fi # Optional: skip uniform-grey bad-signal frames. # A frame is bad-signal if it is BOTH uniform (low stddev) AND not dark. # Night frames are dark so they pass even with low stddev. From 98dcf236873257df262789d1320bee1aa9035ff4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 02:34:26 +0000 Subject: [PATCH 3/4] 4-seasons: replace size-rejection with 1MB fast-skip for grey filter File size can't reliably reject bad frames (200KB can be good or bad). Remove SEASONS_MIN_FRAME_BYTES. Instead, the grey/green uniform-colour filter (SEASONS_GREY_STDDEV_MIN) now skips the expensive ImageMagick check for files above SEASONS_LARGE_FRAME_BYTES (default 1 MB) since those are always good. Only sub-1MB files get the content check, making the filter practical on days with 40k+ images. https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ --- 4-seasons.sh | 30 ++++++++++++++++++------------ sky-cam.conf | 15 ++++++++++----- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/4-seasons.sh b/4-seasons.sh index d051a55..7429505 100755 --- a/4-seasons.sh +++ b/4-seasons.sh @@ -95,11 +95,14 @@ trap cleanup EXIT mapfile -t all_images < <(find "$image_dir" -type f -name "*.jpg" | sort) -# Validate each frame: skip empty files, corrupt JPEGs, and optionally grey/uniform frames. -# SEASONS_GREY_STDDEV_MIN=0 (default) disables the grey filter. -# Set to e.g. 5 in sky-cam.conf to drop uniform grey frames (bad-signal captures). +# Validate each frame: skip empty files and optionally uniform-colour bad-signal frames. +# SEASONS_GREY_STDDEV_MIN=0 (default) disables the content check entirely. +# When enabled, large files (> SEASONS_LARGE_FRAME_BYTES, default 1 MB) are trusted +# as good without running the expensive ImageMagick check — only sub-1MB frames +# are inspected. This keeps the filter fast even on days with 40k+ images. GREY_MIN="${SEASONS_GREY_STDDEV_MIN:-0}" -GREY_DARK_FLOOR="${SEASONS_GREY_DARK_FLOOR:-30}" # frames darker than this are never skipped +GREY_DARK_FLOOR="${SEASONS_GREY_DARK_FLOOR:-30}" +LARGE_BYTES="${SEASONS_LARGE_FRAME_BYTES:-1048576}" # files above this skip content check images=(); bad=0; grey=0 for img in "${all_images[@]}"; do # Skip empty or unreadable files (0-byte writes from camera reconnects) @@ -107,15 +110,18 @@ for img in "${all_images[@]}"; do echo "WARNING: skipping empty/missing frame: $(basename "$img")" ((bad++)) || true; continue fi - # Optional: skip uniform-grey bad-signal frames. - # A frame is bad-signal if it is BOTH uniform (low stddev) AND not dark. - # Night frames are dark so they pass even with low stddev. + # Optional: skip uniform-colour bad-signal frames (green, grey, etc.). + # Only runs ImageMagick on files below SEASONS_LARGE_FRAME_BYTES — large files + # are always good and skip the check entirely. if [ "$GREY_MIN" != "0" ] && command -v convert &>/dev/null; then - read -r stddev mean < <(convert "$img" -colorspace Gray \ - -format "%[fx:int(standard_deviation*255)] %[fx:int(mean*255)]" \ - info: 2>/dev/null || echo "255 128") - if [ "${stddev:-255}" -lt "$GREY_MIN" ] && [ "${mean:-0}" -gt "$GREY_DARK_FLOOR" ]; then - ((grey++)) || true; continue + fsize=$(stat -c%s "$img" 2>/dev/null || echo 0) + if [ "$fsize" -lt "$LARGE_BYTES" ]; then + read -r stddev mean < <(convert "$img" -colorspace Gray \ + -format "%[fx:int(standard_deviation*255)] %[fx:int(mean*255)]" \ + info: 2>/dev/null || echo "255 128") + if [ "${stddev:-255}" -lt "$GREY_MIN" ] && [ "${mean:-0}" -gt "$GREY_DARK_FLOOR" ]; then + ((grey++)) || true; continue + fi fi fi images+=("$img") diff --git a/sky-cam.conf b/sky-cam.conf index ed9d92b..597a8b1 100644 --- a/sky-cam.conf +++ b/sky-cam.conf @@ -256,18 +256,23 @@ ENCODE_PRESET=slow # ffmpeg preset for all encodes (slow/medium/fast) AUDIO_BITRATE=192k # montage-mvt.sh — music track on movement montages # ── Frame validation (4-seasons.sh) ────────────────────────────────────────── -# Corrupt or empty frames (e.g. first frame after a camera reconnect) are always -# skipped automatically via ffprobe. No config needed. +# Empty (0-byte) frames are always skipped. No config needed for that. # # SEASONS_GREY_STDDEV_MIN / SEASONS_GREY_DARK_FLOOR — optional bad-signal filter. +# Catches uniform-colour frames caused by a bad RTSP signal: solid green, grey, etc. +# These are valid JPEGs but contain no real image content. # Uncomment both lines to enable. A frame is dropped only when BOTH are true: # 1. pixel standard deviation (0–255) < SEASONS_GREY_STDDEV_MIN (frame is uniform) # 2. mean brightness (0–255) > SEASONS_GREY_DARK_FLOOR (frame is not dark) -# This keeps genuine night frames (dark + low variation) while dropping -# solid-grey bad-signal frames (mid-bright + no variation). +# Night frames are dark so they always pass even if stddev is low. # Requires ImageMagick (convert). Start with these values and tune from there: -#SEASONS_GREY_STDDEV_MIN=5 # skip if stdev < 5 (very uniform) +#SEASONS_GREY_STDDEV_MIN=5 # skip if stdev < 5 (very uniform colour) #SEASONS_GREY_DARK_FLOOR=30 # …but only if mean brightness > 30 (not a night frame) +# +# Performance: ImageMagick is only called on files below SEASONS_LARGE_FRAME_BYTES. +# Files above that threshold are always good and skip the check entirely. +# Default 1 MB — adjust if your camera produces larger bad-signal frames. +#SEASONS_LARGE_FRAME_BYTES=1048576 # ── Montage attribution overlay ─────────────────────────────────────────────── # A translucent bar across the top of each movement montage, naming the From 8a7739502c14f0fcbd34d8eaf13f4d0bf897c1b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Apr 2026 12:00:57 +0000 Subject: [PATCH 4/4] fix: daily_sunrise_video.sh exits non-zero, blocking OnSuccess= upload Conditional expressions like `[ "$count" -gt 0 ] && echo ...` return 1 when count is 0 (nothing to delete). Under set -euo pipefail with no explicit exit 0, that false becomes the script's exit code. systemd sees failure, OnSuccess= for sunrise2mm.py never fires, and the Mattermost upload is silently skipped every day. Fix: add || true to all end-of-script conditional-echo statements and to the cam_audio cleanup line, then add an explicit exit 0 so the success path always exits cleanly. https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ --- daily_sunrise_video.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/daily_sunrise_video.sh b/daily_sunrise_video.sh index c3076fa..bb0908a 100755 --- a/daily_sunrise_video.sh +++ b/daily_sunrise_video.sh @@ -356,10 +356,10 @@ if $step3b_ok; then -name "*-daily-sunrise-test.mp4" \ -mtime +"$demo_retain" -print -delete 2>/dev/null | wc -l) [ "$deleted_demo" -gt 0 ] && \ - echo "Demo retention: deleted $deleted_demo test file(s) older than ${demo_retain} days" + echo "Demo retention: deleted $deleted_demo test file(s) older than ${demo_retain} days" || true fi else - [ -f "${cam_audio:-}" ] && rm -f "${cam_audio:-}" + [ -f "${cam_audio:-}" ] && rm -f "${cam_audio:-}" || true "$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \ "$(basename "$final_video") — ${actual_dur}s, sunrise at ${SR_TIME} | $final_video" || true @@ -370,7 +370,9 @@ if $step3b_ok; then -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" + echo "Retention: deleted $deleted_old sunrise video(s) older than ${retain} days" || true fi fi fi + +exit 0