import base64 import time 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_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