Make callsign STT matching far more tolerant

Whisper routinely garbles the airline portion of a callsign while
nailing the flight number ("Loftansa three five niner", "Speed bird
27", "Lufthana 359"). Previously these slipped past the matcher.

Two changes:

1) Whole-string fuzzy distance for callsigns bumped to allowedDistance
   + 3 (was +1), which covers ~1–2 character substitutions in the
   airline name.

2) New `callsignMatches()` splits each candidate into its alphabetic
   airline prefix and trailing digit run and matches each part
   independently:
   - The digits (e.g. "359") are the strong anchor and must appear.
   - The airline portion is matched both verbatim and with whitespace
     stripped ("Speed bird" → "speedbird"), with a generous ~25%
     character-distance allowance.
   - Bare flight number without any airline trigger does NOT match —
     verified by a dedicated false-positive test.

7 new test cases cover the realistic Whisper error modes (misspell,
split words, ICAO letter readout, reordered words, telephony glue).
All 69 tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-05-26 21:26:54 +02:00
parent e87e6b79ac
commit c958f65b87
2 changed files with 128 additions and 3 deletions

View File

@@ -180,6 +180,77 @@ describe('matchTranscriptionToFields — false-positive guards', () => {
})
})
describe('matchTranscriptionToFields — callsign tolerance', () => {
const lufthansa = {
key: 'callsign',
expected: 'DLH359',
alternatives: ['DLH359', 'DLH 359', 'Lufthansa 359', 'Lufthansa three five niner'],
isCallsign: true,
} as const
it('tolerates a misspelled airline name (Whisper: Loftansa)', () => {
const result = matchTranscriptionToFields('Loftansa three five niner', [lufthansa])
assert.equal(result.matches['callsign'], 'DLH359')
})
it('tolerates a typo in the airline name (Lufthana)', () => {
const result = matchTranscriptionToFields('Lufthana 359', [lufthansa])
assert.equal(result.matches['callsign'], 'DLH359')
})
it('tolerates "Speed bird" split into two words instead of Speedbird', () => {
const result = matchTranscriptionToFields('Speed bird two seven', [
{
key: 'callsign',
expected: 'BAW27',
alternatives: ['BAW27', 'BAW 27', 'Speedbird 27', 'Speedbird two seven'],
isCallsign: true,
},
])
assert.equal(result.matches['callsign'], 'BAW27')
})
it('matches even when the airline name is completely absent but the flight number is right', () => {
const result = matchTranscriptionToFields('three five niner runway two five right', [
lufthansa,
])
// Without the airline portion we should NOT claim the callsign — too
// weak a signal on its own.
assert.equal(result.matches['callsign'], undefined)
})
it('matches when both airline and number are present but in odd word order', () => {
const result = matchTranscriptionToFields(
'this is Lufthansa flight three five niner inbound',
[lufthansa],
)
assert.equal(result.matches['callsign'], 'DLH359')
})
it('matches a Easy 25 → EZY25 collapse', () => {
const result = matchTranscriptionToFields('Easy two five cleared to land', [
{
key: 'callsign',
expected: 'EZY25',
alternatives: ['EZY25', 'EZY 25', 'Easy 25', 'Easy two five'],
isCallsign: true,
},
])
assert.equal(result.matches['callsign'], 'EZY25')
})
it('matches a Whisper-mangled airline name via the ICAO alternative', () => {
// Whisper sometimes outputs the raw ICAO code letters even when the
// pilot said the telephony name. The "delta lima hotel" alternative
// gives us the catch.
const result = matchTranscriptionToFields(
'delta lima hotel three five niner',
[lufthansa],
)
assert.equal(result.matches['callsign'], 'DLH359')
})
})
describe('matchTranscriptionToFields — realistic radio-check', () => {
it('handles a full spoken radio check readback', () => {
const result = matchTranscriptionToFields(