mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-08 02:25:36 +08:00
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>
This commit is contained in:
@@ -229,10 +229,26 @@ export interface SttFieldDef {
|
||||
isCallsign?: boolean
|
||||
}
|
||||
|
||||
export interface SttFieldReport {
|
||||
key: string
|
||||
expected: string
|
||||
matched: boolean
|
||||
/** The normalized candidate form that matched the transcription, or null. */
|
||||
matchedVia: string | null
|
||||
/** Which transcription view the match landed in. */
|
||||
view: 'raw' | 'spoken' | 'callsign' | null
|
||||
}
|
||||
|
||||
export interface SttMatchResult {
|
||||
matches: Record<string, string>
|
||||
filled: number
|
||||
total: number
|
||||
/** Normalized raw transcription (what Whisper returned, cleaned). */
|
||||
normalized: string
|
||||
/** Spoken→written folded view ("two five right" → "25r"). */
|
||||
denormalized: string
|
||||
/** Per-field diagnostic, in the original field order, for the comm log. */
|
||||
fields: SttFieldReport[]
|
||||
}
|
||||
|
||||
function escapeRegex(value: string): string {
|
||||
@@ -265,11 +281,22 @@ export function matchTranscriptionToFields(
|
||||
const normalized = normalizeForMatch(transcription)
|
||||
const denormalized = normalizeForMatch(denormalizeSpokenAtc(transcription))
|
||||
const matches: Record<string, string> = {}
|
||||
// Per-field diagnostic keyed by field.key (output in original order below).
|
||||
const reportByKey: Record<string, SttFieldReport> = {}
|
||||
let filled = 0
|
||||
|
||||
for (const field of pickLongestExpected(fields)) {
|
||||
const expectedRaw = (field.expected || '').trim()
|
||||
const report: SttFieldReport = {
|
||||
key: field.key,
|
||||
expected: expectedRaw,
|
||||
matched: false,
|
||||
matchedVia: null,
|
||||
view: null,
|
||||
}
|
||||
reportByKey[field.key] = report
|
||||
if (!expectedRaw) continue
|
||||
|
||||
const altList = field.alternatives || []
|
||||
const candidates = Array.from(new Set([expectedRaw, ...altList]
|
||||
.map(c => (c || '').trim())
|
||||
@@ -277,26 +304,36 @@ export function matchTranscriptionToFields(
|
||||
.map(normalizeForMatch)
|
||||
.filter(c => c.length >= 1)))
|
||||
|
||||
let matched = false
|
||||
for (const cand of candidates) {
|
||||
if (!cand) continue
|
||||
if (candidateMatches(normalized, cand) || candidateMatches(denormalized, cand)) {
|
||||
matched = true
|
||||
break
|
||||
if (candidateMatches(normalized, cand)) {
|
||||
report.matched = true; report.matchedVia = cand; report.view = 'raw'; break
|
||||
}
|
||||
if (field.isCallsign && cand.length >= 4) {
|
||||
if (callsignMatches(normalized, cand) || callsignMatches(denormalized, cand)) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
if (candidateMatches(denormalized, cand)) {
|
||||
report.matched = true; report.matchedVia = cand; report.view = 'spoken'; break
|
||||
}
|
||||
if (field.isCallsign && cand.length >= 4
|
||||
&& (callsignMatches(normalized, cand) || callsignMatches(denormalized, cand))) {
|
||||
report.matched = true; report.matchedVia = cand; report.view = 'callsign'; break
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
|
||||
if (report.matched) {
|
||||
matches[field.key] = expectedRaw
|
||||
filled++
|
||||
}
|
||||
}
|
||||
return { matches, filled, total: fields.length }
|
||||
|
||||
return {
|
||||
matches,
|
||||
filled,
|
||||
total: fields.length,
|
||||
normalized,
|
||||
denormalized,
|
||||
fields: fields.map(f => reportByKey[f.key] ?? {
|
||||
key: f.key, expected: (f.expected || '').trim(), matched: false, matchedVia: null, view: null,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function looksLikeCallsignKey(key: string, label?: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user