Merge feat/297-cheaper-ai into feat/299-correlation-simplify

Resolve upload.py (no-scene fallback uses the new _correlate with #298's
non_event_chatter reason) and chatter_classifier docstring (gate built, off by
default). Fold intelligence._llm_bool into incident_correlator.parse_llm_bool (#88).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-27 20:31:54 -04:00
co-authored by Claude Opus 5.5
22 changed files with 2046 additions and 231 deletions
+10
View File
@@ -96,3 +96,13 @@ def _no_real_discord(monkeypatch):
monkeypatch.setattr(discord_rest, "_transport", httpx.MockTransport(_offline))
yield
discord_rest.reset()
@pytest.fixture(autouse=True)
def _fresh_geocode_cache():
"""The geocode cache (server-26#298) is process-wide; a test that stubs
Maps must never be answered by a previous test's stub."""
from app.internal import intelligence
intelligence.clear_geocode_cache()
yield
intelligence.clear_geocode_cache()
+8 -2
View File
@@ -219,15 +219,21 @@ async def test_transcript_correction_is_skipped_when_its_flag_is_off():
patch.object(transcription, "fstore") as fs, \
patch.object(transcription, "ai_health") as health, \
patch.object(transcription, "transcript_correction") as tc, \
patch("asyncio.to_thread", AsyncMock(return_value=("units respond", [], False))):
patch.object(transcription.settings, "openai_api_key", "k"), \
patch.object(transcription.settings, "groq_api_key", None), \
patch.object(transcription, "_download", lambda uri: "/nonexistent.mp3"), \
patch.object(transcription, "_sync_transcribe_file", lambda *a, **k: {
"text": "units respond to the fire on Main Street", "segments": [],
"degenerate": False, "duration": 3.0, "confidence": None}):
fs.doc_set = AsyncMock()
health.report_healthy = AsyncMock()
health.report_failure = AsyncMock()
tc.correct = AsyncMock()
await transcription.transcribe_call(
text, _ = await transcription.transcribe_call(
"call-1", "gs://bucket/call-1.mp3", "PD Dispatch", system_id="sys-1"
)
assert text == "units respond to the fire on Main Street", "the transcript must still be produced"
tc.correct.assert_not_awaited()
+168
View File
@@ -0,0 +1,168 @@
"""
server-26#300 — confidence gate before any push alert.
CrimeRadar pushed "shots fired at an elementary school" after STT heard "fire
alarm" as "firearm". A single uncorroborated transcript hit on a
high-severity term must be held, not pushed; a corroborated incident releases
it; low-confidence matches never push.
"""
import pytest
from unittest.mock import AsyncMock, patch
from app.internal import alerter
from tests.fake_firestore import FakeFirestore
def _ev(**over):
base = {"incident_ids": [], "max_calls": 0, "severity": None,
"stt_confidence": None, "not_speech": False}
base.update(over)
return base
# ── the pure decision ───────────────────────────────────────────────────────
def test_talkgroup_only_rule_always_pushes():
v = alerter._assess(["talkgroup:9048"], [], _ev(stt_confidence=0.1))
assert v["push"] and v["confidence"] == 1.0
def test_transcript_only_firearm_single_call_is_held():
v = alerter._assess(["firearm"], [], _ev())
assert not v["push"] and v["hold_reason"] == "needs_corroboration"
assert v["high_severity"] is True
def test_tagged_shots_fired_still_needs_corroboration():
v = alerter._assess(["shots-fired"], ["shots-fired"], _ev(max_calls=1))
assert not v["push"] and v["confidence"] == 0.8
def test_corroborated_high_severity_pushes():
v = alerter._assess(["shots-fired"], ["shots-fired"], _ev(max_calls=2))
assert v["push"]
def test_major_incident_makes_any_content_match_high_severity():
v = alerter._assess(["school"], [], _ev(severity="major"))
assert v["high_severity"] and not v["push"]
def test_ordinary_tag_match_pushes():
v = alerter._assess(["mva"], ["mva"], _ev(max_calls=1))
assert v["push"] and not v["high_severity"]
def test_low_stt_confidence_holds_even_ordinary_matches():
v = alerter._assess(["mva"], ["mva"], _ev(stt_confidence=0.1))
assert not v["push"] and v["hold_reason"] == "low_confidence"
assert v["confidence"] == round(0.8 * 0.1 / 0.3, 3)
def test_typical_radio_logprob_does_not_hold_a_transcript_match():
"""exp(avg_logprob) of ~0.4 is normal on vocoded audio (review of #300)."""
v = alerter._assess(["mva"], [], _ev(stt_confidence=0.4))
assert v["push"]
def test_not_speech_never_pushes_content():
v = alerter._assess(["mva"], ["mva"], _ev(not_speech=True))
assert not v["push"]
# ── end to end through Firestore ────────────────────────────────────────────
RULE = {"rule_id": "r1", "name": "Weapons", "enabled": True, "org_id": "org-1",
"keywords": ["firearm", "shots-fired"], "discord_webhook": "https://discord.example/wh"}
@pytest.fixture
def fs(monkeypatch):
fake = FakeFirestore.install(monkeypatch)
fake.put("alert_rules", "r1", dict(RULE))
return fake
def _posts():
posted = []
class _Client:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
async def post(self, url, json=None, **k):
posted.append(json)
return posted, patch("httpx.AsyncClient", lambda *a, **k: _Client())
async def _dispatch(call_id, tags, transcript):
await alerter.check_and_dispatch(call_id=call_id, node_id="n1", talkgroup_id=1,
talkgroup_name="PD Dispatch", tags=tags, transcript=transcript)
@pytest.mark.asyncio
async def test_crimeradar_case_is_held_then_released_by_corroboration(fs):
fs.put("calls", "c1", {"org_id": "org-1", "incident_ids": ["inc-1"]})
fs.put("incidents", "inc-1", {"call_ids": ["c1"], "severity": "moderate"})
posted, http = _posts()
with http:
await _dispatch("c1", [], "report of a firearm at the elementary school")
assert posted == [], "a single uncorroborated firearm hit must not push"
assert fs.data.get("alert_events", {}) == {}
(hold,) = fs.data["alert_holds"].values()
assert hold["status"] == "held" and hold["hold_reason"] == "needs_corroboration"
assert hold["incident_id"] == "inc-1"
# A second call joins the incident; it does not itself mention a firearm.
fs.put("calls", "c2", {"org_id": "org-1", "incident_ids": ["inc-1"]})
fs.put("incidents", "inc-1", {"call_ids": ["c1", "c2"], "severity": "moderate"})
with http:
await _dispatch("c2", [], "units on scene, stand by")
assert len(posted) == 1 and "Weapons" in posted[0]["content"]
(event,) = fs.data["alert_events"].values()
assert event["released_from_hold"] is True and event["corroborated_by_call_id"] == "c2"
assert fs.get("alert_holds", hold["alert_id"])["status"] == "released"
@pytest.mark.asyncio
async def test_low_confidence_hold_is_not_released_by_corroboration(fs):
fs.put("calls", "c1", {"org_id": "org-1", "incident_ids": ["inc-1"], "stt_confidence": 0.1})
fs.put("incidents", "inc-1", {"call_ids": ["c1"]})
posted, http = _posts()
with http:
await _dispatch("c1", [], "firearm")
(hold,) = fs.data["alert_holds"].values()
assert hold["hold_reason"] == "low_confidence"
fs.put("calls", "c2", {"org_id": "org-1", "incident_ids": ["inc-1"]})
fs.put("incidents", "inc-1", {"call_ids": ["c1", "c2"]})
with http:
await _dispatch("c2", [], "units on scene")
assert posted == []
assert fs.get("alert_holds", hold["alert_id"])["status"] == "held"
@pytest.mark.asyncio
async def test_rule_pushed_now_supersedes_its_hold_instead_of_alerting_twice(fs):
fs.put("calls", "c1", {"org_id": "org-1", "incident_ids": ["inc-1"]})
fs.put("incidents", "inc-1", {"call_ids": ["c1"]})
posted, http = _posts()
with http:
await _dispatch("c1", ["shots-fired"], "shots fired")
fs.put("calls", "c2", {"org_id": "org-1", "incident_ids": ["inc-1"]})
fs.put("incidents", "inc-1", {"call_ids": ["c1", "c2"]})
with http:
await _dispatch("c2", ["shots-fired"], "more shots fired")
assert len(posted) == 1
(hold,) = fs.data["alert_holds"].values()
assert hold["status"] == "superseded"
@pytest.mark.asyncio
async def test_held_alerts_are_listed_for_admins(fs):
from app.routers import alerts as alerts_router
fs.put("alert_holds", "h1", {"alert_id": "h1", "status": "held", "triggered_at": "2026-10-27T01:00:00"})
fs.put("alert_holds", "h2", {"alert_id": "h2", "status": "released", "triggered_at": "2026-10-27T02:00:00"})
out = await alerts_router.list_held_alerts(status="held", org_id=None, _={})
assert [h["alert_id"] for h in out] == ["h1"]
+194
View File
@@ -0,0 +1,194 @@
"""
server-26#298 — the chatter pre-filter gates every paid call.
Radio housekeeping (roll call, a bare "Post 4, 10-8, 10-4") used to go through
transcript correction, scene extraction, an embedding per scene and a geocode,
only to come back as a thin routine scene. Now it pays for none of them; the
free rule paths (clearance parse, self-initiated/plate backstop) still run.
"""
from unittest.mock import AsyncMock, patch
import pytest
from app.config import settings
from app.internal import intelligence, transcription
from app.internal.chatter_classifier import gate_chatter
@pytest.fixture(autouse=True)
def _gate_on():
"""The gate ships OFF (shadow) until a live window clears it (#127);
these tests exercise it switched on."""
with patch.object(settings, "chatter_gate_enabled", True):
yield
class _NoPaidCalls:
"""Fail the test if any paid extraction/embedding/geocode call happens."""
def __enter__(self):
def _boom(*a, **k):
raise AssertionError("a paid AI call was made for chatter")
async def _aboom(*a, **k):
raise AssertionError("a paid geocode was made for chatter")
self.ps = [
patch.object(intelligence, "_sync_extract", _boom),
patch.object(intelligence, "_sync_embed", _boom),
patch.object(intelligence, "_geocode_lookup", _aboom),
]
for p in self.ps:
p.start()
return self
def __exit__(self, *a):
for p in self.ps:
p.stop()
async def _extract(transcript, talkgroup_name="PD Dispatch"):
saved: dict = {}
async def _doc_set(coll, doc_id, data, merge=True):
saved.update(data)
with patch.object(intelligence.fstore, "doc_get_cached", AsyncMock(return_value=None)), \
patch.object(intelligence.fstore, "doc_set", _doc_set):
scenes = await intelligence.extract_scenes("call-1", transcript, talkgroup_name)
return scenes, saved
@pytest.mark.asyncio
async def test_bare_acknowledgement_over_five_words_skips_extraction():
with _NoPaidCalls():
scenes, saved = await _extract("10-4, 10-4 Central, 98. 10-4, 98.")
assert scenes == []
assert saved["skip_reason"] == "non_event_chatter"
assert saved["severity"] == "routine"
assert saved["chatter_classifier_verdict"] is True
@pytest.mark.asyncio
async def test_whole_roll_call_skips_extraction():
roll = ("Car 100, roll call. Henry 1. Henry 1. Sam 1. Sam 1. 45 Baker. 45 Baker. "
"11 Adam. Adam. 11 Baker. Baker. Staff 1. 1. Staff 2.")
with _NoPaidCalls():
scenes, saved = await _extract(roll)
assert scenes == [] and saved["skip_reason"] == "non_event_chatter"
@pytest.mark.asyncio
async def test_roll_call_mentioned_on_the_way_to_a_real_job_still_extracts():
"""The #127 review's false-positive shape must still reach the model."""
text = "Delayed for roll call, then respond to a structure fire at 12 Main Street"
assert gate_chatter(text) == (False, None)
with patch.object(intelligence, "_sync_extract", return_value=[]) as ext:
await _extract(text)
ext.assert_called_once()
@pytest.mark.asyncio
async def test_plate_read_chatter_still_becomes_a_stop_for_free():
text = "Lincoln Charlie Robert 7-4-0-7, Lincoln Charlie Robert 7-4-0-7."
with _NoPaidCalls():
scenes, saved = await _extract(text)
assert len(scenes) == 1 and scenes[0]["tags"] == ["traffic-stop"]
assert saved["severity"] == "minor"
@pytest.mark.asyncio
async def test_gate_off_restores_shadow_mode():
with patch.object(settings, "chatter_gate_enabled", False), \
patch.object(intelligence, "_sync_extract", return_value=[]) as ext:
_, saved = await _extract("10-4, 10-4 Central, 98. 10-4, 98.")
ext.assert_called_once()
assert saved["chatter_classifier_verdict"] is True, "the shadow verdict is still recorded"
@pytest.mark.asyncio
async def test_real_event_is_not_gated():
with patch.object(intelligence, "_sync_extract", return_value=[]) as ext:
await _extract("Shots fired, officer down, requesting backup immediately at the school.")
ext.assert_called_once()
@pytest.mark.asyncio
async def test_transcription_skips_correction_for_chatter():
flags = {"transcript_correction_enabled": True}
with patch.object(settings, "openai_api_key", "k"), \
patch.object(settings, "groq_api_key", None), \
patch.object(transcription, "_download", lambda uri: "/nonexistent.flac"), \
patch.object(transcription, "_sync_transcribe_file", lambda *a, **k: {
"text": "Post 4, 10-8. 10-4. Copy, 98.", "segments": [],
"degenerate": False, "duration": 2.0, "confidence": None}), \
patch.object(transcription.fstore, "doc_set", AsyncMock()), \
patch.object(transcription, "ai_health") as health, \
patch.object(transcription.transcript_correction, "correct", AsyncMock()) as correct, \
patch("app.internal.feature_flags.resolve_flags",
AsyncMock(return_value=(flags, lambda k: flags.get(k, True)))):
health.report_healthy = AsyncMock()
text, _ = await transcription.transcribe_call("call-1", "gs://b/c.flac", "PD Dispatch")
assert text == "Post 4, 10-8. 10-4. Copy, 98."
correct.assert_not_awaited()
@pytest.mark.asyncio
async def test_chatter_still_reaches_the_free_thin_correlation_path():
"""non_event_chatter joins transcript_too_short in upload.py's no-scene
fallback (rules-only for a thin call — no LLM)."""
from app.routers import upload
with patch.object(upload, "fstore") as fs, \
patch.object(upload, "_correlate", AsyncMock(return_value=(None, None))) as corr:
fs.doc_get = AsyncMock(return_value={"skip_reason": "non_event_chatter", "severity": "routine"})
fs.doc_set = AsyncMock()
await upload._extract_and_correlate(
call_id="c1", node_id="n1", system_id="s1", talkgroup_id=1,
talkgroup_name="PD Dispatch", transcript="10-4 Central, 98.", scenes=[],
)
corr.assert_awaited_once()
@pytest.mark.asyncio
async def test_no_scene_fallback_passes_the_call_doc_severity():
"""server-26#104: a reprocessed call whose doc says 'major' must not be
re-correlated as 'routine' (thin, no fit check)."""
from app.routers import upload
with patch.object(upload, "fstore") as fs, \
patch.object(upload, "_correlate", AsyncMock(return_value=(None, None))) as corr:
fs.doc_get = AsyncMock(return_value={"severity": "major"})
fs.doc_set = AsyncMock()
await upload._extract_and_correlate(
call_id="c1", node_id="n1", system_id="s1", talkgroup_id=1,
talkgroup_name="PD Dispatch", transcript="whatever", scenes=[],
)
assert corr.await_args.kwargs["severity"] == "major"
@pytest.mark.asyncio
async def test_gate_off_still_corrects_chatter():
"""With the gate off, transcription must not skip correction either."""
flags = {"transcript_correction_enabled": True}
with patch.object(settings, "chatter_gate_enabled", False), \
patch.object(settings, "openai_api_key", "k"), \
patch.object(settings, "groq_api_key", None), \
patch.object(transcription, "_download", lambda uri: "/nonexistent.flac"), \
patch.object(transcription, "_sync_transcribe_file", lambda *a, **k: {
"text": "Post 4, 10-8. 10-4. Copy, 98.", "segments": [],
"degenerate": False, "duration": 2.0, "confidence": None}), \
patch.object(transcription.fstore, "doc_set", AsyncMock()), \
patch.object(transcription, "ai_health") as health, \
patch.object(transcription.transcript_correction, "correct",
AsyncMock(return_value=(None, None, False))) as correct, \
patch("app.internal.feature_flags.resolve_flags",
AsyncMock(return_value=(flags, lambda k: flags.get(k, True)))):
health.report_healthy = AsyncMock()
await transcription.transcribe_call("call-1", "gs://b/c.flac", "PD Dispatch")
correct.assert_awaited_once()
def test_gate_ships_off():
from app.config import Settings
assert Settings.model_fields["chatter_gate_enabled"].default is False
+162
View File
@@ -0,0 +1,162 @@
"""
server-26#298 — geocode result cache in front of the Google Geocoding API,
and server-26#160 — place verification gets an anchor from the talkgroup's
own name when nobody configured one.
"""
from unittest.mock import AsyncMock, patch
import pytest
import app.internal.intelligence as intel
from app.config import settings
from app.internal import place_verifier as pv
ANCHOR = {"lat": 41.16, "lng": -73.86, "radius_km": 10.0}
class _Counter:
def __init__(self, payload=None, exc=None):
self.calls = 0
self.payload = payload
self.exc = exc
def patch(self):
outer = self
class _Resp:
def raise_for_status(self):
if outer.exc:
raise outer.exc
def json(self):
return outer.payload
class _Client:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
async def get(self, *a, **k):
outer.calls += 1
return _Resp()
return patch("httpx.AsyncClient", lambda *a, **k: _Client())
def _ok(lat=41.161, lng=-73.861):
return {"status": "OK", "results": [{"geometry": {
"location": {"lat": lat, "lng": lng}, "location_type": "ROOFTOP"}}]}
@pytest.fixture(autouse=True)
def _key():
with patch.object(settings, "google_maps_api_key", "test-key"):
yield
@pytest.mark.asyncio
async def test_repeat_address_is_one_request():
c = _Counter(_ok())
with c.patch():
a = await intel._geocode_location("12 Main Street, Ossining", anchor=ANCHOR)
b = await intel._geocode_location("12 main street, ossining ", anchor=ANCHOR)
assert a == b == {"lat": 41.161, "lng": -73.861, "location_type": "ROOFTOP"}
assert c.calls == 1
assert intel.geocode_cache_stats == {"hits": 1, "misses": 1}
@pytest.mark.asyncio
async def test_cached_answer_is_still_judged_against_the_caller_anchor():
c = _Counter(_ok())
with c.patch():
assert await intel._geocode_location("12 Main Street", anchor=ANCHOR)
far = {"lat": 0.0, "lng": 0.0, "radius_km": 10.0}
# Different bounds -> different key, so a separate request — never a
# cached answer leaking past a different area's distance check.
assert await intel._geocode_location("12 Main Street", anchor=far) is None
assert c.calls == 2
@pytest.mark.asyncio
async def test_zero_results_is_cached_but_errors_are_not():
c = _Counter({"status": "ZERO_RESULTS", "results": []})
with c.patch():
await intel._geocode_location("Cool Parts, Illinois", anchor=ANCHOR)
await intel._geocode_location("Cool Parts, Illinois", anchor=ANCHOR)
assert c.calls == 1
denied = _Counter({"status": "REQUEST_DENIED"})
with denied.patch():
await intel._geocode_location("Snowden Avenue", anchor=ANCHOR)
await intel._geocode_location("Snowden Avenue", anchor=ANCHOR)
assert denied.calls == 2, "a key/quota failure is not an answer about the address"
boom = _Counter(_ok(), exc=RuntimeError("503"))
with boom.patch():
await intel._geocode_location("Croton Point", anchor=ANCHOR)
await intel._geocode_location("Croton Point", anchor=ANCHOR)
assert boom.calls == 2
@pytest.mark.asyncio
async def test_ttl_expiry_and_size_bound():
c = _Counter(_ok())
with c.patch(), patch.object(settings, "geocode_cache_ttl_seconds", 0):
await intel._geocode_location("12 Main Street", anchor=ANCHOR)
await intel._geocode_location("12 Main Street", anchor=ANCHOR)
assert c.calls == 2, "TTL 0 disables the cache"
c = _Counter(_ok())
with c.patch(), patch.object(settings, "geocode_cache_max_entries", 2):
for street in ("A Street", "B Street", "C Street"):
await intel._geocode_location(street, anchor=ANCHOR)
assert len(intel._geocode_cache) == 2
# ── server-26#160 ───────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_unconfigured_channel_gets_an_anchor_from_its_talkgroup_name():
resolved = {"center": {"lat": 40.71, "lng": -74.0}, "radius_km": 25.0}
seen = {}
async def _geo(query, anchor):
seen["query"], seen["anchor"] = query, anchor
return {"lat": 40.7, "lng": -74.0}
with patch.object(pv.settings, "place_verification_enabled", True), \
patch.object(intel.fstore, "doc_get_cached", AsyncMock(return_value={"lat": 41.2, "lon": -73.8})), \
patch.object(intel, "_get_node_state", AsyncMock(return_value="New York")), \
patch.object(pv.area_context, "resolve_anchor", AsyncMock(return_value=resolved)) as res, \
patch.object(pv, "_geocode_in_anchor", _geo):
await pv.verify("c1", "Respond to Lexington Avenue", None, ["Lexington Avenue"],
{}, {}, talkgroup_name="New York City PD", node_id="node-1")
res.assert_awaited_once_with({"municipality": "New York City", "state": "New York"})
assert seen["anchor"] == {"lat": 40.71, "lng": -74.0, "radius_km": 25.0}
assert seen["query"] == "Lexington Avenue, New York City, New York"
@pytest.mark.asyncio
async def test_no_state_means_no_derived_anchor():
"""'Springfield PD' alone could anchor anywhere — skip, never guess."""
with patch.object(pv.settings, "place_verification_enabled", True), \
patch.object(intel.fstore, "doc_get_cached", AsyncMock(return_value={"lat": 41.2, "lon": -73.8})), \
patch.object(intel, "_get_node_state", AsyncMock(return_value="")), \
patch.object(pv, "_geocode_in_anchor") as geo:
out = await pv.verify("c1", "Respond to Main Street", None, ["Main Street"],
{}, {}, talkgroup_name="Springfield PD", node_id="node-1")
assert out == (None, None)
geo.assert_not_called()
@pytest.mark.asyncio
async def test_configured_place_without_anchor_still_skips():
"""A place that is set but too wide (no anchor stored) keeps the
load-bearing skip; the talkgroup-name fallback is only for unset areas."""
with patch.object(pv.settings, "place_verification_enabled", True), \
patch.object(pv, "_talkgroup_anchor", AsyncMock()) as derived, \
patch.object(pv, "_geocode_in_anchor") as geo:
out = await pv.verify("c1", "Main Street", None, ["Main Street"],
{"state": "Colorado"}, {}, talkgroup_name="Denver PD", node_id="n")
assert out == (None, None)
derived.assert_not_awaited()
geo.assert_not_called()
@@ -0,0 +1,14 @@
"""server-26#88 — resolved/reassignment are parsed, not bool()-coerced."""
import pytest
from app.internal.incident_correlator import parse_llm_bool as _llm_bool
@pytest.mark.parametrize("value,expected", [
(True, True), (False, False),
("true", True), ("TRUE", True), (" Yes ", True), ("yes", True),
("false", False), ("False", False), ("no", False), ("NO", False),
("", False), ("maybe", False), (None, False), (1, False), (0, False), ([], False),
])
def test_llm_bool(value, expected):
assert _llm_bool(value) is expected
+279
View File
@@ -0,0 +1,279 @@
"""
server-26#297 — config-driven STT provider with OpenAI as the fallback.
The default must stay OpenAI until a Groq key exists (so prod does not break
before the key is provisioned), a failing primary must fall back to whisper-1
without losing the call, and the vocabulary prompt must never carry a
ten-code series (the hallucination documented in transcription.py).
"""
from unittest.mock import AsyncMock, patch
import pytest
from app.internal import transcription as tr
from app.config import settings
def _settings(**over):
base = dict(stt_provider="auto", groq_api_key=None, openai_api_key="sk-openai",
stt_api_key=None, stt_base_url=None, stt_fallback_to_openai=True,
stt_model="whisper-1", stt_compat_model="whisper-large-v3-turbo")
base.update(over)
return [patch.object(settings, k, v) for k, v in base.items()]
class _patched:
def __init__(self, **over):
self.ps = _settings(**over)
def __enter__(self):
for p in self.ps:
p.start()
def __exit__(self, *a):
for p in self.ps:
p.stop()
# ── provider resolution ─────────────────────────────────────────────────────
def test_default_without_groq_key_is_openai_only():
with _patched():
targets = tr.stt_targets()
assert [(t.provider, t.model, t.tier) for t in targets] == [("openai", "whisper-1", "transcription")]
assert targets[0].vocab_prompt is False, "the OpenAI path keeps today's fixed prompt"
def test_groq_key_makes_groq_primary_with_openai_fallback():
with _patched(groq_api_key="gsk"):
targets = tr.stt_targets()
assert [(t.provider, t.model) for t in targets] == [
("groq", "whisper-large-v3-turbo"), ("openai", "whisper-1")]
assert targets[0].base_url == "https://api.groq.com/openai/v1"
assert targets[0].api_key == "gsk"
assert targets[1].tier == "transcription_fallback"
def test_fallback_can_be_switched_off():
with _patched(groq_api_key="gsk", stt_fallback_to_openai=False):
assert [t.provider for t in tr.stt_targets()] == ["groq"]
def test_no_openai_key_means_no_fallback():
with _patched(groq_api_key="gsk", openai_api_key=None):
assert [t.provider for t in tr.stt_targets()] == ["groq"]
def test_compat_server_from_base_url():
with _patched(stt_base_url="http://whisper.local/v1", stt_compat_model="large-v3"):
targets = tr.stt_targets()
assert targets[0].provider == "compat" and targets[0].base_url == "http://whisper.local/v1"
assert targets[0].model == "large-v3"
def test_explicit_groq_without_key_degrades_to_openai():
with _patched(stt_provider="groq"):
assert [t.provider for t in tr.stt_targets()] == ["openai"]
def test_nothing_configured_is_empty():
with _patched(openai_api_key=None):
assert tr.stt_targets() == []
# ── fallback in transcribe_call ─────────────────────────────────────────────
def _ok(text="Engine 5 responding to a structure fire on Main Street"):
return {"text": text, "segments": [{"start": 0.0, "end": 3.0, "text": text}],
"degenerate": False, "duration": 3.0, "confidence": 0.81}
async def _transcribe(sync_impl, **over):
saved: dict = {}
async def _doc_set(coll, doc_id, data, merge=True):
saved.update(data)
flags = {"transcript_correction_enabled": False}
with _patched(**over), \
patch.object(tr, "_download", lambda uri: "/nonexistent.flac"), \
patch.object(tr, "_sync_transcribe_file", sync_impl), \
patch.object(tr.transcript_correction, "resolve_context", AsyncMock(return_value={})), \
patch.object(tr.fstore, "doc_set", _doc_set), \
patch.object(tr, "ai_health") as health, \
patch("app.internal.feature_flags.resolve_flags",
AsyncMock(return_value=(flags, lambda k: flags.get(k, True)))):
health.report_healthy = AsyncMock()
health.report_degraded = AsyncMock()
health.classify = lambda text: "transient"
result = await tr.transcribe_call("call-1", "gs://b/call-1.flac", "PD Dispatch", system_id="sys-1")
return result, saved, health
@pytest.mark.asyncio
async def test_primary_failure_falls_back_to_openai():
seen = []
def sync(path, target, prompt, glossary=None):
seen.append(target.provider)
if target.provider == "groq":
raise RuntimeError("503 Service Unavailable")
return _ok()
(text, segments), saved, health = await _transcribe(sync, groq_api_key="gsk")
assert seen == ["groq", "openai"]
assert text.startswith("Engine 5")
assert saved["stt_provider"] == "openai" and saved["stt_model"] == "whisper-1"
# The primary stays red; only the fallback tier is marked healthy.
assert health.report_degraded.await_args.args[:3] == ("transcription", "groq", "whisper-large-v3-turbo")
health.report_healthy.assert_awaited_once_with("transcription_fallback")
@pytest.mark.asyncio
async def test_primary_success_records_provider_and_cost_inputs():
(text, _), saved, health = await _transcribe(lambda *a, **k: _ok(), groq_api_key="gsk")
assert saved["stt_provider"] == "groq"
assert saved["stt_model"] == "whisper-large-v3-turbo"
assert saved["stt_audio_seconds"] == 3.0
assert saved["stt_confidence"] == 0.81
health.report_healthy.assert_awaited_once_with("transcription")
@pytest.mark.asyncio
async def test_every_target_failing_returns_nothing():
def sync(*a, **k):
raise RuntimeError("boom")
(text, segments), saved, health = await _transcribe(sync, groq_api_key="gsk")
assert (text, segments) == (None, [])
assert health.report_degraded.await_count == 2
health.report_healthy.assert_not_awaited()
@pytest.mark.asyncio
async def test_default_config_never_calls_groq():
seen = []
def sync(path, target, prompt, glossary=None):
seen.append((target.provider, target.base_url))
return _ok()
await _transcribe(sync)
assert seen == [("openai", None)]
# ── prompt ──────────────────────────────────────────────────────────────────
CONTEXT = {
"area": {"municipality": "Ossining", "county": "Westchester", "state": "New York",
"local_knowledge": [{"term": "Snowden Avenue"}, {"term": "Sing Sing", "meaning": "prison"}]},
"vocabulary": ["10-13", "5-David", "Croton Point", "Snowden Avenue"],
"ten_codes": {"10-4": "ack", "10-13": "officer needs assistance"},
}
def test_no_context_is_the_legacy_prompt():
prompt, glossary = tr.build_stt_prompt("PD Dispatch", None)
assert prompt == "Talkgroup: PD Dispatch. " + tr._WHISPER_PROMPT
assert glossary == []
def test_vocabulary_prompt_carries_places_but_never_ten_codes():
prompt, glossary = tr.build_stt_prompt("PD Dispatch", CONTEXT)
assert "Ossining, Westchester, New York" in prompt
assert glossary == ["Snowden Avenue", "Sing Sing", "5-David", "Croton Point"]
assert "10-13" not in prompt, "a code-shaped vocabulary term must stay out"
assert "officer needs assistance" not in prompt
assert prompt.endswith(tr._WHISPER_PROMPT)
def test_prompt_stays_inside_the_token_budget():
big = {"area": {}, "vocabulary": [f"Placename{i} Boulevard" for i in range(200)]}
with patch.object(settings, "stt_prompt_max_tokens", 200):
prompt, glossary = tr.build_stt_prompt("PD Dispatch", big)
assert tr._estimate_tokens(prompt) <= 200
assert 0 < len(glossary) < 200
def test_token_estimate_is_conservative_for_radio_text():
# GPT-2 tokenises this at ~25 tokens; the estimate must not be lower.
assert tr._estimate_tokens("Engine 5 responding to a structure fire on Main Street, 10-4.") >= 20
# ── degenerate output the vocabulary prompt could add ───────────────────────
def test_prompt_echo_is_discarded():
glossary = ["Snowden Avenue", "Sing Sing", "Croton Point", "Main Street"]
assert tr._is_degenerate("Snowden Avenue, Sing Sing, Croton Point, Main Street.", [], glossary)
assert tr._is_degenerate("Local names: Snowden Avenue, Sing Sing.", [], glossary)
def test_one_real_street_in_a_sentence_is_not_an_echo():
glossary = ["Snowden Avenue", "Sing Sing", "Croton Point"]
real = "Car 4 respond to Snowden Avenue for a report of a disabled vehicle."
assert not tr._is_degenerate(real, [], glossary)
def test_compression_ratio_catches_an_alternating_loop():
loop = "Dispatch, copy. Unit 4, go ahead. " * 12
assert not tr._phrase_loop(loop), "the old checks miss this shape"
assert tr._is_degenerate(loop, [])
def test_real_long_traffic_passes_the_compression_check():
real = (
"Headquarters to 5 David, respond to 12 Snowden Avenue for a report of a "
"two-car accident with injuries, caller states one vehicle is overturned. "
"5 David copies, en route from Route 9. Be advised EMS has been notified "
"and Ossining Fire is responding as well. 5 David on scene, two vehicles, "
"one patient complaining of neck pain, requesting a tow for one."
)
assert len(real) >= tr._MIN_CHARS_FOR_COMPRESSION
assert not tr._is_degenerate(real, [])
def test_sweep_hold_back_counts_the_fallback_attempt():
from app.internal import recorrelation_sweep
with _patched():
single = recorrelation_sweep.pipeline_worst_case_seconds()
with _patched(groq_api_key="gsk"):
double = recorrelation_sweep.pipeline_worst_case_seconds()
stt_one = (2 if settings.stt_retry_on_degenerate else 1) * (1 + settings.openai_max_retries) \
* settings.stt_timeout_seconds
assert double - single == stt_one
assert double / 60 < settings.recorrelation_scan_minutes, "the sweep must still be able to recover the call"
def test_location_readback_is_not_an_echo():
"""Review of #297: intersection/address readbacks are where pins come from."""
glossary = ["Route 9", "Croton Avenue", "North Highland Place", "Main Street", "Ossining"]
for real in ("Route 9 and Croton Avenue, North Highland Place.",
"Main Street, Croton Avenue, Route 9, Ossining."):
assert not tr._is_degenerate(real, [], glossary), real
def test_long_busy_call_is_judged_per_window():
seg_text = "Engine 5 responding, Ladder 2 responding, Medic 1 en route to Main Street. "
segments = [{"start": float(i * 5), "end": float(i * 5 + 4), "text": seg_text} for i in range(60)]
text = " ".join(s["text"] for s in segments)
# One repeated real update for 5 minutes still loops within every window,
# so this is degenerate — but a varied call with one looping window out of three is not.
assert tr._is_degenerate(text, segments)
real = [
"Headquarters to 5 David, respond to 12 Snowden Avenue for a two-car accident.",
"5 David copies, en route from Route 9, ETA four minutes.",
"Be advised EMS has been notified and Ossining Fire is responding as well.",
"5 David on scene, two vehicles, one patient complaining of neck pain.",
"Requesting a tow for the blue sedan, it is blocking the northbound lane.",
"Copy, tow is being contacted, any further units needed at this time?",
"Negative, traffic is backing up toward Croton Point, send one for control.",
"6 Henry, can you clear the larceny and assist 5 David on Snowden?",
"6 Henry clearing, show me en route, coming down from North Highland.",
"Medic 1 on location, patient is being packaged for transport to Phelps.",
"Fire is standing by for fluids, no entrapment, both drivers out of the cars.",
"Received, all units be advised the roadway is now closed southbound.",
]
loop = "Dispatch, copy. Unit 4, go ahead. " * 12
varied = [{"start": float(i * 5), "end": float(i * 5 + 4), "text": t} for i, t in enumerate(real)]
varied.append({"start": 70.0, "end": 98.0, "text": loop}) # its own window
assert not tr._is_degenerate(" ".join(s["text"] for s in varied), varied)
@@ -260,7 +260,10 @@ async def test_segment_code_change_discards_segments_only():
"segments": ["Headquarters, 10-13.", "Show it out to Ossining.", "360 north, back to Route 9."],
}
with _system(), _gemini(payload):
text, segs, _ = await tc.correct("c1", "x y z w", SEGS, system_id="sys-1", talkgroup_id=9048)
# A realistic joined original (code-free, like the correction): a
# placeholder like "x y z w" now trips the server-26#162 growth guard.
text, segs, _ = await tc.correct("c1", "Shout out to Optum, back to Rose.", SEGS,
system_id="sys-1", talkgroup_id=9048)
assert segs is None
assert text == "Show it out to Ossining, back to Route 9."
@@ -279,3 +282,30 @@ async def test_reference_data_reaches_the_prompt():
assert "Sing Sing — state prison" in p, "a term without its meaning is half the information"
assert "Ossining, Westchester, New York" in p, "state must reach the prompt (server-26#36)"
assert "10-13=officer needs assistance" in p
# ── server-26#162, the rest: bare numbers and invented words ────────────────
@pytest.mark.asyncio
async def test_a_bare_number_turned_into_a_word_is_rejected():
"""The live case: a bare "7" came back as "ShotSpotter"."""
payload = {"corrected": "Car ShotSpotter on Home Street and Forest Ave."}
with _system(), _gemini(payload):
text, _, _ = await tc.correct("c1", "Car 7 on Holmes Street and 4th and Rowe.", None, system_id="sys-1")
assert text is None
@pytest.mark.asyncio
async def test_a_place_fix_that_keeps_its_numbers_still_passes():
payload = {"corrected": "Car 7 on Home Street and Forest Ave."}
with _system(), _gemini(payload):
text, _, _ = await tc.correct("c1", "Car 7 on Holmes Street and 4th and Rowe.", None, system_id="sys-1")
assert text == "Car 7 on Home Street and Forest Ave."
@pytest.mark.asyncio
async def test_a_correction_that_writes_new_sentences_is_rejected():
payload = {"corrected": "Car 7 on Main Street. Suspect is armed and fled northbound in a red sedan."}
with _system(), _gemini(payload):
text, _, _ = await tc.correct("c1", "Car 7 on Mane Street.", None, system_id="sys-1")
assert text is None