vigilar/docs/superpowers/specs/2026-04-03-daily-delight-design.md
Aaron D. Lee 93492e33d8 Add daily delight design spec (Q1, Q4, Q6)
Highlight reel video, kiosk ambient mode for magic picture frame,
and on-demand time-lapse generator with scheduling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:20:20 -04:00

294 lines
12 KiB
Markdown

# Daily Delight — Design Spec
## Overview
Three features that make Vigilar enjoyable to look at every day: an auto-generated daily highlight reel video, a kiosk ambient mode for a magic picture frame, and an on-demand time-lapse generator with scheduling.
## Q1: Daily Highlight Reel
### What It Does
Each morning, auto-generates a 60-90 second MP4 montage of yesterday's most interesting events — pet zoomies, wildlife visits, deliveries, notable detections. Generated once, viewable on the dashboard, kiosk, and phone via push notification.
### Event Scoring
Events are ranked by a scoring heuristic to select the top N clips (default 10, configurable):
| Event Type | Score |
|-----------|-------|
| PET_ESCAPE | 10 |
| WILDLIFE_PREDATOR | 9 |
| PACKAGE_DELIVERED | 8 |
| UNKNOWN_ANIMAL | 7 |
| PET_DETECTED (high motion) | 6 |
| WILDLIFE_NUISANCE | 5 |
| PERSON_DETECTED | 4 |
| UNKNOWN_VEHICLE_DETECTED | 4 |
| WILDLIFE_PASSIVE | 3 |
| VEHICLE_DETECTED | 2 |
| PET_DETECTED (normal) | 1 |
"High motion" for pet detections means the motion confidence was above the `zoomie_threshold` (0.8) from pet activity config.
### FFmpeg Pipeline
1. For each selected event, extract a 5-second clip centered on the event timestamp from the associated recording segment (decrypt if needed).
2. Overlay a date/time watermark and event label (e.g., "Angel — Kitchen — 2:34 PM") on each clip using FFmpeg's `drawtext` filter.
3. Concatenate clips with 0.5-second crossfade transitions using the `xfade` filter.
4. Output as H.264 MP4, CRF 23, resolution matching the source (or downscaled to 1280x720 if sources vary).
### New File: `vigilar/highlights/reel.py`
`generate_daily_reel(engine, recordings_dir, date, config) -> str`:
- Queries events for the given date, scores and ranks them
- Extracts clips from recordings via FFmpeg subprocess
- Concatenates with transitions and watermarks
- Saves to `recordings_dir/highlights/YYYY-MM-DD.mp4`
- Inserts a recording row with `trigger=HIGHLIGHT`
- Returns the file path
`ReelConfig` (from config):
```toml
[highlights]
enabled = true
generate_time = "06:00" # when to generate yesterday's reel
max_clips = 10 # max events to include
clip_duration_s = 5 # seconds per clip
cameras = [] # empty = all cameras
event_types = [] # empty = all types
```
### Config Model
```python
class HighlightsConfig(BaseModel):
enabled: bool = True
generate_time: str = "06:00"
max_clips: int = 10
clip_duration_s: int = 5
cameras: list[str] = Field(default_factory=list)
event_types: list[str] = Field(default_factory=list)
```
Add to `VigilarConfig` as `highlights: HighlightsConfig`.
### Scheduling
The daily reel generation runs as a scheduled job in the health monitor process (which already handles the daily digest timing). At `generate_time`, it calls `generate_daily_reel()` for yesterday's date.
### Delivery
- **Dashboard:** "Yesterday's Highlights" card on the main dashboard. Shows thumbnail + duration + "Play" button. Links to `/recordings/<id>/download`.
- **Push notification:** After generation, sends a Web Push notification: "Your daily highlight reel is ready (8 clips, 52s)". Link opens the recording.
- **Kiosk:** Auto-plays at a configurable time (see Q4 kiosk spec). Then returns to ambient mode.
- **Dashboard config:** Settings page section for highlights — toggle cameras, event types, generation time, max clips.
### Recording Integration
Highlight reels are stored as regular recordings with `trigger=HIGHLIGHT`. They appear in the recordings list with a distinct badge. The `RecordingTrigger` enum already has values for different trigger types — add `HIGHLIGHT` and `TIMELAPSE`.
## Q4: Kiosk Ambient Mode
### What It Does
Extends the existing kiosk blueprint with an ambient "all-quiet" state optimized for a magic picture frame (RPi + thin LCD). When nothing is happening, it shows a beautiful home awareness dashboard. When alerts fire, the relevant camera takes over the screen.
### Ambient Screen Layout
Fullscreen, no nav, no scrollbar. Landscape orientation. Dark theme with muted colors to avoid light pollution.
**Top bar (10% height):**
- Current time (large, `hh:mm`, updates every minute)
- Current date (weekday + month + day)
- Weather icon + temperature (from Open-Meteo, cached hourly, "—" if offline)
**Center area (70% height):**
- Rotating camera thumbnail: cycles through cameras every 10 seconds
- Shows the camera name and a subtle "live" indicator
- Thumbnail source: latest HLS segment frame or snapshot endpoint
- Smooth CSS crossfade transition between cameras
**Bottom bar (20% height):**
- Household presence: who's home (icons + names from presence monitor)
- Pet locations: pet name + last-seen camera + time ago (e.g., "Angel — Kitchen — 4m ago")
- If highlight reel is available: small "Play highlights" button
### Alert Takeover
When an alert event arrives via SSE (`/events/stream`):
1. Fade ambient layout to 0 opacity (300ms)
2. Show the relevant camera's live HLS feed fullscreen
3. Overlay event info: type badge, camera name, timestamp
4. After timeout (configurable, default 30s for normal, 60s for predator alerts), if no new events, fade back to ambient
5. If a new event fires during takeover, reset the timer and switch cameras if different
### Highlight Reel Playback
If a daily highlight reel exists for yesterday, auto-play it once at a configurable time. After playback, return to ambient. Configurable in the kiosk settings.
### Power / Performance (RPi Considerations)
- No heavy canvas rendering — all layout via CSS Grid/Flexbox
- Camera thumbnails are static `<img>` elements swapped via JS, not video streams
- CSS `will-change: opacity` on transition elements
- Minimal DOM manipulation — update text content only when values change
- Screen dimming: configurable dark hours (e.g., 23:00-06:00) via a CSS overlay with `opacity: 0.1` that dims the screen. Optionally pair with HDMI CEC `tvservice -o` for full screen-off (documented as optional).
- Auto-refresh: full page reload every 6 hours to prevent memory leaks in long-running browser tabs
### Config
```toml
[kiosk]
ambient_enabled = true
camera_rotation_s = 10 # seconds between camera switches
alert_timeout_s = 30 # seconds before returning to ambient
predator_alert_timeout_s = 60 # predator alerts stay longer
dim_start = "23:00" # screen dim start
dim_end = "06:00" # screen dim end
highlight_play_time = "07:00" # when to auto-play highlight reel
```
### Files
**New file:** `vigilar/web/templates/kiosk/ambient.html` — self-contained fullscreen template
**Modified:** `vigilar/web/blueprints/kiosk.py` — add `GET /kiosk/ambient` route
**Modified:** `vigilar/config.py` — add `KioskConfig` model
### Weather Dependency
Uses `vigilar/detection/weather.py` (WeatherFetcher) from Group C's wildlife spec. If Group C isn't built yet when this is implemented, the weather section simply shows "—" (graceful degradation, no hard dependency).
## Q6: Time-lapse Generator
### What It Does
Generate time-lapse videos on demand for any camera and date range, with optional scheduled presets for automatic daily generation.
### On-Demand Generation
**Route:** `POST /cameras/<id>/timelapse`
Request body:
```json
{
"date": "2026-04-02",
"start_hour": 6,
"end_hour": 20,
"fps": 30
}
```
Process:
1. Find all recording segments for the camera on the given date within the hour range
2. Extract one frame per minute from each segment (decrypt if needed) via FFmpeg
3. Stitch frames into a time-lapse MP4 at the requested FPS (default 30 — one minute of real time per frame at 30fps gives a ~14-hour day in ~28 seconds)
4. Save as a recording with `trigger=TIMELAPSE`
5. Return `{id, status: "generating"}` immediately — generation runs in a background thread
**Status polling:** `GET /cameras/<id>/timelapse/status` returns progress of any in-flight generation:
```json
{"status": "generating", "progress": 0.65, "eta_seconds": 12}
```
Or `{"status": "idle"}` when nothing is running.
### Scheduled Presets
**Route:** `POST /cameras/<id>/timelapse/schedule`
Request body:
```json
{
"name": "Daily backyard",
"start_hour": 6,
"end_hour": 20,
"time": "20:00"
}
```
Creates a daily schedule: every day at the specified time, auto-generate a timelapse for that camera covering the hour range.
**Management routes:**
- `GET /cameras/<id>/timelapse/schedules` — list schedules for a camera
- `DELETE /cameras/<id>/timelapse/schedule/<schedule_id>` — remove a schedule
### Database
**New table: `timelapse_schedules`**
| Column | Type | Notes |
|--------|------|-------|
| id | Integer PK | Autoincrement |
| camera_id | String NOT NULL | Which camera |
| name | String NOT NULL | User-given label |
| start_hour | Integer NOT NULL | Start hour (0-23) |
| end_hour | Integer NOT NULL | End hour (0-23) |
| generate_time | String NOT NULL | "HH:MM" daily generation time |
| enabled | Integer NOT NULL | 1 = active |
| created_at | Float NOT NULL | Timestamp |
### New File: `vigilar/highlights/timelapse.py`
`generate_timelapse(camera_id, date, start_hour, end_hour, fps, recordings_dir, engine) -> str`:
- Queries recordings for the camera + date + hour range
- Extracts frames via FFmpeg (1 per minute)
- Stitches into MP4
- Inserts recording row with `trigger=TIMELAPSE`
- Returns file path
`check_schedules(engine, recordings_dir)`:
- Called periodically by the health monitor (every 60 seconds)
- Finds schedules where `enabled=1` and current time matches `generate_time` (within 60-second window)
- Triggers `generate_timelapse()` for today's date
- Tracks "last generated" to avoid double-generation
### UI
**Camera page:** "Time-lapse" tab alongside the existing stream and heatmap views.
- Date picker + start/end hour sliders + "Generate" button
- Progress bar when generating
- List of generated time-lapses (from recordings with `trigger=TIMELAPSE`)
- "Schedule daily" toggle that creates/removes a preset
### New Event Types
Add to `RecordingTrigger` enum:
- `HIGHLIGHT` — daily highlight reel
- `TIMELAPSE` — time-lapse video
(Note: `HIGHLIGHT` and `TIMELAPSE` are recording triggers, not event types — they go in `RecordingTrigger`, not `EventType`.)
## Dependencies
### New Packages
None — FFmpeg is already used by the recorder. All generation is via subprocess calls.
### New Files
| File | Purpose |
|------|---------|
| `vigilar/highlights/reel.py` | Daily highlight reel generator |
| `vigilar/highlights/timelapse.py` | Time-lapse generator + schedule checker |
| `vigilar/web/templates/kiosk/ambient.html` | Ambient mode fullscreen template |
### Modified Files
| File | Changes |
|------|---------|
| `vigilar/config.py` | Add HighlightsConfig, KioskConfig models |
| `vigilar/constants.py` | Add HIGHLIGHT, TIMELAPSE to RecordingTrigger |
| `vigilar/storage/schema.py` | Add timelapse_schedules table |
| `vigilar/storage/queries.py` | Add timelapse schedule CRUD functions |
| `vigilar/web/blueprints/kiosk.py` | Add /kiosk/ambient route |
| `vigilar/web/blueprints/cameras.py` | Add timelapse generation + schedule routes |
| `vigilar/health/monitor.py` | Add highlight reel + timelapse schedule triggers |
| `vigilar/web/app.py` | No new blueprints (uses existing kiosk + cameras) |
## Out of Scope
- Highlight reel with background music (YAGNI)
- Time-lapse with motion interpolation / smooth zoom (just frame concat)
- Kiosk touch controls (RPi magic frame is view-only)
- Multiple highlight reel presets (one per day is sufficient)
- Streaming time-lapse generation (generate full file, then serve)