diff --git a/drb-c2-core/app/routers/calls.py b/drb-c2-core/app/routers/calls.py index 9c5959d..753acb3 100644 --- a/drb-c2-core/app/routers/calls.py +++ b/drb-c2-core/app/routers/calls.py @@ -1,6 +1,12 @@ -from fastapi import APIRouter, BackgroundTasks, HTTPException, Query +from fastapi import APIRouter, BackgroundTasks, HTTPException, Query, Depends +from pydantic import BaseModel from typing import Optional from app.internal import firestore as fstore +from app.internal.auth import require_admin_token + + +class TranscriptUpdate(BaseModel): + transcript: str router = APIRouter(prefix="/calls", tags=["calls"]) @@ -51,3 +57,41 @@ async def reprocess_call(call_id: str, background_tasks: BackgroundTasks): gcs_uri=gcs_uri, ) return {"ok": True, "call_id": call_id} + + +@router.patch("/{call_id}/transcript") +async def patch_transcript( + call_id: str, + body: TranscriptUpdate, + background_tasks: BackgroundTasks, + _: dict = Depends(require_admin_token), +): + """Overwrite a call's transcript and re-run intelligence extraction.""" + call = await fstore.doc_get("calls", call_id) + if not call: + raise HTTPException(404, f"Call '{call_id}' not found.") + + # Save new transcript, clear stale intelligence fields + await fstore.doc_set("calls", call_id, { + "transcript": body.transcript, + "transcript_corrected": None, + "tags": [], + "severity": "unknown", + "location": None, + "units": [], + "vehicles": [], + "embedding": None, + }) + + from app.routers.upload import _run_extraction_pipeline + background_tasks.add_task( + _run_extraction_pipeline, + call_id=call_id, + node_id=call.get("node_id"), + system_id=call.get("system_id"), + talkgroup_id=call.get("talkgroup_id"), + talkgroup_name=call.get("talkgroup_name"), + transcript=body.transcript, + segments=call.get("segments"), + ) + return {"ok": True, "call_id": call_id} diff --git a/drb-c2-core/app/routers/upload.py b/drb-c2-core/app/routers/upload.py index 27bd2db..6df2f7f 100644 --- a/drb-c2-core/app/routers/upload.py +++ b/drb-c2-core/app/routers/upload.py @@ -83,6 +83,45 @@ def _public_url_to_gcs_uri(url: str) -> Optional[str]: return None +async def _run_extraction_pipeline( + call_id: str, + node_id: str, + system_id: Optional[str], + talkgroup_id: Optional[int], + talkgroup_name: Optional[str], + transcript: str, + segments: Optional[list] = None, +) -> None: + """Run steps 2-4 of the intelligence pipeline using an existing transcript.""" + from app.internal import intelligence, incident_correlator, alerter + + tags, incident_type, location = await intelligence.extract_tags( + call_id, transcript, talkgroup_name, + talkgroup_id=talkgroup_id, system_id=system_id, segments=segments, + ) + + if incident_type: + await incident_correlator.correlate_call( + call_id=call_id, + node_id=node_id, + system_id=system_id, + talkgroup_id=talkgroup_id, + talkgroup_name=talkgroup_name, + tags=tags, + incident_type=incident_type, + location=location, + ) + + await alerter.check_and_dispatch( + call_id=call_id, + node_id=node_id, + talkgroup_id=talkgroup_id, + talkgroup_name=talkgroup_name, + tags=tags, + transcript=transcript, + ) + + async def _run_intelligence_pipeline( call_id: str, node_id: str, diff --git a/drb-frontend/app/calls/page.tsx b/drb-frontend/app/calls/page.tsx index d804f31..55eea20 100644 --- a/drb-frontend/app/calls/page.tsx +++ b/drb-frontend/app/calls/page.tsx @@ -4,11 +4,13 @@ import { useState } from "react"; import { useCalls } from "@/lib/useCalls"; import { useSystems } from "@/lib/useSystems"; import { CallRow } from "@/components/CallRow"; +import { useAuth } from "@/components/AuthProvider"; export default function CallsPage() { const [limitCount, setLimitCount] = useState(100); const { calls, loading } = useCalls(limitCount); const { systems } = useSystems(); + const { isAdmin } = useAuth(); const systemMap = Object.fromEntries(systems.map((s) => [s.system_id, s])); const active = calls.filter((c) => c.status === "active"); @@ -41,7 +43,7 @@ export default function CallsPage() {
{active.map((c) => ( -
diff --git a/drb-frontend/lib/c2api.ts b/drb-frontend/lib/c2api.ts
index 5637471..f9372a5 100644
--- a/drb-frontend/lib/c2api.ts
+++ b/drb-frontend/lib/c2api.ts
@@ -55,6 +55,8 @@ export const c2api = {
const qs = params ? "?" + new URLSearchParams(params).toString() : "";
return request(`/calls${qs}`);
},
+ patchTranscript: (callId: string, transcript: string) =>
+ request(`/calls/${callId}/transcript`, { method: "PATCH", body: JSON.stringify({ transcript }) }),
// Incidents
getIncidents: (params?: { status?: string; type?: string }) => {
diff --git a/drb-frontend/lib/types.ts b/drb-frontend/lib/types.ts
index 903f1ff..8ba947a 100644
--- a/drb-frontend/lib/types.ts
+++ b/drb-frontend/lib/types.ts
@@ -20,6 +20,12 @@ export interface SystemRecord {
config: Record;
}
+export interface TranscriptSegment {
+ start: number;
+ end: number;
+ text: string;
+}
+
export interface CallRecord {
call_id: string;
node_id: string;
@@ -32,6 +38,7 @@ export interface CallRecord {
audio_url: string | null;
transcript: string | null;
transcript_corrected: string | null;
+ segments: TranscriptSegment[] | null;
incident_id: string | null;
location: { lat: number; lng: number } | null;
tags: string[];