daily_sunrise_video.sh: derive CAM_NAME from $1 (defaulting to SUNRISE_CAM)
instead of an undefined variable; this was causing empty image/output paths.
install.sh: pass $SUNRISE_CAM as argument to daily_sunrise_video.sh so it
matches the pattern used by every other per-camera script.
sunrise2mm.py: use SUNRISE_CAM config key (not the undefined CAM_NAME) to
locate the output directory; fixes silent path mismatch on non-default names.
4-seasons.sh: remove stray `set -x` that was flooding systemd logs with
shell trace output.
README.md: correct Python dependencies from `ephem` to `suntime pytz` to
match what sunrise.py actually imports.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
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
daily_sunrise_video.sh: priority chain is camera mic → library random MP3 →
overlay-only → sped video promotion. If audio mixing fails a warning
notification fires and the script retries with overlay-only so a video is
always produced and uploaded.
download-sunrise-sounds.py: downloads CC-licensed ambient sounds from
freesound.org into 11 weather/season category folders (~275 files total at
25 per category), writing a manifest.json with attribution data.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Removes dependency on MotionEye or any NVR software. Everything is now
CLI/ffmpeg configured entirely from sky-cam.conf.
capture.sh
Long-running systemd service (one per camera) that pulls frames from the
camera's RTSP stream at CAPTURE_INTERVAL seconds, writing HH-MM-SS.jpg
into BASE_DIR/<cam>/YYYY-MM-DD/. Restarts at midnight for the new date
directory; auto-reconnects on camera disconnect.
sunrise-audio-capture.sh
One-shot service triggered at 03:00. Waits until (sunrise minus
SUNRISE_PRE_MIN minus AUDIO_PRE_BUFFER_MIN), then records the RTSP audio
stream for the full sunrise window. Output deleted after mixing.
daily_sunrise_video.sh
Step 3 now mixes in sunrise-audio.m4a when AUDIO_ENABLED=true and the
file exists. Audio is centred on actual sunrise time at natural speed
while the video plays as the sped-up timelapse. Uses filter_complex
when audio is present (can't combine -vf with -filter_complex).
install.sh
Generates sky-cam-capture-<cam>.service for each camera with CAM_RTSP_<cam>
set, and sky-cam-audio-capture.{service,timer} when AUDIO_ENABLED=true.
sky-cam.conf
New settings: CAM_RTSP_<cam>, CAPTURE_INTERVAL, AUDIO_ENABLED,
AUDIO_PRE_BUFFER_MIN, CAPTURE_AUDIO_BITRATE.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Sunrise:
- Overlay failure no longer exits 1; the speed-only video is renamed to the
expected final path so OnSuccess still fires and the upload still happens
- sunrise2mm.py now notifies on every upload outcome (success, post failure,
file-not-found) via notify.sh so the user always knows whether the video
reached Mattermost
Montage:
- Music+overlay failure no longer exits 1; the sped video is promoted to the
*-Montage.mp4 path so year-end-join can still include this movement (as a
silent, no-overlay clip) and the user is notified of the degraded output
- Year-end join trigger is now outside the success/failure branch so it fires
in both cases
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Each pipeline now saves intermediate files permanently before the step most
likely to fail, so a partial failure leaves a recoverable artifact:
- daily_sunrise_video.sh: speed-adjusted video (*-sped.mp4) survives if the
drawtext overlay step fails; deleted automatically on success
- montage-mvt.sh: speed-adjusted video (*-Sped.mp4) survives if the
music-mux + attribution overlay step fails; deleted on success
- 4-seasons.sh / montage-mvt.sh / year-end-join.sh: explicit ffmpeg error
handling with targeted notify.sh calls naming the failed step and the
surviving artifact path
- montage-mvt.sh / 4-seasons.sh / year-end-join.sh: clip-duration-sum vs
music-duration drift check warns when cumulative float error exceeds 2 s
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
daily_sunrise_video.sh:
- Fixed: was using undefined \$MOVIES_DIR (now \$BASE_DIR/movies)
- Fixed: sunrise_overlay.py was called but never existed — would fail daily
- Fixed: speed-adjust step had no explicit codec (-c:v libx264 missing)
- New: sunrise time overlaid as stacked vertical characters on the right
side using ffmpeg drawtext — no Python/Pillow dependency
- New: upload removed from script; now triggered by systemd OnSuccess=
- Combines speed-adjust and overlay into one ffmpeg pass (was two)
- SUNRISE_OVERLAY_OPACITY from conf controls transparency
install.sh:
- sky-cam-sunrise.service gains OnSuccess=sky-cam-sunrise-upload.service
so upload only runs when video creation succeeds, with separate log
- New sky-cam-sunrise-upload.service runs sunrise2mm.py
sunrise2mm.py: add shebang + chmod +x so systemd ExecStart= calls it directly
sky-cam.conf: add SUNRISE_OVERLAY_OPACITY=0.45
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Adds six CRF vars, AUDIO_BITRATE, and three MONTAGE_* overlay durations.
Values match previous hardcoded defaults so behavior is unchanged.
The same CRF appeared in several scripts but with intentionally different
values per purpose (28 for draft intermediates, 26 for polished output).
Each script's CRF is now its own variable in conf, so changing one never
silently changes another.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sky-cam.conf:
- CAMERAS array replaces single CAM_NAME; add a name, add its schedules,
re-run install.sh — no script editing needed
- SCHEDULE_SUNRISE, SCHEDULE_SEASONS_<cam>, SCHEDULE_FULLDAY_<cam> replace
hardcoded times in install.sh
- Notification options commented out (uncomment to enable)
- Lat/lon set to mid-Atlantic (25.0, -38.0) as neutral placeholder
install.sh:
- Loops over CAMERAS to generate per-camera sky-cam-seasons-<cam> and
sky-cam-fullday-<cam> timers with schedules from conf
- Sunrise timer remains a single sky-cam-sunrise unit (SUNRISE_CAM only)
fullday-video.sh, 4-seasons.sh, montage-mvt.sh, year-end-join.sh:
- Each accepts camera name as an argument (systemd passes it via ExecStart)
and falls back to conf default when run manually without an arg
- Camera name propagates through the full call chain:
4-seasons → montage-mvt → year-end-join
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Video duration is still controlled by SUNRISE_TARGET_SECS in conf,
but the filename no longer encodes it — so changing the duration
doesn't break anything and the name stays stable day to day.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
The output video was always named -sunrise-10s.mp4 regardless of the
configured duration. Both sunrise_video.10s.sh and sunrise2mm.py now
derive the filename from SUNRISE_TARGET_SECS so changing it in conf
produces correctly named files that the uploader still finds.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- sunrise_video.10s.sh: use $TIMEZONE (was hardcoded America/New_York),
$SUNRISE_PRE_MIN/$SUNRISE_POST_MIN for capture window, $SUNRISE_TARGET_SECS
for output duration (was wrong value 8 in a script named 10s)
- fullday-video.sh: remove hardcoded FULLDAY_FPS/RETENTION_DAYS; both now
come from sky-cam.conf
- 4-seasons.sh, montage-mvt.sh: export TIMEZONE after sourcing conf so
season_info.py subprocess picks it up via env rather than falling back
to its hardcoded America/New_York default
- sky-cam.conf: add SUNRISE_PRE_MIN, SUNRISE_POST_MIN, SUNRISE_TARGET_SECS,
FULLDAY_FPS, RETENTION_DAYS
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sunrise.py now reads LATITUDE, LONGITUDE, and TIMEZONE from sky-cam.conf
instead of hardcoded values, using the same pathlib-based pattern as
sunrise2mm.py. sky-cam.conf gains a comprehensive header with manual
systemd setup instructions and a full script map for future reference.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sky-cam.conf
- Add SCRIPT_DIR (install location) and CAM_NAME (camera/folder name)
- These are the only values that change when deploying to a new machine
install.sh (new)
- Sources sky-cam.conf, generates all systemd .service and .timer units
with correct ExecStart paths, then enables and starts the timers
- Run once after editing sky-cam.conf; run again if paths change
- Supports --system flag for system-wide install
All .sh scripts + sunrise2mm.py
- No hardcoded paths remain
- script_name=$(basename ...) derivation replaced by $CAM_NAME from conf
- sunrise_video.10s.sh now sources sky-cam.conf for BASE_DIR, CAM_NAME,
and SCRIPT_DIR (replaces all /home/motion/... references)
- sunrise2mm.py builds video path from BASE_DIR + CAM_NAME in conf
systemd/ template files kept as reference but no longer need editing
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sky-cam.conf (new)
- Single file replaces mattermost_config.txt reference, notify_config.txt,
and the hardcoded BASE_DIR/MUSIC_DIR/TIMEZONE scattered across scripts
- Bash-sourceable and Python key=value readable (same format)
All .sh scripts
- source sky-cam.conf at startup; BASE_DIR / MUSIC_DIR from config
sunrise2mm.py
- Reads sky-cam.conf relative to script location; no hardcoded path
notify.sh
- Sources sky-cam.conf; Mattermost credentials come from there directly,
no separate mattermost_config.txt lookup needed
systemd/sky-cam-notify-failure@.service (new)
- Template unit called by OnFailure= in each service
- Sends notification via notify.sh when any sky-cam job fails
- All three .service units now have OnFailure=sky-cam-notify-failure@%n.service
Removed: notify_config.txt, sky-cam.crontab (superseded)
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
season_info.py
- Add ASTRO_YEAR: year of the Winter solstice that started the current cycle
(Spring/Summer/Autumn 2026 → 2025; Winter Dec-2026 → 2026)
4-seasons.sh
- Daily clip output path now includes ASTRO_YEAR:
movies/$name/$ASTRO_YEAR/$SEASON/Mvt$N/
montage-mvt.sh (full rewrite)
- Attribution is now a drawtext overlay (top 6 s of the video, fade in/out)
instead of an appended black card — no frame-size matching needed, and
year-end join requires no special handling
- Output path mirrors the new year-based folder structure
- Sends notification via notify.sh when done
- Automatically triggers year-end-join.sh after Autumn Mvt 3
year-end-join.sh (new)
- Concatenates all 12 movement montages for a given ASTRO_YEAR
- Output: movies/$name/$ASTRO_YEAR/$name-$ASTRO_YEAR.mp4
- Sends notification when done
notify.sh + notify_config.txt (new)
- Generic best-effort notification helper: ntfy, email, Mattermost text post
- All methods disabled by default; user fills in notify_config.txt
sky-cam.crontab + systemd/ (new)
- Crontab file: sunrise at 09:00, 4-seasons at 01:00, fullday at 02:00
- systemd .service + .timer units for all three daily jobs
- montage/year-end triggered internally, not by cron
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- Warp only north cylindrically (sunrise completely unmodified)
- Focal-length grid search picks the best phase-correlation alignment
- Soft validity mask (31x31 erosion) fades out black corners of the warp
- Per-channel colour/exposure match: north's treeline strip mean is scaled
to match sunrise's, removing the visible sky-colour seam between cameras
- Corrected overlap blend: invalid north pixels fall back to sunrise
(s_weight = max(alpha, 1-validity)) instead of showing black
- Auto-crops left black-corner columns from the final canvas
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
North is projected onto a cylinder so its right edge curves naturally into
sunrise's left edge, giving a continuous panoramic sweep from North to East.
cyl-f1920-preview.jpg — f=1920 px (~90° HFOV): most natural perspective
cyl-f1550-preview.jpg — f=1550 px (~105° HFOV): more pronounced curve
stitch-cameras.py updated to use cylindrical projection. Focal length is
a tunable argv[4]; refine once the user confirms which curve looks right.
The 36-second inter-shot gap causes cloud drift at the seam — live video
frames captured simultaneously will not have this artefact.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Phase-correlation on the treeline/horizon band (rows 70-90%) found:
tx = -3324 px (north is 3324 px to the left of sunrise)
ty = -85 px (north camera is 85 px higher — slight tilt difference)
overlap = 516 px wide, feather-blended with a horizontal gradient
Result in images/stitch-preview/north-sunrise-aligned-preview.jpg:
7164x2075 px panorama with continuous horizon and treeline.
Cloud "V" at seam is a 36-second inter-shot artefact only; live video
frames captured simultaneously will have no such discontinuity.
stitch-cameras.py: accepts tx/ty overrides as argv[4]/argv[5] so the
offsets can be tuned without code changes.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- images/stitch-preview/north-sunrise-hstack.jpg:
naive side-by-side preview of the two skycams at 14:29 on 2026-04-17
- images/stitch-preview/feature-matches-debug.jpg:
close-up of overlap region (right 40% of north × left 40% of sunrise)
with ORB Lowe-ratio matches drawn — 39 candidates, RANSAC survives only
4 inliers because the 90° perspective offset invalidates a similarity
transform. Overlap IS present (trees, houses, power poles visible in
both) but full homography needs more/better tie points.
- stitch-cameras.py: reusable script that generates both previews plus a
similarity-transform .txt for reuse in ffmpeg filter chains once we
have enough matches (or manual tie-points).
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
season_info.py (new):
- Computes actual equinox/solstice dates via Jean Meeus approximation
- Converts to LOCAL timezone (pytz / zoneinfo) so the calendar date reflects
what the camera sees on the ground, including DST transitions
- Timezone defaults to America/New_York; override via $TIMEZONE env or 2nd arg
- Divides each season into 3 equal movements; handles Winter's year boundary
- Outputs IS_LAST_DAY=true on the final day of each movement
4-seasons.sh:
- Uses season_info.py for all date/season logic (no hardcoded day-of-year)
- Passes $yesterday to season_info.py so astronomical dates are correct
- Removed forced 1280x720 scale; clips stay at native camera resolution
- Auto-triggers montage-mvt.sh "$yesterday" on last day of movement so the
correct movement (not the next day's) is compiled
montage-mvt.sh:
- Accepts optional YYYY-MM-DD argument (passed by 4-seasons.sh) so it
resolves the correct season/movement when run the morning after last day
- Detects source resolution from first daily clip via ffprobe; attribution
card is generated at that exact resolution — no resizing of main video
- Font sizes proportional to frame height so text looks right at any res
- License corrected to CC BY-SA 3.0 (was CC BY-NC-SA 4.0)
fullday-video.sh:
- Removed 30-minute target; video length is natural (num_images / FPS)
- Default FULLDAY_FPS=5 (configurable at top of script)
- Retains RETENTION_DAYS=10 auto-delete for full-day videos only
- Sunrise 10s uploads and montage videos are never auto-deleted
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
4-seasons.sh:
- Fix per-video target duration: divide by days_in_movement (~30) not
days_in_season (~90), so concatenated clips sum to the movement's
music duration
- Force consistent 1280x720 output (scale+pad) to prevent resolution
mismatches when montage-mvt.sh concatenates across days
- Use ffprobe (not ffmpeg stderr parsing) for reliable duration reading
- Add music-dir fallback to repo-local music/ folder
- Remove the 2× music-doubling workaround (root cause fixed above)
montage-mvt.sh — complete rewrite:
- Proper season/movement detection matching 4-seasons.sh logic
- Step 1: concat all *-final.mp4 clips, normalise to 1280x720
- Step 2: speed-adjust concatenated video to exactly match music duration
- Step 3: merge with Vivaldi movement audio + 2 s video/audio fade in/out
- Step 4: generate 7-second black attribution card (drawtext) crediting
John Harrison / Wichita State University Chamber Players / FMA /
CC BY-NC-SA 4.0
- Step 5: concat montage + attribution into final file
- Temp files cleaned up via EXIT trap
fullday-video.sh — new script:
- Full-day timelapse from yesterday's images targeting ~30 minutes
- Per-frame duration = 1800 / num_images (clamped 0.017–10 s/frame)
- 1280x720 output, video-only (no audio)
- Auto-deletes fullday videos older than 10 days
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ