153 lines
6.3 KiB
Python
153 lines
6.3 KiB
Python
|
|
import asyncio
|
||
|
|
import json
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pytest
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
import stt_core as core
|
||
|
|
import stt_service as service
|
||
|
|
from conftest import SR, EnergyProb, FakeEngine, make_wav, silence, tone
|
||
|
|
|
||
|
|
|
||
|
|
def _client(engine=None, **settings):
|
||
|
|
settings = service.ServiceSettings(wyoming_port=None, **settings)
|
||
|
|
app = service.create_app(engine or FakeEngine(), settings,
|
||
|
|
core.VadConfig(silence_ms=400), prob_fn_factory=EnergyProb)
|
||
|
|
return TestClient(app)
|
||
|
|
|
||
|
|
|
||
|
|
def test_health():
|
||
|
|
with _client() as client:
|
||
|
|
data = client.get("/health").json()
|
||
|
|
assert data["status"] == "ok" and data["model"] == "fake" and data["queue"] == 0
|
||
|
|
|
||
|
|
|
||
|
|
def test_openai_transcriptions_formats():
|
||
|
|
wav = make_wav(tone(1.5))
|
||
|
|
with _client() as client:
|
||
|
|
files = {"file": ("a.wav", wav, "audio/wav")}
|
||
|
|
assert client.post("/v1/audio/transcriptions", files=files).json() == {"text": "audio 1.5s"}
|
||
|
|
r = client.post("/v1/audio/transcriptions", files=files, data={"response_format": "text"})
|
||
|
|
assert r.text == "audio 1.5s"
|
||
|
|
r = client.post("/v1/audio/transcriptions", files=files,
|
||
|
|
data={"response_format": "verbose_json", "language": "en"})
|
||
|
|
body = r.json()
|
||
|
|
assert body["language"] == "en" and body["segments"][0]["text"] == "audio 1.5s"
|
||
|
|
r = client.post("/v1/audio/transcriptions", files=files, data={"response_format": "srt"})
|
||
|
|
assert r.status_code == 400
|
||
|
|
|
||
|
|
|
||
|
|
def test_transcribe_raw_pcm_with_resampling_and_channel():
|
||
|
|
engine = FakeEngine()
|
||
|
|
stereo = np.stack([tone(1.0, rate=48000), silence(1.0, rate=48000)], axis=1)
|
||
|
|
pcm = (stereo * 32767).astype("<i2").tobytes()
|
||
|
|
with _client(engine) as client:
|
||
|
|
r = client.post("/transcribe?format=pcm&sample_rate=48000&channels=2&channel=0", content=pcm)
|
||
|
|
assert r.status_code == 200, r.text
|
||
|
|
assert r.json()["duration"] == 1.0
|
||
|
|
assert abs(engine.calls[0]["samples"] - SR) <= 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_transcribe_raw_wav_and_errors():
|
||
|
|
with _client(max_upload_mb=1) as client:
|
||
|
|
assert client.post("/transcribe", content=make_wav(tone(0.5))).json()["text"] == "audio 0.5s"
|
||
|
|
assert client.post("/transcribe", content=b"kaputt" * 100).status_code == 400
|
||
|
|
assert client.post("/transcribe", content=b"").status_code == 400
|
||
|
|
assert client.post("/transcribe", content=b"\0" * (2 * 1024 * 1024)).status_code == 413
|
||
|
|
|
||
|
|
|
||
|
|
def test_overload_returns_503():
|
||
|
|
engine = FakeEngine(delay=0.3)
|
||
|
|
app = service.create_app(engine, service.ServiceSettings(wyoming_port=None, max_queue=1),
|
||
|
|
prob_fn_factory=EnergyProb)
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
async def scenario():
|
||
|
|
transport = httpx.ASGITransport(app=app)
|
||
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://t") as client:
|
||
|
|
wav = make_wav(tone(0.5))
|
||
|
|
results = await asyncio.gather(*[client.post("/transcribe", content=wav) for _ in range(3)])
|
||
|
|
return sorted(r.status_code for r in results)
|
||
|
|
|
||
|
|
codes = asyncio.run(scenario())
|
||
|
|
assert codes[0] == 200 and 503 in codes
|
||
|
|
|
||
|
|
|
||
|
|
def test_health_stays_responsive_during_transcription():
|
||
|
|
"""Nicht blockierend: /health antwortet, während der Worker rechnet."""
|
||
|
|
import time
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
engine = FakeEngine(delay=1.0)
|
||
|
|
app = service.create_app(engine, service.ServiceSettings(wyoming_port=None),
|
||
|
|
prob_fn_factory=EnergyProb)
|
||
|
|
|
||
|
|
async def scenario():
|
||
|
|
transport = httpx.ASGITransport(app=app)
|
||
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://t") as client:
|
||
|
|
job = asyncio.create_task(client.post("/transcribe", content=make_wav(tone(0.5))))
|
||
|
|
deadline = time.perf_counter() + 5
|
||
|
|
while app.state.worker.pending == 0 and time.perf_counter() < deadline:
|
||
|
|
await asyncio.sleep(0.01) # warten, bis der Auftrag im Worker rechnet
|
||
|
|
t0 = time.perf_counter()
|
||
|
|
health = await client.get("/health")
|
||
|
|
elapsed = time.perf_counter() - t0
|
||
|
|
await job
|
||
|
|
return health.json(), elapsed
|
||
|
|
|
||
|
|
health, elapsed = asyncio.run(scenario())
|
||
|
|
assert health["queue"] == 1 and elapsed < 0.5
|
||
|
|
|
||
|
|
|
||
|
|
def _receive_until(ws, wanted_type, limit=20):
|
||
|
|
events = []
|
||
|
|
for _ in range(limit):
|
||
|
|
event = ws.receive_json()
|
||
|
|
events.append(event)
|
||
|
|
if event["type"] == wanted_type:
|
||
|
|
return events
|
||
|
|
raise AssertionError(f"{wanted_type} nicht empfangen: {events}")
|
||
|
|
|
||
|
|
|
||
|
|
def _pcm(audio):
|
||
|
|
return (np.clip(audio, -1, 1) * 32767).astype("<i2").tobytes()
|
||
|
|
|
||
|
|
|
||
|
|
def test_ws_stream_with_vad_emits_transcripts_per_utterance():
|
||
|
|
audio = np.concatenate([silence(0.3), tone(1.0), silence(0.8), tone(0.6), silence(0.3)])
|
||
|
|
with _client() as client, client.websocket_connect("/ws/stream") as ws:
|
||
|
|
assert ws.receive_json()["type"] == "ready"
|
||
|
|
ws.send_text(json.dumps({"type": "start", "sample_rate": 16000, "language": "de"}))
|
||
|
|
assert ws.receive_json()["type"] == "ready"
|
||
|
|
for i in range(0, audio.size, 1600):
|
||
|
|
ws.send_bytes(_pcm(audio[i:i + 1600]))
|
||
|
|
ws.send_text(json.dumps({"type": "end"}))
|
||
|
|
events = _receive_until(ws, "done")
|
||
|
|
transcripts = [e for e in events if e["type"] == "transcript"]
|
||
|
|
assert [e["reason"] for e in transcripts] == ["silence", "flush"]
|
||
|
|
assert sum(e["type"] == "speech_start" for e in events) == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_ws_stream_without_vad_and_resampling():
|
||
|
|
audio = tone(2.0, rate=48000)
|
||
|
|
with _client() as client, client.websocket_connect("/ws/stream") as ws:
|
||
|
|
ws.receive_json()
|
||
|
|
ws.send_text(json.dumps({"type": "start", "sample_rate": 48000, "vad": False}))
|
||
|
|
ws.receive_json()
|
||
|
|
for i in range(0, audio.size, 4800):
|
||
|
|
ws.send_bytes(_pcm(audio[i:i + 4800]))
|
||
|
|
ws.send_text(json.dumps({"type": "end"}))
|
||
|
|
events = _receive_until(ws, "done")
|
||
|
|
(transcript,) = [e for e in events if e["type"] == "transcript"]
|
||
|
|
assert transcript["duration"] == pytest.approx(2.0, abs=0.02)
|
||
|
|
|
||
|
|
|
||
|
|
def test_ws_invalid_control_frame_keeps_connection():
|
||
|
|
with _client() as client, client.websocket_connect("/ws/stream") as ws:
|
||
|
|
ws.receive_json()
|
||
|
|
ws.send_text("kein json")
|
||
|
|
assert ws.receive_json()["type"] == "error"
|
||
|
|
ws.send_text(json.dumps({"type": "end"}))
|
||
|
|
assert ws.receive_json()["type"] == "done"
|