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.
78 lines
3.5 KiB
Python
78 lines
3.5 KiB
Python
"""실시간 LocalAgreement 단위 테스트 — 버퍼 절단 불변식, 단조 타임스탬프."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from luke_scribe.pipeline.realtime import LocalAgreement
|
|
from luke_scribe.results.models import Segment
|
|
|
|
|
|
def _segs(texts: list[str], start: float = 0.0) -> list[Segment]:
|
|
out = []
|
|
t = start
|
|
for i, text in enumerate(texts):
|
|
out.append(Segment(index=i, start=t, end=t + 1.0, text=text))
|
|
t += 1.0
|
|
return out
|
|
|
|
|
|
class TestAgreement:
|
|
def test_no_confirmation_until_agreement_count(self):
|
|
la = LocalAgreement(agreement_count=2)
|
|
e1 = la.feed(_segs(["첫 번째 문장"]), 2.0)
|
|
assert e1["confirmed"] == [] # 1회만으로는 미확정
|
|
|
|
def test_stable_prefix_confirmed(self):
|
|
la = LocalAgreement(agreement_count=2)
|
|
la.feed(_segs(["오늘 API 서버"]), 2.0)
|
|
e2 = la.feed(_segs(["오늘 API 서버에서 vLLM"]), 2.0)
|
|
confirmed = e2["confirmed"]
|
|
assert confirmed and confirmed[0].text == "오늘 API 서버"
|
|
assert e2["truncated"] is True
|
|
|
|
def test_confirmed_immutable(self):
|
|
"""확정 세그먼트는 이후 가설에 의해 절대 변경되지 않는다 (불변식 1)."""
|
|
la = LocalAgreement(agreement_count=2)
|
|
la.feed(_segs(["오늘 API"]), 2.0)
|
|
e2 = la.feed(_segs(["오늘 API 서버"]), 2.0)
|
|
first_confirmed = [s.text for s in e2["confirmed"]]
|
|
la.feed(_segs(["오늘 API 서버에서 vLLM 사용"]), 2.0)
|
|
# 확정분은 그대로
|
|
assert [s.text for s in la.state.confirmed_segments][
|
|
: len(first_confirmed)
|
|
] == first_confirmed
|
|
|
|
def test_no_duplicate_confirmation(self):
|
|
"""이미 확정된 텍스트는 다음 가설에서 재발행되지 않는다 (Eng P18)."""
|
|
la = LocalAgreement(agreement_count=2)
|
|
la.feed(_segs(["안정된 발화"]), 2.0)
|
|
e2 = la.feed(_segs(["안정된 발화", "다음 문장"]), 2.0)
|
|
assert [s.text for s in e2["confirmed"]] == ["안정된 발화"]
|
|
# 다음 피드: "안정된 발화"는 재발행 금지, 새 텍스트만 확정
|
|
e3 = la.feed(_segs(["안정된 발화", "다음 문장", "셋째 문장"]), 2.0)
|
|
assert [s.text for s in e3["confirmed"]] == ["다음 문장"]
|
|
assert [s.text for s in la.state.confirmed_segments] == ["안정된 발화", "다음 문장"]
|
|
|
|
def test_monotonic_timestamps(self):
|
|
"""확정 세그먼트 시간은 단조 증가 (불변식 3)."""
|
|
la = LocalAgreement(agreement_count=2)
|
|
la.feed(_segs(["첫 문장", "둘째 문장"]), 4.0)
|
|
la.feed(_segs(["첫 문장", "둘째 문장", "셋째 문장"]), 4.0)
|
|
ends = [s.end for s in la.state.confirmed_segments]
|
|
assert all(b >= a for a, b in zip(ends, ends[1:], strict=False))
|
|
|
|
def test_buffer_truncation_invariant(self):
|
|
"""확정 방출 후 보류 버퍼는 left_context 이하로 절단 (불변식 2)."""
|
|
la = LocalAgreement(agreement_count=2, retained_left_context_sec=1.0)
|
|
la.feed(_segs(["a", "b", "c"]), 3.0)
|
|
e2 = la.feed(_segs(["a", "b", "c", "d", "e", "f"]), 3.0)
|
|
assert len(e2["confirmed"]) >= 1
|
|
# audio offset은 마지막 확정 end - left_context
|
|
assert la.state.audio_offset_sec >= 0
|
|
|
|
def test_flush_confirms_pending(self):
|
|
la = LocalAgreement(agreement_count=2)
|
|
la.feed(_segs(["마지막 발화"]), 2.0)
|
|
flushed = la.flush()
|
|
assert flushed["confirmed"]
|
|
assert "마지막 발화" in flushed["new_text"]
|