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
551 lines
30 KiB
Plaintext
551 lines
30 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 picks
|
||
# the best east frame from the collection window,
|
||
# composites a high-res lunar texture into it,
|
||
# 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
|
||
# Each picks the frame closest in time to the
|
||
# exact phase moment that meets quality / altitude
|
||
# / illumination thresholds, composites the
|
||
# cached lunar texture into it (sky/halo/angle
|
||
# real from east, surface detail borrowed),
|
||
# uploads to Mattermost. 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: the surface texture is not from your camera (no software
|
||
# can recover detail your camera didn't capture). The framing — sky color,
|
||
# halo, parallactic angle, exact moment of capture — is all real-from-east.
|
||
#
|
||
# 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
|
||
|
||
# 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
|
||
|
||
# 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_FULL_MIN_QUALITY=0.55 # 0..1 from moon_detect (roundness × halo × isolation)
|
||
MOON_FULL_MIN_ALTITUDE_DEG=15 # below this the moon is in trees / on the horizon
|
||
MOON_FULL_MIN_ILLUMINATION=0.95 # 0..1 — ~95% lit covers ±2 days from exact full
|
||
|
||
# Output frame. Default 1920×1080 to match the sunrise videos.
|
||
MOON_FULL_OUTPUT_W=1920
|
||
MOON_FULL_OUTPUT_H=1080
|
||
MOON_FULL_HEIGHT_PCT=0.70 # moon disk fills this fraction of frame height
|
||
|
||
# Lunar reference texture — downloaded once by install.sh, reused forever.
|
||
# Override MOON_REFERENCE_URL in .env to use a different image. Any high-res
|
||
# photo of a full moon on a black background works. Default is the Wikipedia
|
||
# "FullMoon2010" by Gregory H. Revera (CC BY-SA 3.0), 3500×3500.
|
||
#MOON_REFERENCE_URL=https://upload.wikimedia.org/wikipedia/commons/e/e1/FullMoon2010.jpg
|
||
#MOON_REFERENCE_DIR="$SCRIPT_DIR/moon-ref"
|
||
#MOON_REFERENCE_PATH="$MOON_REFERENCE_DIR/full-moon.jpg"
|
||
|
||
# ── 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
|