Incomplete: step 1 (fixture markers + track_truth.json), step 6 (frontend wiring), step 8 (tests) not done; suite not yet run. Salvaged from interrupted prior session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
197 lines
6.8 KiB
Python
197 lines
6.8 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 | features | direct |
|
|
track | capsule | 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_features(args: argparse.Namespace) -> int:
|
|
from festival4d import audio_features
|
|
|
|
result = audio_features.run_features()
|
|
log.info("features: %s", result)
|
|
return 0
|
|
|
|
|
|
def _cmd_direct(args: argparse.Namespace) -> int:
|
|
import json
|
|
|
|
from festival4d import director
|
|
|
|
path = director.generate_path(top_n=args.top_n, lead_s=args.lead_s)
|
|
print(json.dumps(path, indent=2))
|
|
return 0
|
|
|
|
|
|
def _cmd_track(args: argparse.Namespace) -> int:
|
|
"""Detect wearable markers and solve them into 3D friend tracks (lane H / M19-M20).
|
|
|
|
Runs detect -> solve by default; ``--detect-only`` / ``--solve-only`` run one stage.
|
|
Both stages dispatch into lane-H stubs, so while unimplemented this prints the
|
|
"not implemented yet" message and exits 2 (same as every other stubbed subcommand)."""
|
|
from festival4d import tracker_detect, tracker_solve
|
|
|
|
if not args.solve_only:
|
|
result = tracker_detect.run_detect()
|
|
log.info("track detect: %s", result)
|
|
if not args.detect_only:
|
|
result = tracker_solve.run_solve()
|
|
log.info("track solve: %s", result)
|
|
return 0
|
|
|
|
|
|
def _cmd_capsule(args: argparse.Namespace) -> int:
|
|
from festival4d import capsule
|
|
|
|
result = capsule.build_capsule(out_dir=args.out)
|
|
log.info("capsule: %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_feat = sub.add_parser("features", help="beat/onset analysis of the reference audio (lane E / M10)")
|
|
p_feat.set_defaults(func=_cmd_features)
|
|
|
|
p_dir = sub.add_parser("direct", help="auto-director: events -> camPath JSON on stdout (lane E / M11)")
|
|
p_dir.add_argument("--top-n", dest="top_n", type=int, default=8,
|
|
help="number of top-confidence events to cover (default 8)")
|
|
p_dir.add_argument("--lead-s", dest="lead_s", type=float, default=2.0,
|
|
help="seconds of lead-in before each event (default 2.0)")
|
|
p_dir.set_defaults(func=_cmd_direct)
|
|
|
|
p_trk = sub.add_parser("track", help="detect markers + solve friend tracks (lane H / M19-M20)")
|
|
trk_mode = p_trk.add_mutually_exclusive_group()
|
|
trk_mode.add_argument("--detect-only", dest="detect_only", action="store_true",
|
|
help="run marker detection only (write detections.json)")
|
|
trk_mode.add_argument("--solve-only", dest="solve_only", action="store_true",
|
|
help="run track solving only (from an existing detections.json)")
|
|
p_trk.set_defaults(func=_cmd_track, detect_only=False, solve_only=False)
|
|
|
|
p_cap = sub.add_parser("capsule", help="bake a zero-backend shareable bundle (lane G / M17)")
|
|
p_cap.add_argument("--out", default=None, help="output dir (default data/capsule/)")
|
|
p_cap.set_defaults(func=_cmd_capsule)
|
|
|
|
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())
|