"""공유 테스트 픽스처. 모든 테스트는 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": [], }