- daily_sunrise_video.sh: add live RTSP capture as final fallback in production
audio selection (fires when today, yesterday, and library all miss)
- On audio mix failure, retry once with a fresh live RTSP capture before
giving up — handles corrupt/incompatible pre-recorded files
- When mix still fails after retry, append last 30 lines of
sky-cam-audio-capture.service journal to the ntfy notification body
- Move sunrise-time text overlay from 5% to 15% above the bottom edge
https://claude.ai/code/session_01DANuzLCLhhQ7Li12RTcswY
Two new jobs operate on the SUNRISE_CAM, sharing three Python helpers
(moon_phase, moon_detect, moon_composite) and one cached lunar texture:
- moon-track.sh: nightly batch detects the moon in each east frame,
crops a 480x480 box around it, stitches into an mp4 that holds the
moon roughly centred while clouds and stars drift past.
- moon-phase-monthly.sh: daily check that runs whichever phase composite
is due that day. Handles full moon (posts D+3), first quarter (D+2,
best-effort due to daytime-only geometry from east), and third quarter
(D+2). Picks the frame closest in time to the exact phase moment that
meets quality / altitude / illumination thresholds, then composites
the cached lunar texture into it -- sky/halo/parallactic-angle/timing
real from east, surface detail borrowed from the reference image.
Honest-by-design: a 38-px white blob from a wide-field IP camera cannot
be enhanced into crater detail by software. The composite makes the
borrowing explicit and constrains everything else (when, where, sky,
orientation) to match what east actually saw.
install.sh now downloads the skyfield ephemeris (de421.bsp) and the
default lunar reference (Wikipedia CC BY-SA full-moon photo) on first
run. Both can be overridden via .env.
https://claude.ai/code/session_015PBVDESC3KLMbq1LpA6qLn
Drops clear/cloudy/fog/windy and the wind-speed override — those
conditions sound the same on the recordings, so they don't earn a tag.
Also renames "rainshower" → "rainstorm" and rebalances the rain
mapping so 503/504/522 (extreme + heavy showers) get the storm tag.
Final vocabulary: thunderstorm, rain, rainstorm, snow, snowstorm.
Anything else falls through to the un-tagged filename.
Adds weather-tag.sh, a small failure-safe helper that queries OpenWeather's
free "Current weather" endpoint and prints one tag from a fixed vocabulary
(thunderstorm, rain, rainshower, snow, snowstorm, fog, windy, cloudy, clear)
or nothing on any failure.
ambient-record.sh calls it just before each 30-minute chunk so the filename
reflects conditions at the moment of capture, e.g.
east-08-30-00-thunderstorm.m4a
east-12-00-00-clear.m4a
A foggy dawn no longer mislabels the sunny afternoon files.
Off by default. Enable with WEATHER_ENABLED=true and an
OPENWEATHER_API_KEY entry in .env; missing key / API error / disabled
toggle all fall through to the un-tagged filename, never blocking the
recorder.
The retention block ended with `[ test ] && echo`, which under `set -e`
returns exit 1 when `deleted` is 0 — the normal case on most days. As
the script's last command, that bash exit propagated to systemd, which
marked the unit failed and fired the OnFailure ntfy even though the
audio file was written successfully.
Add `|| true` and an explicit `exit 0` so a no-op retention pass is
treated as success.
Adds a quick-edit cheatsheet covering font, color, size, position, and
shadow/border for the sunrise-time overlay so the layout can be tweaked
without grokking ffmpeg's drawtext syntax.
Replaces the fixed 0.3s fade with SUNRISE_TARGET_SECS * 0.085, so a 10s
clip now fades 0.85s in and out. The fade scales automatically if the
target duration is tuned later.
- Shorten audio fade in/out to 0.3s so they fit comfortably in the 10s clip.
- Lift the sunrise-time overlay by ~5% of frame height to clear the
burnt-in timestamp on the source images.
- When today's sunrise recording is missing, fall back to yesterday's
clip before reaching for the library fallback.
- Print "Validating N frames..." before the loop, including active
filter thresholds when grey-checking is on
- Print "X / N frames checked..." every 500 frames so the user can
see the loop is alive during the long ImageMagick pass
- Print "Validation done: N valid frames" after the loop
- Note "(may take several minutes)" on the Step 1 encode echo
- Show the speed factor and target together: "28.8x → 11.08s target"
https://claude.ai/code/session_01U29A8NpgJ41tboiggZNmk8
When SEASONS_GREY_STDDEV_MIN is enabled, ImageMagick's convert outputs
the stddev/mean values without a trailing newline. read exits 1 on EOF-
without-newline even when it successfully populated the variables. With
set -euo pipefail this kills the script silently on the very first image
that enters the grey-filter code path — producing the observed symptom:
script stops after "Images found:" with no error and no video.
Fix: append || true so read's EOF exit code is absorbed. The variables
are still set correctly; only the exit code changes.
https://claude.ai/code/session_01U29A8NpgJ41tboiggZNmk8
When no corrupt or grey frames exist (bad=0, grey=0), the two
conditional-echo lines
[ "$bad" -gt 0 ] && echo "WARNING: ..."
[ "$grey" -gt 0 ] && echo "INFO: ..."
exit with code 1 because [ 0 -gt 0 ] is false and && short-circuits.
With set -euo pipefail the script treats that 1 as an error and exits
immediately with no output — producing exactly the "ran without making
a video, no error, no notification" symptom on any normal day where all
frames are valid.
Fix: append || true to each line so the expression always exits 0.
https://claude.ai/code/session_01U29A8NpgJ41tboiggZNmk8
Without explicit duration in the concat file, ffmpeg assigns timestamps
from EXIF metadata or defaults — producing a variable frame-rate raw
video with uneven frame spacing. Applying setpts without fps= then
preserved that unevenness into the final clip, causing the observed
pause-then-jump-minutes/hours behaviour.
Fix 1: write `duration 0.04` (1/25 s) for every JPEG in the concat
list, making the raw video a true 25 fps CFR stream. The duplicate
final-entry is appended as required by the ffmpeg concat demuxer so the
last content frame keeps its full display duration.
Fix 2: add `fps=25` to the setpts filter in step 2, resampling to
standard 25 fps CFR regardless of the speed factor, which guarantees
smooth, uniform playback in the final clip.
https://claude.ai/code/session_01U29A8NpgJ41tboiggZNmk8
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
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
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
Covers: installing git, first clone, pulling updates, stash vs reset
--hard, checking diffs, switching branches, and a note that .env is
never touched by git pulls.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Allows processing any past date instead of always yesterday:
./4-seasons.sh east 2026-04-19
Useful for backfilling days with existing images.
Systemd (no date arg) continues to default to yesterday.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
duration=INTERVAL in the concat file made a 0.1fps raw video, which
x264 couldn't speed up 4000x cleanly and produced 20s of the last
frame instead. The original script used plain 'file path' entries
with no duration, letting ffmpeg default to 25fps internally. The
speed_factor calculation against raw_duration compensates correctly
either way, but only the standard-fps raw video survives the extreme
setpts transform. Also removed the coverage_pct check which was
based on images*INTERVAL and became meaningless without duration.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Accessing ${images[-1]} on an empty array under set -u causes a bash
error. Guard with an explicit check so a fully-corrupt image directory
exits with a clear message instead of a cryptic crash.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
fps=25 caused ffmpeg to discard 15 of every 16 source frames, making
the final clip jump by ~2.5 minutes per frame. The original script
had no fps filter, so all source frames were preserved. Without it,
the setpts speed-adjust keeps every frame; the effective framerate in
the output rises proportionally (e.g. ~400fps for a full day at 10s
intervals into an 11s clip), which is fine for H.264 and all
downstream montage steps.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
The SEASONS_GREY_STDDEV_MIN and SEASONS_GREY_DARK_FLOOR settings are
now commented out so the filter is fully inert by default and requires
a deliberate edit to activate.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
A stddev-only filter would wrongly drop near-black night frames.
Now only skips a frame when BOTH stddev < GREY_MIN AND mean brightness
> GREY_DARK_FLOOR (default 30/255). Night frames are dark so they
always pass; bad-signal grey frames are mid-bright so they are caught.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
The previous size check caught empty files but not corrupt JPEGs with
a partial header (common first frame after a camera reconnect).
Replace with ffprobe codec detection — if ffprobe can't identify a
video stream in the file it's skipped with a clear "corrupt" message.
Also adds SEASONS_GREY_STDDEV_MIN (default 0 = off): when enabled,
frames with near-zero pixel standard deviation (uniform grey = bad
RTSP signal) are silently dropped before encoding.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Corrupt or 0-byte JPEGs (e.g. first frame after a camera reconnect)
caused ffmpeg concat to silently truncate the output, missing all
images after the bad file. Now filters those out before building the
concat list and logs a warning + notification when raw video covers
less than 50% of the expected image count.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ