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
read_config / _read_conf now strips trailing inline shell comments
(whitespace + # + anything) before processing the value. Without this,
a line like:
SUNRISE_CAM=east # which camera faces east
produced SUNRISE_CAM = 'east # which camera faces east',
causing sunrise2mm.py to build a path with the comment embedded in it.
The regex \s+#.*$ requires at least one whitespace before # so passwords
or URLs containing # (e.g. %23 URL-encoded) are unaffected.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Audio:
AUDIO_DIR/<cam>/YYYY-MM-DD/<cam>-HH-MM-SS.m4a
Movies — cam name now always leads the filename:
daily_sunrise_video.sh:
east-2026-04-22-daily-sunrise.mp4
east-2026-04-22-daily-sunrise-sped.mp4 (intermediate)
east-2026-04-22-daily-sunrise-test.mp4 (demo)
4-seasons.sh:
east-2026-04-21_Mvt2-Day2of31-final.mp4 (was: 2026-04-21_east_Mvt2-...)
montage-mvt.sh:
east-2026-03-20_Spring_Mvt1-Sped.mp4
east-2026-03-20_Spring_Mvt1-Montage.mp4
year-end-join.sh:
east-2026.mp4 (already had cam at front)
sunrise2mm.py updated to look for the new east-YYYY-MM-DD-daily-sunrise.mp4
filename. Retention find patterns (wildcards) already match both formats.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
New AUDIO_DIR variable (like BASE_DIR/MOVIES_DIR) is the root for all
audio recordings. Set in .env to put audio on a separate drive.
Directory layout:
AUDIO_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.m4a — ambient recordings
AUDIO_DIR/sunrise/<cam>/YYYY-MM-DD/sunrise-audio.m4a — sunrise clips
sky-cam.conf:
- Add AUDIO_DIR="${AUDIO_DIR:-$BASE_DIR/audio}" in storage section
- Remove AMBIENT_DIR (replaced by AUDIO_DIR)
- Add SUNRISE_AUDIO_RETENTION_DAYS=7 (sunrise clips kept separately from ambient)
- Add per-camera ambient retention examples:
AMBIENT_RETENTION_DAYS_south=60 (keep bird recordings longer)
ambient-record.sh:
- Use AUDIO_DIR instead of AMBIENT_DIR
- Per-camera retention: AMBIENT_RETENTION_DAYS_<cam> overrides global default
sunrise-audio-capture.sh:
- Save clips to AUDIO_DIR/sunrise/<cam>/<date>/sunrise-audio.m4a
- Rolling retention of sunrise clips (SUNRISE_AUDIO_RETENTION_DAYS)
daily_sunrise_video.sh:
- Look for cam_audio in new AUDIO_DIR/sunrise path first, fall back to
old BASE_DIR path so existing recordings keep working
install.sh:
- Pre-create AUDIO_DIR/<cam> and AUDIO_DIR/sunrise/<SUNRISE_CAM> dirs
- Add AUDIO_DIR to .env.example storage section
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
daily_sunrise_video.sh:
- Test output now saves to DEMO_DIR/<cam>/ (default: MOVIES_DIR/demoio/<cam>/)
instead of the production sunrise-only directory
- After each test run, deletes test files older than DEMO_RETENTION_DAYS (8)
- Production mode: warns if the last selected frame is >3 intervals before
the window end, which indicates the capture service had a gap near sunrise:
WARNING: last frame is 20min 0s before window end — capture gap near sunrise
Check: systemctl --user status sky-cam-capture-east.service
sky-cam.conf: add DEMO_DIR and DEMO_RETENTION_DAYS settings
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
daily_sunrise_video.sh:
- Fix "Sunrise (UTC)" label — sunrise.py has always returned local time;
now labelled correctly as "Sunrise (local)"
- After image selection, log first and last frame timestamps alongside the
overlay time so the window can be verified at a glance:
Images: 481 (05:22:10 → 06:43:00 overlay: 06:32)
sunrise2mm.py:
- Cleanup now only deletes posts whose message matches "Sunrise YYYY-MM-DD"
(the format this script posts), so other posts with attachments in the
channel are never touched
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sunrise2mm.py: after a successful upload, scan the channel and delete any
post older than MM_SUNRISE_RETENTION_DAYS that is not pinned and has a file
attachment. Pinned posts are always preserved regardless of age.
sky-cam.conf: add MM_SUNRISE_RETENTION_DAYS=8 (independent of the local
disk SUNRISE_RETENTION_DAYS so the two retentions can differ).
Also modernises sunrise2mm.py: uses the same ${VAR:-default} aware
read_config as sunrise.py, consistent error handling, cleaner structure.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Rule is now consistent throughout:
- *_ENABLED flags are plain true/false in sky-cam.conf — flip them there
- Private values (URLs, credentials, location) go in .env only
sky-cam.conf:
- NTFY_ENABLED, EMAIL_ENABLED, MM_NOTIFY_ENABLED are now plain false (not
using the ${VAR:-} pattern, which implied they could go in .env)
- NTFY_URL, EMAIL_TO, EMAIL_FROM, MM_NOTIFY_CHANNEL_ID use ${VAR:-} pattern
so they are set in .env
- LATITUDE/LONGITUDE comment clarified with example .env lines
install.sh .env.example: updated notification comment to match
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Adds TIMEZONE, BASE_DIR, MOVIES_DIR (commented out) so .env.example
is a complete reference — location, storage paths, RTSP URLs, Mattermost
credentials, and notification endpoints all in one place.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sky-cam.conf:
- MUSIC_DIR now uses override pattern (MUSIC_DIR="${MUSIC_DIR:-$SCRIPT_DIR/music}")
so the real path can be set in .env without hardcoding it
- LATITUDE/LONGITUDE use override pattern; set real coords in .env to keep
location private and out of git
- NTFY_URL now uses override pattern with empty default; set in .env
- NTFY_ENABLED moved out of comments into active config with false default
sunrise.py:
- Reads .env after sky-cam.conf so .env values override (same source order
as sky-cam.conf itself)
- Handles ${VAR:-default} bash syntax when parsing sky-cam.conf values
All completion notifications now include the full output file path so you
can tell at a glance where the file landed:
- daily_sunrise_video.sh: "filename — Xs, sunrise at HH:MM | /full/path"
- 4-seasons.sh: "filename — Xs | /full/path"
- montage-mvt.sh: "filename | checks | /full/path | verify: ..."
- year-end-join.sh: "filename — Xmin | /full/path" (also adds [cam] to title)
install.sh .env.example now includes LATITUDE, LONGITUDE, MUSIC_DIR, NTFY_URL
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- chmod +x all *.sh and *.py in install.sh so permissions survive git checkouts
- Fix daily_sunrise_video.sh and Python scripts missing executable bit
- Add [CAM_NAME] prefix to all 4-seasons.sh notify.sh calls so multi-camera
setups show which camera each daily clip came from
- Include camera name in output filename: YYYY-MM-DD_<cam>_MvtN-DayXofY-final.mp4
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Per-camera capture interval:
CAPTURE_INTERVAL_<cam> in sky-cam.conf overrides the global value for
a specific camera. capture.sh and 4-seasons.sh both respect it.
Example: south at 30s saves storage; north at 5s gives smoother motion.
4-seasons.sh one-frame bug fix:
Same root cause as the sunrise scripts — concat list had no 'duration'
entries, collapsing all JPEG frames to timestamp 0. Fixed by writing
'duration INTERVAL' per frame. Added fps=25 to the speed-adjust step
so the VFR source resamples to a standard frame rate.
ambient-record.sh (new):
Continuous AAC recorder for natural sounds (birds, rain, wind).
Saves AMBIENT_CHUNK_SECS chunks to AMBIENT_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.m4a.
Reconnects automatically on stream failure. Rolling retention via
AMBIENT_RETENTION_DAYS. Enabled by setting AMBIENT_ENABLED=true and
listing cameras in AMBIENT_CAMS in sky-cam.conf, then re-running install.sh.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Notification config (NTFY_URL, MM_NOTIFY_CHANNEL_ID, etc.) lives in
.env but notify.sh was only sourcing sky-cam.conf, so notifications
were silently skipped even when configured.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
enable --now only starts a service that is not yet running — it leaves
an already-running service untouched, so changes to .env credentials
are never picked up without a manual restart.
Switching to enable + restart means every install.sh run brings capture
services in sync with the current sky-cam.conf and .env: new cameras
start, reconfigured cameras restart, and nothing needs manual follow-up.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
CAM_RTSP_<cam> is set in .env, not sky-cam.conf, but capture.sh was
only sourcing sky-cam.conf — causing the service to fail with "not set"
even after credentials were added to .env.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Root cause: the concat list had no 'duration' entries, so all JPEG
frames collapsed to timestamp 0 and only one frame survived encoding.
Fix: add 'duration CAPTURE_INTERVAL' per entry in both scripts.
Also add fps=25 to the speed-adjust step so the VFR source is resampled
to a standard frame rate — without this, players see ~6fps and may drop
frames to reach the declared rate.
daily_sunrise_video.sh --test [MINUTES]:
Skips sunrise.py lookup and capture-window wait. Uses the last MINUTES
minutes of captured frames (default 2). Overlay shows the mid-point
frame's timestamp. Audio grabbed live from RTSP immediately.
Output: <date>-daily-sunrise-test.mp4 — no notifications or upload.
Usage: ./daily_sunrise_video.sh east --test 2
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- fontcolor: white → yellow (both scripts)
- y position: (h-th)/2 → h-th-18 (vertically centered → lower-right corner)
- test-sunrise.sh: reverted to reading last 60 frames from capture directory
(~10 minutes at 10s interval) instead of live RTSP capture
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Overlay fix (both scripts):
Switch from text='...\n...' to drawtext's textfile= option. Writing
each character on its own line eliminates the \n escape ambiguity in
ffmpeg's filter-string single-quote parser, which was causing 'n' to
appear literally instead of as a line break.
test-sunrise.sh redesign:
Now captures NUM_FRAMES fresh RTSP frames (120s at default interval)
at run time instead of reading pre-existing frames from the capture
directory. The RTSP URL must be set in .env — the script fails early
with a clear message if it is not.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- Overlay text was showing characters run together (e.g. '1n2n') because
inside ffmpeg single-quoted filter text, \n is consumed as just 'n'.
Fix: use \\\\n in bash double-quotes which stores \\n in the variable,
which ffmpeg then parses as \n, which drawtext renders as a newline.
Applied to both test-sunrise.sh (MID_VERT) and daily_sunrise_video.sh
(SR_VERT, replacing the awk one-liner with the same bash approach).
- test-sunrise.sh concat list was missing per-frame duration entries,
causing 12 frames to encode as 0.48s (25fps default) and then slow
down instead of compress. Fixed by writing 'duration INTERVAL' per
frame so 12 frames × 10s = 120s raw → 12× compression to 10s.
- Removed 2>/dev/null from audio ffmpeg call in test-sunrise.sh so
capture failures are visible for diagnosis.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Runs the same 4-step pipeline as daily_sunrise_video.sh but uses the
most recent ~2 minutes of captured frames instead of the sunrise window,
grabs live RTSP audio for the audio mix step, and accepts an arbitrary
output path — so it can be run at any time of day to verify the full
pipeline (speed-adjust, audio mix, vertical time overlay) without
waiting for tomorrow's sunrise.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ