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.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""공유 테스트 픽스처.
|
||||
|
||||
모든 테스트는 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": [],
|
||||
}
|
||||
Reference in New Issue
Block a user