"""Track solving (spec M20, lane H) — STUB with the frozen contract in the docstring. foundation3 ships the stub; lane H fills :func:`run_solve` (and adds ``backend/tests/test_tracker_solve.py``). This is pure orchestration over the FROZEN geometry primitives — it must NEVER inline pose math (read ``resolve.py`` first; it is the exact per-timestep pattern), and it writes through the ``db`` track helpers only. ================================================================================ FROZEN CONTRACTS (plan/30-phase6.md — copy, do not reinterpret) ================================================================================ Input — ``config.DETECTIONS_JSON`` (contract #3, written by ``tracker_detect.run_detect``):: {"videos": {"": [{"t_video_s", "marker_key", "cx", "cy", "area", "conf"}]}, "generated_by": "festival4d.tracker_detect"} ``cx``/``cy`` are normalized 0..1; multiply by the video's width/height for pixel coords. Output — the ``tracks`` / ``track_points`` tables (contract #2), via ``db`` helpers:: tracks(id, marker_key, label, color, created_at) track_points(id, track_id, t_global_s, x, y, z, quality, views) # ordered by t_global_s # coordinates in Three.js scene space; views = cameras used (1 ⇒ ground-plane fallback) Algorithm (spec M20 — the parts that bite): 1. Load detections; group by ``marker_key``. 2. **Time alignment is the whole game** (pitfall #1): a detection is found at ``t_video``; association happens at ``t_global``. Convert with the FROZEN ``config.t_global_from_video(t_video, offset_ms, drift_ppm)`` ONLY — never re-derive it. 3. Per time bucket (**≈0.25 s** of ``t_global``), collect same-``marker_key`` detections across cameras at matching ``t_global``: - **2+ views → triangulate** through ``geometry`` primitives exactly as ``resolve.py`` does (``geometry.ray_from_pixel`` per view + ``geometry.triangulate_rays``, or a multi-ray least squares built on them). Record ``views`` and a ``quality`` derived from the residual/gap. - **1 view → ray ∩ ground plane** (contract #5): the synthetic ground is ``y = 0``; real projects estimate the 5th-percentile-y horizontal plane of the point cloud and record it in the result dict. Such points get ``views = 1`` and ``quality ≤ 0.5``. 4. Smooth each track (moving average or Catmull-Rom — your call; **document which**). 5. Solid color for hue markers, palette color for code markers. 6. Write via ``db.add_track`` + ``db.set_track_points``. **Re-solve replaces that marker's track** (idempotent — never stack duplicates on repeated runs). 7. Degradation: no detections → zero tracks + a note; never crash. ``POST /api/tracks/solve`` and ``python -m festival4d track`` both run detect→solve end-to-end once this and ``tracker_detect`` are filled. """ from __future__ import annotations import logging log = logging.getLogger("festival4d.tracker_solve") # Poses are keyed every ~0.5 s; bucket association at 0.25 s (spec M20). Kept here so lane H # and any test reference one number. BUCKET_S: float = 0.25 GROUND_PLANE_Y: float = 0.0 # synthetic fixture ground (contract #5, scene space) SINGLE_VIEW_MAX_QUALITY: float = 0.5 # ground-plane fallback quality cap (contract #5) def run_solve() -> dict: """Solve detections into 3D friend tracks (spec M20, see module docstring). Lane H implements the full detect-output → ``tracks``/``track_points`` pipeline described above. Returns a summary dict (tracks written, per-track point counts + coverage, the ground-plane used, and any degradation note). Idempotent: re-running replaces each marker's track rather than stacking duplicates. """ raise NotImplementedError( "track solving not implemented yet (lane H / M20) — fill tracker_solve.run_solve" )