Files
sky-cam/sky-cam.conf
T
Claude ec8eb7aebf 4-seasons: protect night frames from grey filter with brightness floor
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
2026-04-22 16:17:22 +00:00

408 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 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
#
#
# 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
#
# =============================================================================
# ── 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}"
# ── 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
# 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 1830.
# 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) ──────────────────────────────────────────
# Corrupt or empty frames (e.g. first frame after a camera reconnect) are always
# skipped automatically via ffprobe. No config needed.
#
# SEASONS_GREY_STDDEV_MIN: also skip uniform-grey frames caused by a bad RTSP
# signal (valid JPEGs, but near-solid grey — no real image content).
# A frame is only skipped when BOTH conditions hold:
# 1. pixel standard deviation (0255) is below SEASONS_GREY_STDDEV_MIN
# 2. mean brightness (0255) is above SEASONS_GREY_DARK_FLOOR
# This keeps genuine night frames (dark + low stddev) while dropping
# bad-signal grey frames (mid-bright + low stddev).
#
# SEASONS_GREY_STDDEV_MIN=0 disables the filter entirely (default).
# Try 510 if your camera produces solid-grey frames on reconnect.
# Requires ImageMagick (convert) to be installed.
SEASONS_GREY_STDDEV_MIN=0
SEASONS_GREY_DARK_FLOOR=30 # frames with mean brightness below this are always kept
# ── 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=30
# ── 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=false # 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; 6496k 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
# ── 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=false
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