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>
131 lines
4.0 KiB
Python
131 lines
4.0 KiB
Python
"""Command-line entrypoint: ``python -m festival4d <cmd>``.
|
|
|
|
FROZEN after foundation. Lanes fill in the bodies of the functions this dispatches
|
|
to (in ``ingest.py``, ``audio_sync.py``, ``sfm.py``, ``events_ai.py``); they never
|
|
edit this file or ``api.py``.
|
|
|
|
Subcommands: ``synthetic | ingest | sync | reconstruct | events | serve``.
|
|
|
|
Every subcommand dispatches to a single lane entrypoint. While a lane is still a stub,
|
|
its entrypoint raises :class:`NotImplementedError`; we catch that here and print a clear
|
|
"not implemented yet" message with a non-zero exit code, rather than a traceback. Once the
|
|
lane fills the body, the same dispatch runs it for real — no change to this file needed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
|
|
log = logging.getLogger("festival4d.cli")
|
|
|
|
|
|
def _cmd_synthetic(args: argparse.Namespace) -> int:
|
|
from festival4d import synthetic
|
|
|
|
synthetic.build()
|
|
return 0
|
|
|
|
|
|
def _cmd_ingest(args: argparse.Namespace) -> int:
|
|
from festival4d import ingest
|
|
|
|
ingest.run_ingest()
|
|
return 0
|
|
|
|
|
|
def _cmd_sync(args: argparse.Namespace) -> int:
|
|
from festival4d import audio_sync
|
|
|
|
audio_sync.run_sync()
|
|
return 0
|
|
|
|
|
|
def _cmd_reconstruct(args: argparse.Namespace) -> int:
|
|
from festival4d import sfm
|
|
|
|
sfm.run_reconstruct()
|
|
return 0
|
|
|
|
|
|
def _cmd_events(args: argparse.Namespace) -> int:
|
|
from festival4d import events_ai
|
|
|
|
result = events_ai.run_events(t_global_s=args.t_global_s, window_s=args.window_s)
|
|
log.info("events: %s", result)
|
|
return 0
|
|
|
|
|
|
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
import uvicorn
|
|
|
|
uvicorn.run(
|
|
"festival4d.api:app",
|
|
host=args.host,
|
|
port=args.port,
|
|
reload=args.reload,
|
|
)
|
|
return 0
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
from festival4d import config
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog="festival4d",
|
|
description="Festival 4D — synchronized, explorable 4D concert replay.",
|
|
)
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
p_syn = sub.add_parser("synthetic", help="generate the synthetic fixture project (M0)")
|
|
p_syn.set_defaults(func=_cmd_synthetic)
|
|
|
|
p_ing = sub.add_parser("ingest", help="probe videos + extract audio (lane A / M1)")
|
|
p_ing.set_defaults(func=_cmd_ingest)
|
|
|
|
p_sync = sub.add_parser("sync", help="GCC-PHAT audio alignment (lane A / M1)")
|
|
p_sync.set_defaults(func=_cmd_sync)
|
|
|
|
p_rec = sub.add_parser("reconstruct", help="COLMAP SfM + pose export (lane B / M2)")
|
|
p_rec.set_defaults(func=_cmd_reconstruct)
|
|
|
|
p_evt = sub.add_parser("events", help="audio candidates + AI classification (lane D / M7)")
|
|
p_evt.add_argument("--t-global-s", dest="t_global_s", type=float, default=None,
|
|
help="detect within a single window centered here (default: whole track)")
|
|
p_evt.add_argument("--window-s", dest="window_s", type=float, default=None,
|
|
help="window width in seconds when --t-global-s is given")
|
|
p_evt.set_defaults(func=_cmd_events)
|
|
|
|
p_srv = sub.add_parser("serve", help="run the FastAPI app (M3)")
|
|
p_srv.add_argument("--host", default=config.API_HOST)
|
|
p_srv.add_argument("--port", type=int, default=config.API_PORT)
|
|
p_srv.add_argument("--reload", action="store_true", help="uvicorn autoreload (dev)")
|
|
p_srv.set_defaults(func=_cmd_serve)
|
|
|
|
return parser
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
|
)
|
|
parser = build_parser()
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
return args.func(args)
|
|
except NotImplementedError as exc:
|
|
detail = f" ({exc})" if str(exc) else ""
|
|
print(
|
|
f"festival4d {args.command}: not implemented yet{detail}.\n"
|
|
f"This subcommand is owned by a feature lane that hasn't landed. "
|
|
f"Run `python -m festival4d synthetic` for the working demo path.",
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|