Files
luke_scribe/tests/conftest.py
T
lukehemmin 7327145d7a feat: implement full-platform STT API (v2.3 consensus plan)
Batch+realtime transcription API: faster-whisper engine w/ EngineOwner
(single GPU owner, OOM downgrade chain, persisted attempted_profiles),
hardware-adaptive device manager (T0-T3 VRAM tiers), Redis/in-proc job
queue w/ leases + crash recovery, postprocess (glossary/rules/LLM w/
egress guard), privacy-first result store (UUID keys, source deleted
after transcribe), retention sweeper, API-key auth (HMAC digests,
scopes, job ownership), WebSocket realtime lane (LocalAgreement),
CLI (detect/transcribe/bench/serve/key), Docker, benchmark runner.

127 mock-based tests pass; ruff clean. Includes verification checklist
and autoplan review notes.
2026-08-12 16:01:21 +09:00

191 lines
5.7 KiB
Python

"""공유 테스트 픽스처.
모든 테스트는 GPU/모델/ffmpeg 없이 mock 기반으로 동작한다:
- ``FakeEngineOwner``: 세그먼트를 즉시 생성 (실제 decode 없음)
- ``FakeIngestor``: ffprobe/ffmpeg 대체
"""
from __future__ import annotations
from pathlib import Path
import pytest
from luke_scribe.config import Settings
class FakeSegments:
"""세그먼트 목록을 반복하는 mock — 취소 시 중단."""
def __init__(self, segments: list[dict], cancel_on: callable | None = None) -> None:
self._segs = iter(segments)
self._cancel_on = cancel_on or (lambda: False)
def __iter__(self):
return self
def __next__(self):
if self._cancel_on():
raise StopIteration
return next(self._segs)
def close(self) -> None:
pass
class FakeEngine:
def __init__(self, segments: list[dict], *, fail_on: list[str] | None = None) -> None:
self.segments = segments
self.fail_on = fail_on or []
self.calls: list[dict] = []
def transcribe(self, audio_path, options, should_cancel=None, download_progress=None) -> object:
self.calls.append({"audio": audio_path, "options": options})
# 실제 faster-whisper를 import하지 않는 mock
return type(
"Outcome",
(),
{
"segments": FakeSegments(self.segments, cancel_on=should_cancel),
"info": {"language": "ko"},
},
)()
class FakeEngineOwner:
"""EngineOwner mock — 프로파일 강등/캡을 검증 가능하게 함."""
def __init__(
self, engine: FakeEngine | None = None, segments: list[dict] | None = None
) -> None:
self.engine = engine or FakeEngine(segments or _default_segments())
self.last_request = None
def transcribe(self, req) -> dict:
self.last_request = req
# 실제 EngineOwner처럼 첫 시도 프로파일을 기록 (downgrade_attempts 계산용)
req.attempted_profiles.append(f"{req.options.device}/{req.options.compute_type or 'int8'}")
outcome = self.engine.transcribe(
req.audio_path, req.options, should_cancel=req.should_cancel
)
return {
"segments": outcome.segments,
"info": outcome.info,
"device": req.options.device if req.options.device != "auto" else "cpu",
"compute_type": req.options.compute_type or "int8",
"attempted_profiles": list(req.attempted_profiles),
}
def emit_hypothesis(self, pcm_chunk: bytes) -> dict:
return {"segments": [], "audio_sec": len(pcm_chunk) / 2 / 16000}
def unload_all(self) -> None:
pass
def _default_segments() -> list[dict]:
return [
{
"index": 0,
"start": 0.0,
"end": 2.5,
"text": "오늘 API 서버에서 vLLM을 사용해 보겠습니다.",
"avg_logprob": -0.2,
"no_speech_prob": 0.01,
},
{
"index": 1,
"start": 2.5,
"end": 4.0,
"text": "Kubernetes 클러스터에 배포합니다.",
"avg_logprob": -0.3,
"no_speech_prob": 0.02,
},
]
class FakeIngestor:
"""ffprobe/ffmpeg 없는 mock ingestor."""
def __init__(self, duration_sec: float = 10.0, codec: str = "mp3") -> None:
self.duration_sec = duration_sec
self.codec = codec
self.cancelled = False
def ingest(self, source: Path, *, should_cancel=None):
if should_cancel and should_cancel():
from luke_scribe.errors import CancelledError
self.cancelled = True
raise CancelledError("취소됨")
from luke_scribe.audio.ingest import IngestResult, ProbeResult
from luke_scribe.results.models import NormalizedAudio
class _Cleanup:
def close(self):
pass
return IngestResult(
normalized_path="/tmp/fake-normalized.wav",
normalized=NormalizedAudio(duration_sec=self.duration_sec),
probe=ProbeResult(duration_sec=self.duration_sec, codec=self.codec, size_bytes=100),
temp_dir="/tmp/fake-tmp",
cleanup=lambda: None,
)
@pytest.fixture
def settings(tmp_path: Path) -> Settings:
return Settings(
_env_file=None,
results_dir=str(tmp_path / "results"),
api_key_file=str(tmp_path / "api_keys.json"),
queue_backend="inproc",
model_cache_dir=None,
max_queue=10,
)
@pytest.fixture
def fake_owner() -> FakeEngineOwner:
return FakeEngineOwner()
@pytest.fixture
def fake_ingestor() -> FakeIngestor:
return FakeIngestor()
@pytest.fixture
def transcript_result() -> dict:
return {
"schema_version": "1.0",
"status": "completed",
"source": {"name": "test.mp3", "codec": "mp3", "size_bytes": 100},
"normalized_audio": {
"duration_sec": 4.0,
"audio_format": "pcm_s16le",
"sample_rate": 16000,
"channels": 1,
},
"execution": {
"model": "large-v3-turbo",
"device": "cpu",
"compute_type": "int8",
"language_requested": "ko",
},
"timings": {"transcription_sec": 0.5, "rtf": 0.125},
"text": "오늘 API 서버에서 vLLM을 사용해 보겠습니다.",
"segments": [
{
"index": 0,
"start": 0.0,
"end": 2.5,
"text": "오늘 API 서버에서 vLLM을 사용해 보겠습니다.",
"avg_logprob": -0.2,
"no_speech_prob": 0.01,
}
],
"warnings": [],
}