feat: extract opengait_studio monorepo module

Move demo implementation into opengait_studio, retire Sports2D runtime integration, and align packaging with root-level monorepo dependency management.
This commit is contained in:
2026-03-03 17:16:17 +08:00
parent 5c6bef1ca1
commit 00fcda4fe3
39 changed files with 359 additions and 270 deletions
@@ -251,7 +251,7 @@ class TestNatsPublisherIntegration:
except ImportError:
pytest.skip("nats-py not installed")
from opengait.demo.output import NatsPublisher, create_result
from opengait_studio.output import NatsPublisher, create_result
# Create publisher
publisher = NatsPublisher(nats_url, subject=NATS_SUBJECT)
@@ -341,7 +341,7 @@ class TestNatsPublisherIntegration:
def test_nats_publisher_graceful_when_server_unavailable(self) -> None:
"""Test that publisher handles missing server gracefully."""
try:
from opengait.demo.output import NatsPublisher
from opengait_studio.output import NatsPublisher
except ImportError:
pytest.skip("output module not available")
@@ -380,7 +380,7 @@ class TestNatsPublisherIntegration:
import asyncio
import nats # type: ignore[import-untyped]
from opengait.demo.output import NatsPublisher, create_result
from opengait_studio.output import NatsPublisher, create_result
except ImportError as e:
pytest.skip(f"Required module not available: {e}")
@@ -15,7 +15,7 @@ from numpy.typing import NDArray
import pytest
import torch
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
REPO_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
SAMPLE_VIDEO_PATH: Final[Path] = REPO_ROOT / "assets" / "sample.mp4"
@@ -31,7 +31,7 @@ def _device_for_runtime() -> str:
def _run_pipeline_cli(
*args: str, timeout_seconds: int = 120
) -> subprocess.CompletedProcess[str]:
command = [sys.executable, "-m", "opengait.demo", *args]
command = [sys.executable, "-m", "opengait_studio", *args]
return subprocess.run(
command,
cwd=REPO_ROOT,
@@ -728,14 +728,14 @@ def test_pipeline_visualizer_updates_on_no_detection() -> None:
This is a regression test for the window freeze issue when no person is detected.
The window should refresh every frame to prevent freezing.
"""
from opengait.demo.pipeline import ScoliosisPipeline
from opengait_studio.pipeline import ScoliosisPipeline
# Create a minimal pipeline with mocked dependencies
with (
mock.patch("opengait.demo.pipeline.YOLO") as mock_yolo,
mock.patch("opengait.demo.pipeline.create_source") as mock_source,
mock.patch("opengait.demo.pipeline.create_publisher") as mock_publisher,
mock.patch("opengait.demo.pipeline.ScoNetDemo") as mock_classifier,
mock.patch("opengait_studio.pipeline.YOLO") as mock_yolo,
mock.patch("opengait_studio.pipeline.create_source") as mock_source,
mock.patch("opengait_studio.pipeline.create_publisher") as mock_publisher,
mock.patch("opengait_studio.pipeline.ScoNetDemo") as mock_classifier,
):
# Setup mock detector that returns no detections (causing process_frame to return None)
mock_detector = mock.MagicMock()
@@ -791,16 +791,16 @@ def test_pipeline_visualizer_updates_on_no_detection() -> None:
def test_pipeline_visualizer_clears_bbox_on_no_detection() -> None:
from opengait.demo.pipeline import ScoliosisPipeline
from opengait_studio.pipeline import ScoliosisPipeline
# Create a minimal pipeline with mocked dependencies
with (
mock.patch("opengait.demo.pipeline.YOLO") as mock_yolo,
mock.patch("opengait.demo.pipeline.create_source") as mock_source,
mock.patch("opengait.demo.pipeline.create_publisher") as mock_publisher,
mock.patch("opengait.demo.pipeline.ScoNetDemo") as mock_classifier,
mock.patch("opengait.demo.pipeline.select_person") as mock_select_person,
mock.patch("opengait.demo.pipeline.mask_to_silhouette") as mock_mask_to_sil,
mock.patch("opengait_studio.pipeline.YOLO") as mock_yolo,
mock.patch("opengait_studio.pipeline.create_source") as mock_source,
mock.patch("opengait_studio.pipeline.create_publisher") as mock_publisher,
mock.patch("opengait_studio.pipeline.ScoNetDemo") as mock_classifier,
mock.patch("opengait_studio.pipeline.select_person") as mock_select_person,
mock.patch("opengait_studio.pipeline.mask_to_silhouette") as mock_mask_to_sil,
):
# Create mock detection result for frames 0-1 (valid detection)
mock_box = mock.MagicMock()
@@ -919,7 +919,7 @@ def test_pipeline_visualizer_clears_bbox_on_no_detection() -> None:
def test_frame_pacer_emission_count_24_to_15() -> None:
from opengait.demo.pipeline import _FramePacer
from opengait_studio.pipeline import _FramePacer
pacer = _FramePacer(15.0)
interval_ns = int(1_000_000_000 / 24)
@@ -928,7 +928,7 @@ def test_frame_pacer_emission_count_24_to_15() -> None:
def test_frame_pacer_requires_positive_target_fps() -> None:
from opengait.demo.pipeline import _FramePacer
from opengait_studio.pipeline import _FramePacer
with pytest.raises(ValueError, match="target_fps must be positive"):
_FramePacer(0.0)
@@ -950,6 +950,6 @@ def test_resolve_stride_modes(
mode: Literal["manual", "sliding", "chunked"],
expected: int,
) -> None:
from opengait.demo.pipeline import resolve_stride
from opengait_studio.pipeline import resolve_stride
assert resolve_stride(window, stride, mode) == expected
@@ -8,7 +8,7 @@ import pytest
from beartype.roar import BeartypeCallHintParamViolation
from jaxtyping import TypeCheckError
from opengait.demo.preprocess import mask_to_silhouette
from opengait_studio.preprocess import mask_to_silhouette
class TestMaskToSilhouette:
@@ -18,7 +18,7 @@ import torch
from torch import Tensor
if TYPE_CHECKING:
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
# Constants for test configuration
CONFIG_PATH = Path("configs/sconet/sconet_scoliosis1k.yaml")
@@ -27,7 +27,7 @@ CONFIG_PATH = Path("configs/sconet/sconet_scoliosis1k.yaml")
@pytest.fixture
def demo() -> "ScoNetDemo":
"""Create ScoNetDemo without loading checkpoint (CPU-only)."""
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
return ScoNetDemo(
cfg_path=str(CONFIG_PATH),
@@ -71,7 +71,7 @@ class TestScoNetDemoConstruction:
def test_construction_from_config_no_checkpoint(self) -> None:
"""Test construction with config only, no checkpoint."""
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
demo = ScoNetDemo(
cfg_path=str(CONFIG_PATH),
@@ -87,7 +87,7 @@ class TestScoNetDemoConstruction:
def test_construction_with_relative_path(self) -> None:
"""Test construction handles relative config path correctly."""
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
demo = ScoNetDemo(
cfg_path="configs/sconet/sconet_scoliosis1k.yaml",
@@ -100,7 +100,7 @@ class TestScoNetDemoConstruction:
def test_construction_invalid_config_raises(self) -> None:
"""Test construction raises with invalid config path."""
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
with pytest.raises((FileNotFoundError, TypeError)):
_ = ScoNetDemo(
@@ -180,7 +180,7 @@ class TestScoNetDemoPredict:
self, demo: "ScoNetDemo", dummy_sils_single: Tensor
) -> None:
"""Test predict returns (str, float) tuple with valid label."""
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
result_raw = demo.predict(dummy_sils_single)
result = cast(tuple[str, float], result_raw)
@@ -217,7 +217,7 @@ class TestScoNetDemoNoDDP:
def test_no_distributed_init_in_construction(self) -> None:
"""Test that construction does not call torch.distributed."""
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
with patch("torch.distributed.is_initialized") as mock_is_init:
with patch("torch.distributed.init_process_group") as mock_init_pg:
@@ -327,14 +327,14 @@ class TestScoNetDemoLabelMap:
def test_label_map_has_three_classes(self) -> None:
"""Test LABEL_MAP has exactly 3 classes."""
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
assert len(ScoNetDemo.LABEL_MAP) == 3
assert set(ScoNetDemo.LABEL_MAP.keys()) == {0, 1, 2}
def test_label_map_values_are_valid_strings(self) -> None:
"""Test LABEL_MAP values are valid non-empty strings."""
from opengait.demo.sconet_demo import ScoNetDemo
from opengait_studio.sconet_demo import ScoNetDemo
for value in ScoNetDemo.LABEL_MAP.values():
assert isinstance(value, str)
@@ -7,14 +7,14 @@ from unittest import mock
import numpy as np
import pytest
from opengait.demo.input import create_source
from opengait.demo.visualizer import (
from opengait_studio.input import create_source
from opengait_studio.visualizer import (
DISPLAY_HEIGHT,
DISPLAY_WIDTH,
ImageArray,
OpenCVVisualizer,
)
from opengait.demo.window import select_person
from opengait_studio.window import select_person
REPO_ROOT = Path(__file__).resolve().parents[2]
SAMPLE_VIDEO_PATH = REPO_ROOT / "assets" / "sample.mp4"
@@ -8,7 +8,7 @@ import pytest
import torch
from numpy.typing import NDArray
from opengait.demo.window import SilhouetteWindow, select_person
from opengait_studio.window import SilhouetteWindow, select_person
class TestSilhouetteWindow: