feat(api): sync test API (serve) + opt-in LLM correction + cloudflared tunnel

- api/: FastAPI app, X-API-Key 인증(미설정 시 임시키), 엔진 load-once 풀
  (+transcribe lock), POST /v1/transcribe(multipart, 동기), /health, /v1/system,
  /v1/models. 업로드 임시파일 finally 삭제(프라이버시).
- postprocess/: llm.correct(scripts/llm_correct.py 승격; opt-in·allowlist·감사로그·재시도)
  + rules.normalize(EmbeddingGemma 등 정규화).
- results/formats.py: txt/srt/vtt. connectivity/tunnel.py: cloudflared quick tunnel(Colab).
- cli serve: uvicorn 단일워커 + --tunnel cloudflare; config llm_* 필드;
  pyproject api/queue extra 분리(+python-multipart, dev httpx).

검증: 22 단위테스트(API TestClient·formats·postprocess) + 실서버 e2e
(/health·auth 401·실제 전사(JFK)·SRT·임시파일 삭제). KO 품질은 turbo/large-v3 필요(tiny는 한국어 degenerate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 23:20:01 +09:00
parent 480a36edfe
commit 8f6f8969fd
22 changed files with 744 additions and 28 deletions
+86
View File
@@ -0,0 +1,86 @@
"""API — FastAPI TestClient. 엔진은 monkeypatch(가짜)로 모델 로드 회피."""
from __future__ import annotations
from types import SimpleNamespace
import pytest
from fastapi.testclient import TestClient
import luke_scribe.api.routes.transcribe as route
from luke_scribe.api.app import create_app
from luke_scribe.config import settings
class _FakeSeg:
def __init__(self, start: float, end: float, text: str) -> None:
self.start = start
self.end = end
self.text = text
class _FakeEngine:
def transcribe(self, _audio, **_kw):
return [_FakeSeg(0.0, 1.0, "안녕 vLLM"), _FakeSeg(1.0, 2.0, "두번째")], SimpleNamespace(
language="ko"
)
@pytest.fixture
def client(monkeypatch):
monkeypatch.setattr(route, "get_engine", lambda *a, **k: _FakeEngine())
monkeypatch.setattr(
route, "probe_media", lambda p: SimpleNamespace(path=p, duration_s=2.0, size_bytes=1234)
)
monkeypatch.setattr(settings, "api_keys", ["testkey"])
return TestClient(create_app())
def _files():
return {"file": ("a.wav", b"RIFF0000WAVE", "audio/wav")}
def test_health(client):
assert client.get("/health").json() == {"status": "ok"}
def test_requires_key(client):
assert client.post("/v1/transcribe", files=_files()).status_code == 401
def test_transcribe_ok(client):
r = client.post(
"/v1/transcribe", files=_files(), headers={"X-API-Key": "testkey"}, data={"language": "ko"}
)
assert r.status_code == 200
body = r.json()
assert body["segments"][0]["text"] == "안녕 vLLM"
assert body["model_used"]
assert body["corrected"] is False
def test_413(client, monkeypatch):
monkeypatch.setattr(
route, "probe_media", lambda p: SimpleNamespace(path=p, duration_s=999999, size_bytes=1)
)
r = client.post("/v1/transcribe", files=_files(), headers={"X-API-Key": "testkey"})
assert r.status_code == 413
def test_srt_format(client):
r = client.post(
"/v1/transcribe",
files=_files(),
headers={"X-API-Key": "testkey"},
data={"response_format": "srt"},
)
assert r.status_code == 200
assert "00:00:00,000 --> 00:00:01,000" in r.text
def test_correct_path(client, monkeypatch):
monkeypatch.setattr(route.llm_correct, "correct", lambda text, **k: text + " [보정]")
r = client.post(
"/v1/transcribe", files=_files(), headers={"X-API-Key": "testkey"}, data={"correct": "true"}
)
assert r.status_code == 200
assert r.json()["corrected"] is True