Refactor into modular app structure

Split monolithic files into focused modules:
- app/core: settings, logging, lifecycle
- app/signaling: websocket server, ICE parser, message models
- app/webrtc: peer session, video receiver, frame source
- app/vision: pose landmarker wrapper, model config, pose types
- app/exercises/dead_bug: detector, metrics, rules, state machine, types
- app/rendering: skeleton renderer, status overlay, window display
- app/audio: rep announcer
- app/diagnostics: perf timer, crash handler
- configs: environment-based settings
- tests: unit tests for rules, state machine, ICE parser
- run.py: entry point
This commit is contained in:
2026-06-10 10:14:43 +08:00
parent 8b878cb9e5
commit 4485cbf702
44 changed files with 1230 additions and 648 deletions
View File
+23
View File
@@ -0,0 +1,23 @@
from __future__ import annotations
import cv2
import numpy as np
TARGET_WIDTH = 1280
TARGET_HEIGHT = 720
def resize_to_target(image: np.ndarray, width: int = TARGET_WIDTH, height: int = TARGET_HEIGHT) -> np.ndarray:
h, w = image.shape[:2]
if w == width and h == height:
return image
return cv2.resize(image, (width, height))
def bgr_to_rgba(bgr: np.ndarray) -> np.ndarray:
return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGBA)
def bgr_to_rgb(bgr: np.ndarray) -> np.ndarray:
return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
+57
View File
@@ -0,0 +1,57 @@
from __future__ import annotations
import threading
import time
from typing import Callable
import mediapipe as mp
from loguru import logger
from app.vision.pose_models import DEFAULT_MODEL_PATH
PoseLandmarker = mp.tasks.vision.PoseLandmarker
PoseLandmarkerOptions = mp.tasks.vision.PoseLandmarkerOptions
VisionRunningMode = mp.tasks.vision.RunningMode
BaseOptions = mp.tasks.BaseOptions
class PoseLandmarkerWrapper:
def __init__(
self,
*,
model_path: str | None = None,
prefer_gpu: bool = True,
result_callback: Callable | None = None,
) -> None:
self.model_path = model_path or DEFAULT_MODEL_PATH
if prefer_gpu:
try:
self.delegate = BaseOptions.Delegate.GPU
self._landmarker = self._create(PoseLandmarker.Delegate.GPU)
logger.info("MediaPipe PoseLandmarker initialized with GPU delegate")
return
except Exception as exc:
logger.warning("MediaPipe GPU delegate unavailable, falling back to CPU: {}", exc)
self.delegate = BaseOptions.Delegate.CPU
self._landmarker = self._create(PoseLandmarker.Delegate.CPU, result_callback)
logger.info("MediaPipe PoseLandmarker initialized with CPU delegate")
def _create(self, delegate, result_callback=None):
options = PoseLandmarkerOptions(
base_options=BaseOptions(model_asset_path=self.model_path, delegate=delegate),
running_mode=VisionRunningMode.LIVE_STREAM,
result_callback=result_callback,
num_poses=1,
min_pose_detection_confidence=0.5,
min_pose_presence_confidence=0.5,
min_tracking_confidence=0.5,
)
return PoseLandmarker.create_from_options(options)
def detect_async(self, mp_image, timestamp_ms: int) -> None:
return self._landmarker.detect_async(mp_image, timestamp_ms)
def close(self) -> None:
self._landmarker.close()
+14
View File
@@ -0,0 +1,14 @@
from __future__ import annotations
from pathlib import Path
import mediapipe as mp
BaseOptions = mp.tasks.BaseOptions
_MODELS_DIR = Path(__file__).resolve().parent.parent.parent / "pose_models"
POSE_LANDMARKER_FULL = _MODELS_DIR / "pose_landmarker_full.task"
POSE_LANDMARKER_LITE = _MODELS_DIR / "pose_landmarker_lite.task"
DEFAULT_MODEL_PATH = str(POSE_LANDMARKER_FULL) if POSE_LANDMARKER_FULL.exists() else str(POSE_LANDMARKER_LITE)
+80
View File
@@ -0,0 +1,80 @@
from __future__ import annotations
_POSE_CONNECTIONS = (
(11, 12),
(11, 13),
(13, 15),
(12, 14),
(14, 16),
(11, 23),
(12, 24),
(23, 24),
(23, 25),
(25, 27),
(24, 26),
(26, 28),
)
LANDMARK_NAMES: dict[int, str] = {
0: "nose",
1: "left_eye_inner",
2: "left_eye",
3: "left_eye_outer",
4: "right_eye_inner",
5: "right_eye",
6: "right_eye_outer",
7: "left_ear",
8: "right_ear",
9: "mouth_left",
10: "mouth_right",
11: "left_shoulder",
12: "right_shoulder",
13: "left_elbow",
14: "right_elbow",
15: "left_wrist",
16: "right_wrist",
17: "left_pinky",
18: "right_pinky",
19: "left_index",
20: "right_index",
21: "left_thumb",
22: "right_thumb",
23: "left_hip",
24: "right_hip",
25: "left_knee",
26: "right_knee",
27: "left_ankle",
28: "right_ankle",
29: "left_heel",
30: "right_heel",
31: "left_foot_index",
32: "right_foot_index",
}
LEFT_SHOULDER = 11
RIGHT_SHOULDER = 12
LEFT_ELBOW = 13
RIGHT_ELBOW = 14
LEFT_WRIST = 15
RIGHT_WRIST = 16
LEFT_HIP = 23
RIGHT_HIP = 24
LEFT_KNEE = 25
RIGHT_KNEE = 26
LEFT_ANKLE = 27
RIGHT_ANKLE = 28
REQUIRED_LANDMARKS = (
LEFT_SHOULDER,
RIGHT_SHOULDER,
LEFT_ELBOW,
RIGHT_ELBOW,
LEFT_WRIST,
RIGHT_WRIST,
LEFT_HIP,
RIGHT_HIP,
LEFT_KNEE,
RIGHT_KNEE,
LEFT_ANKLE,
RIGHT_ANKLE,
)