Files
luke_scribe/tests/unit/test_engine_faster_whisper.py
T
lukehemmin 20777386fe fix: normalize faster-whisper segments to dicts + in-proc auto-worker
Colab run 3 (A100) surfaced three GPU/API-path bugs mocks couldn't catch:

1. faster-whisper yields namedtuple Segments, but batch/bench consume
   them as dicts (.get) -> AttributeError 'Segment' has no attribute
   'get' on every real transcription. Engine now normalizes segments
   to dicts (_to_dict_segments) at the boundary.

2. API TranscribeOptions carries engine-irrelevant keys (formats,
   timestamps, glossary_id, post_correction, diarize); worker's
   TranscriptionOptions(**job.options) crashed with TypeError. Worker
   now filters job.options to TranscriptionOptions.__slots__.

3. in-proc server never consumed its own queue (jobs stayed queued
   forever). Added opt-in Settings.auto_worker (default off): lifespan
   starts a daemon Worker thread for inproc backend, stopped on
   shutdown. Notebook enables it via LUKESCRIBE_AUTO_WORKER=true so the
   API upload -> completed flow works end to end.

Notebook: bench manifest now uses clips schema (audio_path/duration_sec/
entities); cell 22 reads error_message/error_code; upload poll window
raised to 4min (first-run model download).

+ 5 tests (namedtuple/dict segments, API-style options, auto_worker
on/off); 136 tests pass, ruff clean.
2026-08-12 17:32:18 +09:00

140 lines
4.8 KiB
Python

"""FasterWhisperEngine 단위 테스트 — CTranslate2 호출 계약 (device 분리).
faster-whisper/CTranslate2는 ``device`` 문자열로 "cuda"만 허용하며 인덱스는
별도 ``device_index`` 인자로 받는다. DeviceManager가 내려주는 "cuda:N"을
제대로 분리해 전달하는지 mock으로 검증한다 (GPU 환경에서만 재현되는 버그
— Colab 실전 테스트에서 ``unsupported device cuda:0``으로 확인됨).
"""
from __future__ import annotations
import sys
import types
from luke_scribe.engine.base import TranscriptionOptions
from luke_scribe.engine.faster_whisper_engine import FasterWhisperEngine
class FakeWhisperModel:
"""faster_whisper.WhisperModel 대체 — 생성 인자/세그먼트를 기록한다."""
calls: list[dict] = []
segments: list = [] # transcribe()가 yield할 세그먼트 (기본: namedtuple)
def __init__(self, *args, **kwargs) -> None:
self.kwargs = kwargs
self.__class__.calls.append(kwargs)
def transcribe(self, audio_path, **kwargs):
return iter(list(self.__class__.segments)), {}
def _install_fake(monkeypatch) -> None:
mod = types.ModuleType("faster_whisper")
mod.WhisperModel = FakeWhisperModel
monkeypatch.setitem(sys.modules, "faster_whisper", mod)
FakeWhisperModel.calls.clear()
FakeWhisperModel.segments = []
def _opts(**kw) -> TranscriptionOptions:
kw.setdefault("model", "large-v3-turbo")
kw.setdefault("device", "cuda:0")
kw.setdefault("compute_type", "float16")
return TranscriptionOptions(**kw)
def test_cuda_device_index_split(monkeypatch):
"""'cuda:0' → device='cuda', device_index=0 (Colab A100 첫 GPU)."""
_install_fake(monkeypatch)
FasterWhisperEngine().transcribe("/tmp/x.wav", _opts())
kwargs = FakeWhisperModel.calls[-1]
assert kwargs["device"] == "cuda"
assert kwargs["device_index"] == 0
assert kwargs["compute_type"] == "float16"
def test_cuda_second_device_index(monkeypatch):
"""'cuda:1' → device='cuda', device_index=1."""
_install_fake(monkeypatch)
FasterWhisperEngine().transcribe("/tmp/x.wav", _opts(device="cuda:1"))
kwargs = FakeWhisperModel.calls[-1]
assert kwargs["device"] == "cuda"
assert kwargs["device_index"] == 1
def test_cpu_passthrough(monkeypatch):
"""'cpu' → 그대로 device='cpu', device_index=0."""
_install_fake(monkeypatch)
FasterWhisperEngine().transcribe("/tmp/x.wav", _opts(device="cpu", compute_type="int8"))
kwargs = FakeWhisperModel.calls[-1]
assert kwargs["device"] == "cpu"
assert kwargs["device_index"] == 0
assert kwargs["compute_type"] == "int8"
def test_split_device_helper():
assert FasterWhisperEngine._split_device("cuda:0") == ("cuda", 0)
assert FasterWhisperEngine._split_device("cuda:3") == ("cuda", 3)
assert FasterWhisperEngine._split_device("cpu") == ("cpu", 0)
assert FasterWhisperEngine._split_device("cuda") == ("cuda", 0)
# 파싱 불가 인덱스는 그대로 전달 (모델 로드 시 명시적 오류로 fail-explicit)
assert FasterWhisperEngine._split_device("cuda:xx") == ("cuda:xx", 0)
def test_namedtuple_segments_normalized_to_dicts(monkeypatch):
"""faster-whisper Segment(namedtuple) → dict 정규화 (GPU 실전 버그)."""
from collections import namedtuple
_install_fake(monkeypatch)
Seg = namedtuple(
"Segment",
[
"id",
"seek",
"start",
"end",
"text",
"tokens",
"temperature",
"avg_logprob",
"compression_ratio",
"no_speech_prob",
],
)
FakeWhisperModel.segments = [
Seg(
id=0,
seek=0,
start=0.0,
end=2.5,
text="오늘 vLLM을 배포합니다.",
tokens=[1, 2],
temperature=0.0,
avg_logprob=-0.2,
compression_ratio=1.0,
no_speech_prob=0.01,
)
]
outcome = FasterWhisperEngine().transcribe(
"/tmp/x.wav", _opts(device="cpu", compute_type="int8")
)
segs = list(outcome.segments)
assert len(segs) == 1
seg = segs[0]
# dict 계약: .get() 사용 가능 (파이프라인이 이걸로 접근)
assert seg.get("text") == "오늘 vLLM을 배포합니다."
assert seg.get("end") == 2.5
assert seg.get("avg_logprob") == -0.2
def test_dict_segments_passthrough(monkeypatch):
"""이미 dict인 세그먼트는 그대로 (mock 계약과 호환)."""
_install_fake(monkeypatch)
FakeWhisperModel.segments = [{"index": 0, "start": 0.0, "end": 1.0, "text": "x"}]
outcome = FasterWhisperEngine().transcribe(
"/tmp/x.wav", _opts(device="cpu", compute_type="int8")
)
segs = list(outcome.segments)
assert segs == [{"index": 0, "start": 0.0, "end": 1.0, "text": "x"}]