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