mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-11 03:45:33 +08:00
feat(atis): ICAO phraseology normalizer for spoken ATIS
The ATIS loop sent raw VATSIM `text_atis` to TTS, producing "Q-N-H one thousand twenty-four", "WIND oh thirty degrees", "RUNWAY oh eight L". New `normalizeAtisForSpeech` applies ATIS-specific transforms — info letter → phonetic alphabet, wind/temperature/time digit-by-digit, TRL expansion, NOSIG → "no significant change", cloud layers (BKN030 → broken three thousand), visibility, bare runway designators — then hands off to `normalizeRadioPhrase` for QNH/RWY/FL/freq. pm.vue calls the normalizer before posting to /api/atc/say so the disk cache keys the spoken form. Adds 4 test cases covering a full real-world EDDM broadcast plus edge cases (cloud layers, negative temperatures, km/m visibility). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
normalizeAtisForSpeech,
|
||||
normalizeMetarPhrase,
|
||||
normalizeRadioPhrase,
|
||||
spellIcaoDigits,
|
||||
@@ -38,4 +39,47 @@ describe('radioSpeech', () => {
|
||||
assert.match(metar, /gusting too fife knots/)
|
||||
assert.match(metar, /QNH wun zero wun tree/)
|
||||
})
|
||||
|
||||
it('normalizes a full VATSIM ATIS into ICAO phraseology', () => {
|
||||
const eddmAtis = `MUENCHEN INFORMATION A AUTOMATIC MET REPORT TIME 0620 EXPECT VECTORS FOR INDEPENDENT PARALLEL ILS APPROACH RUNWAY 08L AND 08R RUNWAYS IN USE 08L AND 08R TRL 60 WIND 030 DEGREES 4 KNOTS VARIABLE BETWEEN 340 AND 060 DEGREES CAVOK TEMPERATURE 18 DEW POINT 7 QNH 1024 TREND NOSIG MUENCHEN INFORMATION A OUT`
|
||||
const spoken = normalizeAtisForSpeech(eddmAtis)
|
||||
|
||||
assert.match(spoken, /Information Alfa/)
|
||||
assert.match(spoken, /time zero six too zero/)
|
||||
assert.match(spoken, /wind zero tree zero/)
|
||||
assert.match(spoken, /four knots/)
|
||||
assert.match(spoken, /between tree four zero and zero six zero degrees/)
|
||||
assert.match(spoken, /runway zero eight left/)
|
||||
assert.match(spoken, /zero eight right/)
|
||||
assert.match(spoken, /transition level six zero/)
|
||||
assert.match(spoken, /temperature wun eight/)
|
||||
assert.match(spoken, /dew point seven/)
|
||||
assert.match(spoken, /QNH wun zero too four/)
|
||||
assert.match(spoken, /no significant change/)
|
||||
assert.doesNotMatch(spoken, /\bNOSIG\b/)
|
||||
assert.doesNotMatch(spoken, /\bTRL 60\b/)
|
||||
assert.doesNotMatch(spoken, /TEMPERATURE 18/)
|
||||
})
|
||||
|
||||
it('normalizes ATIS cloud layers', () => {
|
||||
const out = normalizeAtisForSpeech('BKN030 SCT025 FEW005 CB OVC100')
|
||||
assert.match(out, /broken tree thousand/)
|
||||
assert.match(out, /scattered too thousand five hundred/)
|
||||
assert.match(out, /few five hundred cumulonimbus/)
|
||||
assert.match(out, /overcast wun zero thousand/)
|
||||
})
|
||||
|
||||
it('normalizes negative temperatures and low QNH', () => {
|
||||
const out = normalizeAtisForSpeech('TEMPERATURE -5 DEW POINT -12 QNH 0995')
|
||||
assert.match(out, /temperature minus fife/)
|
||||
assert.match(out, /dew point minus wun too/)
|
||||
assert.match(out, /QNH zero niner niner fife/)
|
||||
})
|
||||
|
||||
it('normalizes visibility in km and meters', () => {
|
||||
const out = normalizeAtisForSpeech('VIS 10 KM VIS 5000 M VISIBILITY 1500 M')
|
||||
assert.match(out, /visibility wun zero kilometers/)
|
||||
assert.match(out, /fife thousand meters/)
|
||||
assert.match(out, /wun thousand five hundred meters/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user