big ui and intel updates

This commit is contained in:
Logan
2026-04-19 16:48:55 -04:00
parent 0df53df92e
commit 303c5b13cf
11 changed files with 527 additions and 169 deletions
+76 -24
View File
@@ -1,10 +1,10 @@
"""
Gemini-powered intelligence extraction from call transcripts.
GPT-4o-mini intelligence extraction from call transcripts.
Sends the transcript to Gemini Flash with a tight JSON schema prompt.
Sends the transcript to GPT-4o mini with a tight JSON schema prompt.
Returns structured data: incident type, tags, location, vehicles, units, severity.
Falls back gracefully if Gemini is unavailable or returns malformed output.
Falls back gracefully if the API is unavailable or returns malformed output.
"""
import asyncio
import json
@@ -36,6 +36,9 @@ System: {system_id}
Talkgroup: {talkgroup_name}
{transcript_block}"""
# Nominatim viewbox half-width in degrees (~35 km at mid-latitudes)
_GEO_DELTA = 0.3
async def extract_tags(
call_id: str,
@@ -44,38 +47,52 @@ async def extract_tags(
talkgroup_id: Optional[int] = None,
system_id: Optional[str] = None,
segments: Optional[list[dict]] = None,
) -> tuple[list[str], Optional[str], Optional[str]]:
node_id: Optional[str] = None,
) -> tuple[list[str], Optional[str], Optional[str], Optional[dict]]:
"""
Extract incident tags, type, location, and corrected transcript via Gemini.
Extract incident tags, type, location, and corrected transcript via GPT-4o mini.
Geocodes the extracted location string via Nominatim using the node's position as bias.
Returns:
(tags, primary_type, location)
(tags, primary_type, location_str, location_coords)
where location_coords is {"lat": float, "lng": float} or None.
Side-effect: updates calls/{call_id} in Firestore with tags, location,
vehicles, units, severity, transcript_corrected; also stores the call embedding.
location_coords, vehicles, units, severity, transcript_corrected; also stores embedding.
"""
result = await asyncio.to_thread(_sync_extract, transcript, talkgroup_name, talkgroup_id, system_id, segments)
result = await asyncio.to_thread(
_sync_extract, transcript, talkgroup_name, talkgroup_id, system_id, segments
)
tags: list[str] = result.get("tags") or []
tags: list[str] = result.get("tags") or []
incident_type: Optional[str] = result.get("incident_type") or None
location: Optional[str] = result.get("location") or None
vehicles: list[str] = result.get("vehicles") or []
units: list[str] = result.get("units") or []
severity: str = result.get("severity") or "unknown"
location: Optional[str] = result.get("location") or None
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
# Geocode the location string if we have one and a node to bias toward
location_coords: Optional[dict] = None
if location and node_id:
node_doc = await fstore.doc_get("nodes", node_id)
if node_doc:
node_lat = node_doc.get("lat")
node_lon = node_doc.get("lon")
if node_lat is not None and node_lon is not None:
location_coords = await _geocode_location(location, node_lat, node_lon)
# Store embedding alongside structured data
embedding = await asyncio.to_thread(_sync_embed, _embed_text(transcript, incident_type))
updates: dict = {
"tags": tags,
"severity": severity,
}
updates: dict = {"tags": tags, "severity": severity}
if location:
updates["location"] = location
if location_coords:
updates["location_coords"] = location_coords
if vehicles:
updates["vehicles"] = vehicles
if units:
@@ -92,10 +109,49 @@ 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}, coords={location_coords}, severity={severity}, "
f"corrected={transcript_corrected is not None}"
)
return tags, incident_type, location
return tags, incident_type, location, location_coords
async def _geocode_location(
location_str: str, node_lat: float, node_lon: float
) -> Optional[dict]:
"""
Geocode a location string using Nominatim, biased toward the node's area.
Returns {"lat": float, "lng": float} or None if geocoding fails.
"""
import httpx
viewbox = (
f"{node_lon - _GEO_DELTA},{node_lat - _GEO_DELTA},"
f"{node_lon + _GEO_DELTA},{node_lat + _GEO_DELTA}"
)
params = {
"q": location_str,
"format": "json",
"limit": 1,
"viewbox": viewbox,
"bounded": 1,
}
headers = {"User-Agent": "DRB-Dispatch/1.0 (public-safety radio monitor)"}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
r = await client.get(
"https://nominatim.openstreetmap.org/search",
params=params,
headers=headers,
)
r.raise_for_status()
results = r.json()
if results:
coords = {"lat": float(results[0]["lat"]), "lng": float(results[0]["lon"])}
logger.info(f"Geocoded '{location_str}'{coords}")
return coords
except Exception as e:
logger.warning(f"Geocoding failed for '{location_str}': {e}")
return None
def _build_transcript_block(transcript: str, segments: Optional[list[dict]]) -> str:
@@ -154,10 +210,7 @@ def _sync_embed(text: str) -> Optional[list[float]]:
try:
client = OpenAI(api_key=settings.openai_api_key)
result = client.embeddings.create(
model="text-embedding-3-small",
input=text,
)
result = client.embeddings.create(model="text-embedding-3-small", input=text)
return result.data[0].embedding
except Exception as e:
logger.warning(f"Embedding generation failed: {e}")
@@ -165,6 +218,5 @@ def _sync_embed(text: str) -> Optional[list[float]]:
def _embed_text(transcript: str, incident_type: Optional[str]) -> str:
"""Build the text string to embed — transcript + type context."""
prefix = f"[{incident_type}] " if incident_type else ""
return f"{prefix}{transcript}"