"""Pose analysis pipeline — MediaPipe frame extraction with mock fallback.""" from __future__ import annotations from pathlib import Path from app.ai.pose_metrics import FrameLandmarks, build_movement_report, build_screening_report from app.mock_data import MOCK_MOVEMENT_REPORT, MOCK_REPORT MAX_FRAMES = 32 FRAME_STRIDE = 6 def analyze_task(task_type: str, video_path: Path | None = None) -> dict: if video_path and video_path.exists(): mediapipe_result = _analyze_video(video_path, task_type) if mediapipe_result: return mediapipe_result return _mock_result(task_type) def _mock_result(task_type: str) -> dict: if task_type == "movement_scoring": return { "engine": "mock", "confidence": 0.88, "report": {**MOCK_MOVEMENT_REPORT}, "modelVersion": "mock-movement-v1", } return { "engine": "mock", "confidence": 0.88, "report": {**MOCK_REPORT}, "modelVersion": "mock-screening-v1", } def _analyze_video(video_path: Path, task_type: str) -> dict | None: frames = _extract_pose_frames(video_path) if not frames: return None try: if task_type == "movement_scoring": report = build_movement_report(frames) else: report = build_screening_report(frames) except ValueError: return None confidences = [] if task_type != "movement_scoring": for metric in report.get("metrics", []): if "confidence" in metric: confidences.append(metric["confidence"]) return { "engine": "mediapipe", "confidence": round(mean(confidences) if confidences else 0.9, 2), "report": report, "modelVersion": "mediapipe-pose-v2", "frameCount": len(frames), "sourceFile": video_path.name, } def _extract_pose_frames(video_path: Path) -> list[FrameLandmarks]: try: import cv2 import mediapipe as mp except ImportError: return [] capture = cv2.VideoCapture(str(video_path)) if not capture.isOpened(): return [] pose = mp.solutions.pose.Pose( static_image_mode=False, model_complexity=1, min_detection_confidence=0.5, min_tracking_confidence=0.5, ) frames: list[FrameLandmarks] = [] index = 0 try: while capture.isOpened() and len(frames) < MAX_FRAMES: ok, image = capture.read() if not ok: break if index % FRAME_STRIDE != 0: index += 1 continue index += 1 rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) result = pose.process(rgb) if not result.pose_landmarks: continue frame: FrameLandmarks = {} for idx, landmark in enumerate(result.pose_landmarks.landmark): frame[idx] = { "x": landmark.x, "y": landmark.y, "visibility": landmark.visibility, } frames.append(frame) finally: pose.close() capture.release() return frames def mean(values: list[float]) -> float: return sum(values) / len(values) if values else 0.0