diff --git a/pyproject.toml b/pyproject.toml index a81c490..ab8dea9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,10 @@ finetune = [ "accelerate>=0.33", "bitsandbytes>=0.43; platform_system != 'Darwin'", ] -dev = ["pytest>=8.0", "ruff>=0.6"] +# webrtcvad 는 포터블 exe 의존성이라 CI 환경에는 항상 있다. 개발 venv 에만 없으면 +# `_is_speech` 의 webrtcvad 경로가 로컬에서 통째로 안 돌고, 그 결과 같은 테스트가 +# 로컬에서는 통과하고 Windows CI 에서만 깨진다 (실제로 그랬다). 환경을 맞춘다. +dev = ["pytest>=8.0", "ruff>=0.6", "webrtcvad-wheels>=2.0.14"] [project.scripts] livesub = "livesub.app:main" diff --git a/tests/test_segmenter.py b/tests/test_segmenter.py index 75b61ff..0e8deac 100644 --- a/tests/test_segmenter.py +++ b/tests/test_segmenter.py @@ -1,8 +1,30 @@ import numpy as np +import pytest +from livesub.audio import segmenter as segmenter_mod from livesub.audio.segmenter import Segmenter, SegmenterConfig from livesub.constants import SAMPLE_RATE +#: webrtcvad 는 선택 의존성이다. 있으면 아래 게이트 테스트가 돌고 없으면 skip 된다. +REAL_WEBRTCVAD = segmenter_mod.webrtcvad + + +@pytest.fixture(autouse=True) +def _energy_vad_only(monkeypatch): + """이 파일의 테스트는 에너지 VAD 경로로 고정한다. + + 입력이 220Hz 순음이기 때문이다. webrtcvad 는 사람 목소리로 학습돼 있어 + 순음을 발화로 보지 않고, `_is_speech` 는 에너지 판정과 webrtcvad 를 AND 로 + 묶는다. 그래서 webrtcvad 가 깔린 환경에서는 이 파일의 테스트 5개가 깨졌다 — + 포터블 exe 의 requirements 에 webrtcvad-wheels 가 들어 있어 Windows CI 가 + 정확히 그 환경이고, 개발용 venv 에는 없어서 여태 안 드러났다. + + 고칠 대상은 판정기가 아니라 테스트의 숨은 전제다. 여기서 보려는 건 분할 + 상태 기계이므로 선택 의존성을 끊어 환경과 무관하게 같은 경로를 시험한다. + 게이트 동작 자체는 test_webrtcvad_gate_filters_pure_tone 이 본다. + """ + monkeypatch.setattr(segmenter_mod, "webrtcvad", None) + def tone(ms: int, amplitude: float = 0.3) -> np.ndarray: n = SAMPLE_RATE * ms // 1000 @@ -76,3 +98,24 @@ def test_lead_in_is_prepended(): assert len(finals) == 1 # 0.6초 발화 + 최대 0.2초 리드인 assert finals[0].duration_s > 0.6 + + +@pytest.mark.skipif(REAL_WEBRTCVAD is None, reason="webrtcvad 미설치 — 게이트 경로가 없다") +def test_webrtcvad_gate_filters_pure_tone(monkeypatch): + """webrtcvad 가 깔리면 순음은 에너지 VAD 만일 때보다 덜 통과한다. + + 포터블 exe 와 Windows CI 가 이 경로를 쓴다. 두 경로가 실제로 다르게 + 동작한다는 사실을 테스트로 고정해 둔다 — 이게 깨져 있는 걸 몰라서 + 같은 테스트가 플랫폼에 따라 다른 결과를 냈다. + """ + energy_only = Segmenter(config()) + energy_out = energy_only.push(tone(800)) + energy_only.push(silence(600)) + + monkeypatch.setattr(segmenter_mod, "webrtcvad", REAL_WEBRTCVAD) + gated = Segmenter(config()) + gated_out = gated.push(tone(800)) + gated.push(silence(600)) + + energy_total = sum(s.duration_s for s in energy_out if s.is_final) + gated_total = sum(s.duration_s for s in gated_out if s.is_final) + assert energy_total > 0.7 # 에너지 VAD 는 순음을 발화로 본다 + assert gated_total < energy_total # webrtcvad 는 사람 목소리가 아니라고 걸러낸다