로컬 whisper STT 사이드카 디바이스 폴백과 deprecated audioop 제거

cuDNN 부재 환경에서 ctranslate2 는 Python 예외가 아니라 네이티브 크래시로 죽어
프로세스가 통째로 사라진다. 같은 프로세스의 try/except 로는 절대 잡을 수 없어
디바이스 확인을 버릴 수 있는 자식 프로세스(--self-check)로 분리했다.
auto 는 CUDA 프로브 실패 시 CPU(int8)로 폴백하고, 명시적 --device cuda 는 조용히
강등하지 않는다.

함께 고친 것:
- warmup PCM 생성의 연산자 우선순위 버그(b"\x00\x00" * N // 2 가 바이트열 정수
  나눗셈이 되어 TypeError). warmup_pcm() 으로 분리하고 테스트로 고정했다.
- audioop 은 Python 3.13 에서 제거되므로 array + 정수 연산으로 RMS 를 직접 구한다.

실측(저장소 무참조 synthetic seed 8.72초, CPU int8 small):
interim 9 · final 5 · word timestamp 12개 present, 전사는 원문과 한 글자 차이.
사이드카 회귀 37/37.
This commit is contained in:
Yun Chan 2026-08-08 01:39:58 +09:00
parent 16e791e044
commit 05aa7b312e
9 changed files with 252 additions and 30 deletions

View file

@ -326,42 +326,72 @@ class _FakeTranscriberFactory:
class TranscriberBuildTest(unittest.TestCase):
def test_auto_falls_back_to_cpu_when_cuda_warmup_fails(self) -> None:
"""cuDNN 부재처럼 '장치는 보이지만 추론이 죽는' 경우를 잡는다."""
def test_auto_falls_back_to_cpu_when_the_cuda_probe_fails(self) -> None:
"""cuDNN 부재는 네이티브 크래시라 자식 프로세스 프로브로만 잡힌다."""
factory = _FakeTranscriberFactory()
probes: list[tuple[str, str]] = []
def prober(model: str, device: str) -> bool:
probes.append((model, device))
return False
factory = _FakeTranscriberFactory(failing_device="cuda")
transcriber = MODULE.build_transcriber(
"small", "auto", factory=factory, cuda_available=True
"small", "auto", factory=factory, cuda_available=True, prober=prober
)
self.assertEqual(probes, [("small", "cuda")])
self.assertEqual(transcriber.device, "cpu")
self.assertEqual(transcriber.compute_type, "int8")
self.assertEqual(
factory.built, [("small", "cuda", "float16"), ("small", "cpu", "int8")]
)
# CUDA 로는 아예 모델을 올리지 않는다. 올렸다면 그 자리에서 죽는다.
self.assertEqual(factory.built, [("small", "cpu", "int8")])
def test_auto_keeps_cuda_when_warmup_succeeds(self) -> None:
def test_auto_keeps_cuda_when_the_probe_succeeds(self) -> None:
factory = _FakeTranscriberFactory()
transcriber = MODULE.build_transcriber(
"small", "auto", factory=factory, cuda_available=True
"small",
"auto",
factory=factory,
cuda_available=True,
prober=lambda model, device: True,
)
self.assertEqual(transcriber.device, "cuda")
self.assertEqual(len(factory.built), 1)
def test_explicit_cuda_never_downgrades_silently(self) -> None:
factory = _FakeTranscriberFactory(failing_device="cuda")
with self.assertRaises(MODULE.TranscriptionError):
MODULE.build_transcriber(
"small", "cuda", factory=factory, cuda_available=True
)
self.assertEqual(factory.built, [("small", "cuda", "float16")])
def test_cpu_warmup_failure_is_not_retried(self) -> None:
def test_explicit_cuda_never_downgrades_silently(self) -> None:
factory = _FakeTranscriberFactory()
with self.assertRaises(MODULE.TranscriptionError):
MODULE.build_transcriber(
"small",
"cuda",
factory=factory,
cuda_available=True,
prober=lambda model, device: False,
)
self.assertEqual(factory.built, [])
def test_cpu_path_is_not_probed(self) -> None:
factory = _FakeTranscriberFactory()
probes: list[tuple[str, str]] = []
MODULE.build_transcriber(
"small",
"cpu",
factory=factory,
cuda_available=True,
prober=lambda model, device: probes.append((model, device)) or True,
)
self.assertEqual(probes, [])
self.assertEqual(factory.built, [("small", "cpu", "int8")])
def test_python_level_warmup_failure_still_fails_closed(self) -> None:
factory = _FakeTranscriberFactory(failing_device="cpu")
with self.assertRaises(MODULE.TranscriptionError):
MODULE.build_transcriber(
"small", "auto", factory=factory, cuda_available=False
"small",
"auto",
factory=factory,
cuda_available=False,
prober=lambda model, device: True,
)
self.assertEqual(factory.built, [("small", "cpu", "int8")])
class CliTest(unittest.TestCase):