Move transcript correction into transcription, with per-talkgroup vocabulary and area context #36
Open
opened 2026-08-23 13:53:29 -04:00 by logan
·
2 comments
No Branch/Tag Specified
Labels
Clear labels
area/ai-pipeline
area/auth
area/billing
area/correlation
area/discord
area/frontend
area/governance
area/growth
area/infra-deploy
area/legal
area/map
area/mqtt
area/node-ota
area/osint
area/recording-audio
area/sdr-radio
area/transcription
board
board:agenda
minutes:draft
minutes:final
role:ceo
role:cfo
role:cio
role:ciso
role:cmo
role:coo
role:cto
Scene/entity extraction, summarizer, AI providers, embeddings
AuthN/Z, roles, tenancy/org scoping, API keys, credentials
Plans, entitlements, AI spend accounting and caps
Incident correlation, linking, lifecycle, severity, corr_debug
Discord bot, radio bots, voice streams, bot tokens, webhooks
Web UI: Next.js app and the edge-node local dashboard
Board process, minutes, unattended runner, agent tooling
Go-to-market, outreach, cold email, discovery conversations
CI/CD, VM, registry, Firestore rules/indexes deploy, Terraform/Caddy
ToS, retention, licences/DAAs, CAN-SPAM, E&O, name suppression
Live map: pins, paths, tiles, legend, geocoding
Node-C2 MQTT, checkins, commands, node sweeper, dynsec
Node install, enrollment, OTA updater, image skew
External feeds: cameras, 511, ADS-B, AIS, outages, and their licences
Call capture, boundaries, PulseAudio, ring buffer, trimming, attribution
OP25 decode, SDR hardware/tuning, conventional, second SDR, mesh radio
STT, transcript correction, place verification, name accuracy
Board process: minutes and board-filed items
Needs board scoping at the next sitting; CEO removes the label once scoped
One exec pre-read, closed by CEO
CEO ruling; open until enacted
Filed by the CEO
Filed by the CFO
Filed by the CIO
Filed by the CISO
Filed by the CMO
Filed by the COO
Filed by the CTO
kind
behavior-bug
Wrong product behavior; the instrument reads it correctly
kind
measurement-bug
Corrupts the recording/instrument (debug data, evals, health checks), not behavior
priority
p0
Drop everything: prod broken, live exposure, or owner-ruled P0
priority
p1
Next up: blocks a gate, a customer, or core incident quality
priority
p2
Normal: real defect or planned work, schedule in turn
priority
p3
Low: minor, cosmetic, or someday
status
blocked
Waiting on an external party, spend, or another issue
status
in-progress
Branch or PR open against it
status
needs-decision
Waiting on an owner or board ruling before work
status
needs-info
Root cause or repro unknown; needs a reading first
type
bug
Something built is wrong: code, data, or a measurement
type
chore
Maintenance, refactor, tests, pinning, cleanup: no user-visible change
type
decision
A ruling to make or record: board minutes, owner calls, legal reviews
type
docs
Docs, runbooks, CLAUDE.md, skills, SOPs
type
feature
New capability or an extension of an existing one
type
security
Security, privacy, credential or tenant-isolation exposure
type
tracking
Tracker, checklist, initiative or punch-list spanning several changes
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: logan/server-26#36
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The problem
Transcript correction already exists — but it is fused into the Gemini extraction step.
internal/intelligence.py:69asks the extraction model fortranscript_correctedalongside scene, entity, location and severity extraction:Two consequences, both bad:
1. The correction arrives after the decisions that depend on it. The same model call that extracts units, locations and severity is the one emitting the correction — so extraction reasons over the raw transcript and hands back a fix afterwards. Entities are pulled from text already known to be wrong. Embeddings, incident type, severity and correlation are all computed upstream of the correction.
2. It only runs when correlation is on. Correction lives behind
correlation_enabled. In the 2026-08-23 12:31–13:31 STT window correlation was deliberately off, so nothing was ever corrected. That is the normal state during cost-controlled development, which means the correction path is off exactly when transcripts are being evaluated.Evidence from that window — 29 of 29 calls transcribed, and this one stored intact:
Proposal
Move correction into transcription, as its own step.
The correction pass takes the raw transcript plus reference context and returns a corrected transcript. It is a reference lookup, not a transcription prior — the distinction that already prevents vocabulary from going into the Whisper prompt (
transcription.py:21-27: an enumerated ten-code prompt made Whisper hallucinate ten-code runs). A corrector that sees the audio's transcript and a list of local names can fix sound-alikes without being able to invent a series.No schema change is needed.
transcriptandtranscript_correctedboth already exist, and every consumer already readstranscript_corrected or transcript.intelligence.pythen drops correction from its prompt and simply consumes the corrected text — a smaller prompt doing one job.Keep
preserve_transcript_correction: a human edit viaPATCH /calls/{id}/transcriptmust still outrank the machine.Reference data: set it, stop deriving it
The corrector needs to know what the local nouns are. Today the only area context is guessed from talkgroup names —
vocabulary_learner.py:103:For a single-municipality system that is thin; for a multi-county system it is close to useless. Vocabulary is also per-system only — there is no per-talkgroup vocabulary anywhere.
Wanted, on both the system and the talkgroup:
Scope resolution: merge, with talkgroup ranked above system. The specific beats the general — a multi-county system whose talkgroup covers one municipality must not have that municipality's streets swamped by a county-wide list. A single-municipality system is then the degenerate case: populate the system level only and every talkgroup inherits it.
This needs UI on the systems page: today
config.talkgroups[]holds only id and name.Scope of this change
internal/transcription.py; per-talkgroup vocabulary + area fields on the system doc; systems-page editing for them.transcript_correctedresponsibility inintelligence.py'sEXTRACTION_PROMPT.vocabulary_induction_loop. It is the only thing that discovers new terms from live traffic. Feed it the corrector's own edits — a corrector that changed "Cool Parts, Illinois" to a real street is a stronger learning signal than sampling raw transcripts, and it is the same shape as the existinglearn_from_correction.Open questions
stt_enabledrather thancorrelation_enabled, since it is part of producing a transcript. Worth confirming that is the intent before wiring it._RUN_RATIO/_MAX_PHRASE_REPEATSfurther — but a corrector that can delete a transcript needs care.Not in scope
Whether
_is_degeneratefailed to fire on the 16:54 call above is a separate question and still open — the filter returns True for that exact stored string when run locally, so either it did not run or it ran against different text. c2-core restarted mid-window, so the logs for that call are gone. Settle it by reprocessing that call, not here.Reopening. The correction pass shipped in
58efdbdand works, but the reference-data shape it shipped is wrong in three ways that only surfaced when we tried to describe a real multi-county system. The pass itself, the retry, the talkgroup-over-system ranking and the removal fromEXTRACTION_PROMPTall stand.What's wrong with the shipped shape
No state.
area_contextis{municipality, county, roads[], landmarks[]}. "Ossining" is not a place — there are Ossinings, Springfields and Fairviews everywhere. Without a state the corrector can anchor to the wrong half of the country and be confidently wrong, which is the exact failure mode this feature exists to stop.roads[]/landmarks[]cannot hold what radio traffic actually references. Intersections, schools, housing developments, rail stations, hospitals, and local nicknames ("the flats", "the north end") do not fit two lists — and a bare term is half the information anyway.11-X-rayis useless on its own;11-X-ray — MTA PD patrol unitis what lets a corrector recognise it. This also collapses the overlap withvocabulary, which was the other objection: a term list with meanings is the vocabulary, better shaped.The two scopes have different shapes. System level goes through a pydantic body; the talkgroup copy rides inside
config.talkgroups[]and is validated by nothing but the frontend.Revised shape — identical at both scopes, every field nullable
Talkgroup wins where set, system fills the gaps, absent everywhere is legal.
Nullability is the mechanism, not a convenience. System level is only meaningful when it is true of every talkgroup on that system. White Plains PD — it is, so fill it once and every talkgroup inherits. A statewide Colorado system — it is not, so leave it null and fill per talkgroup. Which level an operator fills is their declaration of how homogeneous the system is, and that is what lets one schema serve both without a
system_typeflag.Anchor rule: no anchor is better than a useless one. If municipality/county/state resolve to a radius too wide to discriminate — a whole state — write no
center/radius_kmat all. A statewide anchor would validate any location inside it, so the geocode sanity check would rubber-stamp everything while appearing to work. Absent anchor must mean "skip the check", never "accept anything".Ownership
The derived geo fields go in
area_context, not in a server-owned sibling key as first proposed. The problem that suggestion was dodging —PUT /systems/{id}taking the wholeconfigblob from the frontend and overwriting anything the server computed — is the same defect as theten_codeswipe fixed in58efdbd, and the fix is the same: the backend merges its own fields rather than taking dictation from the client. The frontend does not decide what is in a system document.Work
area_contextgainsstateandlocal_knowledge[{term, meaning}];roads[]/landmarks[]go.config.PUT /systems/{id}merges backend-ownedarea_contextsubfields instead of accepting the client'sconfigwholesale.resolved_fromis what makes that decidable), not on read. It changes when someone edits a town name, not every five minutes.Maps-based verification and the induction loop re-pointing are their own issue — this one is the schema and the pass.
Flagging while researching a separate STT-quality issue: this reads as already implemented, not just proposed.
drb-c2-core/app/internal/transcript_correction.pytoday is exactly this shape — a standalone pass between transcription and everything else (correct(), called fromtranscription.py, not fused intointelligence.py's extraction prompt), with per-talkgroup vocabulary/area_context merged talkgroup-first (resolve_context()), andvocabulary_learner.pystill doing induction.intelligence.py's extraction prompt no longer appears to owntranscript_corrected.Not verifying the remaining open question myself (systems-page UI for editing per-talkgroup vocabulary/area fields — didn't check the frontend) or the flag placement (
stt_enabledvscorrelation_enabled— #83 suggests it's still coupled to correlation, worth checking against this issue's "Flag" open question specifically). If someone confirms both of those, this is closable; leaving open rather than closing on an incomplete check.