Merge pull request #13 from outis1one/claude/seasonal-sunrise-montage-nn268
README: full rewrite — dependencies first, accurate quick start, curr…
This commit is contained in:
@@ -2,35 +2,30 @@
|
|||||||
|
|
||||||
Automated sky / timelapse camera scripts that produce:
|
Automated sky / timelapse camera scripts that produce:
|
||||||
|
|
||||||
- **Daily sunrise clip** — a 10-second speed-adjusted video of the sunrise window, uploaded to Mattermost each morning
|
- **Daily sunrise clip** — a speed-adjusted video of the sunrise window, uploaded to Mattermost each morning
|
||||||
- **Four Seasons timelapse** — daily clips sized to each Vivaldi movement's music duration, assembled automatically into per-movement montages (with music + attribution overlay) and a full-year video
|
- **Four Seasons timelapse** — daily clips sized to each Vivaldi movement's music duration, assembled automatically into per-movement montages (with music + attribution overlay) and a full-year video
|
||||||
- **Full-day timelapse** — a fixed-fps timelapse of every image captured that day, kept for a configurable retention window
|
- **Full-day timelapse** — a fixed-fps timelapse of every image captured that day, kept for a configurable retention window
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Prerequisites
|
## Quick start
|
||||||
|
|
||||||
| Dependency | Notes |
|
### 1. Install dependencies
|
||||||
|---|---|
|
|
||||||
| `ffmpeg` + `ffprobe` | Encoding, capture, audio recording, duration probing |
|
|
||||||
| `python3` | `suntime pytz` for sunrise calculation, `requests` for Mattermost upload — `pip3 install suntime pytz requests` |
|
|
||||||
| `fonts-dejavu` | DejaVu fonts for text overlays — `sudo apt install fonts-dejavu` |
|
|
||||||
| `bc` | Shell arithmetic (floating-point speed factors) |
|
|
||||||
| `fontconfig` (`fc-match`) | Font detection for overlays — optional, falls back to hardcoded paths |
|
|
||||||
| IP camera with RTSP stream | `capture.sh` pulls frames directly — no NVR software needed |
|
|
||||||
| Vivaldi Four Seasons audio | 12 MP3 files named so that `*Spring*Mvt*1*`, `*Summer*Mvt*2*`, etc. match with `find -iname` |
|
|
||||||
|
|
||||||
Install Python dependencies:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
sudo apt install ffmpeg bc fonts-dejavu curl python3-pip
|
||||||
pip3 install suntime pytz requests
|
pip3 install suntime pytz requests
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
| Package | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `ffmpeg` + `ffprobe` | Encoding, RTSP capture, audio recording, duration probing |
|
||||||
|
| `bc` | Floating-point arithmetic for speed factors |
|
||||||
|
| `fonts-dejavu` | Text overlays (sunrise time, attribution) |
|
||||||
|
| `python3` + `suntime pytz` | Astronomical sunrise calculation — pure math, no internet, works indefinitely |
|
||||||
|
| `python3` + `requests` | Mattermost upload |
|
||||||
|
|
||||||
## Quick start
|
### 2. Download
|
||||||
|
|
||||||
### 1. Download
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash <(curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh)
|
bash <(curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh)
|
||||||
@@ -44,42 +39,65 @@ curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh
|
|||||||
cd /opt/sky-cam
|
cd /opt/sky-cam
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Configure
|
### 3. Configure
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$EDITOR sky-cam.conf
|
$EDITOR sky-cam.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Minimum settings to fill in (everything else has sensible defaults):
|
`SCRIPT_DIR` and `BASE_DIR` are auto-detected — you only need to fill in settings specific to your setup:
|
||||||
|
|
||||||
| Setting | What it is |
|
| Setting | What it is |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `SCRIPT_DIR` | Full path to this directory |
|
| `BASE_DIR` | Root where camera images live — override if storing on a separate drive (`BASE_DIR/<cam>/<date>/<HH-MM-SS>.jpg`) |
|
||||||
| `BASE_DIR` | Root where camera images live (`BASE_DIR/<cam>/<date>/<HH-MM-SS>.jpg`) |
|
|
||||||
| `MOVIES_DIR` | Where finished videos are written (default: `BASE_DIR/movies`) |
|
| `MOVIES_DIR` | Where finished videos are written (default: `BASE_DIR/movies`) |
|
||||||
| `MUSIC_DIR` | Directory containing the 12 Vivaldi Four Seasons audio files |
|
| `MUSIC_DIR` | Directory containing the 12 Vivaldi Four Seasons MP3 files |
|
||||||
| `CAMERAS` | Space-separated list of camera names, e.g. `(sunrise north)` |
|
| `CAMERAS` | Space-separated list of camera names, e.g. `(sunrise north)` |
|
||||||
| `SUNRISE_CAM` | Which camera faces east and gets the sunrise job |
|
| `SUNRISE_CAM` | Which camera faces east and gets the sunrise job |
|
||||||
| `LATITUDE` / `LONGITUDE` / `TIMEZONE` | Your location for sunrise calculation |
|
| `LATITUDE` / `LONGITUDE` / `TIMEZONE` | Your location for sunrise calculation |
|
||||||
| `CAM_RTSP_<cam>` | RTSP stream URL for each camera, e.g. `rtsp://admin:pass@192.168.1.100:554/stream1` |
|
|
||||||
| `CAPTURE_INTERVAL` | Seconds between captured frames (default: 10) |
|
| `CAPTURE_INTERVAL` | Seconds between captured frames (default: 10) |
|
||||||
| `mattermost_url` / `access_token` / `channel_id` | Mattermost upload credentials |
|
| `SCHEDULE_SUNRISE` | When to start the sunrise job — default `03:00`, script waits internally until the capture window closes |
|
||||||
|
| `SCHEDULE_SEASONS_<cam>` | When to run the Four Seasons daily clip — processes **yesterday's** images; runs after midnight |
|
||||||
|
| `SCHEDULE_FULLDAY_<cam>` | When to run the full-day timelapse — also processes yesterday; schedule after SEASONS |
|
||||||
|
|
||||||
### 3. Install systemd timers
|
### 4. Set up credentials
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./install.sh # user-level timers (~/.config/systemd/user), no root needed
|
./install.sh # generates .env.example alongside systemd units
|
||||||
# or
|
cp .env.example .env
|
||||||
./install.sh --system # system-wide (/etc/systemd/system), requires sudo
|
$EDITOR .env
|
||||||
```
|
```
|
||||||
|
|
||||||
Re-run `install.sh` any time `sky-cam.conf` changes.
|
`.env` holds all sensitive values and is never committed to git:
|
||||||
|
|
||||||
### 4. Verify
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# RTSP stream URL — one per camera (variable name matches camera name)
|
||||||
|
CAM_RTSP_sunrise=rtsp://admin:password@192.168.1.100:554/stream1
|
||||||
|
|
||||||
|
# Mattermost upload
|
||||||
|
mattermost_url=https://your-mattermost.example.com
|
||||||
|
access_token=your-token
|
||||||
|
channel_id=your-channel-id
|
||||||
|
|
||||||
|
# Notifications (uncomment what you use)
|
||||||
|
#NTFY_URL=https://ntfy.sh/your-topic
|
||||||
|
#EMAIL_TO=you@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-run `./install.sh` any time `sky-cam.conf` changes (schedules, cameras, etc.).
|
||||||
|
|
||||||
|
### 5. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Confirm cameras are capturing
|
||||||
|
systemctl --user status sky-cam-capture-sunrise.service
|
||||||
|
ls BASE_DIR/sunrise/$(date +%Y-%m-%d)/ # images should appear within CAPTURE_INTERVAL seconds
|
||||||
|
|
||||||
|
# Check all timers are scheduled
|
||||||
systemctl --user list-timers 'sky-cam-*'
|
systemctl --user list-timers 'sky-cam-*'
|
||||||
journalctl --user -u sky-cam-sunrise.service -f
|
|
||||||
|
# Test sunrise calculation
|
||||||
|
python3 sunrise.py
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -92,19 +110,23 @@ Camera RTSP stream
|
|||||||
├─ capture.sh <cam> (long-running systemd service, one per camera)
|
├─ capture.sh <cam> (long-running systemd service, one per camera)
|
||||||
│ ffmpeg pulls one frame every CAPTURE_INTERVAL seconds
|
│ ffmpeg pulls one frame every CAPTURE_INTERVAL seconds
|
||||||
│ Writes: BASE_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.jpg
|
│ Writes: BASE_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.jpg
|
||||||
│ Restarts at midnight for the new date directory; auto-reconnects
|
│ Restarts at midnight for the new date directory; auto-reconnects after 30s on loss
|
||||||
│
|
│
|
||||||
├─ sunrise-audio-capture.sh (runs at 03:00, waits for sunrise window)
|
├─ sunrise-audio-capture.sh (starts at 03:00, waits internally for sunrise)
|
||||||
│ ffmpeg records audio-only from RTSP during sunrise window
|
│ Calculates today's sunrise, then records exactly SUNRISE_TARGET_SECS of audio
|
||||||
|
│ centred on the moment of sunrise (odd second goes to post-sunrise)
|
||||||
│ Writes: BASE_DIR/<cam>/YYYY-MM-DD/sunrise-audio.m4a
|
│ Writes: BASE_DIR/<cam>/YYYY-MM-DD/sunrise-audio.m4a
|
||||||
│ Deleted automatically after being mixed into the sunrise video
|
│ Deleted automatically after being mixed into the sunrise video
|
||||||
│
|
│
|
||||||
Camera JPEGs + audio
|
Camera JPEGs + audio
|
||||||
│
|
│
|
||||||
├─ daily_sunrise_video.sh (runs at SCHEDULE_SUNRISE)
|
├─ daily_sunrise_video.sh (starts at SCHEDULE_SUNRISE, waits for capture window)
|
||||||
|
│ Wakes at SCHEDULE_SUNRISE, sleeps until (sunrise + SUNRISE_POST_MIN)
|
||||||
│ Step 1: encode raw video from sunrise-window JPEGs
|
│ Step 1: encode raw video from sunrise-window JPEGs
|
||||||
│ Step 2: speed-adjust to SUNRISE_TARGET_SECS → saved permanently
|
│ Step 2: speed-adjust to SUNRISE_TARGET_SECS → saved permanently
|
||||||
│ Step 3: burn sunrise time overlay + mix camera audio (if available)
|
│ Step 3a: mix audio (camera mic → library fallback → no audio)
|
||||||
|
│ Step 3b: burn sunrise time overlay (optional, set SUNRISE_OVERLAY_ENABLED=false to skip)
|
||||||
|
│ Each step degrades independently — upload always fires
|
||||||
│ OnSuccess → sunrise2mm.py uploads to Mattermost
|
│ OnSuccess → sunrise2mm.py uploads to Mattermost
|
||||||
│
|
│
|
||||||
├─ 4-seasons.sh <cam> (runs at SCHEDULE_SEASONS_<cam>, processes yesterday)
|
├─ 4-seasons.sh <cam> (runs at SCHEDULE_SEASONS_<cam>, processes yesterday)
|
||||||
@@ -123,10 +145,18 @@ Camera JPEGs + audio
|
|||||||
|
|
||||||
### Resilience
|
### Resilience
|
||||||
|
|
||||||
Each pipeline saves an intermediate file before the step most likely to fail, so a partial failure leaves a recoverable artifact:
|
Every pipeline saves an intermediate file before the riskiest step, so a partial failure always leaves something uploadable:
|
||||||
|
|
||||||
- **Sunrise**: if the overlay (step 3) fails, the speed-only video is promoted to the upload target — the upload still happens and you get a notification of the overlay failure
|
**Sunrise video — four-tier fallback, upload always fires:**
|
||||||
- **Montage**: if music + overlay (step 3) fails, the speed-adjusted silent video is promoted to `*-Montage.mp4` — `year-end-join.sh` still includes the movement and you get a warning notification
|
|
||||||
|
| Audio | Overlay | What gets uploaded |
|
||||||
|
|---|---|---|
|
||||||
|
| ✓ | ✓ | Final video with audio + timestamp |
|
||||||
|
| ✓ | ✗ | Audio-mixed video, no timestamp — overlay failure notified |
|
||||||
|
| ✗ | ✓ | Video with timestamp, no audio — audio failure notified |
|
||||||
|
| ✗ | ✗ | Speed-only video — both failures notified |
|
||||||
|
|
||||||
|
**Montage:** if music + overlay (step 3) fails, the speed-adjusted silent video is promoted to `*-Montage.mp4` — `year-end-join.sh` still includes the movement and you get a warning notification.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -140,8 +170,8 @@ Each pipeline saves an intermediate file before the step most likely to fail, so
|
|||||||
| Email | `EMAIL_ENABLED=true`, `EMAIL_TO=you@example.com` |
|
| Email | `EMAIL_ENABLED=true`, `EMAIL_TO=you@example.com` |
|
||||||
| Mattermost text post | `MM_NOTIFY_ENABLED=true`, `MM_NOTIFY_CHANNEL_ID=<channel-id>` |
|
| Mattermost text post | `MM_NOTIFY_ENABLED=true`, `MM_NOTIFY_CHANNEL_ID=<channel-id>` |
|
||||||
|
|
||||||
You receive notifications for:
|
Notifications fire for:
|
||||||
- Sunrise: video ready, upload success/failure, overlay failure
|
- Sunrise video ready (with audio/overlay status), upload success/failure
|
||||||
- Each daily seasons clip saved
|
- Each daily seasons clip saved
|
||||||
- Montage complete (or degraded if audio/overlay failed)
|
- Montage complete (or degraded if audio/overlay failed)
|
||||||
- Year-end Four Seasons video complete
|
- Year-end Four Seasons video complete
|
||||||
@@ -151,24 +181,24 @@ You receive notifications for:
|
|||||||
|
|
||||||
## Camera audio (optional)
|
## Camera audio (optional)
|
||||||
|
|
||||||
If your camera has a microphone, sky-cam can mix a natural-speed 10-second audio clip (birds, rain, wind — whatever was actually happening at sunrise) into the daily sunrise video.
|
If your camera has a microphone, sky-cam records exactly `SUNRISE_TARGET_SECS` of natural-speed audio centred on the actual sunrise moment and mixes it into the timelapse video.
|
||||||
|
|
||||||
1. Set `AUDIO_ENABLED=true` in `sky-cam.conf`
|
1. Set `AUDIO_ENABLED=true` in `sky-cam.conf`
|
||||||
2. Set `CAM_RTSP_<cam>` for the sunrise camera (needed for both image capture and audio)
|
2. Set `CAM_RTSP_<cam>` in `.env` for the sunrise camera
|
||||||
3. Re-run `./install.sh` to generate the `sky-cam-audio-capture.timer`
|
3. Re-run `./install.sh` to generate the `sky-cam-audio-capture.timer`
|
||||||
|
|
||||||
The audio is recorded during the same window as the images, stored alongside them, and deleted automatically after being mixed into the final video. No audio library or AI required — it's the real sound from your camera.
|
The recording is split evenly around sunrise (e.g. 5 s before + 5 s after for a 10 s clip; odd second goes to post-sunrise). It is deleted automatically after mixing.
|
||||||
|
|
||||||
### Audio fallback library (optional)
|
### Audio fallback library (optional)
|
||||||
|
|
||||||
If the camera has no mic, or audio capture fails, `daily_sunrise_video.sh` picks a random ambient sound from `sunrise-sounds/` instead. The library is organised into 11 weather/season folders (`clear-spring`, `rain`, `thunder`, `windy`, etc.) — populate it once with:
|
If the camera has no mic, or audio capture fails, `daily_sunrise_video.sh` picks a random ambient sound from `sunrise-sounds/` instead. The library is organised into 11 weather/season folders (`clear-spring`, `rain`, `thunder`, `windy`, etc.) — populate it once with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get a free API key at https://freesound.org/apiv2/apply/
|
# Get a free API key at https://freesound.org/apiv2/apply/
|
||||||
python3 download-sunrise-sounds.py --api-key YOUR_KEY
|
python3 download-sunrise-sounds.py --api-key YOUR_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
Default: ~275 CC-licensed 128 kbps MP3 previews (~25 per category). Re-run any time to top up:
|
Default: ~275 CC-licensed 128 kbps MP3 previews (~25 per category). Re-run any time to top up:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 download-sunrise-sounds.py --api-key YOUR_KEY --per-category 40
|
python3 download-sunrise-sounds.py --api-key YOUR_KEY --per-category 40
|
||||||
@@ -176,29 +206,29 @@ python3 download-sunrise-sounds.py --api-key YOUR_KEY --per-category 40
|
|||||||
|
|
||||||
Attribution data for every file is written to `sunrise-sounds/manifest.json`.
|
Attribution data for every file is written to `sunrise-sounds/manifest.json`.
|
||||||
|
|
||||||
**Priority order for sunrise audio:**
|
**Audio priority order:**
|
||||||
1. Camera mic recording (`sunrise-audio.m4a`) — real ambient sound at actual sunrise
|
1. Camera mic recording (`sunrise-audio.m4a`) — real ambient sound at actual sunrise
|
||||||
2. Random file from `sunrise-sounds/` library — weather/season matched in a future update
|
2. Random file from `sunrise-sounds/` library
|
||||||
3. No audio — overlay-only video (always produced regardless)
|
3. No audio — overlay-only video (always produced regardless)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Manual operations
|
## Manual operations
|
||||||
|
|
||||||
**Re-run today's sunrise** (e.g. after fixing a font issue):
|
**Re-run today's sunrise** (e.g. after a config fix):
|
||||||
```bash
|
```bash
|
||||||
./daily_sunrise_video.sh
|
./daily_sunrise_video.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Re-run a daily seasons clip** for a specific date:
|
**Re-run a daily seasons clip** for yesterday:
|
||||||
```bash
|
```bash
|
||||||
./4-seasons.sh <cam> # reprocesses yesterday
|
./4-seasons.sh sunrise
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rebuild a movement montage** (e.g. to retry audio after a failure):
|
**Rebuild a movement montage** (e.g. to retry audio after a failure):
|
||||||
```bash
|
```bash
|
||||||
./montage-mvt.sh # uses today's movement
|
./montage-mvt.sh # uses today's movement
|
||||||
./montage-mvt.sh 2025-06-15 sunrise # use a specific reference date + camera
|
./montage-mvt.sh 2025-06-15 sunrise # specific date + camera
|
||||||
```
|
```
|
||||||
|
|
||||||
**Rebuild the year-end video**:
|
**Rebuild the year-end video**:
|
||||||
|
|||||||
Reference in New Issue
Block a user