""" Director — deterministic state machine orchestrating the FormScout pipeline. NOT an LLM. Runs each agent in sequence, applies quality gates, and assembles the final PipelineState. Exposes run(video_path, config) -> PipelineState. """ from __future__ import annotations from pathlib import Path from formscout import config from formscout.types import ( PipelineState, Body3DResult, MovementResult, ) from formscout.agents.ingest import IngestAgent from formscout.agents.pose2d import Pose2DAgent from formscout.agents.body3d import Body3DAgent from formscout.agents.biomechanics import BiomechanicsAgent from formscout.agents.classifier import MovementClassifierAgent from formscout.agents.judge import JudgeAgent from formscout.agents.report import ReportAgent from formscout.rubric import score_test class Director: """ Orchestrates the FormScout agent pipeline as a deterministic state machine. Quality gates are applied after each agent — never silently passes bad data. """ def __init__(self): self._ingest = IngestAgent() self._pose2d = Pose2DAgent() self._body3d = Body3DAgent() self._biomechanics = BiomechanicsAgent() self._classifier = MovementClassifierAgent() self._judge = JudgeAgent() self._report = ReportAgent() def run(self, video_path: str, test_name: str = "deep_squat", side: str = "na", model_key: str | None = None) -> PipelineState: """ Run the full pipeline on a single video. test_name/side serve as manual override when provided (skips classifier). model_key selects the pose backend (see config.POSE_MODELS). """ state = PipelineState(video_path=video_path) # ─── Ingest ─── state.ingest = self._ingest.run(video_path) if state.ingest.confidence < config.MIN_CONFIDENCE: state.errors.append("ingest: low confidence — video may be corrupt") return state # ─── Pose 2D ─── state.pose2d = self._pose2d.run(state.ingest, model_key=model_key) if state.pose2d.confidence < config.MIN_CONFIDENCE: state.warnings.append("pose2d: low confidence — no clear person detected") # ─── Body 3D (optional) ─── masks = state.segment.masks if state.segment else [] frames = state.ingest.frames if state.ingest else [] state.body3d = self._body3d.run(state.pose2d, masks, frames=frames) # ─── Movement classification ─── if test_name and test_name != "unknown": # Manual override state.movement = MovementResult( test_name=test_name, side=side, confidence=1.0, notes="manually specified", ) else: state.movement = self._classifier.run(state.ingest, state.pose2d) # Gate: unknown test → stop if state.movement.test_name == "unknown": state.errors.append("movement classifier returned 'unknown' — manual override required") return state # ─── Biomechanics ─── state.features = self._biomechanics.run( state.pose2d, state.body3d or Body3DResult(used=False, joints_3d=[]), state.movement, ) if state.features.confidence < config.MIN_CONFIDENCE: state.warnings.append( f"biomechanics: low confidence ({state.features.confidence:.2f}) — physio review recommended" ) # ─── Rubric Score ─── rubric_result = score_test(state.features) state.stgcn_score = rubric_result # Reusing field for rubric until ST-GCN is built # ─── Judge ─── state.judge = self._judge.run( state.features, rubric_result, state.movement, state.ingest, ) # ─── Quality gates ─── # Gate: score disagreement if (state.judge.score is not None and rubric_result.score is not None and abs(state.judge.score - rubric_result.score) >= config.SCORE_DISAGREE_THRESH): state.warnings.append( f"score disagreement: rubric={rubric_result.score} vs judge={state.judge.score} — review recommended" ) # Gate: needs_human if state.judge.needs_human: state.warnings.append("judge flagged needs_human — no auto-score emitted") return state