festifun/backend/festival4d/cli.py
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

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())