Commit Graph

32 Commits

Author SHA1 Message Date
type-two
ddb8dea6d8 M12: cinematic export — camPath ride -> festival4d-cut.webm
frontend/src/exportVideo.js fills the foundation2 stub: the ⏺ button
records the scene3d canvas captureStream(30) muxed with the live WebAudio
mix via the transport.captureAudioStream() hook (spatial included, silent
tap when no track — degrade, don't block), MediaRecorder vp9/opus webm
(vp8/webm fallbacks), spanning first -> last camPath keyframe. Playback is
driven through the existing #btn-path flow at forced rate 1 (file duration
must equal the path span), stops at the last keyframe (or early on a
second click / anything that halts the ride), downloads
festival4d-cut.webm, then restores ALL prior transport/UI state (playhead,
rate, play state, path state, followed camera) and releases the audio tap
+ canvas tracks. Button disabled under 2 keyframes by chaining main.js's
camPath.onChange (frozen files untouched).

Requires live-browser verification (focused tab — captureStream delivers
no frames hidden): see plan/status/lane-E.md. Frontend build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:53:34 +10:00
type-two
52e891204d M11: auto-director — top-N events -> beat-snapped camPath (backend + UI)
generate_path() fills the foundation2 stub with the deterministic v1 rules:
top-N events by confidence (ties -> earlier, then shot chronologically);
per event, the registered camera whose pose track is nearest the event
time; keyframes at t_event - lead_s (clamped >= 0) and t_event + duration;
nearest-beat snapping when beats.json exists (missing/corrupt file -> no
snapping); FOV = 2*atan(h/(2*fy)) like scene3d.snapTo. Every pose goes
through the frozen geometry.colmap_to_threejs and the camPath quaternion
is emitted in Three.js [x,y,z,w] order (pitfall #1 — locked by test).
Degradation: no events / no registered poses -> valid empty camPath with a
note; the response always round-trips through camPath.fromJSON.

frontend/src/director.js wires the pre-wired hidden 🎬 button: POST
/api/director, camPath.fromJSON(response), auto-play via the existing
#btn-path flow; empty path or fetch failure surfaces on the button and
recovers — never a crash.

Acceptance (test_director.py, 14 tests): structure-locked against the
frozen camPath shape (exact key sets, unit quats, monotone times);
top-3 grid == [1,4,8,11,14] incl. shared-keyframe dedupe; conversion
matches colmap_to_threejs + mat_to_quat exactly; snapping puts every key
on the beat grid; unregistered poses excluded; API route + CLI dispatch
real output. Suite 148 passed; frontend build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:51:52 +10:00
type-two
0f8685b49e M10: audio features — beats & onsets from the reference ingest WAV
run_features() fills the foundation2 stub: librosa beat/tempo tracking +
onset-strength detection at a 10 ms hop (librosa's default 32 ms grid at
16 kHz quantizes past the +/-50 ms acceptance bound), tempo from the median
inter-beat interval, times mapped to t_global via the frozen timebase
helpers. Degradation per house style: missing videos/WAV -> summary with
note, nothing written; silent/short/beatless audio -> valid-empty
beats.json, never a crash.

Acceptance (test_audio_features.py, 7 tests): synthetic 120 BPM grid
recovered — tempo 120.0 (+/-2 required), worst beat 30 ms off grid
(+/-50 required); the three ground-truth bangs appear as strong onsets;
silence/short/missing all degrade cleanly; CLI dispatch end-to-end.

CR-4: minimal stub-era assertion updates in test_phase5_api.py (CLI stub
loop shrinks to capsule) per the approved CR-1 precedent — see
plan/CHANGE_REQUESTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:48:31 +10:00
type-two
547450f5ca Merge foundation2 (phase 5a): phase-5 contracts frozen, lanes E/F/G may start
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:31:20 +10:00
type-two
51650c728e foundation2 (phase 5a): contracts, stubs, hooks & UI wiring for lanes E/F/G
- DB: paths table (contract #3) + helpers; patch_anchor (label/color)
- API: GET /api/beats, POST /api/director (valid-empty degradation),
  /api/paths CRUD (validated POST -> 422), PATCH /api/anchors/{id};
  manifest gains capsule:false (test_api.py key-set lock updated consciously)
- CLI: features | direct | capsule subcommands -> lane stubs (exit-2 degradation)
- Backend stubs with frozen contracts in docstrings: audio_features, director, capsule
- Frontend: six lane stub modules imported from main.js; all phase-5 buttons
  pre-wired hidden in index.html; write-ui/body.capsule read-only mode;
  __F4D_API_BASE__ runtime override in state.js
- Hooks (contract #6): transport.captureAudioStream/releaseAudioStream,
  scene3d.focusOn, scene3d.setHelpersVisible (+registerHelper; camPath gizmos)
- Suite 121 -> 127 passed; npm build clean; live-browser verified (status file)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:31:08 +10:00
m3ultra
545f7a347d Plan: Phase 5 spec + directives round 4 (M10-M17, lanes E/F/G)
plan/20-phase5.md is the phase-5 canonical spec: M10 beats/onsets,
M11 auto-director, M12 cinematic export (lane E); M13 anchor manager +
friend tags, M14 photo mode, M15 moment FX (lane F); M16 server-side
camera paths, M17 static memory-capsule export (lane G). foundation2
freezes the new contracts (beats.json, camPath JSON, paths table,
PATCH anchors, __F4D_API_BASE__ / manifest.capsule, transport/scene3d
hooks) and pre-wires all UI so parallel lanes never touch shared files.

DIRECTIVES Round 4 records the fresh-eyes upgrades (FFT caching,
WebAudio master clock, spatial audio; suite floor now 121), ratifies
the additive contract amendments, and sets run order:
foundation2 -> E, G, F (own worktrees, mandatory) -> integration2
(real-footage field test, v0.2.0 proposal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:08:44 +10:00
m3ultra
db42c42932 Chore: lock python-multipart (declared but never locked), launch configs
uv.lock drifted from pyproject when live capture added python-multipart;
uv run resynced it. launch.json gains festifun-web-dist (uv + http.server
over frontend/dist) for sandboxed sessions where node/npm can't start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:02:48 +10:00
m3ultra
c24af8029e Audio is the master clock + spatial audio free-roam (fresh-eyes opts 1 & feature 1)
Master clock: the selected soundtrack (new GET /api/audio/{id} —
listening-quality stereo AAC, lazily ffmpeg-extracted and cached in
data/work/audio_hq/) plays through WebAudio, and t_global derives from
AudioContext.currentTime. Every <video> is muted picture-only, so hard
seeks and playbackRate corrections are inaudible (the unmuted video
used to warble on every nudge). Graceful fallback to the old
performance.now() clock + <video> audio when a track can't load, with
an in-place upgrade if the decode lands mid-play. Drift is compensated
in the track's playbackRate; _resync() now recomputes from the live
clock since the soundtrack keeps playing while rAF is throttled.

Spatial audio (🎧 3D audio, shown when poses exist): every enabled
camera's track plays through an HRTF PannerNode at its live
reconstructed pose, gains normalized 1/sqrt(N); the listener follows
the viewer camera each frame (free roam, follow-cam, and M9 paths).
Tracks that finish decoding mid-play join the mix in place; toggling,
seeking, rate and enable changes rebuild the graph at the current
t_global with no timeline jump.

The window.__f4d debug hook is now also available in production builds
via localStorage f4dDebug=1 (headless panes throttle rAF; the pump is
the only way to drive the loop there).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:02:48 +10:00
m3ultra
95b71788a3 Sync solver: cache per-signal FFTs across the pairwise GCC-PHAT stage
pairwise_offsets now computes each signal's rFFT once at a shared padded
length (2*max_len) and reuses it for every pair: O(K) forward FFTs +
cheap per-pair spectrum products instead of O(K^2) full FFTs. Each
pair's lag search stays capped at +/-(la+lb)/2 — the same window the
per-pair transform used — and gcc_phat() itself is unchanged for
callers (drift windows, tests). Measured 1.8x on 6 cams x 10 min;
the advantage grows with camera count.

New test locks the cached path against direct gcc_phat, including
unequal-length signals where the padded sizes genuinely differ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:02:27 +10:00
m3ultra
36bacf67f0 Live capture: USB webcams, laptop cams, and phones -> data/raw
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>
2026-07-17 15:43:51 +10:00
m3ultra
f12b6e0847 Splat upgrade (ideas.md #1) + MODELBEAST crossover
- GET /api/splat serves data/work/splat.ply; manifest reports has_splat
- scene3d: lazy-load @mkkellogg/gaussian-splats-3d DropInViewer when a splat
  exists (rigs/anchors/paths overlay as before); point-cloud fallback intact
- scripts/splat_via_modelbeast.sh: train the splat on the MODELBEAST fleet
  (colmap_poses -> brush_train via the mb queue) and drop it in place
- docs/modelbeast-crossover.md: full crossover map (fleet compute, volumetric
  capture w/ CorridorKey + audio sync, per-moment splats)
Verified in-browser: 32MB room splat rendered via /api/splat with overlays live.
2026-07-16 22:32:01 +10:00
m3ultra
931cf81a6c Deploy-ready: path-prefix hosting (digalot.fyi/festifun) + deploy artifacts
Frontend: API_BASE env-driven (VITE_API_BASE, default localhost:8000 so dev is
unchanged); vite.config.js base from FESTIVAL4D_BASE. Prod build
'FESTIVAL4D_BASE=/festifun/ VITE_API_BASE=/festifun' serves same-origin under
the prefix. Validated end-to-end via a local proxy mirroring the nginx config:
app boots, media Range 206, 3D + overlays + timeline render under /festifun.

deploy/: nginx location blocks (prefix strip + Range), systemd unit
(uvicorn 127.0.0.1:8000, no keys on the public box), DEPLOY.md. Labels baked
into the shipped DB locally so detect can't spend credits from an anonymous
public endpoint. Backend suite 103 passed; default build unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:20:36 +10:00
m3ultra
7c13f519d2 Phase 4 polish: anchor supersede + delete, docs/ideas, field-test scaffolding
Fixes the sole Round-3 wart: annotating one object from two views now yields
ONE anchor (the single-view fallback is superseded in place by the
triangulation, same id) instead of a sibling pair. Adds DELETE /api/anchors/{id}
(unlinks annotations, 404 on unknown) + a deletable anchor list in the
correction panel. db gains get/update/delete_anchor + get_annotations(event_id).
CR-3 filed (additive). Suite 101 -> 103; frontend build clean; delete UI
verified live (state + DOM + 3D scene stay consistent).

Also: docs/ideas.md (future extensions), plan/ISSUES.md (seeded with the
gemini-2.5-flash retirement, FIXED), plan/status/phase4.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 12:15:35 +10:00
m3ultra
8dca2f5154 Coordinator hotfix: Gemini model env-overridable, default gemini-3.1-flash-lite
Google retired gemini-2.5-flash for new API keys (404). Verified
gemini-3.1-flash-lite live: inline video + structured output, 3/3 synthetic
candidates classified. FESTIVAL4D_GEMINI_MODEL overrides. Round 3 addendum
documents the local .env (gitignored) with Gemini + OpenRouter creds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:18:43 +10:00
m3ultra
b83e577870 DIRECTIVES round 3: v0.1.0 accepted; phase 4 = anchor polish + real-footage field test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:06:01 +10:00
m3ultra
d313c8dce8 DIRECTIVES round 2: integration complete, v0.1.0 shipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:00:47 +10:00
m3ultra
e5d6b2c47f Integration (M8 + M9): annotation→3D, camera paths, README, v0.1.0
M8: POST /api/annotations resolves a bbox to a 3D point (ray → two-view
triangulation / nearest-point / centroid-depth) via lane B's geometry
primitives; correction panel + bbox-drawing UI; resolved anchors render live
in overlays and the 3D scene. CR-2 (additive): annotations.event_id +
PATCH /api/events/{id} + db.update_event.

M9: keyframed god's-eye camera paths (Catmull-Rom position + slerp
orientation, export/import), gizmos, path-mode camera hooks.

Verified: 101 backend tests pass (two-view corner recovery err 0.00000);
frontend build clean; live in-browser M8 triangulation (gap 0.21) + M9 path
(camera on keyframe at t) + M4 sync ~16ms. Cleanup: worktrees removed, debris
stash dropped, pristine demo DB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 09:59:51 +10:00
m3ultra
58c01559c1 DIRECTIVES round 1: all lanes merged & verified (96 tests), integration begins
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 09:04:29 +10:00
m3ultra
9d17b474ea Merge branch 'lane/c-viewer'
# Conflicts:
#	plan/status/lane-C.md
2026-07-16 09:02:13 +10:00
m3ultra
2cad2f7b02 Merge branch 'lane/b-recon' 2026-07-16 09:01:59 +10:00
m3ultra
e28dc78e06 Merge branch 'lane/a-media' 2026-07-16 09:01:59 +10:00
m3ultra
ec22cbdf1f Lane C (M4+M5+M6 + timeline): synchronized viewer frontend
Full frontend built against the synthetic API (no dependence on lanes A/B/D):

M4 — synchronized playback: master clock from performance.now() (never a <video>);
per-video correction via requestVideoFrameCallback (exact mediaTime) with a
currentTime+half-frame fallback — hard-seek >150ms / nudge playbackRate +/-5% 20-150ms /
lock <20ms; out-of-range videos pause+dim; single audio source; dev sync-error overlay.
Timebase mirrors config.py (lib/timebase.js). Verified: seek is exact to 0ms; continuous
inter-video desync 13ms mean / 46ms max.

M5 — 3D viewer: PLY point cloud, per-video camera paths + current-pose frusta, OrbitControls,
snap-to-camera (intrinsics->PerspectiveCamera fov) + free roam. All COLMAP->Three.js via the
frozen lib/pose.js; pose interpolation in lib/poseTrack.js (slerp+lerp of contract inputs).

M6 — anchor overlays: letterbox-correct per-video canvas; anchors projected via pose.js;
behind-camera cull. Projection agrees with an independent pinhole to 1.45e-13 px.

Timeline — colored event markers + legend, hover tooltip, click-to-jump; draggable scrubber.

Phase-3 seams (annotate.js M8, camPath.js M9) left as stubs. No frozen files edited; no new
deps; no change requests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:45:51 +10:00
m3ultra
bb542960c2 Lane B (M2 + M8 geometry): COLMAP reconstruction, geometry math, graceful degradation
Implements spec M2 (frame sampling, COLMAP orchestration, TXT-model parsing, scene
normalization, pose interpolation, PLY + DB export) and the M8 geometry functions.

geometry.py (M8): slerp_pose (shortest-arc, double-cover), ray_from_pixel (COLMAP
+y-down back-projection), triangulate_rays (closest-point + parallel guard),
nearest_point_on_ray (in-front radius cylinder). Frozen colmap_to_threejs untouched.

frames.py (M2): variance-of-Laplacian sharpness + windowed sharpest-frame sampling.

sfm.py (M2): hand-written COLMAP images/cameras/points3D parsers; normalize_scene
(centroid->0, camera sphere r->10, up->+Y) as one similarity transform over points+poses;
interpolate_poses (slerp+lerp, no extrapolation); build-aware COLMAP CLI orchestration
(3.x/4.x option detection, CPU SIFT, single-camera-per-folder, undistort->TXT, largest
component by images.bin header count); run_reconstruct with full graceful degradation
(COLMAP absent / <60% / <2 videos / no frames / missing files -> poses left untouched).

Tests: 61 pass (24 foundation + 37 lane-B) with independent oracles (scipy Slerp,
projection-inverse, closed-form geometry) and every degradation/DB-safety branch covered.
Validated end-to-end against real COLMAP 4.1.0 (6 components, largest selected, weak ->
graceful degradation, exit 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:40:46 +10:00
m3ultra
6ec55c70e8 lane D: status -> merged (M7 done, 5c2d7c6; review findings fixed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:38:47 +10:00
m3ultra
5c2d7c6ea3 lane D (M7): audio moment detection + pluggable AI classification
Fills events_ai.py against the frozen M7 contract:
- detect_candidates: RMS x spectral-flux, local-max over a 10s neighborhood,
  >=P90, with an AC-RMS (std) silence/DC floor so flat audio yields no events.
  Matches the fixture ground-truth pulses [3,10,17] within ~0.02s (<0.5s).
- GeminiClassifier (native video), ClaudeClassifier (claude-opus-4-8,
  messages.parse, adaptive thinking, no temperature), LocalClassifier
  (OpenAI-compatible, 6 frames, 1 retry). SDKs imported lazily.
- get_classifier: FESTIVAL4D_CLASSIFIER selection; unconfigured -> None
  (candidates-only, logged).
- prepare_inputs: ffmpeg 3s clip (<=720p, audio kept) + 6 JPEGs (<=768px).
- run_events: reference-audio detect -> classify (insert-once in final state,
  since db.py has no update-event helper); per-candidate exception isolation;
  whole-track replaces machine events (keeps user edits), windowed is additive.

test_events_ai.py: candidate-vs-ground-truth (no ffmpeg/API), stub-classifier
orchestration, exception isolation, degradation, provider selection, silence.
Also updates the now-obsolete api stub test (test_detect_events_endpoint) to the
real M3 {result, events} shape; frozen api.py unchanged (see CR-1). pytest: 90 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:34:56 +10:00
m3ultra
88caecb660 lane A status: M1 complete, ready_to_merge (isolated worktree)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:15:23 +10:00
m3ultra
157c783e25 Lane A (M1): ingest + GCC-PHAT audio sync
Fills the foundation stubs in ingest.py and audio_sync.py.

- ingest: ffprobe probe + mono 16 kHz WAV extraction per video (ffmpeg);
  idempotent run_ingest reuses existing rows so synthetic -> ingest -> sync
  works without unique-constraint collisions.
- sync: PHAT-whitened GCC-PHAT pairwise offsets with sub-sample parabolic
  peak refinement, scored by peak-to-second-peak ratio; confidence-weighted
  global least-squares solve with cycle-consistency rejection (>50 ms);
  windowed drift estimation (ppm) with a 5 ppm deadband; disconnected
  sync-graph components -> offset_ms=None (spec pitfall #5).
- Persists offset_ms/drift_ppm/sync_confidence via db.update_video_sync and
  exports data/work/sync.json.

Verified on the synthetic fixture: synthetic -> ingest -> sync recovers the
ground-truth offsets (0 / +1370 / -842 ms) to <0.001 ms and drift 0 ppm,
well inside the spec M1 tolerances (+/-10 ms, +/-3 ppm). pytest: 43 passed
(24 foundation + 19 new in test_ingest.py and test_audio_sync.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:13:56 +10:00
m3ultra
4399bc90e2 lane C: session start — verified frozen contracts against live synthetic API
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:55:42 +10:00
m3ultra
5fa7301148 status: foundation merged to main (M0 + M3 + frozen contracts)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:46:41 +10:00
m3ultra
cf6a6fb2c8 Foundation (M0 + M3 + frozen contracts): scaffold, synthetic fixtures, full API
What now works:
- M0 scaffold: pyproject (all spec deps), uv/py3.12 env, `python -m festival4d`
  CLI registering synthetic|ingest|sync|reconstruct|events|serve. Vite hello page.
- Synthetic fixture (synthetic.py): 3 shifted-audio videos (offsets 0/+1370/-842 ms),
  camera-arc poses, stage point cloud -> points.ply, seeded events + anchors,
  ground_truth.json. `python -m festival4d synthetic` populates data/ + DB.
- DB schema exactly per spec §2 (db.py) + CRUD helpers all lanes use.
- M3 API (api.py) full against synthetic data: manifest/poses/pointcloud/anchors/
  events/detect/annotations; Range-capable video serving (206 verified); CORS for any
  localhost origin.
- Frozen geometry contract: geometry.colmap_to_threejs (M5 math) + unit test (3 known
  vectors, random round-trip, scipy oracle); mirrored frontend/src/lib/pose.js with
  identical POSE_TEST_VECTORS. Lane-B stubs: slerp_pose, ray_from_pixel, triangulate_rays,
  nearest_point_on_ray.
- Classifier contract (events_ai.py): MomentClassification model + MomentClassifier
  protocol + Gemini/Claude/Local provider stubs.
- Lane-owned modules stubbed with final signatures (ingest, audio_sync, frames, sfm,
  events_ai); cli/api catch NotImplementedError and degrade gracefully.
- plan/CHANGE_REQUESTS.md created; plan/status/foundation.md updated.

Acceptance: pytest 24 passed; serve endpoints verified via curl + browser (video seek,
manifest fetch cross-origin, pose.js self-test, 0 console errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:45:55 +10:00
m3ultra
47bec93bc5 Add coordination loop: run order (DIRECTIVES.md), per-agent status protocol
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 00:08:54 +10:00
m3ultra
e31e8ac559 Festival 4D build spec + parallel lane plan for Opus agents
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 00:06:54 +10:00