From feb5588a1b03b94a2f6df2cea8b24f4ae2234485 Mon Sep 17 00:00:00 2001 From: type-two Date: Fri, 17 Jul 2026 17:48:22 +1000 Subject: [PATCH] =?UTF-8?q?lane=20G=20M17:=20memory=20capsule=20=E2=80=94?= =?UTF-8?q?=20zero-backend=20shareable=20bundle=20+=20Range-capable=20serv?= =?UTF-8?q?e.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build_capsule copies frontend/dist (clear error if missing), copies referenced media, bakes every read-only API response extension-less at the live API's relative paths (manifest with capsule:true + has_capture:false, events, anchors, poses, beats, pointcloud, splat, audio via the same lazy extract_audio_hq cache, plus M16 saved paths — listing baked as api/paths/index.html so per-id files can share the prefix), injects window.__F4D_API_BASE__="" ahead of the app script, and ships a stdlib serve.py (206 Range slicing incl. suffix ranges + 416, JSON content types for the extension-less api/* files). Optional artifacts degrade by omission. test_capsule.py: bundle structure, baked-JSON deep-equal vs the live route functions, injection position, degrade/overwrite-guard/no-dist errors, CLI dispatch (CR-4), and serve.py exercised over real HTTP in a subprocess (200/206/416/404, content types, mp4+m4a magic bytes). Suite: 169 passed. Co-Authored-By: Claude Fable 5 --- backend/festival4d/capsule.py | 295 ++++++++++++++++++++++++++++- backend/tests/test_capsule.py | 337 ++++++++++++++++++++++++++++++++++ 2 files changed, 628 insertions(+), 4 deletions(-) create mode 100644 backend/tests/test_capsule.py diff --git a/backend/festival4d/capsule.py b/backend/festival4d/capsule.py index bc94fd1..d8e4f0e 100644 --- a/backend/festival4d/capsule.py +++ b/backend/festival4d/capsule.py @@ -1,4 +1,4 @@ -"""Memory capsule: zero-backend shareable export (spec M17, lane G). STUB — lane G fills. +"""Memory capsule: zero-backend shareable export (spec M17, lane G). Contract: :func:`build_capsule` writes a self-contained bundle to ``out_dir`` (default ``config.CAPSULE_DIR``): @@ -16,13 +16,300 @@ Contract: :func:`build_capsule` writes a self-contained bundle to ``out_dir`` (d Skip what doesn't exist (no splat -> no ``api/splat``) — degrade, don't block. Returns a summary dict (bundle path, file count, bytes) for the CLI log. + +Layout mirror (why paths look the way they do): the frontend resolves every URL as +``API_BASE + path`` with API_BASE == "" in a capsule, so requests hit the bundle root: +``fetch("/api/manifest")`` (no extension — bake the file extension-less; serve.py maps +extension-less ``api/*`` files to a JSON content type), ``video.src = "/media/"`` +(manifest ``url`` fields are ``/media/{filename}`` — media files are copied under +``media/``), PLY loader hits ``/api/pointcloud``, the WebAudio clock fetches +``/api/audio/{id}``. Saved camera paths (M16) are read-only in a capsule, so +``api/paths`` + ``api/paths/{id}`` are baked too — the load dropdown keeps working. """ from __future__ import annotations +import json +import logging +import shutil from pathlib import Path +from festival4d import config, db -def build_capsule(out_dir: str | Path | None = None) -> dict: - """Build the static shareable bundle (see module doc).""" - raise NotImplementedError("memory capsule (lane G / M17)") +log = logging.getLogger("festival4d.capsule") + +# Injected verbatim (phase-5 contract #5): a runtime override that beats the build-time +# VITE_API_BASE, turning the same dist bundle zero-backend. Must land in index.html before +# the app module reads it (modules are deferred, but we inject ahead of the first ' + + +# --------------------------------------------------------------------------- +# serve.py shipped in the bundle root (stdlib-only, Range-capable — pitfall #4) +# --------------------------------------------------------------------------- +SERVE_PY = r'''#!/usr/bin/env python3 +"""Serve this Festival 4D memory capsule locally. + +Plain `python -m http.server` cannot answer HTTP Range requests, and