238 lines
7.4 KiB
Python
238 lines
7.4 KiB
Python
import base64
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
import services.streaming_asr_sidecar_service.app as sidecar_module
|
|
|
|
|
|
def _admin_headers() -> dict[str, str]:
|
|
return {"X-User": "admin", "X-Role": "admin"}
|
|
|
|
|
|
def _pcm_chunk(duration_ms: int = 400, *, amplitude: int = 1000) -> bytes:
|
|
sample_count = int(8000 * (duration_ms / 1000.0))
|
|
return int(amplitude).to_bytes(2, "little", signed=True) * sample_count
|
|
|
|
|
|
class _FakeEngine:
|
|
def __init__(self, *, text: str = "привет") -> None:
|
|
self.text = text
|
|
self.calls: list[tuple[int, int, str | None]] = []
|
|
|
|
def transcribe_pcm(
|
|
self,
|
|
pcm_bytes: bytes,
|
|
*,
|
|
sample_rate_hz: int,
|
|
language_hint: str | None = None,
|
|
) -> sidecar_module.SidecarTranscript:
|
|
self.calls.append((len(pcm_bytes), sample_rate_hz, language_hint))
|
|
return sidecar_module.SidecarTranscript(
|
|
text=self.text,
|
|
language=language_hint or "ru",
|
|
confidence=0.91,
|
|
)
|
|
|
|
|
|
def _reset_sidecar(
|
|
monkeypatch,
|
|
*,
|
|
engine: _FakeEngine | None = None,
|
|
idle_ttl_seconds: float = 45.0,
|
|
supported_languages: set[str] | None = None,
|
|
) -> _FakeEngine:
|
|
fake_engine = engine or _FakeEngine()
|
|
monkeypatch.setattr(sidecar_module, "_ENGINE_INSTANCE", None)
|
|
monkeypatch.setattr(sidecar_module, "_ENGINE_FACTORY", lambda: fake_engine)
|
|
monkeypatch.setattr(sidecar_module, "_STREAMS", sidecar_module.StreamStore(idle_ttl_seconds=idle_ttl_seconds))
|
|
monkeypatch.setattr(
|
|
sidecar_module,
|
|
"_supported_languages",
|
|
lambda: set(supported_languages or {"ru"}),
|
|
)
|
|
return fake_engine
|
|
|
|
|
|
def test_streaming_asr_sidecar_stream_lifecycle(monkeypatch):
|
|
engine = _reset_sidecar(monkeypatch)
|
|
client = TestClient(sidecar_module.app)
|
|
|
|
open_response = client.post(
|
|
"/internal/asr/streams",
|
|
headers=_admin_headers(),
|
|
json={
|
|
"session_id": "avs_sidecar_1",
|
|
"language_hint": "ru",
|
|
"sample_rate_hz": 8000,
|
|
"encoding": "pcm_s16le",
|
|
},
|
|
)
|
|
assert open_response.status_code == 200
|
|
stream_id = open_response.json()["stream_id"]
|
|
|
|
chunk = _pcm_chunk(400)
|
|
push_response = client.post(
|
|
f"/internal/asr/streams/{stream_id}/chunks",
|
|
headers=_admin_headers(),
|
|
json={
|
|
"pcm_b64": base64.b64encode(chunk).decode("ascii"),
|
|
"sample_rate_hz": 8000,
|
|
"encoding": "pcm_s16le",
|
|
},
|
|
)
|
|
assert push_response.status_code == 200
|
|
assert push_response.json()["received_bytes"] == len(chunk)
|
|
|
|
partial_one = client.get(
|
|
f"/internal/asr/streams/{stream_id}/partial",
|
|
headers=_admin_headers(),
|
|
)
|
|
assert partial_one.status_code == 200
|
|
first_payload = partial_one.json()
|
|
assert first_payload["text"] == "привет"
|
|
assert first_payload["is_stable"] is False
|
|
|
|
second_chunk = _pcm_chunk(80)
|
|
client.post(
|
|
f"/internal/asr/streams/{stream_id}/chunks",
|
|
headers=_admin_headers(),
|
|
json={
|
|
"pcm_b64": base64.b64encode(second_chunk).decode("ascii"),
|
|
"sample_rate_hz": 8000,
|
|
"encoding": "pcm_s16le",
|
|
},
|
|
)
|
|
partial_two = client.get(
|
|
f"/internal/asr/streams/{stream_id}/partial",
|
|
headers=_admin_headers(),
|
|
)
|
|
assert partial_two.status_code == 200
|
|
second_payload = partial_two.json()
|
|
assert second_payload["text"] == "привет"
|
|
assert second_payload["is_stable"] is True
|
|
|
|
finalize_response = client.post(
|
|
f"/internal/asr/streams/{stream_id}/finalize",
|
|
headers=_admin_headers(),
|
|
)
|
|
assert finalize_response.status_code == 200
|
|
assert finalize_response.json() == {
|
|
"text": "привет",
|
|
"language": "ru",
|
|
"confidence": 0.91,
|
|
}
|
|
|
|
close_response = client.delete(
|
|
f"/internal/asr/streams/{stream_id}",
|
|
headers=_admin_headers(),
|
|
)
|
|
assert close_response.status_code == 200
|
|
assert close_response.json() == {"ok": True, "stream_id": stream_id}
|
|
assert len(engine.calls) >= 3
|
|
|
|
|
|
def test_streaming_asr_sidecar_partial_uses_recent_audio_tail(monkeypatch):
|
|
engine = _reset_sidecar(monkeypatch)
|
|
monkeypatch.setattr(sidecar_module, "_partial_max_audio_ms", lambda: 1800)
|
|
client = TestClient(sidecar_module.app)
|
|
|
|
open_response = client.post(
|
|
"/internal/asr/streams",
|
|
headers=_admin_headers(),
|
|
json={
|
|
"session_id": "avs_sidecar_tail",
|
|
"language_hint": "ru",
|
|
"sample_rate_hz": 8000,
|
|
"encoding": "pcm_s16le",
|
|
},
|
|
)
|
|
assert open_response.status_code == 200
|
|
stream_id = open_response.json()["stream_id"]
|
|
|
|
long_chunk = _pcm_chunk(3200)
|
|
push_response = client.post(
|
|
f"/internal/asr/streams/{stream_id}/chunks",
|
|
headers=_admin_headers(),
|
|
json={
|
|
"pcm_b64": base64.b64encode(long_chunk).decode("ascii"),
|
|
"sample_rate_hz": 8000,
|
|
"encoding": "pcm_s16le",
|
|
},
|
|
)
|
|
assert push_response.status_code == 200
|
|
|
|
partial_response = client.get(
|
|
f"/internal/asr/streams/{stream_id}/partial",
|
|
headers=_admin_headers(),
|
|
)
|
|
assert partial_response.status_code == 200
|
|
assert engine.calls
|
|
partial_len, sample_rate_hz, language_hint = engine.calls[-1]
|
|
assert sample_rate_hz == 8000
|
|
assert language_hint == "ru"
|
|
assert partial_len < len(long_chunk)
|
|
assert partial_len <= int(1.8 * 8000 * 2)
|
|
|
|
|
|
def test_streaming_asr_sidecar_rejects_unsupported_language(monkeypatch):
|
|
_reset_sidecar(monkeypatch, supported_languages={"ru"})
|
|
client = TestClient(sidecar_module.app)
|
|
|
|
response = client.post(
|
|
"/internal/asr/streams",
|
|
headers=_admin_headers(),
|
|
json={
|
|
"session_id": "avs_sidecar_kk",
|
|
"language_hint": "kk",
|
|
"sample_rate_hz": 8000,
|
|
"encoding": "pcm_s16le",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert "Unsupported streaming ASR language" in response.json()["detail"]
|
|
|
|
|
|
def test_streaming_asr_sidecar_cleans_up_idle_streams(monkeypatch):
|
|
_reset_sidecar(monkeypatch, idle_ttl_seconds=0.01)
|
|
client = TestClient(sidecar_module.app)
|
|
|
|
open_response = client.post(
|
|
"/internal/asr/streams",
|
|
headers=_admin_headers(),
|
|
json={
|
|
"session_id": "avs_sidecar_idle",
|
|
"language_hint": "ru",
|
|
"sample_rate_hz": 8000,
|
|
"encoding": "pcm_s16le",
|
|
},
|
|
)
|
|
assert open_response.status_code == 200
|
|
stream_id = open_response.json()["stream_id"]
|
|
|
|
with sidecar_module._STREAMS._lock:
|
|
sidecar_module._STREAMS._streams[stream_id].last_activity_monotonic = time.monotonic() - 1.0
|
|
|
|
health_response = client.get("/health")
|
|
assert health_response.status_code == 200
|
|
|
|
missing_response = client.get(
|
|
f"/internal/asr/streams/{stream_id}/partial",
|
|
headers=_admin_headers(),
|
|
)
|
|
assert missing_response.status_code == 404
|
|
|
|
|
|
def test_streaming_asr_sidecar_requirements_include_requests_dependency():
|
|
requirements_path = Path(__file__).resolve().parents[1] / "requirements.txt"
|
|
requirements = {
|
|
line.strip()
|
|
for line in requirements_path.read_text(encoding="utf-8").splitlines()
|
|
if line.strip() and not line.lstrip().startswith("#")
|
|
}
|
|
|
|
assert any(line.startswith("faster-whisper==") for line in requirements)
|
|
assert any(line.startswith("requests==") for line in requirements)
|