Files
VoiceStudio/tests/test_capture_null_segment_end.py
Palash DebnathandClaude Opus 5 7d46563ea8 fix(i18n): finish the zh-CN locale and make its own parity suite pass
#1877 completes the zh-CN translation and drops its ratchet to zero, but the
PR never ran today's gates — it has been conflicting, so CI reported nothing —
and the file it lands does not pass tests/test_locale_parity.py.

Three things fixed here:

- Twelve keys were declared twice inside the same object (timing_concise,
  autofit_quality, the plan_* set, the role_* set). Python's parser rejects a
  duplicate key outright, so the whole suite errored rather than failing one
  assertion. Deduped keeping the first occurrence, which is the block #1877
  actually translated.
- The `player` section appeared twice: the complete new one and an older
  two-key stub. JSON keeps the LAST, so the stub silently won and six keys
  vanished at runtime. The stub is gone.
- `settings.hf_source_*_label` appeared twice with slightly different wording.

The file is rewritten as canonical JSON (indent 2, non-ASCII preserved), which
is byte-identical to how en.json already serialises, so the format matches the
other locales exactly. zh-CN now has zero keys missing and zero beyond en.

Also fixes the review finding on #1959: the capture route picks its engine from
a `mode` form field, not an `accurate` flag, so parametrising on `accurate`
sent a field the route ignores and ran the default fast path twice. Both
engines are exercised now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
2026-09-09 13:24:06 -07:00

132 lines
4.6 KiB
Python

"""
A segment timing the engine could not determine arrives as ``end: None``, and
the REST `/transcribe` response builder used to raise on it.
`_sherpa_result()` (services/asr_backend.py) sets ``duration = None`` when it
cannot derive one from the sample rate, and `OpenAICompatASRBackend`
`_adapt_response()` emits ``end: None`` for every plain-text (`json`/`text`)
response from a server that rejects `verbose_json`. Both reach this endpoint —
sherpa is the first capture engine and the OpenAI-compatible backend is
selectable as the active one.
`round(s.get("end", 0), 2)` does not defend against that: ``.get`` returns the
stored ``None`` rather than the default, because the key is present. The route
answered 500 for a transcript that was otherwise fine, which is the server-side
half of #1904.
"""
import os
import pytest
os.environ.setdefault("OMNIVOICE_MODEL", "test")
os.environ.setdefault("OMNIVOICE_DISABLE_FILE_LOG", "1")
pytestmark = pytest.mark.usefixtures("asr_model_installed")
class _UntimedBackend:
"""What sherpa and the OpenAI-compatible server both hand back when no
timing is available: text, a start of 0.0, and an honest null end."""
id = "untimed"
def transcribe(self, _path, **_kw):
return {
"text": "the meeting is at three",
"segments": [
{"start": 0.0, "end": None, "text": "the meeting is at three"},
],
"language": "en",
}
class _PartlyTimedBackend:
"""One timed segment and one the engine gave up on — the duration must come
from the half that is known, not from the null."""
id = "partly-timed"
def transcribe(self, _path, **_kw):
return {
"text": "first second",
"segments": [
{"start": 0.0, "end": 1.25, "text": "first"},
{"start": 1.25, "end": None, "text": "second"},
],
"language": "en",
}
def _client(monkeypatch, backend):
from fastapi.testclient import TestClient
monkeypatch.setattr(
"services.asr_backend.get_capture_asr_backend", lambda **_k: backend())
monkeypatch.setattr(
"services.asr_backend.get_active_asr_backend", lambda **_k: backend())
monkeypatch.setattr(
"services.asr_backend.load_active_asr_backend", lambda **_k: backend())
from main import app
return TestClient(app, client=("127.0.0.1", 50000))
def _post(client, **data):
return client.post(
"/transcribe",
files={"audio": ("a.wav", b"\x00" * 32000, "audio/wav")},
data=data,
)
# The route picks its engine from a `mode` form field, not an `accurate`
# flag, so parametrising on `accurate` sent a field the route ignores and ran
# the default fast path twice. Both engines must pass a null end through.
@pytest.mark.parametrize("mode", ["fast", "accurate"])
def test_null_end_is_passed_through_not_rounded(monkeypatch, mode):
client = _client(monkeypatch, _UntimedBackend)
r = _post(client, mode=mode)
assert r.status_code == 200, r.text
body = r.json()
assert body["segments"][0]["end"] is None
assert body["segments"][0]["start"] == 0.0
assert body["segments"][0]["text"] == "the meeting is at three"
# Nothing known to measure, so the duration stays 0 rather than becoming null.
assert body["duration_s"] == 0.0
def test_duration_comes_from_the_timed_segments(monkeypatch):
client = _client(monkeypatch, _PartlyTimedBackend)
r = _post(client)
assert r.status_code == 200, r.text
body = r.json()
assert [s["end"] for s in body["segments"]] == [1.25, None]
assert body["duration_s"] == 1.25
# ── The live-dictation socket's own final-result builder ────────────────────
#
# Every existing capture_ws test stubs `_transcribe_buffer_full` out, so its
# response builder was never exercised. Call it directly: sherpa is the first
# capture engine and its `_sherpa_result` degrades to end=None, so this half is
# reachable through the default dictation path.
def test_ws_full_result_passes_null_end_through(monkeypatch):
import asyncio
from api.routers import capture_ws as cw
monkeypatch.setattr(
"services.asr_backend.get_capture_asr_backend",
lambda **_k: _PartlyTimedBackend())
async def _straight_through(_pool, run, **_kw):
return run()
monkeypatch.setattr(
"services.asr_backend.run_transcribe_guarded", _straight_through)
result = asyncio.run(
cw._transcribe_buffer_full([b"\x00" * 32000], pcm_sr=16000))
assert [s["end"] for s in result["segments"]] == [1.25, None]
assert result["duration_s"] == 1.25