diff --git a/discovery.py b/discovery.py index c0def93..f7f0116 100644 --- a/discovery.py +++ b/discovery.py @@ -34,6 +34,14 @@ class Camera: extra_video_devices: list[str] = field(default_factory=list) +def select_cameras(cameras: list[Camera], spec: list[str]) -> list[Camera]: + """`--cameras 1,5` means identities cam-01 and cam-05, not discovery index.""" + if spec == ["all"]: + return list(cameras) + wanted = {f"cam-{int(i):02d}" for i in spec} + return [c for c in cameras if c.identity in wanted] + + def _run(cmd: list[str]) -> str: return subprocess.run(cmd, capture_output=True, text=True, check=False).stdout diff --git a/pyproject.toml b/pyproject.toml index 1dedf71..55c8b50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,3 +11,6 @@ dependencies = [ "opencv-python-headless", "scipy", ] + +[project.optional-dependencies] +dev = ["pytest>=8"] diff --git a/run.py b/run.py index 760cd71..663ad08 100644 --- a/run.py +++ b/run.py @@ -21,7 +21,7 @@ import time from pathlib import Path from config import load_config, _load_dotenv -from discovery import discover_cameras +from discovery import discover_cameras, select_cameras from tokens import make_token log = logging.getLogger("cameras.main") @@ -126,12 +126,10 @@ def main() -> int: c.audio_card if c.audio_card is not None else "-", c.usb_port or "-") - if cfg.cameras != ["all"]: - wanted = set(int(i) - 1 for i in cfg.cameras) - cameras = [c for c in cameras if c.index in wanted] - if not cameras: - log.error("no cameras match --cameras %s", args.cameras) - return 2 + cameras = select_cameras(cameras, cfg.cameras) + if not cameras: + log.error("no cameras match --cameras %s", args.cameras) + return 2 sup = Supervisor(cfg, cameras) for cam in cameras: diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..94bf69b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) diff --git a/tests/test_audio_cleanup.py b/tests/test_audio_cleanup.py new file mode 100644 index 0000000..a1cb746 --- /dev/null +++ b/tests/test_audio_cleanup.py @@ -0,0 +1,30 @@ +import inspect + +import numpy as np +import pytest + +from audio_cleanup import AudioCleaner + + +def test_enhance_batch_without_lengths_typeerror(): + c = AudioCleaner(mode="force") + assert c.backend == "model" + import torch + + w = torch.zeros(1, 8000) + with pytest.raises(TypeError): + c._enhancer.enhance_batch(w) + + +def test_process_uses_relative_lengths_one(): + src = inspect.getsource(AudioCleaner.process) + assert "tensor([1.0])" in src + + +def test_process_preserves_length_and_int16(): + c = AudioCleaner(mode="force") + n = 16000 + pcm = (np.sin(2 * np.pi * 440 * np.arange(n) / 16000) * 8000).astype(np.int16) + out = c.process(pcm) + assert out.dtype == np.int16 + assert out.shape == pcm.shape diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..bff5236 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,7 @@ +from config import _parse_cameras + + +def test_parse_cameras_list(): + assert _parse_cameras("1,5") == ["1", "5"] + assert _parse_cameras("all") == ["all"] + assert _parse_cameras("1-3") == ["1", "2", "3"] diff --git a/tests/test_discovery.py b/tests/test_discovery.py new file mode 100644 index 0000000..76f6898 --- /dev/null +++ b/tests/test_discovery.py @@ -0,0 +1,49 @@ +import re + +from discovery import Camera, discover_cameras, select_cameras + +CARD_RE = re.compile(r"\s*(\d+)\s+\[([A-Za-z0-9_]+)\s*\]:\s*(.*)") + + +def test_padded_c920_card_line_matches(): + line = " 2 [C920 ]: USB-Audio - HD Pro Webcam C920" + m = CARD_RE.match(line) + assert m is not None + assert int(m.group(1)) == 2 + assert m.group(2) == "C920" + + +def test_naive_word_regex_misses_padding(): + line = " 2 [C920 ]: USB-Audio - HD Pro Webcam C920" + assert re.match(r"\s*(\d+)\s+\[(\w+)\]:", line) is None + + +def test_live_discover_present_cameras(): + cams = discover_cameras() + assert len(cams) >= 1 + assert all(c.audio_card is not None for c in cams) + assert all(c.serial for c in cams) + assert all(re.match(r"cam-\d{2}$", c.identity) for c in cams) + ids = [c.identity for c in cams] + assert ids == sorted(ids) + + +def test_select_cameras_by_identity_not_index(): + cams = [ + Camera(index=0, identity="cam-01", video_device="/dev/cam01", + audio_card=2, serial="A"), + Camera(index=1, identity="cam-05", video_device="/dev/cam05", + audio_card=6, serial="B"), + Camera(index=2, identity="cam-20", video_device="/dev/cam20", + audio_card=21, serial="C"), + ] + out = select_cameras(cams, ["1", "5"]) + assert [c.identity for c in out] == ["cam-01", "cam-05"] + + +def test_select_cameras_all(): + cams = [ + Camera(index=0, identity="cam-01", video_device="/dev/cam01", + audio_card=2, serial="A"), + ] + assert select_cameras(cams, ["all"]) == cams