Transcription: config-driven OpenAI-compatible STT with OpenAI fallback (server-26#297, server-26#83)

STT_PROVIDER=auto picks Groq whisper-large-v3-turbo only when GROQ_API_KEY is
set (else OpenAI, unchanged); compat covers any OpenAI-compatible server via
STT_BASE_URL/STT_API_KEY. A failed primary falls back to OpenAI STT_MODEL on
the same downloaded audio; the fallback reports to its own ai_health tier so
a dead primary stays red. The non-OpenAI provider gets a token-budgeted
glossary prompt (area places, local_knowledge, vocabulary; never ten-codes),
guarded by a narrow prompt-echo check. A per-30s-window zlib compression
check (Whisper's 2.4) adds a noise gate that survives correction being off
(#83, partial). Call docs record stt_provider/model/audio_seconds/confidence.
The sweep hold-back counts the fallback attempt. gate_chatter added to
chatter_classifier (used by transcription to skip correction when
CHATTER_GATE_ENABLED). GROQ_API_KEY templated with default ''.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-27 20:28:30 -04:00
co-authored by Claude Opus 5.5
parent 7ecfca7648
commit d485eae41a
10 changed files with 865 additions and 142 deletions
+4
View File
@@ -73,6 +73,9 @@ _DERIVED = {
"vehicles", "cleared_units", "severity", "incident_type", "type",
"embedding", "skip_reason", "intelligence_started_at", "reassignment",
"resolved", "has_updates", "audio_url",
# server-26#297: which STT produced the transcript, and its confidence —
# a re-transcription that comes back empty must not keep the live values.
"stt_provider", "stt_model", "stt_audio_seconds", "stt_confidence",
}
_DERIVED_PREFIXES = ("corr_", "chatter_classifier_", "eval_")
_TRANSCRIPT_FIELDS = (
@@ -406,6 +409,7 @@ async def _prepare(call: dict, mode: str, source_scenes: dict[str, dict]) -> dic
transcript, segments = await transcription.transcribe_call(
call_id, call["audio_gcs_uri"], talkgroup_name,
system_id=call.get("system_id"), talkgroup_id=call.get("talkgroup_id"),
node_id=call.get("node_id"),
)
else:
transcript, segments = _stored_input(call)