Correction: reject lost numbers and growth; place verifier anchors from the talkgroup name (server-26#162, server-26#160)
transcript_correction now also discards a correction that drops or changes a bare number the recogniser heard (the live "7" -> "ShotSpotter" case) or grows the transcript past max(4 words, 25%). place_verifier.verify() derives an anchor from the talkgroup's own name plus the receiving node's state when no area is configured; no state means no anchor, and a configured place without an anchor keeps the load-bearing skip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f4d78aba77
commit
7ecfca7648
@@ -23,7 +23,8 @@ permanently improves the reference data for that channel.
|
||||
|
||||
NO ANCHOR MEANS SKIP, NOT ACCEPT. An anchor too wide to discriminate is not
|
||||
stored at all (see `area_context`), and without one this module returns
|
||||
immediately. A statewide radius would confirm anything inside it, which is worse
|
||||
immediately. A channel nobody described may still get one from its own
|
||||
talkgroup name plus the node's state (server-26#160, `_talkgroup_anchor`). A statewide radius would confirm anything inside it, which is worse
|
||||
than not checking — it looks like verification and is not.
|
||||
|
||||
THE FREE TIER RUNS FIRST. A sound-alike among the terms the operator already
|
||||
@@ -169,6 +170,50 @@ def _known_soundalike(heard: str, area: dict) -> Optional[dict]:
|
||||
return best
|
||||
|
||||
|
||||
async def _talkgroup_anchor(
|
||||
talkgroup_name: Optional[str], node_id: Optional[str],
|
||||
) -> tuple[Optional[dict], str]:
|
||||
"""
|
||||
An anchor derived from the talkgroup's own name plus the receiving node's
|
||||
state, or (None, ""). Same width rule as an operator anchor
|
||||
(area_context.resolve_anchor drops anything wider than
|
||||
area_anchor_max_radius_km, and caches per place for the process).
|
||||
|
||||
A STATE IS REQUIRED. "Springfield PD" alone geocodes to whichever
|
||||
Springfield Google prefers, and an anchor in the wrong half of the
|
||||
country would let Maps rewrite real local streets into sound-alikes from
|
||||
there (the failure server-26#36 added `state` to prevent). The node's
|
||||
reverse-geocoded state is coarse enough to be right even when the node
|
||||
sits far from the area the channel covers.
|
||||
"""
|
||||
from app.internal import firestore as fstore
|
||||
from app.internal.intelligence import _get_node_state, _municipality_from_tg
|
||||
|
||||
muni = _municipality_from_tg(talkgroup_name)
|
||||
if not muni or not node_id:
|
||||
return None, ""
|
||||
node = await fstore.doc_get_cached("nodes", node_id) or {}
|
||||
lat, lon = node.get("lat"), node.get("lon")
|
||||
if lat is None or lon is None:
|
||||
return None, ""
|
||||
state = await _get_node_state(node_id, lat, lon)
|
||||
if not state:
|
||||
return None, ""
|
||||
place = {"municipality": muni, "state": state}
|
||||
resolved = await area_context.resolve_anchor(place)
|
||||
if not resolved:
|
||||
return None, ""
|
||||
try:
|
||||
anchor = {
|
||||
"lat": float(resolved["center"]["lat"]),
|
||||
"lng": float(resolved["center"]["lng"]),
|
||||
"radius_km": float(resolved["radius_km"]),
|
||||
}
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return None, ""
|
||||
return anchor, f"{muni}, {state}"
|
||||
|
||||
|
||||
# -- Public --------------------------------------------------------------------
|
||||
|
||||
def _substitute(text: str, swaps: list[tuple[str, str]]) -> str:
|
||||
@@ -186,6 +231,8 @@ async def verify(
|
||||
tg_area: Optional[dict],
|
||||
system_id: Optional[str] = None,
|
||||
talkgroup_id: Optional[Any] = None,
|
||||
talkgroup_name: Optional[str] = None,
|
||||
node_id: Optional[str] = None,
|
||||
) -> tuple[Optional[str], Optional[list[dict]]]:
|
||||
"""
|
||||
Check the corrector's location nouns against the talkgroup's anchor.
|
||||
@@ -197,12 +244,18 @@ async def verify(
|
||||
if not settings.place_verification_enabled or not locations:
|
||||
return None, None
|
||||
|
||||
area = area_context.effective(system_area, tg_area)
|
||||
anchor = area_context.anchor_for(system_area, tg_area)
|
||||
suffix = _place_suffix(area)
|
||||
if not anchor and not area_context.has_place(area):
|
||||
# server-26#160: nobody described this channel, but its own name may
|
||||
# (e.g. "New York City - NYPD Citywide 2 Patch") — the same signal
|
||||
# #159 trusted in intelligence.py's geocoder. Only when an operator set
|
||||
# NOTHING: a place that is set but has no anchor was too wide or is
|
||||
# stale, and that skip stays load-bearing.
|
||||
anchor, suffix = await _talkgroup_anchor(talkgroup_name, node_id)
|
||||
if not anchor:
|
||||
return None, None # load-bearing: no anchor means skip, never accept
|
||||
|
||||
area = area_context.effective(system_area, tg_area)
|
||||
suffix = _place_suffix(area)
|
||||
swaps: list[tuple[str, str]] = []
|
||||
proposals: list[dict] = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user