로컬 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

@ -29,6 +29,7 @@ import math
import sys
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Iterable, Protocol
from urllib.parse import parse_qs, urlsplit
@ -447,31 +448,66 @@ def resolve_device(requested: str, *, cuda_available: bool | None = None) -> tup
return ("cuda", "float16") if cuda_available else ("cpu", "int8")
def probe_device(model: str, device: str, *, python: str | None = None) -> bool:
"""별도 프로세스에서 디바이스를 실제로 한 번 돌려 본다.
cuDNN 없으면 ctranslate2 Python 예외가 아니라 **네이티브 크래시**
죽는다. 같은 프로세스의 try/except 로는 절대 잡을 없으므로, 살릴 없는
실패는 버릴 있는 자식 프로세스에서 먼저 확인한다.
"""
import subprocess
argv = [
python or sys.executable,
"-X",
"utf8",
str(Path(__file__).resolve()),
"--self-check",
"--model",
model,
"--device",
device,
]
try:
completed = subprocess.run(
argv,
check=False,
capture_output=True,
timeout=600,
shell=False,
)
except (OSError, subprocess.TimeoutExpired):
return False
return completed.returncode == 0
def build_transcriber(
model: str,
requested_device: str,
*,
factory: Callable[[str, str, str], Any],
cuda_available: bool | None = None,
prober: Callable[[str, str], bool] | None = None,
) -> Any:
"""디바이스를 해석하고 warmup 까지 통과한 transcriber 만 돌려준다.
"""디바이스를 해석하고 실제로 추론이 되는 transcriber 만 돌려준다.
`auto` CUDA 보이면 먼저 시도하되, 추론이 실패하면 CPU 내려간다.
명시적 `--device cuda` 조용히 강등하지 않고 그대로 실패시킨다.
`auto` CUDA 보이면 먼저 자식 프로세스로 확인하고, 거기서 실패하면
CPU 내려간다. 명시적 `--device cuda` 조용히 강등하지 .
"""
device, compute_type = resolve_device(requested_device, cuda_available=cuda_available)
check = prober if prober is not None else (lambda m, d: probe_device(m, d))
if device == "cuda" and not check(model, "cuda"):
if requested_device != "auto":
raise TranscriptionError("device_unusable:cuda")
device, compute_type = "cpu", "int8"
try:
transcriber = factory(model, device, compute_type)
transcriber.warmup()
return transcriber
except Exception as exc:
if requested_device != "auto" or device == "cpu":
raise TranscriptionError(f"device_unusable:{device}") from exc
device, compute_type = "cpu", "int8"
transcriber = factory(model, device, compute_type)
transcriber.warmup()
return transcriber
raise TranscriptionError(f"device_unusable:{device}") from exc
async def serve(args: argparse.Namespace) -> int: # pragma: no cover - I/O 진입점
@ -573,11 +609,34 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--model", default=DEFAULT_MODEL, choices=list(ALLOWED_MODELS))
parser.add_argument("--device", default="auto", choices=["auto", "cuda", "cpu"])
parser.add_argument("--enable", action="store_true")
parser.add_argument(
"--self-check",
action="store_true",
help="load the model on --device once and exit; used as a crash-safe probe",
)
return parser
def run_self_check(args: argparse.Namespace) -> int: # pragma: no cover - 서브프로세스
device, compute_type = resolve_device(
"cpu" if args.device == "cpu" else args.device
)
transcriber = FasterWhisperTranscriber(
args.model, device=device, compute_type=compute_type
)
transcriber.warmup()
print(json.dumps({"ok": True, "device": device}, separators=(",", ":")))
return 0
def main(argv: Iterable[str] | None = None) -> int: # pragma: no cover - CLI
args = build_parser().parse_args(list(argv) if argv is not None else None)
if args.self_check:
try:
return run_self_check(args)
except Exception as exc:
print(json.dumps({"ok": False, "error": type(exc).__name__}), file=sys.stderr)
return 3
if not args.enable:
print("local whisper STT server is disabled; pass --enable", file=sys.stderr)
return 2