Files
server-26/drb-frontend/lib/types.ts
T
Logan CusanoandClaude Opus 5 58efdbd6eb
Build & Deploy / Build & push images (push) Successful in 4m0s
Build & Deploy / Deploy to VM (push) Successful in 2m28s
Build & Deploy / Report a failed deploy (push) Skipped
Correct the transcript before anything reads it
Correction existed, but as a line in intelligence.py's EXTRACTION_PROMPT --
which put it in the wrong place twice over. The same model call that extracted
units, location and severity emitted the correction afterwards, so extraction
reasoned over text already known to be wrong; and it sat behind
correlation_enabled, so during a cost-controlled STT-only window nothing was
ever corrected at all. That is the normal state during development.

internal/transcript_correction.py is now its own pass, between the degenerate
filter and the Firestore write. It receives an already-produced transcript plus
a reference list, so unlike a Whisper prompt it has no series to extend -- the
distinction that keeps vocabulary out of the recogniser's prompt, where an
enumerated ten-code list once made it hallucinate ten-code runs.

Reference data is merged from the talkgroup and the system, TALKGROUP FIRST. A
system spanning several counties can have a talkgroup covering one
municipality, and that municipality's streets must not be buried under a
county-wide list. A single-municipality system is the degenerate case: populate
the system level and every talkgroup inherits it. Area context is now SET --
municipality, county, roads, landmarks, on both scopes -- rather than guessed
from talkgroup names, which is what vocabulary_learner did and which is close
to useless across multiple counties.

Segments are corrected too, not just the joined text. extract_scenes builds its
prompt from numbered segments whenever there is more than one, so a correction
that only fixed the transcript would have been discarded on exactly the
multi-transmission calls carrying the most content. Alignment is enforced: an
array of the wrong length or type is dropped whole, because scenes map back to
transmissions by index and a shifted array would misattribute audio silently.

Whisper is also retried once on degenerate output. Call e49ea32c produced a
56-word ten-code counting run on one attempt and ordinary speech on the next --
same clip, same temperature=0 -- so a hallucination is a coin-flip, and
discarding on the first bad roll threw away a recoverable transcript.

Two things found on the way:

PUT /systems/{id} wiped ten_codes on every save. The systems form sends only
{name, type, config}, and model_dump() wrote every omitted field as its default
over the top. Now exclude_unset. area_context would have been the next victim,
which is why it gets its own route alongside ten-codes rather than a field on
that payload.

Closes server-26#36.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:23:41 -04:00

246 lines
7.0 KiB
TypeScript

export type NodeStatus = "online" | "offline" | "recording" | "unconfigured";
export type ApprovalStatus = "pending" | "approved" | "rejected";
export type UserRole = "admin" | "operator" | "viewer";
export interface UserRecord {
uid: string;
email: string | null;
display_name: string | null;
role: UserRole;
owned_node_ids: string[];
disabled: boolean;
creation_time: string | null;
last_sign_in: string | null;
discord_linked: boolean;
discord_username: string | null;
discord_user_id: string | null;
// only present on GET /admin/users/{uid}
sessions?: UserSession[];
// only present on POST /admin/users response
invite_link?: string | null;
}
export interface UserSession {
session_id: string;
uid: string;
email: string;
timestamp: string;
ip: string | null;
user_agent: string | null;
}
export interface AuditEntry {
log_id: string;
action: string;
actor_uid: string;
actor_email: string;
target_uid: string | null;
target_email: string | null;
details: Record<string, unknown>;
timestamp: string;
}
export interface NodeRecord {
node_id: string;
name: string;
lat: number;
lon: number;
status: NodeStatus;
configured: boolean;
last_seen: string | null;
assigned_system_id: string | null;
approval_status: ApprovalStatus | null;
hardware_preset?: string;
ppm_override?: number | null;
node_type?: string;
enforce_override_timeout?: boolean;
is_overridden?: boolean;
override_system_id?: string | null;
override_timeout_at?: string | null;
}
export interface VocabularyPendingTerm {
term: string;
source: "induction" | "correction";
added_at: string;
source_call_ids?: string[];
}
export interface SystemRecord {
system_id: string;
name: string;
type: string; // P25 | DMR | NBFM
config: Record<string, unknown>;
vocabulary?: string[];
vocabulary_pending?: VocabularyPendingTerm[];
vocabulary_bootstrapped?: boolean;
ten_codes?: Record<string, string>; // {"10-10": "Commercial Alarm", ...}
preferred_token_id?: string | null;
}
export interface TranscriptSegment {
start: number;
end: number;
text: string;
}
export interface CallRecord {
call_id: string;
node_id: string;
system_id: string | null;
talkgroup_id: number | null;
talkgroup_name: string | null;
freq: number | null;
started_at: string;
ended_at: string | null;
/** Private gs:// object location. Present on the Firestore doc; not playable. */
audio_gcs_uri?: string | null;
/**
* Short-lived playback link. Minted per read by the API — only populated on
* calls fetched via c2api, never on docs read straight from Firestore.
* On pre-fix docs this holds a legacy (unplayable) gs:// URI instead.
*/
audio_url: string | null;
transcript: string | null;
transcript_corrected: string | null;
segments: TranscriptSegment[] | null;
/** New: one entry per scene detected in the recording. */
incident_ids: string[];
/** Legacy field — present on calls recorded before the multi-scene migration. */
incident_id?: string | null;
location: string | null;
/** Per-call geocode, written by intelligence.py. Powers the incident-path polyline on the map. */
location_coords?: { lat: number; lng: number } | null;
/** Unit callsigns mentioned in this specific transmission (e.g. "15-9", "E-41"). */
units?: string[];
vehicles?: string[];
/** Units this call reported as clearing/back in service. */
cleared_units?: string[];
/** Set when dedup.py identifies this as a second node's recording of the same transmission — the canonical copy has this null. */
duplicate_of?: string | null;
/** Radio source address, when the system exposes it. */
srcaddr?: string | null;
tags: string[];
status: "active" | "ended";
/** Four-level ladder: routine | minor | moderate | major. Legacy docs may still carry "unknown". */
severity?: string | null;
// Correlation debug — written by the correlator, present after a call is linked
corr_path?: string | null;
corr_score?: number | null;
corr_distance_km?: number | null;
corr_incident_idle_min?: number | null;
corr_shared_units?: number | null;
corr_candidates?: number | null;
}
export interface IncidentRecord {
incident_id: string;
title: string | null;
type: string | null;
status: "active" | "resolved";
location: string | null;
location_coords: { lat: number; lng: number } | null;
call_ids: string[];
system_ids: string[];
talkgroup_ids: string[];
units: string[];
vehicles: string[];
/** Units currently believed on scene — maintained by incident_correlator.py `_attach`. */
units_active?: string[];
/** Units that reported clearing/back in service on this incident. */
units_cleared?: string[];
/** Free-text location mentions accumulated across the incident's calls, beyond the primary `location`. */
location_mentions?: string[];
/** ISO timestamp of the last thin/status-only call attached (doesn't refresh `updated_at`). */
last_thin_at?: string | null;
severity: string | null;
started_at: string;
updated_at: string;
summary: string | null;
tags: string[];
}
export interface AlertRule {
rule_id: string;
name: string;
keywords: string[];
talkgroup_ids: number[];
enabled: boolean;
discord_webhook: string | null;
created_at?: string;
}
export interface TripEvent {
event_id: string;
trip_id: string;
title: string;
date: string;
start_time: string | null;
end_time: string | null;
location: string;
location_inherited: boolean;
maps_link: string | null;
place_id: string | null;
notes: string | null;
tags: string[];
attendees: Record<string, string>;
created_at: string;
}
export interface PlaceResult {
name: string;
address: string;
place_id: string;
lat: number;
lng: number;
maps_link: string;
rating?: number;
}
export interface TripRecord {
trip_id: string;
name: string;
location: string;
maps_link: string | null;
start_date: string;
end_date: string;
attendees: Record<string, string>;
available_tags: string[];
overlap_tags: string[];
visibility: "public" | "private";
invited_discord_ids: string[];
created_at: string;
events?: TripEvent[];
}
export interface AlertEvent {
alert_id: string;
rule_id: string;
rule_name: string;
call_id: string;
node_id: string;
talkgroup_id: number | null;
talkgroup_name: string | null;
matched_keywords: string[];
transcript_snippet: string | null;
triggered_at: string;
acknowledged: boolean;
}
/**
* Ground truth about the area a system or talkgroup covers, handed to the
* transcript corrector so it can recognise local street, town and business
* names that Whisper mangles (server-26#36).
*
* Set on the system, and optionally narrowed per talkgroup inside
* `config.talkgroups[]` — the talkgroup's entries rank ABOVE the system's, so
* a multi-county system can be specific per channel without its wider list
* burying the detail.
*/
export interface AreaContext {
municipality?: string;
county?: string;
roads?: string[];
landmarks?: string[];
}