stt updates and intelligence updates

This commit is contained in:
Logan
2026-04-13 00:01:19 -04:00
parent 7b6fd640d9
commit 616c06f09c
6 changed files with 76 additions and 24 deletions
+21 -8
View File
@@ -12,7 +12,7 @@ from typing import Optional
from app.internal.logger import logger
from app.internal import firestore as fstore
_PROMPT_TEMPLATE = """You are analyzing a P25 public safety radio transcript. Extract structured information and respond ONLY with a single valid JSON object — no markdown, no explanation.
_PROMPT_TEMPLATE = """You are analyzing a P25 public safety radio transcript. The audio was transcribed by Whisper through a digital radio vocoder, which introduces errors. Extract structured information and respond ONLY with a single valid JSON object — no markdown, no explanation.
Schema:
{{
@@ -21,7 +21,8 @@ Schema:
"location": "most specific location string found, or empty string",
"vehicles": [vehicle descriptions mentioned, e.g. "Hyundai Tucson", "black sedan"],
"units": [unit IDs or officer numbers mentioned, e.g. "Unit 511", "Car 4"],
"severity": one of "minor" | "moderate" | "major" | "unknown"
"severity": one of "minor" | "moderate" | "major" | "unknown",
"transcript_corrected": "corrected transcript string, or null if no corrections needed"
}}
Rules:
@@ -29,7 +30,9 @@ Rules:
- tags: be specific and lowercase, hyphenated. Do not repeat incident_type as a tag.
- units: only identifiers explicitly mentioned, not inferred.
- Do not invent details not present in the transcript.
- transcript_corrected: fix only clear STT errors caused by vocoder distortion (e.g. "Several""10-4", misheard street names, garbled unit IDs). Keep all radio language as-is — do NOT decode codes into plain English. Return null if the transcript looks accurate.
Talkgroup: {talkgroup_name}
Transcript:
{transcript}"""
@@ -37,17 +40,18 @@ Transcript:
async def extract_tags(
call_id: str,
transcript: str,
talkgroup_name: Optional[str] = None,
) -> tuple[list[str], Optional[str], Optional[str]]:
"""
Extract incident tags, type, and location from a transcript via Gemini.
Extract incident tags, type, location, and corrected transcript via Gemini.
Returns:
(tags, primary_type, location)
Side-effect: updates calls/{call_id} in Firestore with tags, location,
vehicles, units, severity; also stores the call embedding.
vehicles, units, severity, transcript_corrected; also stores the call embedding.
"""
result = await asyncio.to_thread(_sync_extract, transcript)
result = await asyncio.to_thread(_sync_extract, transcript, talkgroup_name)
tags: list[str] = result.get("tags") or []
incident_type: Optional[str] = result.get("incident_type") or None
@@ -55,6 +59,7 @@ async def extract_tags(
vehicles: list[str] = result.get("vehicles") or []
units: list[str] = result.get("units") or []
severity: str = result.get("severity") or "unknown"
transcript_corrected: Optional[str] = result.get("transcript_corrected") or None
if incident_type in ("unknown", "other", ""):
incident_type = None
@@ -74,6 +79,8 @@ async def extract_tags(
updates["units"] = units
if embedding:
updates["embedding"] = embedding
if transcript_corrected:
updates["transcript_corrected"] = transcript_corrected
try:
await fstore.doc_set("calls", call_id, updates)
@@ -82,12 +89,13 @@ async def extract_tags(
logger.info(
f"Intelligence: call {call_id} → type={incident_type}, "
f"tags={tags}, location={location!r}, severity={severity}"
f"tags={tags}, location={location!r}, severity={severity}, "
f"corrected={transcript_corrected is not None}"
)
return tags, incident_type, location
def _sync_extract(transcript: str) -> dict:
def _sync_extract(transcript: str, talkgroup_name: Optional[str]) -> dict:
"""Call Gemini Flash and parse the JSON response."""
from app.config import settings
import google.generativeai as genai
@@ -102,8 +110,13 @@ def _sync_extract(transcript: str) -> dict:
generation_config={"response_mime_type": "application/json"},
)
prompt = _PROMPT_TEMPLATE.format(
transcript=transcript,
talkgroup_name=talkgroup_name or "unknown",
)
try:
response = model.generate_content(_PROMPT_TEMPLATE.format(transcript=transcript))
response = model.generate_content(prompt)
return json.loads(response.text)
except json.JSONDecodeError as e:
logger.warning(f"Gemini returned non-JSON: {e}")