10 Commits

Author SHA1 Message Date
leubeem
592ec5912c feat(stt): seed Whisper prompt with expected readback + per-field debug UI
Whisper prompt seeding (per request):
- ptt.post.ts builds the prompt as generic ICAO bias + this state's expected
  readback appended LAST (survives the 224-token truncation), in both raw token
  form and spoken ICAO form via new radioSpeech.speakToken().
- pm.vue passes the expected phrase + active variable values; classroom.vue
  passes the lesson's expected field values.

Per-field readback debug:
- sttMatch.matchTranscriptionToFields returns fields[] (matched/missing + which
  view matched) plus normalized/denormalized transcription views.
- useRadioBackend types readback_report on the transmit response.
- pm.vue renders a "Readback check" panel in the right log rail; classroom.vue
  renders per-field rows under the STT panel.

Radio-pronunciation fixes (radioSpeech.ts):
- callsign expander handles multi-letter suffixes (DLH6RK -> Lufthansa six Romeo
  Kilo).
- toRadioSpeech now expands airports (EDDC -> Echo Delta Delta Charlie).
- bare altitudes >=1000 in a clearance context are spoken ("climb initially
  5000" -> "climb initially five thousand feet"); speeds/headings untouched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:12:54 +02:00
leubeem
237f924f56 test: cover the core engine and auth rotation/JWT hardening (TEST-03, TEST-04)
TEST-03 — communicationsEngine had zero tests. Add tests/shared/
communicationsEngine.test.ts exercising the deterministic core: system load &
ready state, VariableDefinition unwrapping, dual {{}}/{} template rendering,
patchVariables, moveToSilent (cursor advance + state actions + controller log),
unknown-state handling, getStateDetails, and normalizeATCText expansion.

TEST-04 — auth utils were tested but rotation and JWT verification were not.
Extend tests/server/auth.test.ts with refresh-token rotation (valid rotate,
missing cookie, access-token-as-refresh, version mismatch) and JWT hardening
(alg-confusion rejection, tampered signature, expired, malformed).

97 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:14:59 +02:00
leubeem
9619bcbe40 ci: fix red radioSpeech assertion and add a test/typecheck pipeline
The suite was red on main: the SID test still expected the old
letter-by-letter spelling ("Mike Alfa Romeo...") after the pronunciation
change to speak named waypoints as words ("Marun seven Foxtrot"). Update the
stale assertion.

Add a GitHub Actions workflow: yarn install + yarn test as the required gate,
plus a non-blocking vue-tsc job (TS strict mode is still off — promote to
required after that cleanup). Suite now 80/80.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:47:09 +02:00
itsrubberduck
f894eb4928 feat(atis): inline METAR expansion, airport name lookup, acronym fix
ATIS text often arrives in raw METAR form (e.g.
"METAR EDDF 281050Z AUTO 02008KT 320V070 CAVOK 24/02 Q1025 NOSIG"),
which TTS reads as letter-by-letter spelling. The normalizer now expands
the full WMO Code Form FM 15-XV vocabulary inline: DDHHMMZ date stamps,
compressed wind (with gusts, VRB, calm), wind variability ranges, RVR
(R25L/1500N), wind shear, slash-form temp/dewpoint, Q/A pressure,
NSC/SKC/CLR/NCD/VV cloud codes, weather phenomena (with intensity and
descriptors), recent-weather RE prefix, BECMG/TEMPO/FM/TL/AT trend
codes, and strips RMK remarks. Plus ATIS/METAR/SPECI get lowercased
so TTS pronounces them as words (pilots SPELL ILS/QNH/VOR so those
stay uppercase).

Airport ICAO codes are substituted with their OpenAIP name when the
frequencies endpoint returns one. New `airportName` field added to
the FrequencyResponse for that. Adds 7 test cases covering the user-
reported EDDF sample plus calm/VRB/gust winds, RVR, weather codes,
cloud specials, trend codes, and RMK stripping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:45:15 +02:00
itsrubberduck
c165167d6b 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>
2026-05-28 09:09:12 +02:00
itsrubberduck
ac6618e347 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>
2026-05-26 21:26:54 +02:00
itsrubberduck
3b64908461 Make classroom STT production-ready
Last pass fixed the crashes but the UX wasn't trustworthy — you'd hit
the mic, something happened, fields silently changed, and there was no
clear way to see what Whisper actually heard or which fields it touched.
This rebuilds that flow:

UI
- Dedicated transcription panel below the controls row replaces the
  single-line "Heard:" hint. Has explicit states: recording (red,
  pulsing dot, live MM:SS timer), transcribing (spinner), result
  (editable textarea + summary chip), or error (red body text).
- Mic button label shows the elapsed recording time so the pilot knows
  recording is actually running.
- Per-field mic icon appears on every blank that was filled by the
  current transcription, so it's obvious what came from speech vs.
  what was typed.
- Result panel exposes three explicit actions: Apply to fields (re-runs
  the mapping after edits), Record again, Dismiss.
- Hard auto-stop at 45s (well under the server's 2 MB / ~60s cap).
- 503/unreachable responses from the PTT endpoint now flip
  `sttServerAvailable` so the mic button gracefully hides itself.

Matching reliability (shared/utils/sttMatch.ts)
- Process fields longest-expected-first so a 6-char callsign claims its
  substring before a 1-char digit field grabs an overlapping character.
- Short candidates (<3 chars) now require a whole-word boundary match,
  so the digit "5" in callsign "359" no longer auto-fills an unrelated
  readability field.
- Two new test cases cover both false-positive guards.

62 / 62 tests green, vue-tsc clean, dev server starts and serves the
classroom page without TDZ / hydration warnings in the log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:24:41 +02:00
itsrubberduck
4a12719395 Fix STT readback — TDZ crash, spoken-form mismatch, hydration
Three bugs in yesterday's STT addition:

1) **TDZ crash** — `sttSupported` referenced `isClient` before its const
   declaration, throwing on setup and breaking the whole classroom page.
   `sttSupported` is now a ref that's populated in `onMounted`.

2) **Spoken vs. written mismatch** — Whisper returns natural ATC speech
   ("runway two five right", "lufthansa three five niner"), but the
   lesson fields hold the canonical written form ("25R", "DLH359"). The
   old `normalized.includes(...)` check never matched. Matching now lives
   in `shared/utils/sttMatch.ts` and searches both the raw normalized
   transcription *and* a denormalized projection that folds spoken
   digits/letters back to written tokens (incl. SID suffix `7S`, runway
   `25R`, scale words `five thousand → 5000`, frequency `decimal` as a
   digit-run boundary).

3) **SSR hydration mismatch** — `sttSupported` evaluated differently on
   server vs. client, causing visible-vs-hidden button divergence on
   hydration. The ref-set-on-mount approach resolves it.

The new helper is fully unit-tested (15 cases covering radio check,
departure clearance, SIDs, squawks, Speedbird telephony, decimal
frequencies and edge cases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:18:39 +02:00
itsrubberduck
0b386e873a Address classroom tester feedback (Detlef / FSC e.V.)
Fixes the most impactful issues from ~1.5h of testing:

- TTS: spell SID prefixes phonetically (ANEKI → Alfa November Echo Kilo
  India) so unfamiliar waypoint names are intelligible without prior
  briefing context.
- TTS: expand standalone uppercase waypoints after via/direct (with a
  skip list for common ATC English tokens like MAYDAY, CLEARED, …).
- TTS: join taxi route tokens with ", " so pauses land between
  taxiways (C5, Z5, U10, …) instead of running together.
- TTS: handle "ILS Z 25C" variant before the runway → "ILS Zulu runway
  two five center" (was previously read as "Zee twentyfive cee").
- Scenarios: derive arrivalRunway from the chosen approach so the
  controller no longer clears a flight for ILS 25C onto runway 18.
- Radio check: accept any readability 1–5 (numeric or spoken), shorten
  placeholder so it fits the sm-width field.
- Line-up readback: clearer hint about the runway-first ICAO order.
- Classroom UI: disable browser autocomplete/autocorrect on readback
  inputs (Edge autofill was injecting unrelated values).
- Classroom UI: "Speak answer" button replays the expected readback as
  TTS so students can hear the correct phrasing.

Tests adjusted for the new SID and taxi-route phonetics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:24:48 +02:00
itsrubberduck
ff4a1c3ee5 smoketests 2026-02-17 18:26:20 +01:00