Upload, primary/failover backends, polling, caching, and where the AI stops
SurfVision Pipeline
Analysis runs on SurfVision, an external computer-vision service (its models and server are not in this repo). The app talks to it over HTTP and runs dual-backend: a self-hosted GPU primary with automatic Modal cloud failover.
Backend resolution
src/lib/modal.js (server-only) resolves the backend at request time: if SURFVISION_PRIMARY_URL is set and its GET /health answers as SurfVision within ~3 seconds (redirects are never followed; a backend without /health yet is asked GET /), the primary is used; otherwise SURFVISION_MODAL_URL. /api/clips/analyze persists the resolved backend on the clip row (session_clips.api_url), so polling and storing always talk to the backend the job actually ran on.
SSRF guard: both backend URLs are server-only, and a recorded
api_urlis honored only when its origin is exactly one of them (never a host suffix); the clip's pipeline columns are written by the server only (a database trigger refuses them from a person's login). A client-supplied URL is honored only outside production (local-GPU development). See ADR 0069.
Endpoints called
| Endpoint | Purpose |
|---|---|
POST {base}/analyze | Submit a clip (cloud: a signed video_url; local: a multipart file). Returns { job_id } |
GET {base}/status/{job_id} | Poll status and progress |
GET {base}/result/{job_id}/video | Download the processed video (server-side, then cached) |
GET {base}/result/{job_id}/stats | Download the stats JSON |
GET {base}/ | Health probe |
End-to-end flow
- Upload. The browser (or the iPhone via
/api/clips/native-ingest) puts each raw clip inraw-uploads(resumable TUS, two at a time on the web). - Analyze.
POST /api/clips/analyzemints a one-hour signed URL for the clip and forwards it to{base}/analyze, then persistsjob_idand statusprocessing. - Poll. The client polls
{base}/status/{job_id}every 3 seconds across clips. A server-side safety net (reconcile-clips, every two minutes) finalizes jobs whose tab closed. - Store. On completion,
POST /api/clips/storedownloads the processed video (retry with backoff) and the overlay sidecars, uploads them tosession-videos/session-data, savesstats_json, and restores the iPhone's display rotation on the processed MP4 and sidecars. - Stream. Playback goes through the HMAC-gated
/api/video/streamproxy; sidecars through/api/clips/sidecar.
Where the AI stops
SurfVision measures; it does not coach. The stats and sidecars feed the deterministic Session Insights in src/lib/sessionInsights.js — no model call, every number reproducible from the inputs — and the overlay layers the player draws. Automatic action tags on the timeline are labelled automatic, unscored. Per-clip maneuver labels (/api/clips/[clipId]/maneuvers) exist as a coach-corrected training set behind an accuracy gate; maneuver detection is not a shipped claim.
The only language-model call in the product is guided-review voice-note transcription (POST /api/reviews/[id]/build, Gemini via GOOGLE_AI_API_KEY), which is best-effort: if it fails the review still publishes with the coach's typed captions. No video is ever sent to a language model, and nothing generates coaching text for an athlete.