- moon_composite.py: remove _make_atmosphere_layer() and all atmosphere
parameters from render_phase_closeup(); add when_utc parameter and apply
parallactic angle rotation so the NASA disk is oriented to match east's sky
- moon_phase_monthly.py: change obs time default to 22:00 (aligns with NASA
hourly renders); scan east frames in 22:00-23:00 window; binary go/no-go
(quality >= MIN_QUALITY = post, no clear shot = skip entirely); remove
_atmosphere_opacity_from_quality(), ATMOSPHERE_BLUR, CLOUDY_POST_ENABLED
- sky-cam.conf: update MOON_OBS_TIME_LOCAL to 22:00, remove
MOON_OBS_WINDOW_MIN, remove the atmospheric overlay and cloudy fallback
sections and their config variables
- test_moon_composite.py: single clean test — fetch NASA for 22:00 UTC on
2026-04-29, verify east frame quality, render with parallactic angle; no
procedural fallback
https://claude.ai/code/session_01HkTxpNSTWtViZzxbrbKytR
Previous approach: scan a 3-day window, find best moon detection,
use that timestamp. Problems: fuzzy composite (multi-day scan could
pick a frame far from actual full moon), no clear tie between the
NASA render and a specific observable moment.
New approach — MOON_OBS_TIME_LOCAL (default 22:30 local):
On the night of the exact phase event, look at east frames in a
±MOON_OBS_WINDOW_MIN (default 30 min) window around the configured
time. The frame closest to that time determines atmosphere opacity;
the same time (rounded to hour) drives the NASA Dial-a-Moon fetch.
This gives one definitive moment per phase per month.
moon_phase_monthly.py:
- PHASE_SPEC stripped to just post_delay + enabled_key (all window/
illumination filtering removed — obs time is the only selector)
- run_phase() replaced with obs-time logic; opacity derived from
detect_moon quality on that single frame
- MOON_FULL_POST_DELAY_DAYS / MOON_QUARTER_POST_DELAY_DAYS default
changed to 1 (post morning after the phase, frames already on disk)
moon_composite.py:
- atmosphere blur default: output_width//10 (192px) → output_width//20
(96px) so cloud shapes survive the blur
sky-cam.conf:
- MOON_OBS_TIME_LOCAL=22:30, MOON_OBS_WINDOW_MIN=30
- Post delay defaults updated to 1
test_moon_composite.py:
- Tries real NASA SVS Dial-a-Moon API first; procedural disc only as
fallback if API unreachable
- Runs detect_moon on the east frame to compute real opacity
- Single output (test_composite_out.jpg)
https://claude.ai/code/session_01HkTxpNSTWtViZzxbrbKytR
The core mental model was wrong: clouds are between the observer and
the moon, so they occlude the disk — they don't sit behind it.
moon_composite.py:
_make_east_sky_backdrop() → _make_atmosphere_layer()
render_phase_closeup() pipeline is now:
1. Black background
2. NASA moon disk centred (correct phase / libration / shadows)
3. East frame scaled + blurred → composited OVER the disk at
atmosphere_opacity (0.0 = clear, 0.68 = heavy overcast)
Parameters: east_sky_enabled/blur → atmosphere_opacity/blur
moon_phase_monthly.py:
_atmosphere_opacity_from_quality() maps detection quality to opacity:
quality >= 0.85 → 0.00 (clear)
quality 0.70 → 0.20 (light haze)
quality 0.55 → 0.40 (notable cloud, still detected)
quality None → 0.68 (no detection, heavy overcast)
Frame scan now tracks two lists:
qualifying — frames passing quality + illumination (existing)
above_horizon — frames where moon is up but detection failed
When qualifying is empty and MOON_CLOUDY_POST_ENABLED=true, the
above-horizon frame closest to the exact phase moment is used with
opacity 0.45–0.68. The month is always represented — full moon,
first quarter, and third quarter each get a post even in cloudy
months, showing the moon as a faint glow behind cloud.
sky-cam.conf:
MOON_EAST_SKY_ENABLED/BLUR → MOON_ATMOSPHERE_BLUR (opacity is
computed automatically from quality, not configured directly)
New: MOON_CLOUDY_POST_ENABLED=true
test_moon_composite.py:
Generates three outputs at opacity 0.00 / 0.20 / 0.65 so the
full opacity range is visible in one test run.
https://claude.ai/code/session_01HkTxpNSTWtViZzxbrbKytR
The annular-ring veil approach sampled too small a region (38 px moon
in 3840×2160 gives a tiny annulus) and was invisible in practice.
More fundamentally, using the whole east sky is what "relayed to where
it was taken" actually means.
New approach — _make_east_sky_backdrop():
Scale the full east camera frame to output size, apply GaussianBlur
r = output_width // 6 (≈ 320 px at 1920 wide). The blur erases RTSP
artefacts, OSD overlays, and the wide-angle look while preserving real
sky colour, cloud / haze gradients, and the dark ground silhouette.
The NASA moon disk is pasted sharp on top. The result reads as east
photographed the moon through a telephoto with its actual sky that night.
Clear dark sky → nearly black backdrop (same feel as before).
Thin cloud cover → soft grey-blue haze behind the sharp moon.
Heavy overcast → the moon detection would not qualify, so this case
never reaches rendering.
Config rename: MOON_CLOUD_OVERLAY_* → MOON_EAST_SKY_ENABLED / _BLUR.
moon_phase_monthly.py: CLOUD_OVERLAY_* → EAST_SKY_*.
render_phase_closeup(): cloud_overlay_* params → east_sky_*.
test_moon_composite.py: replace full_moon_closeup.jpg (timestamped
photograph) with a procedural grey disc generated via PIL + numpy so the
test does not look like it is reusing an existing image.
https://claude.ai/code/session_01HkTxpNSTWtViZzxbrbKytR
Three changes driven by the same goal: make each monthly moon post
look like east captured it at that exact moment.
Tighter illumination windows (±4% of target phase):
Full moon: 95% → 96–100% (was a 5-point band; now hugs exact full)
Quarters: 40–65% → 46–54% (was a 25-point band; now ±4% of 50%)
This ensures the NASA Dial-a-Moon render timestamp is pulled within
4 percentage points of the true phase, making the fetched image
genuinely represent that night's moon. One fresh fetch per event,
~3/month, cached by hour — never reused across months.
Atmospheric cloud veil from east's surrounding sky:
_extract_cloud_veil() samples the annular sky region just outside
east's moon disk (2×–5× radius), scales it to the output frame,
blurs heavily (GaussianBlur r≈output_width/10) so it reads as haze
rather than an upscaled photo, then blends it over the NASA composite
at an opacity proportional to sky brightness:
sky < 5% mean brightness → no veil (clear dark night)
sky ~10% → ~17% veil (thin haze / airglow)
sky ≥ 20% → 40% veil (max, MOON_CLOUD_OVERLAY_MAX_OPACITY)
After the veil pass the NASA moon disk is re-pasted sharply so the
haze sits naturally behind the crisp lunar surface.
This is the honest answer to "east can't capture high-res clouds":
east's real atmospheric fingerprint becomes the veil texture.
New sky-cam.conf keys:
MOON_CLOUD_OVERLAY_ENABLED=true
MOON_CLOUD_OVERLAY_MAX_OPACITY=0.40
#MOON_CLOUD_OVERLAY_BLUR=0 (0 = auto)
https://claude.ai/code/session_01HkTxpNSTWtViZzxbrbKytR
The previous east-flavored composite (tight-crop east region + lunar
texture overlay) was honest but visually limited: 38-px source moon, no
real terminator shadows on quarters, and a faint upscaled-halo
background that was less compelling than just a clean lunar render.
New flow:
- East stays the witness (verifies the moon was visible during the
collection window) and supplies the timestamp.
- moon_dialamoon.py fetches the NASA SVS Dial-a-Moon render for that
exact UTC hour. Free, public-domain, real-physics: correct phase,
libration, and crater shadows for any timestamp.
- moon_composite.render_phase_closeup() places that render at ~92% of
frame height on a 1920x1080 black background -- the long-telephoto
look the user asked for.
- One API call per phase event (~36/year), cached forever in
moon-ref/dialamoon/.
Quarter moons now show real 3D crater shadows along the terminator,
which the prior algorithmic phase-shadow couldn't simulate from a
full-moon reference.
Adds MOON_REQUIRE_EAST_VERIFY=true|false toggle so the user can choose
"only post when east saw it" (default, current behavior) vs "post every
cycle regardless of weather over east."
Drops the install-time lunar reference download (no longer needed) and
the now-unused per-phase phase-shadow path stays in moon_composite.py
for reference / future reuse.
https://claude.ai/code/session_015PBVDESC3KLMbq1LpA6qLn
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.
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
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
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
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
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
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
Short adagio movements (Summer Mvt 2 at 107s, Winter Mvt 2 at 132s, etc.)
compress each day to only 3-5 seconds of screen time at 1×. Looping the
music N times multiplies the per-day target duration proportionally so the
footage is less aggressively sped up and more watchable.
- sky-cam.conf: add MONTAGE_MUSIC_LOOPS and per-movement overrides
(Summer Mvt2=3×, Winter Mvt2=3×, Autumn Mvt2=2×, Summer Mvt3=2×, Spring Mvt2=2×)
- 4-seasons.sh: multiply target_per_clip by loops
- montage-mvt.sh: compute effective_duration, use -stream_loop for audio,
fix fade_out_start, -t, and post-build duration check
- make-finals.sh: multiply target by loops
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- sky-cam.conf: add ENCODE_PRESET=slow (applied to all encodes);
split CRF_MONTAGE (intermediates, H.264) from CRF_MONTAGE_FINAL=22
(archive montage, H.265); add SUNRISE_RETENTION_DAYS=10; improve
CAPTURE_INTERVAL comment with storage/density table
- montage-mvt.sh: steps 1+2 use preset; step 3 final switches to
libx265 + CRF_MONTAGE_FINAL + -tag:v hvc1 for broad compatibility
- 4-seasons.sh: both encode steps use ENCODE_PRESET
- daily_sunrise_video.sh: all encode steps use ENCODE_PRESET; rolling
retention deletes sunrise videos older than SUNRISE_RETENTION_DAYS
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- verify-mvt.sh: remove dead first _info= line (used MVT_START before
defined); fix JPEG size loop to accumulate bytes correctly and display
human-readable total; show size in delete confirmation
- montage-mvt.sh: grep -c uses || echo 0 instead of || true to be
unambiguous about the no-match value under set -e
- capture.sh, capture-watchdog.sh: require camera name arg, remove
silent SUNRISE_CAM fallback
- migrate-seasons.sh: require --cam arg, error if missing
- README.md, sky-cam.conf: remove stale fullday-video.sh references;
README pipeline diagram updated to show verify-mvt.sh
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- sky-cam.conf: CAMERAS now includes east, north, south with staggered
SCHEDULE_SEASONS times (01:00, 01:30, 02:00); fullday schedules and
tuning removed
- install.sh: fullday service/timer generation removed entirely
- fullday-video.sh + systemd units: deleted
- montage-mvt.sh: post-build technical checks (duration drift, video/audio
stream presence) added; notification now includes check results and the
exact verify-mvt.sh command to run
- verify-mvt.sh: new interactive CLI for montage review — shows checks,
plays video, deletes source JPEG folders on approval, or re-runs montage
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
capture-watchdog.sh: long-running script that checks the newest JPEG mtime
in each camera's output directory every CAPTURE_STALE_SECS/3 seconds (min 5s).
Sends a stall notification via notify.sh if no new frame has arrived within
CAPTURE_STALE_SECS. Sends a recovery notification once new frames resume.
Only alerts if the camera was previously working (avoids false positives on
first start or overnight before the first frame of the day).
sky-cam.conf: CAPTURE_STALE_SECS=30 added next to CAPTURE_INTERVAL with a
note that it should be at least 2x the interval.
install.sh: generates sky-cam-watchdog-<cam>.service for every camera that
has a RTSP URL configured. The watchdog service starts after and alongside
the capture service, restarts on failure, and is enabled/started with the
same loop as the capture service.
README.md: note about watchdog and journalctl command for its logs.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Cameras are now named by direction (east, north, south, west) throughout
config defaults, examples, and documentation. The config key SUNRISE_CAM
is unchanged — it still means 'which camera faces east and gets the sunrise
job'. Only the camera instance name value changes from 'sunrise' to 'east'.
sky-cam.conf: CAMERAS=(east), SUNRISE_CAM=east, SCHEDULE_*_east,
example comments updated to show all four compass points.
README.md: all command examples updated (4-seasons.sh east, journalctl
sky-cam-capture-east, etc.) — phenomenon references ('sunrise window',
'sunrise time') left unchanged.
bootstrap.sh: example updated to show compass directions.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Previously sunrise-audio-capture.sh recorded the entire capture window
(70+ minutes) and daily_sunrise_video.sh sought to the right position with
a complex offset calculation.
Now: record floor(TARGET/2) seconds before sunrise and ceil(TARGET/2) after —
odd second goes to post-sunrise. The file is exactly SUNRISE_TARGET_SECS long
and already centred, so no offset is needed when mixing.
AUDIO_PRE_BUFFER_MIN removed — it was part of the old whole-window scheme
and is no longer meaningful.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sky-cam.conf:
- SCRIPT_DIR auto-detects from its own location via BASH_SOURCE — no manual
path entry needed after bootstrap; override still works via environment.
- BASE_DIR defaults to \$SCRIPT_DIR/data; MOVIES_DIR defaults to \$BASE_DIR/movies.
Override either to point at a drive/mount as before.
- SCHEDULE_SUNRISE changed to 03:00 — the script now waits internally until
the capture window closes before touching images, so the schedule just needs
to be early enough, not timed exactly to sunrise.
- Schedule section rewritten to explain what each job does and what day's data
it processes (SEASONS/FULLDAY always process yesterday).
- SUNRISE_PRE_MIN/POST_MIN comments explain the window and its relationship to
SUNRISE_TARGET_SECS.
- SUNRISE_OVERLAY_ENABLED=true added; set false to skip the timestamp entirely.
daily_sunrise_video.sh:
- After calculating the capture window, sleep until end_sec+30s if we fired
before the window finished — guarantees all images are present.
- DT (drawtext filter) is only built when SUNRISE_OVERLAY_ENABLED=true.
- Step 3b skips ffmpeg entirely when overlay is disabled, moving work_video
directly to final_video (faster, no re-encode).
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sky-cam.conf now sources .env from the same directory at the bottom of the
file, so RTSP URLs, Mattermost tokens, and notification credentials can live
outside version control. Plaintext credential placeholders removed from conf.
install.sh generates .env.example on every run with the correct CAM_RTSP_*
variable names derived from the CAMERAS list, so users always know exactly
what to put in .env for their specific camera setup.
.gitignore: add .env and sunrise-sounds/ (downloaded audio library).
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
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
- 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