Records rather than streams: each device records a clip WITH AUDIO and uploads
it; the normal offline pipeline takes over. Fits the design almost for free —
MediaRecorder's webm/mp4 are already ingestible, and audio-based sync means
devices need no clock sync and needn't start together.
- capture.py: POST /api/capture/upload (streamed, sanitized name, size cap,
partial cleanup), heartbeat/devices registry, GET /capture page.
- static/capture.html: standalone page (no build step) — device picker, live
preview, record->upload, 1fps snapshot heartbeat, 'who's shooting what'
monitor, and secure-context detection that tells you to use
(getUserMedia needs HTTPS — the #1 gotcha for phones).
- OPT-IN via FESTIVAL4D_CAPTURE=1; manifest reports has_capture and the viewer
only then shows a Capture link. Public deploy leaves it off (open upload
endpoint would be unsafe) — noted in DEPLOY.md.
- Adds python-multipart dep. Suite 103 -> 119.
Also fixes test_manifest_shape, which was already RED on main: f12b6e0 added
has_splat to the manifest without updating the exact-set lock. Updated to the
true set (now also has_capture); the lock is what caught both drifts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
4.9 KiB
Markdown
119 lines
4.9 KiB
Markdown
# Deploying Festival 4D to digalot.fyi/festifun
|
|
|
|
Target: `dealgod@100.94.195.115` (tailscale), served under `https://digalot.fyi/festifun` by the
|
|
existing web server that already terminates TLS for the domain.
|
|
|
|
The app runs as two pieces behind that web server:
|
|
- **Static SPA** — the built frontend (`frontend/dist`), served under `/festifun/`.
|
|
- **API backend** — uvicorn on `127.0.0.1:8000` (localhost-only), reached via `/festifun/api`
|
|
and `/festifun/media`.
|
|
|
|
This topology was validated locally end-to-end against a proxy that mirrors the nginx config in
|
|
this directory (static + prefix-stripping API/media proxy with Range) — the app loads, plays
|
|
video, and renders the 3D scene correctly under the `/festifun` prefix.
|
|
|
|
---
|
|
|
|
## Deploy mode: fully open
|
|
|
|
Per the owner's decision, **all features are public and unauthenticated** — playback, 3D, and the
|
|
mutating endpoints (annotate, correct event type, add/delete anchors). Be aware this means anyone
|
|
who finds the URL can edit or delete the project's annotations/anchors/events. If that becomes a
|
|
problem, add HTTP basic-auth on the `/festifun/` location, or run the backend read-only.
|
|
|
|
### AI classification & API keys — deliberately OFF on the public box
|
|
`POST /api/events/detect` runs ffmpeg and, if a classifier key is present, a paid AI call. To
|
|
avoid an unauthenticated public endpoint that spends real credits, **do not put `GEMINI_API_KEY`
|
|
or the OpenRouter creds on the server.** Instead bake the moment labels in *before* deploy:
|
|
|
|
```bash
|
|
# locally, with keys loaded (set -a; . ./.env; set +a):
|
|
python -m festival4d events # writes AI labels into data/project.db
|
|
```
|
|
|
|
Then ship that DB. On the server, `detect` still works but degrades to candidate-only (no spend),
|
|
exactly as designed. If you *want* live classification on the public site, set the key in the
|
|
systemd unit — but add rate-limiting first (nginx `limit_req`), or it's a bill-run-up vector.
|
|
|
|
---
|
|
|
|
## One-time server setup
|
|
|
|
```bash
|
|
# on dealgod@100.94.195.115
|
|
sudo mkdir -p /var/www/festifun
|
|
sudo chown dealgod:dealgod /var/www/festifun
|
|
# Python venv for the backend
|
|
python3 -m venv /var/www/festifun/.venv
|
|
```
|
|
|
|
## Build + push (run locally)
|
|
|
|
```bash
|
|
# 1. Build the frontend for the /festifun prefix + same-origin API
|
|
cd frontend
|
|
FESTIVAL4D_BASE=/festifun/ VITE_API_BASE=/festifun npm run build
|
|
|
|
# 2. Generate the project to ship (synthetic demo, or your real footage pipeline first),
|
|
# with labels baked in if you loaded keys:
|
|
cd ..
|
|
python -m festival4d synthetic # or: ingest -> sync -> reconstruct -> events
|
|
|
|
# 3. Push build + backend + data to the server
|
|
rsync -az --delete frontend/dist/ dealgod@100.94.195.115:/var/www/festifun/dist/
|
|
rsync -az --delete backend/ dealgod@100.94.195.115:/var/www/festifun/backend/
|
|
rsync -az pyproject.toml dealgod@100.94.195.115:/var/www/festifun/
|
|
rsync -az --delete data/ dealgod@100.94.195.115:/var/www/festifun/data/
|
|
```
|
|
|
|
## Install backend deps + service (on the server)
|
|
|
|
```bash
|
|
cd /var/www/festifun
|
|
.venv/bin/pip install -e ".[dev]" # or a runtime-only extra if defined
|
|
sudo cp backend/../deploy/festifun-api.service /etc/systemd/system/ # adjust path to the repo copy
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now festifun-api
|
|
curl -s localhost:8000/api/health # -> {"status":"ok"}
|
|
```
|
|
|
|
## Wire up the web server
|
|
|
|
Append the blocks from `deploy/festifun.nginx.conf` into the existing `server { }` for
|
|
digalot.fyi, adjusting `alias` paths to `/var/www/festifun/dist/`. Then:
|
|
|
|
```bash
|
|
sudo nginx -t && sudo systemctl reload nginx
|
|
```
|
|
|
|
Visit `https://digalot.fyi/festifun/`.
|
|
|
|
> Using Caddy instead of nginx? The equivalent is a `handle_path /festifun/*` block: `file_server`
|
|
> for the SPA, `reverse_proxy 127.0.0.1:8000` for `/festifun/api/*` and `/festifun/media/*`
|
|
> (Caddy strips the matched prefix with `handle_path`). Ask and I'll write the Caddyfile once I
|
|
> can see which server is actually running there.
|
|
|
|
---
|
|
|
|
## Updating later
|
|
|
|
Re-run the build + rsync steps, then `sudo systemctl restart festifun-api` (only needed if the
|
|
backend or data changed; a frontend-only change just needs the `dist` rsync).
|
|
|
|
## Notes / caveats
|
|
|
|
- **CORS** is irrelevant in this topology — everything is same-origin under `/festifun`. The
|
|
dev-only wide CORS in `config.py` stays as-is; it doesn't affect the hosted site.
|
|
- **Single project.** The app assumes one project at a time (SQLite at `data/project.db`).
|
|
- The backend binds `127.0.0.1` only; nginx is the sole public entry point.
|
|
|
|
---
|
|
|
|
## Live capture on the public box: leave it OFF
|
|
|
|
`FESTIVAL4D_CAPTURE` is **not** set in `deploy/festifun-api.service`, so `/capture` and
|
|
`/api/capture/*` are never mounted publicly — an open upload endpoint on a public host would let
|
|
anyone write files to the server. Capture is for your local/tailnet machine (see the README's
|
|
Capture section: `FESTIVAL4D_CAPTURE=1` + `tailscale serve`). Record locally, run the pipeline,
|
|
then deploy the resulting project.
|