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
588 lines
32 KiB
Plaintext
588 lines
32 KiB
Plaintext
# =============================================================================
|
||
# sky-cam.conf — master configuration for all sky-cam scripts
|
||
# =============================================================================
|
||
#
|
||
# This is the ONLY file you need to edit. After editing, run:
|
||
#
|
||
# ./install.sh # installs systemd timers (no root needed)
|
||
# ./install.sh --system # system-wide install (requires sudo)
|
||
#
|
||
# Re-run install.sh any time this file changes.
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# MANUAL SYSTEMD SETUP (if you prefer not to use install.sh)
|
||
# -----------------------------------------------------------------------------
|
||
# After editing this file, generate the unit files:
|
||
#
|
||
# mkdir -p ~/.config/systemd/user
|
||
# cp systemd/*.service systemd/*.timer ~/.config/systemd/user/
|
||
#
|
||
# Then edit ExecStart= in each .service file to match your SCRIPT_DIR, then:
|
||
#
|
||
# systemctl --user daemon-reload
|
||
# systemctl --user enable --now sky-cam-sunrise.timer
|
||
# # one sky-cam-seasons-<cam>.timer per camera, e.g.:
|
||
# systemctl --user enable --now sky-cam-seasons-east.timer
|
||
#
|
||
# Check status:
|
||
# systemctl --user list-timers 'sky-cam-*'
|
||
# journalctl --user -u sky-cam-seasons-sunrise.service -f
|
||
#
|
||
# System-wide (replace --user with no flag, prepend sudo):
|
||
# sudo cp systemd/*.service systemd/*.timer /etc/systemd/system/
|
||
# sudo systemctl daemon-reload && sudo systemctl enable --now sky-cam-*.timer
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# SCRIPT MAP — what each file does and when it runs
|
||
# -----------------------------------------------------------------------------
|
||
#
|
||
# You run / schedule:
|
||
# daily_sunrise_video.sh — daily at SCHEDULE_SUNRISE (via systemd, SUNRISE_CAM only)
|
||
# grabs images around sunrise, makes a short video,
|
||
# uploads it to Mattermost
|
||
#
|
||
# 4-seasons.sh <cam> — daily at SCHEDULE_SEASONS_<cam> (via systemd, per camera)
|
||
# makes yesterday's daily clip sized to its share
|
||
# of the matching Vivaldi movement's music duration;
|
||
# on the last day of a movement, auto-triggers
|
||
# montage-mvt.sh
|
||
#
|
||
# moon-track.sh — nightly at SCHEDULE_MOON_TRACK (SUNRISE_CAM only)
|
||
# detects the moon in each east night frame, crops
|
||
# a tracked sequence, stitches into an mp4
|
||
#
|
||
# moon-phase-monthly.sh — daily at SCHEDULE_MOON_PHASE (SUNRISE_CAM only)
|
||
# no-ops except on phase post-days, when it
|
||
# confirms east saw the moon during the
|
||
# collection window, fetches NASA SVS Dial-a-Moon
|
||
# for that exact UTC hour, renders the moon
|
||
# full-screen on a black background, and posts to
|
||
# Mattermost. Handles full moon, first quarter
|
||
# (waxing half), and third quarter (waning half).
|
||
#
|
||
#
|
||
# install.sh — run once at setup, and again if this file changes
|
||
# generates and installs systemd units from conf
|
||
#
|
||
# stitch-cameras.py — run manually when needed
|
||
# stitches north + sunrise into a panorama
|
||
# usage: python3 stitch-cameras.py north.jpg sunrise.jpg prefix [focal]
|
||
#
|
||
# Auto-triggered (do not run directly):
|
||
# montage-mvt.sh — called by 4-seasons.sh on last day of each movement
|
||
# concatenates all daily clips for the movement,
|
||
# speed-adjusts to match music, overlays attribution,
|
||
# sends completion notification; on last Autumn
|
||
# movement also triggers year-end-join.sh
|
||
#
|
||
# year-end-join.sh — called by montage-mvt.sh at end of Autumn Mvt 3
|
||
# concatenates all 12 movement montages into one
|
||
# Four Seasons year video, sends notification
|
||
#
|
||
# Helpers (called by the scripts above, not run directly):
|
||
# notify.sh — sends notifications via ntfy / email / Mattermost
|
||
# season_info.py — outputs astronomical season/movement/year for a date
|
||
# sunrise.py — outputs today's sunrise time (used by daily_sunrise_video)
|
||
# sunrise2mm.py — uploads the sunrise video to Mattermost
|
||
# sunrise_overlay.py — burns a timestamp overlay onto the sunrise video
|
||
#
|
||
# -----------------------------------------------------------------------------
|
||
# FILE DEPENDENCIES — who calls whom, and what to update if you rename a file
|
||
# -----------------------------------------------------------------------------
|
||
#
|
||
# sky-cam.conf (this file)
|
||
# ← sourced by every .sh script; read by sunrise.py, sunrise2mm.py
|
||
# → if renamed: update the 'source' line at the top of ALL scripts
|
||
# (don't rename it — just edit it in place)
|
||
#
|
||
# install.sh
|
||
# ← run manually; reads this conf
|
||
# → if renamed: update the note at the top of this conf file only
|
||
# generates → systemd unit files (sky-cam-*.service / .timer)
|
||
# and sky-cam-notify-failure@.service
|
||
#
|
||
# daily_sunrise_video.sh
|
||
# ← called by systemd sky-cam-sunrise.timer (SCHEDULE_SUNRISE)
|
||
# → if renamed: update install.sh (write_service call ~line 70)
|
||
# then re-run install.sh to regenerate the unit
|
||
# calls → sunrise.py, sunrise_overlay.py, sunrise2mm.py
|
||
#
|
||
# 4-seasons.sh
|
||
# ← called by systemd sky-cam-seasons-<cam>.timer (SCHEDULE_SEASONS_<cam>)
|
||
# → if renamed: update install.sh (write_service call ~line 85)
|
||
# then re-run install.sh
|
||
# calls → season_info.py, montage-mvt.sh
|
||
#
|
||
# montage-mvt.sh
|
||
# ← called by 4-seasons.sh on the last day of each movement
|
||
# → if renamed: update 4-seasons.sh (the exec line near the bottom)
|
||
# calls → season_info.py, notify.sh, year-end-join.sh
|
||
#
|
||
# year-end-join.sh
|
||
# ← called by montage-mvt.sh at end of Autumn Mvt 3
|
||
# → if renamed: update montage-mvt.sh (the exec line at the bottom)
|
||
# calls → notify.sh
|
||
#
|
||
# notify.sh
|
||
# ← called by montage-mvt.sh, year-end-join.sh
|
||
# also referenced in install.sh for the OnFailure= failure template
|
||
# → if renamed: update montage-mvt.sh, year-end-join.sh, install.sh
|
||
#
|
||
# season_info.py
|
||
# ← called by 4-seasons.sh and montage-mvt.sh
|
||
# → if renamed: update 4-seasons.sh and montage-mvt.sh (the eval lines)
|
||
#
|
||
# sunrise.py
|
||
# ← called by daily_sunrise_video.sh
|
||
# → if renamed: update daily_sunrise_video.sh
|
||
#
|
||
# sunrise_overlay.py
|
||
# ← called by daily_sunrise_video.sh
|
||
# → if renamed: update daily_sunrise_video.sh
|
||
#
|
||
# sunrise2mm.py
|
||
# ← called by daily_sunrise_video.sh
|
||
# → if renamed: update daily_sunrise_video.sh
|
||
#
|
||
# stitch-cameras.py
|
||
# ← run manually only; no script calls it
|
||
# → safe to rename with no other changes needed
|
||
#
|
||
# moon-track.sh
|
||
# ← called by systemd sky-cam-moon-track.timer (SCHEDULE_MOON_TRACK)
|
||
# → if renamed: update install.sh (the moon-track write_service block)
|
||
# calls → moon_detect.py (Python lib via heredoc), ffmpeg
|
||
#
|
||
# moon-phase-monthly.sh
|
||
# ← called by systemd sky-cam-moon-phase.timer (SCHEDULE_MOON_PHASE)
|
||
# → if renamed: update install.sh (the moon-phase write_service block)
|
||
# calls → moon_phase_monthly.py
|
||
#
|
||
# moon_phase_monthly.py
|
||
# ← called by moon-phase-monthly.sh
|
||
# → if renamed: update moon-phase-monthly.sh (the exec line at the bottom)
|
||
# calls → moon_phase.py, moon_detect.py, moon_composite.py, notify.sh
|
||
#
|
||
# moon_phase.py / moon_detect.py / moon_composite.py
|
||
# ← Python libraries used by moon-track.sh and moon_phase_monthly.py
|
||
# → if renamed: update moon-track.sh, moon_phase_monthly.py, README
|
||
#
|
||
# =============================================================================
|
||
|
||
# ── Install location ──────────────────────────────────────────────────────────
|
||
# Auto-detected from where this file lives — override only if you move the
|
||
# scripts to a different location after install.
|
||
SCRIPT_DIR="${SCRIPT_DIR:-$(dirname "$(realpath "${BASH_SOURCE[0]}")")}"
|
||
|
||
# ── Storage ───────────────────────────────────────────────────────────────────
|
||
# BASE_DIR: root folder where camera images are stored.
|
||
# Structure created automatically: BASE_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.jpg
|
||
# Defaults to a 'data' subfolder next to the scripts; override to point at
|
||
# an external drive or network mount (recommended for production).
|
||
BASE_DIR="${BASE_DIR:-$SCRIPT_DIR/data}"
|
||
MOVIES_DIR="${MOVIES_DIR:-$BASE_DIR/movies}" # override if videos live on a different drive
|
||
MUSIC_DIR="${MUSIC_DIR:-$SCRIPT_DIR/music}"
|
||
AUDIO_DIR="${AUDIO_DIR:-$BASE_DIR/audio}" # root for all audio recordings; override in .env
|
||
# Structure: AUDIO_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.m4a (ambient)
|
||
# AUDIO_DIR/sunrise/<cam>/YYYY-MM-DD/sunrise-audio.m4a (sunrise clips)
|
||
|
||
# ── Location & timezone ───────────────────────────────────────────────────────
|
||
# Used by sunrise.py to calculate sunrise time each day.
|
||
#
|
||
# To find your coordinates:
|
||
# Google Maps — right-click your location → the first item shown is "lat, lon"
|
||
# Or: maps.google.com, drop a pin, coordinates appear in the URL and sidebar
|
||
#
|
||
# ── Location & timezone ───────────────────────────────────────────────────────
|
||
# Set your real values in .env — do not put exact coordinates here since
|
||
# this file is committed to git. Add these three lines to your .env:
|
||
#
|
||
# LATITUDE=43.1234
|
||
# LONGITUDE=-76.5678
|
||
# TIMEZONE=America/Chicago
|
||
#
|
||
# Placeholder values below are used only if .env has not set them.
|
||
LATITUDE="${LATITUDE:-0.0000}"
|
||
LONGITUDE="${LONGITUDE:-0.0000}"
|
||
TIMEZONE="${TIMEZONE:-America/New_York}"
|
||
|
||
# ── Weather tagging (OpenWeather) ─────────────────────────────────────────────
|
||
# When enabled, ambient-record.sh queries OpenWeather's "Current weather"
|
||
# endpoint just before each 30-minute audio chunk and appends a one-word
|
||
# weather tag to the filename, e.g.:
|
||
# east-08-30-00-thunderstorm.m4a
|
||
# east-09-00-00-rain.m4a
|
||
# east-09-30-00-cloudy.m4a
|
||
# That way each chunk is labelled with what was happening DURING that chunk —
|
||
# a foggy dawn won't mislabel the sunny afternoon files.
|
||
#
|
||
# Tag vocabulary (fixed set — only conditions that change the audio):
|
||
# thunderstorm rain rainstorm snow snowstorm
|
||
# Clear / cloudy / fog / wind sound the same on the recordings, so those
|
||
# conditions fall through to the un-tagged filename.
|
||
#
|
||
# Setup:
|
||
# 1. Get a free API key: https://openweathermap.org/api
|
||
# (the free "Current weather" tier — 60 calls/min — is plenty)
|
||
# 2. Add to .env: OPENWEATHER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
|
||
# 3. Confirm LATITUDE / LONGITUDE are set in .env (already needed by sunrise.py)
|
||
# 4. Flip WEATHER_ENABLED=true below
|
||
#
|
||
# The query uses the same LATITUDE / LONGITUDE configured above — one location
|
||
# for the whole installation; per-camera coordinates are not supported.
|
||
#
|
||
# Failure-safe: if the toggle is off, the key is missing, the network is down,
|
||
# or the API errors out, weather-tag.sh prints nothing and the recorder falls
|
||
# back to the un-tagged filename. ambient-record.sh never blocks on this.
|
||
#
|
||
# Manual test: ./weather-tag.sh (prints e.g. "cloudy" or nothing)
|
||
WEATHER_ENABLED=false
|
||
OPENWEATHER_API_KEY="${OPENWEATHER_API_KEY:-}"
|
||
|
||
# ── Cameras ───────────────────────────────────────────────────────────────────
|
||
# List every camera name here (space-separated inside the parentheses).
|
||
# Each name becomes a subfolder under BASE_DIR (images) and BASE_DIR/movies.
|
||
# To add a camera: add its name here, add its schedules below, re-run install.sh.
|
||
#
|
||
CAMERAS=(east north south) # add west here + uncomment its schedule below when ready
|
||
SUNRISE_CAM=east # which camera faces east (gets the sunrise video job)
|
||
|
||
# ── Schedules ─────────────────────────────────────────────────────────────────
|
||
# All times are HH:MM:SS (24-hour local time).
|
||
# install.sh reads these and generates one systemd timer per camera per job.
|
||
#
|
||
# SCHEDULE_SUNRISE
|
||
# When to START the sunrise video job. The script wakes up, calculates
|
||
# today's sunrise time, then WAITS internally until the capture window is
|
||
# fully finished before touching any images. Set this early enough to
|
||
# cover your earliest possible sunrise minus SUNRISE_PRE_MIN — 03:00 works
|
||
# year-round for most locations. The video is ready a few minutes after the
|
||
# capture window closes, not at this start time.
|
||
#
|
||
# SCHEDULE_SEASONS_<cam> (one per camera in CAMERAS)
|
||
# When to run the Four Seasons daily clip job. Processes YESTERDAY'S images
|
||
# (not today's) — always runs after midnight. On the last day of a season
|
||
# movement this automatically triggers the movement montage build.
|
||
# Space multiple cameras at least 30 min apart to avoid disk contention.
|
||
#
|
||
SCHEDULE_SUNRISE=03:00:00
|
||
|
||
# Moon jobs (see "Moon jobs" section further down)
|
||
SCHEDULE_MOON_TRACK=02:30:00 # nightly tracker — runs after midnight, before seasons
|
||
SCHEDULE_MOON_PHASE=09:30:00 # daily check; no-ops except on phase post-days
|
||
|
||
# Four Seasons daily clip — one per camera, staggered 30 min apart.
|
||
# Processes yesterday's images; on the last day of a movement auto-triggers
|
||
# montage-mvt.sh. Space cameras at least 30 min apart to avoid disk contention.
|
||
SCHEDULE_SEASONS_east=01:00:00
|
||
SCHEDULE_SEASONS_north=01:30:00
|
||
SCHEDULE_SEASONS_south=02:00:00
|
||
# To add west: uncomment below, add west to CAMERAS above, add CAM_RTSP_west to .env
|
||
#SCHEDULE_SEASONS_west=02:30:00
|
||
|
||
# ── Sunrise video tuning ──────────────────────────────────────────────────────
|
||
# Capture window around sunrise:
|
||
# Images from (sunrise - SUNRISE_PRE_MIN) to (sunrise + SUNRISE_POST_MIN)
|
||
# are included in the clip. Larger windows give more context; they are all
|
||
# speed-adjusted to fit SUNRISE_TARGET_SECS regardless of window size.
|
||
SUNRISE_PRE_MIN=70 # minutes before sunrise to start capturing
|
||
SUNRISE_POST_MIN=10 # minutes after sunrise to stop capturing
|
||
# Target length of the final sunrise video (seconds).
|
||
SUNRISE_TARGET_SECS=10
|
||
# Burn the local sunrise time vertically on the right side of the video.
|
||
# Set false to skip the overlay entirely (cleaner look, faster encoding).
|
||
SUNRISE_OVERLAY_ENABLED=true
|
||
# Opacity of the sunrise-time text (0.0 = invisible, 1.0 = fully opaque).
|
||
SUNRISE_OVERLAY_OPACITY=0.45
|
||
# Delete daily sunrise videos older than this many days (rolling window).
|
||
# Set 0 to keep forever.
|
||
SUNRISE_RETENTION_DAYS=10
|
||
# Delete unpinned sunrise posts from Mattermost older than this many days.
|
||
# Pinned posts are always kept regardless. Set 0 to disable Mattermost cleanup.
|
||
MM_SUNRISE_RETENTION_DAYS=8
|
||
# Output directory for --test runs. Kept separate from production sunrise videos.
|
||
# Test files older than DEMO_RETENTION_DAYS are deleted automatically.
|
||
DEMO_DIR="${DEMO_DIR:-$MOVIES_DIR/demoio}"
|
||
DEMO_RETENTION_DAYS=8
|
||
|
||
# ── Video encoding quality ────────────────────────────────────────────────────
|
||
# FFmpeg CRF: lower = higher quality / larger files. Typical range 18–30.
|
||
# 18 ≈ visually lossless 23 = ffmpeg default 28 = smaller/lower
|
||
# H.265 (libx265) CRF is not directly comparable — H.265 CRF 22 ≈ H.264 CRF 26
|
||
# in visual quality but ~40-50% smaller file. Final montages use H.265;
|
||
# intermediates (deleted after use) stay H.264.
|
||
#
|
||
# ENCODE_PRESET: slow = better compression, same CRF quality, ~2× encode time.
|
||
# Use 'medium' on slower machines, 'slow' or 'slower' on fast ones.
|
||
# All encodes run at night so 'slow' is the right default.
|
||
#
|
||
CRF_SUNRISE=28 # daily_sunrise_video.sh — daily Mattermost upload (H.264)
|
||
CRF_SEASONS_RAW=28 # 4-seasons.sh — raw concat before speed-adjust (H.264)
|
||
CRF_SEASONS_FINAL=26 # 4-seasons.sh — final daily clip (goes into montage) (H.264)
|
||
CRF_MONTAGE=28 # montage-mvt.sh — intermediate concat + sped steps (H.264)
|
||
CRF_MONTAGE_FINAL=22 # montage-mvt.sh — final archive montage (H.265)
|
||
ENCODE_PRESET=slow # ffmpeg preset for all encodes (slow/medium/fast)
|
||
AUDIO_BITRATE=192k # montage-mvt.sh — music track on movement montages
|
||
|
||
# ── Frame validation (4-seasons.sh) ──────────────────────────────────────────
|
||
# Empty (0-byte) frames are always skipped. No config needed for that.
|
||
#
|
||
# SEASONS_GREY_STDDEV_MIN / SEASONS_GREY_DARK_FLOOR — optional bad-signal filter.
|
||
# Catches uniform-colour frames caused by a bad RTSP signal: solid green, grey, etc.
|
||
# These are valid JPEGs but contain no real image content.
|
||
# Uncomment both lines to enable. A frame is dropped only when BOTH are true:
|
||
# 1. pixel standard deviation (0–255) < SEASONS_GREY_STDDEV_MIN (frame is uniform)
|
||
# 2. mean brightness (0–255) > SEASONS_GREY_DARK_FLOOR (frame is not dark)
|
||
# Night frames are dark so they always pass even if stddev is low.
|
||
# Requires ImageMagick (convert). Start with these values and tune from there:
|
||
SEASONS_GREY_STDDEV_MIN=5 # skip if stdev < 5 (very uniform colour)
|
||
SEASONS_GREY_DARK_FLOOR=30 # …but only if mean brightness > 30 (not a night frame)
|
||
#
|
||
# Performance: ImageMagick is only called on files below SEASONS_LARGE_FRAME_BYTES.
|
||
# Files above that threshold are always good and skip the check entirely.
|
||
# Default 1 MB — adjust if your camera produces larger bad-signal frames.
|
||
#SEASONS_LARGE_FRAME_BYTES=1048576
|
||
|
||
# ── Montage attribution overlay ───────────────────────────────────────────────
|
||
# A translucent bar across the top of each movement montage, naming the
|
||
# music source ("The Four Seasons — Antonio Vivaldi ...").
|
||
# Appears for MONTAGE_ATTR_DUR seconds from the start, with ATTR_FADE-second
|
||
# fade in and fade out. FADE_DUR controls the video/audio fade in/out at
|
||
# the very start and very end of the montage.
|
||
#
|
||
MONTAGE_FADE_DUR=2.0 # video + audio fade in/out (seconds)
|
||
MONTAGE_ATTR_DUR=6 # attribution overlay total duration (seconds)
|
||
MONTAGE_ATTR_FADE=1 # attribution fade-in and fade-out length (seconds)
|
||
|
||
# ── Music loop multiplier ─────────────────────────────────────────────────────
|
||
# Short movements (especially Mvt 2 adagios) give each day only 3-5 seconds of
|
||
# screen time. Looping the music N times multiplies the per-day target so days
|
||
# are less compressed and more watchable. Per-movement overrides take the form
|
||
# MONTAGE_MUSIC_LOOPS_<Season>_<MvtNum>.
|
||
#
|
||
# Recommended starting point based on movement durations:
|
||
# Summer Mvt 2 (107s) → 3× Winter Mvt 2 (132s) → 3×
|
||
# Autumn Mvt 2 (152s) → 2× Summer Mvt 3 (159s) → 2×
|
||
# Spring Mvt 2 (171s) → 2× others 1× (already ≥10 s/day)
|
||
#
|
||
MONTAGE_MUSIC_LOOPS=1 # default for all movements
|
||
MONTAGE_MUSIC_LOOPS_Summer_2=3
|
||
MONTAGE_MUSIC_LOOPS_Winter_2=3
|
||
MONTAGE_MUSIC_LOOPS_Autumn_2=2
|
||
MONTAGE_MUSIC_LOOPS_Summer_3=2
|
||
MONTAGE_MUSIC_LOOPS_Spring_2=2
|
||
|
||
# ── Direct RTSP capture (replaces MotionEye / any NVR) ────────────────────────
|
||
# capture.sh pulls frames directly from each camera's RTSP stream.
|
||
# No NVR software required — just ffmpeg and the camera's stream URL.
|
||
#
|
||
# Per-camera RTSP URL: CAM_RTSP_<cam>=rtsp://user:pass@host:port/path
|
||
# Find the URL in your camera's web UI (usually under Network → Video → RTSP)
|
||
# or check your camera's manual.
|
||
#
|
||
# CAM_RTSP_<cam> goes in .env (see bottom of this file), not here.
|
||
|
||
# Seconds between captured frames (applies to all cameras).
|
||
# Lower = smoother timelapse + larger storage footprint:
|
||
# 10 s → 8,640 frames/day ~2.5 GB/day (default, good balance)
|
||
# 5 s → 17,280 frames/day ~5 GB/day (smoother motion)
|
||
# 2 s → 43,200 frames/day ~12 GB/day (very smooth, high storage)
|
||
# CAPTURE_STALE_SECS should always be at least 2× CAPTURE_INTERVAL.
|
||
CAPTURE_INTERVAL=10
|
||
|
||
# Per-camera overrides — uncomment and set to override the global value above.
|
||
# Useful when cameras serve different purposes (e.g. south records ambient audio
|
||
# so fewer frames is fine; north faces a busy scene and benefits from 5s).
|
||
#CAPTURE_INTERVAL_east=10
|
||
#CAPTURE_INTERVAL_north=10
|
||
#CAPTURE_INTERVAL_south=30
|
||
|
||
# Seconds without a new frame before the watchdog sends a stall notification.
|
||
# A recovery notification fires automatically when capture resumes.
|
||
# Must always be at least 2× the effective CAPTURE_INTERVAL for that camera.
|
||
CAPTURE_STALE_SECS=500
|
||
|
||
# ── Sunrise audio capture ──────────────────────────────────────────────────────
|
||
# Records the camera's RTSP audio stream during the sunrise window so that
|
||
# daily_sunrise_video.sh can mix in natural ambient sound (birds, rain, wind)
|
||
# at real speed, while the video plays as the sped-up timelapse.
|
||
# The audio file is deleted automatically after it is mixed into the video.
|
||
#
|
||
AUDIO_ENABLED=true # set true if your camera has a working mic
|
||
CAPTURE_AUDIO_BITRATE=96k # bitrate for the sunrise audio recording
|
||
# Audio is recorded for exactly SUNRISE_TARGET_SECS, centred on actual sunrise:
|
||
# floor(TARGET/2) seconds before, ceil(TARGET/2) after (odd second → post-sunrise).
|
||
# Clips are saved to AUDIO_DIR/sunrise/<cam>/YYYY-MM-DD/sunrise-audio.m4a.
|
||
# After mixing into the video the clip is kept for SUNRISE_AUDIO_RETENTION_DAYS, then deleted.
|
||
SUNRISE_AUDIO_RETENTION_DAYS=7
|
||
|
||
# ── Ambient audio library ──────────────────────────────────────────────────────
|
||
# Continuously records natural sounds (birds, rain, wind) from AMBIENT_CAMS to
|
||
# build a reusable audio library. Runs as a permanent systemd service alongside
|
||
# capture.sh. Camera audio is 8 kHz mono (phone quality) — enough for nature
|
||
# sounds and white-noise style playback.
|
||
#
|
||
AMBIENT_ENABLED=false
|
||
AMBIENT_CAMS=(south) # cameras to record from
|
||
AMBIENT_CHUNK_SECS=1800 # seconds per file (default 30 min)
|
||
AMBIENT_BITRATE=96k # AAC bitrate; 64–96k is plenty at 8 kHz
|
||
# Retention — global default, then optional per-camera overrides.
|
||
# Uncomment and set per-cam lines to keep different cameras for different durations.
|
||
AMBIENT_RETENTION_DAYS=30 # global default; 0 = keep forever
|
||
#AMBIENT_RETENTION_DAYS_east=14
|
||
#AMBIENT_RETENTION_DAYS_north=30
|
||
#AMBIENT_RETENTION_DAYS_south=60 # keep south longer for nature sound library
|
||
|
||
# ── Moon jobs (nightly tracker + monthly phase close-ups) ────────────────────
|
||
# All moon jobs operate on the SUNRISE_CAM (the east-facing camera that already
|
||
# has a clear view of the eastern sky). They share three Python helpers
|
||
# (moon_phase.py, moon_detect.py, moon_composite.py) and one cached lunar
|
||
# reference texture downloaded by install.sh.
|
||
#
|
||
# moon-track.sh nightly batch — detects the moon in each frame,
|
||
# crops a 480×480 box around it, stitches the
|
||
# tracked sequence into an mp4. Output:
|
||
# $MOVIES_DIR/<cam>/moon-track/YYYY/YYYY-MM-DD-moon-track.mp4
|
||
#
|
||
# moon-phase-monthly.sh daily check; runs whichever phase composite is
|
||
# due today. Three phases handled:
|
||
# 🌕 Full Moon posts D + MOON_FULL_POST_DELAY_DAYS
|
||
# 🌓 First Quarter posts D + MOON_QUARTER_POST_DELAY_DAYS
|
||
# 🌗 Third Quarter posts D + MOON_QUARTER_POST_DELAY_DAYS
|
||
# East acts as the WITNESS — it confirms the moon
|
||
# was actually visible in your sky during the
|
||
# collection window and supplies the timestamp.
|
||
# The image itself is a NASA SVS Dial-a-Moon
|
||
# render for that exact UTC hour, sized to fill
|
||
# the frame on a black background (like the look
|
||
# of a long-telephoto shot). Output:
|
||
# $MOVIES_DIR/<cam>/full-moons/YYYY-MM-full.jpg
|
||
# $MOVIES_DIR/<cam>/first-quarter/YYYY-MM-first-quarter.jpg
|
||
# $MOVIES_DIR/<cam>/third-quarter/YYYY-MM-third-quarter.jpg
|
||
#
|
||
# Honest-by-design: a 38-px white blob from a wide-field IP camera cannot be
|
||
# enhanced into crater detail. The image you see in Mattermost is a NASA
|
||
# render for the exact moment east captured the moon — the strongest possible
|
||
# match for what your sky actually looked like, with full real-physics
|
||
# crater shadows, libration, and phase. East's role is verifying that you
|
||
# had a clear view of the moon that night.
|
||
#
|
||
# Geometry caveat: east only sees the eastern sky. At first quarter the moon
|
||
# is up only from noon to midnight, transiting south at sunset, so east only
|
||
# catches it during DAYTIME. The brightness-based detector often fails on
|
||
# daytime moon shots — first quarter is best-effort. Full moon and third
|
||
# quarter both rise after dark and stay in east's view; those should land
|
||
# cleanly most months.
|
||
#
|
||
# Toggles (any can be disabled independently):
|
||
MOON_TRACK_ENABLED=true
|
||
MOON_FULL_ENABLED=true
|
||
MOON_FIRST_QUARTER_ENABLED=true # set false if daytime-detection misses are noisy
|
||
MOON_THIRD_QUARTER_ENABLED=true
|
||
|
||
# Set false to skip east verification entirely and post the NASA render for
|
||
# the exact phase moment regardless of clouds / camera outage. Default true
|
||
# means "only post when east actually saw the moon that month."
|
||
MOON_REQUIRE_EAST_VERIFY=true
|
||
|
||
# Nightly tracker tuning ──────────────────────────────────────────────────────
|
||
MOON_TRACK_CROP_PX=480 # pixels — box size around the moon (source coords)
|
||
MOON_TRACK_FPS=12 # output mp4 framerate
|
||
MOON_TRACK_CRF=24 # output mp4 CRF
|
||
MOON_TRACK_RETENTION_DAYS=90 # delete tracker mp4s older than this; 0 = forever
|
||
|
||
# Full-moon monthly tuning ────────────────────────────────────────────────────
|
||
# How many days after the exact full moon to post. 3 = waits for D-1..D+2
|
||
# nights to be on disk, then runs the morning of D+3.
|
||
MOON_FULL_POST_DELAY_DAYS=3
|
||
|
||
# Quarter (half-moon) tuning ──────────────────────────────────────────────────
|
||
# 2 = waits for D-1, D, D+1 nights, runs the morning of D+2.
|
||
MOON_QUARTER_POST_DELAY_DAYS=2
|
||
MOON_QUARTER_MIN_ILLUMINATION=0.46 # ±4% of 50%: waxing/waning within 4% of exact quarter
|
||
MOON_QUARTER_MAX_ILLUMINATION=0.54
|
||
|
||
# Frame-acceptance thresholds — a candidate must beat all three to qualify.
|
||
# Lower = more permissive (accept hazier nights / lower moon). If you find
|
||
# the script never finds a clear shot, loosen these.
|
||
MOON_MIN_QUALITY=0.55 # 0..1 from moon_detect (roundness × halo × isolation)
|
||
MOON_MIN_ALTITUDE_DEG=15 # below this the moon is in trees / on the horizon
|
||
MOON_FULL_MIN_ILLUMINATION=0.96 # ±4% of 100%: accept 96–100% illumination for full moon
|
||
|
||
# Output frame. Default 1920×1080 to match the sunrise videos.
|
||
MOON_OUTPUT_W=1920
|
||
MOON_OUTPUT_H=1080
|
||
MOON_HEIGHT_PCT=0.92 # moon disk fills this fraction of frame height (full-screen feel)
|
||
|
||
# NASA SVS Dial-a-Moon ────────────────────────────────────────────────────────
|
||
# Hourly pre-rendered moon images, free, public domain, real physics
|
||
# (correct phase, libration, crater shadows for any UTC hour).
|
||
# https://svs.gsfc.nasa.gov/api/dialamoon/<ISO-DATE>
|
||
# We hit it once per phase event (~36 calls/year), cache the result forever.
|
||
#MOON_DIALAMOON_API=https://svs.gsfc.nasa.gov/api/dialamoon
|
||
#MOON_DIALAMOON_CACHE_DIR="$SCRIPT_DIR/moon-ref/dialamoon"
|
||
MOON_DIALAMOON_TARGET_PX=2048 # cached PNG longest side; downsampled on save
|
||
MOON_DIALAMOON_TIMEOUT_SEC=30
|
||
|
||
# East sky backdrop ───────────────────────────────────────────────────────────
|
||
# When east verifies a moon sighting, its full camera frame is scaled to
|
||
# output size and blurred heavily (GaussianBlur r ≈ output_width / 6) to
|
||
# produce an atmospheric backdrop behind the NASA moon disk.
|
||
#
|
||
# The blur removes RTSP artefacts, OSD text, and the wide-angle look while
|
||
# preserving the real sky colour, any cloud or haze gradients, and the dark
|
||
# tree/ground silhouette at the bottom of frame. The NASA moon is pasted
|
||
# sharp on top — the result looks like east shot it through a telephoto, with
|
||
# its actual sky that night as the background.
|
||
#
|
||
# This is the honest solution to "east doesn't capture high-res clouds": east's
|
||
# real atmospheric fingerprint (dark and clear, softly hazy, or cloud-diffused)
|
||
# becomes the background without pretending to photograph detail that isn't there.
|
||
#
|
||
# Set false to use a plain black background instead (original behaviour).
|
||
MOON_EAST_SKY_ENABLED=true
|
||
#MOON_EAST_SKY_BLUR=0 # blur radius in px; 0 = auto (output_width / 6)
|
||
|
||
# ── Mattermost — daily sunrise upload ─────────────────────────────────────────
|
||
# mattermost_url, access_token, channel_id go in .env (see bottom of this file).
|
||
|
||
# ── Notifications — montage / year-end complete + any job failure ─────────────
|
||
# Notifications fire when a movement montage or the year video finishes,
|
||
# and via systemd OnFailure= if any daily job fails.
|
||
# Uncomment and configure one or more methods to enable.
|
||
|
||
# ntfy — push notifications, zero signup for self-hosted or free cloud tier:
|
||
# Self-hosted: https://docs.ntfy.sh/install/
|
||
# Cloud: pick any topic name at ntfy.sh (no account needed)
|
||
# Set NTFY_URL=https://ntfy.sh/your-topic in .env, then flip the toggle below.
|
||
NTFY_ENABLED=true
|
||
NTFY_URL="${NTFY_URL:-}"
|
||
|
||
# Email — requires the 'mail' command (package: mailutils or s-nail).
|
||
# Configure outbound SMTP via /etc/ssmtp/ssmtp.conf or msmtp.
|
||
# Set EMAIL_TO and EMAIL_FROM in .env, then flip the toggle below.
|
||
EMAIL_ENABLED=false
|
||
EMAIL_TO="${EMAIL_TO:-}"
|
||
EMAIL_FROM="${EMAIL_FROM:-skycam@localhost}"
|
||
|
||
# Mattermost text post — reuses mattermost_url and access_token above.
|
||
# Can be the same daily channel or a separate admin/private channel.
|
||
# Set MM_NOTIFY_CHANNEL_ID in .env, then flip the toggle below.
|
||
MM_NOTIFY_ENABLED=false
|
||
MM_NOTIFY_CHANNEL_ID="${MM_NOTIFY_CHANNEL_ID:-}"
|
||
|
||
# ── Credentials (.env override) ───────────────────────────────────────────────
|
||
# Sensitive values (RTSP URLs with passwords, API tokens) can be kept out of
|
||
# this file — and out of git — by placing them in a .env file in the same
|
||
# directory. install.sh generates .env.example showing exactly which variables
|
||
# to define for your camera list.
|
||
#
|
||
# To use:
|
||
# cp .env.example .env
|
||
# $EDITOR .env ← fill in real values
|
||
# (never commit .env)
|
||
#
|
||
_conf_dir="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
|
||
[ -f "$_conf_dir/.env" ] && source "$_conf_dir/.env"
|
||
unset _conf_dir
|