Hear the short replies, and read the other kind of callsign

Two things, both found by measuring rather than by reading the code.

The voice-activity filter inside the recogniser is off. It was costing
words: across a night of land-mobile captures it dropped 5-15% of what
the same model finds without it -- 491 against 507, 339 against 384,
263 against 310 -- because a single-word over between two
transmissions looks to a VAD exactly like the noise it exists to
remove, and on a scanner those short replies are the ones worth
having.

Turning it off has a cost, and the cost is that Whisper hands back
"You" for five seconds of hiss as confidently as it hands back a
sentence. So the whole capture is now asked once whether anything in
it rises above its own noise. Digital silence measures 0.0 dB of
contrast and hiss at any level 0.7, while the quietest real capture of
that night measures 8.9 and most measure 10-27; the bar sits at 3, an
order of magnitude clear of both. It can veto a capture but never trim
one, which is the whole difference between it and the filter it
replaces.

The second thing: callsigns like WQVF960 were being missed entirely.
The shape being matched was the amateur one -- prefix, district digit,
suffix -- and everything else the FCC licenses is written the other
way round, the letters first and then the digits. On the GMRS and
business channels that is most of what is said: nine callsigns across
five transcripts of one evening went by unrecognised, and now do not.

The shape is written as the three allocations that exist rather than
as "letters then digits", which claims KN95, WD40 and KC135. Its
letters are checked against the word list even when they arrive as a
single token, which the amateur shape does not need -- no English word
has a digit in the middle of it, but "west 120" and "word 100" fit
this one exactly.

Lookups now fall back to hamdb.org when callook has nothing. Not a
spare copy: callook holds United States amateur licences only, so
DL1ABC and VE3ABC are INVALID there and resolve perfectly well from
the other. And a GMRS callsign is not looked up at all -- every
database reachable without an account is an amateur register, so
reporting WQVF960 as "unlisted" would blame the callsign for the
absence of a source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
The Dust Council 2026-09-02 00:30:02 -07:00
parent 0f378c4d6c
commit 8a789e57e1
14 changed files with 679 additions and 74 deletions

View file

@ -14,10 +14,11 @@ import time
import pytest
from bandsaunter.callsign import (Callsign, CallsignBook, FILLERS, HEADING,
NOT_CALLSIGNS, PHONETIC, SHAPE,
_phonetic_parts, describe_prefix,
find_callsigns, person_case, report,
from bandsaunter.callsign import (BACKUP_URL, Callsign, CallsignBook, FILLERS,
HEADING, NOT_CALLSIGNS, PHONETIC,
SERVICE_SHAPE, SHAPE, _phonetic_parts,
describe_prefix, find_callsigns,
is_service_call, person_case, report,
split_postcode)
@ -28,7 +29,7 @@ def no_network(monkeypatch):
Every lookup test uses a stub; this makes forgetting one fail loudly
rather than quietly pass with somebody's real address in the output.
"""
def refuse(self, call):
def refuse(self, url, call):
raise AssertionError(f"a test tried to look up {call} for real")
monkeypatch.setattr(CallsignBook, "_request", refuse)
@ -288,14 +289,27 @@ VALID = {
class StubBook(CallsignBook):
"""A book whose lookups are answered from a dict, not the network."""
"""A book whose lookups are answered from a dict, not the network.
def __init__(self, answers, **kw):
Only the first source is stubbed; the second is switched off unless a
test asks for it, so "how many requests did that take" stays a question
about one database.
"""
def __init__(self, answers, backup=None, **kw):
self.answers = answers
self.backup = backup or {}
self.requested = []
self.backup_requested = []
kw.setdefault("backup_url", BACKUP_URL if backup else "")
super().__init__(**kw)
def _request(self, call):
def _request(self, url, call):
if url == self.backup_url:
self.backup_requested.append(call)
if call not in self.backup:
raise OSError("no route to host")
return self.backup[call]
self.requested.append(call)
if call not in self.answers:
raise OSError("no route to host")
@ -327,9 +341,9 @@ def test_rendering_never_waits_for_the_network(tmp_path):
assertion and the test would pass whether or not the call blocked.
"""
class Slow(StubBook):
def _request(self, call):
def _request(self, url, call):
time.sleep(1.0)
return super()._request(call)
return super()._request(url, call)
b = Slow({"KU0W": VALID}, cache=tmp_path / "cache.json")
started = time.time()
@ -571,3 +585,211 @@ def test_a_record_cached_before_the_street_is_asked_about_again(tmp_path):
entry = b.get("W1AW")
b.wait(5.0)
assert entry.street == "225 Main St"
# -- GMRS, business and public-safety callsigns ------------------------------
#
# The commonest kind on 462 and 464 MHz, and the ones the amateur shape could
# never match: the letters come first and the digits after, with no district
# digit in the middle to anchor on.
@pytest.mark.parametrize("call", [
"WQVF960", # GMRS, four letters and three digits
"WSKL204",
"WSLB491",
"WXG204", # an older Part 90 licence, three letters
"WRF5506", # three letters and four digits
"KA12345", # two letters and five
])
def test_a_service_callsign_is_recognised(call):
assert SERVICE_SHAPE.match(call), f"{call} did not match"
assert is_service_call(call)
assert find_callsigns(f"copy that, {call} out") == [call]
@pytest.mark.parametrize("said,call", [
("Usually they don't need to be. WQVF960. Copy that.", "WQVF960"),
("I'm here at WSKL204. The name is Chris.", "WSKL204"),
("so thank you, WSLB491 out. All right, man.", "WSLB491"),
])
def test_a_service_callsign_is_found_in_what_was_said(said, call):
"""The exact sentences a night on the GMRS channels produced."""
assert call in find_callsigns(said)
def test_a_service_callsign_spelled_out_is_put_back_together():
"""Seven tokens, one callsign -- and longer than any amateur one."""
assert find_callsigns(
"whiskey quebec victor foxtrot nine six zero, monitoring"
) == ["WQVF960"]
@pytest.mark.parametrize("text", [
"I bought a KN95 mask",
"spray some WD40 on it",
"a KC135 went over",
"the K9 unit is here",
])
def test_letters_then_digits_is_not_enough_to_be_a_callsign(text):
"""The shape is two branches, not "letters then digits", for this reason:
a loose pattern claims every model number ever spoken."""
assert find_callsigns(text) == []
def test_the_digit_in_a_service_callsign_is_not_a_district():
"""Only amateur callsigns carry a district there. WQVF960's 9 means
nothing, and "district 9 (IL IN WI)" would be a confident answer to a
question nobody asked."""
country, district = describe_prefix("WQVF960")
assert country == "United States"
assert district == ""
assert describe_prefix("W9ABC")[1].startswith("district 9")
def test_a_service_callsign_is_not_asked_of_the_amateur_databases(tmp_path):
"""Both reachable databases are amateur registers and answer INVALID for
every one of these, so the request could only ever cost a second."""
b = book(tmp_path, {})
entry = b.get("WQVF960")
b.wait(2)
assert b.requested == []
assert entry.status == "service"
assert "GMRS or business" in entry.summary()
assert "unlisted" not in entry.summary()
def test_a_service_callsign_is_remembered_between_runs(tmp_path):
b = book(tmp_path, {})
b.get("WQVF960")
b.save()
again = book(tmp_path, {})
assert again.get("WQVF960").status == "service"
# -- the second database -----------------------------------------------------
HAMDB = {"hamdb": {"callsign": {
"call": "VE3ABC", "class": "", "expires": "", "grid": "FN25ci",
"lat": "45.3606600", "lon": "-75.7667900",
"fname": "Lawrence Carson", "mi": "", "name": "Kearns", "suffix": "",
"addr1": "2205 BEL-AIR DRIVE", "addr2": "OTTAWA", "state": "ON",
"zip": "K2C0X2", "country": "Canada"}}}
def test_the_second_database_is_asked_when_the_first_has_nothing(tmp_path):
"""callook holds United States amateur licences and nothing else, so a
Canadian callsign is INVALID there and perfectly well known elsewhere."""
b = book(tmp_path, {"VE3ABC": {"status": "INVALID"}},
backup={"VE3ABC": HAMDB})
entry = b.get("VE3ABC")
b.wait(5)
assert entry.known
assert entry.source == "hamdb.org"
assert entry.name == "Lawrence Carson Kearns"
assert entry.location == "Ottawa, ON"
assert entry.street == "2205 Bel-Air Drive"
assert entry.country == "Canada"
assert entry.position == (45.36066, -75.76679)
def test_the_second_database_answers_when_the_first_is_unreachable(tmp_path):
"""One source down should not take every lookup with it."""
b = book(tmp_path, {}, backup={"VE3ABC": HAMDB})
entry = b.get("VE3ABC")
b.wait(5)
assert entry.known and entry.source == "hamdb.org"
def test_the_second_database_is_not_asked_when_the_first_answered(tmp_path):
b = book(tmp_path, {"KU0W": VALID}, backup={"KU0W": HAMDB})
b.get("KU0W")
b.wait(5)
assert b.requested == ["KU0W"]
assert b.backup_requested == []
def test_both_databases_failing_is_offline_not_unlisted(tmp_path):
""""Unlisted" is a claim about the register. Nothing was reached here,
so nothing can be said about it."""
b = book(tmp_path, {}, backup={})
b.get("W1AW")
b.wait(5)
assert b.get("W1AW").status == "offline"
def test_the_second_database_says_not_found_in_every_field(tmp_path):
"""It writes the string NOT_FOUND rather than saying so once, which is
why the callsign field is tested before any of the rest is believed."""
missing = {"hamdb": {"callsign": {
k: "NOT_FOUND" for k in
("call", "class", "expires", "grid", "lat", "lon", "fname", "mi",
"name", "addr1", "addr2", "state", "zip", "country")}}}
b = book(tmp_path, {"ZZ9ZZZ": {"status": "INVALID"}},
backup={"ZZ9ZZZ": missing})
b.get("ZZ9ZZZ")
b.wait(5)
entry = b.get("ZZ9ZZZ")
assert entry.status == "unlisted"
assert entry.name == "" and entry.street == ""
def test_a_second_address_line_that_repeats_the_street_is_dropped(tmp_path):
"""Outside North America that field holds the street again, and printing
it would put the address in twice."""
body = {"hamdb": {"callsign": dict(
HAMDB["hamdb"]["callsign"],
call="DL1ABC", fname="", name="Klaus-Dietmar Brackmann",
addr1="Olxheim 3a", addr2="Olxheim 3 a", state="",
zip="37574 Einbeck-Olxheim", country="Germany", **{"class": "A"})}}
b = book(tmp_path, {"DL1ABC": {"status": "INVALID"}},
backup={"DL1ABC": body})
b.get("DL1ABC")
b.wait(5)
entry = b.get("DL1ABC")
assert entry.street == "Olxheim 3a"
assert entry.location == ""
assert entry.address == "Olxheim 3a, 37574 Einbeck-Olxheim"
# "A" is Advanced in the United States and a national class elsewhere,
# so it is left as it was written.
assert entry.oper_class == "A"
def test_a_united_states_class_letter_is_spelled_out(tmp_path):
body = {"hamdb": {"callsign": dict(
HAMDB["hamdb"]["callsign"], call="KK7SVX", country="United States",
**{"class": "T"})}}
b = book(tmp_path, {"KK7SVX": {"status": "INVALID"}},
backup={"KK7SVX": body})
b.get("KK7SVX")
b.wait(5)
assert b.get("KK7SVX").oper_class == "Technician"
def test_a_spelled_out_callsign_does_not_run_into_the_next_sentence():
"""From a real transcript. The lengths in the shape are what stop it:
with four letters and four digits allowed, the joiner read the 0 that
began the next callsign as part of this one."""
said = ("learn calls by Jeff. 7-3-W-F-K-L-2-0-4. 0-4-W-R-C-U, "
"Charlie Uniform.")
assert "WFKL204" in find_callsigns(said)
assert "WFKL2040" not in find_callsigns(said)
def test_a_service_callsign_said_with_a_pause_is_put_back_together():
""""WRUC 242 with WSAB 543, see you later" -- the recogniser put those
gaps in, and they mean nothing."""
found = find_callsigns("WRUC 242 with WSAB 543, see you later.")
assert found == ["WRUC242", "WSAB543"]
@pytest.mark.parametrize("text", [
"unit west 120 responding",
"the new 2024 model",
"the net 100 is up",
" ".join(f"word{n}" for n in range(120)),
])
def test_a_word_and_a_number_is_not_a_service_callsign(text):
"""The amateur shape can trust a single token -- no English word has a
digit in the middle of it. This one cannot: its letters are a run of two
to four, which is the length of a short word."""
assert find_callsigns(text) == []