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
+78
View File
@@ -28,8 +28,39 @@ class Settings(BaseSettings):
# OpenAI (STT + intelligence)
openai_api_key: Optional[str] = None
# The OpenAI transcription model. Used when OpenAI is the STT provider AND
# as the fallback when another provider fails (server-26#297).
stt_model: str = "whisper-1" # whisper-1 | gpt-4o-mini-transcribe | gpt-4o-transcribe
# ── STT provider (server-26#297) ──────────────────────────────────────────
# Any OpenAI-compatible /audio/transcriptions endpoint. Groq serves
# whisper-large-v3-turbo at $0.04/audio-hour against whisper-1's $0.36, same
# API (min billed length 10s per request, prompt capped at 224 tokens --
# console.groq.com/docs/speech-to-text, checked 2026-10-27).
# auto -- groq when GROQ_API_KEY is set, compat when STT_BASE_URL is
# set, else openai. The default, so a deploy without either
# behaves exactly as before.
# openai -- OpenAI only (stt_model).
# groq -- Groq (stt_compat_model), OpenAI fallback.
# compat -- any other OpenAI-compatible server: stt_base_url + stt_api_key
# + stt_compat_model (e.g. a self-hosted faster-whisper, #296).
stt_provider: str = "auto"
stt_base_url: Optional[str] = None # None -> the provider's own default
stt_api_key: Optional[str] = None # None -> GROQ_API_KEY / OPENAI_API_KEY
groq_api_key: Optional[str] = None
stt_compat_model: str = "whisper-large-v3-turbo" # model on a non-OpenAI provider
# Retry a failed non-OpenAI request once on OpenAI stt_model. Needs
# OPENAI_API_KEY; without it a primary failure just means no transcript.
stt_fallback_to_openai: bool = True
# Domain vocabulary in the STT prompt (area places, local_knowledge terms,
# vocabulary). Applied to the non-OpenAI provider only: whisper-1 echoed
# vocabulary into silence before (see transcription.py), so the OpenAI
# path -- including the fallback -- keeps today's fixed prompt.
stt_vocab_prompt_enabled: bool = True
# Whisper keeps only the LAST 224 prompt tokens and silently drops the
# rest. The prompt is built against this estimated budget, with headroom.
stt_prompt_max_tokens: int = 200
# AI request timeouts (server-26#153). Without these the OpenAI SDK waits
# up to 600s per attempt and retries twice, and the google-genai client
# waits forever, so a hung provider call had no upper bound at all — and
@@ -48,8 +79,31 @@ class Settings(BaseSettings):
# google-genai does not retry unless told to, so this is the whole bound.
gemini_timeout_seconds: float = 60.0
# Scene extraction + embeddings (server-26#298). Were literals in
# intelligence.py; any OpenAI chat model that supports JSON mode works.
extraction_model: str = "gpt-4o-mini"
embedding_model: str = "text-embedding-3-small"
# Skip correction + extraction (+ embedding, geocoding) for transcripts
# chatter_classifier.gate_chatter calls radio housekeeping. OFF by default:
# a gated call carries no units, so it is thin and takes the no-fit-check
# thin attach (mechanism A) instead of the fit-tested path or the #115
# orphan gate, and server-26#127 set "0 false positives in a live shadow
# window" as the bar for going live. Read chatter_classifier_verdict off a
# shadow window first, then set CHATTER_GATE_ENABLED=true.
chatter_gate_enabled: bool = False
# Google Maps (geocoding)
google_maps_api_key: Optional[str] = None
# In-process cache of Geocoding API answers (server-26#298). Dispatch
# repeats the same address on every follow-up call of a job, and the
# place verifier re-geocodes the same nouns. In-process rather than
# Firestore: a Firestore read costs about what it saves, adds latency on
# every miss, and would persist Maps content -- the Maps terms allow
# caching lat/lng for at most 30 days, and an in-memory TTL can never
# outlive that. Lost on restart, which only costs a few repeat lookups.
geocode_cache_ttl_seconds: int = 24 * 60 * 60
geocode_cache_negative_ttl_seconds: int = 60 * 60 # ZERO_RESULTS etc.
geocode_cache_max_entries: int = 5000
# 511NY developer key (server-26#183/#198). Issued with the NYSDOT Developer
# Access Agreement grant; the keyless legacy API is gone since the 511x cutover.
ny511_api_key: Optional[str] = None
@@ -105,6 +159,30 @@ class Settings(BaseSettings):
# else's PII. Both gates must be open before any snippet leaves.
alert_transcript_snippet_enabled: bool = False
place_verify_max_per_call: int = 3
# ── Alert confidence gate (server-26#300) ─────────────────────────────────
# Nothing in the pipeline emits a confidence, so the alerter derives one:
# how the rule matched (talkgroup 1.0 / extracted tag 0.8 / raw transcript
# text only 0.5 -- the CrimeRadar "fire alarm" -> "firearm" failure was a
# transcript match) scaled down when Whisper's own per-token probability
# (calls.stt_confidence) is below alert_stt_reference. Below the floor ->
# held, never pushed. High-severity matches also need the incident to
# have alert_corroboration_min_calls calls, or a confidence at or above
# alert_high_severity_min_confidence; held ones are released when a
# later call corroborates the incident. Starting values, not measured.
alert_min_confidence: float = 0.5
alert_high_severity_min_confidence: float = 0.85
alert_corroboration_min_calls: int = 2
# Low on purpose: exp(avg_logprob) on vocoded radio audio is often well
# under 0.5 for speech that is right, and Groq/OpenAI logprobs are not on
# a shared scale. Only clearly garbled audio should pull the score down.
alert_stt_reference: float = 0.3
alert_high_severity_terms: list[str] = [
"shots-fired", "shots fired", "shooting", "shot", "gun", "firearm", "weapon",
"stabbing", "stabbed", "officer-down", "officer down", "10-13",
"officer-needs-assistance", "active-shooter", "hostage", "barricade",
"explosion", "structure-fire", "structure fire", "working-fire",
"entrapment", "cardiac-arrest", "vehicle-pursuit", "pursuit",
]
# How close a candidate has to sound before it may rewrite a transcript.
# Below this, Places Text Search will confidently hand back the nearest
# business for any garbage string.