mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-08 18:45:33 +08:00
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>
This commit is contained in:
@@ -181,6 +181,29 @@ export interface SttMatchResult {
|
||||
total: number
|
||||
}
|
||||
|
||||
function escapeRegex(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
/** Match a candidate string in the haystack. Short (1–2 char) candidates only
|
||||
* hit when they appear as a standalone token — this prevents the digit "5"
|
||||
* from matching anywhere inside a callsign like "359". */
|
||||
function candidateMatches(haystack: string, candidate: string): boolean {
|
||||
if (!candidate || !haystack) return false
|
||||
if (candidate.length >= 3) return haystack.includes(candidate)
|
||||
const re = new RegExp(`(^|\\s)${escapeRegex(candidate)}(\\s|$)`)
|
||||
return re.test(haystack)
|
||||
}
|
||||
|
||||
function pickLongestExpected(fields: SttFieldDef[]): SttFieldDef[] {
|
||||
// Longer expected values are more discriminating and should claim the
|
||||
// transcription substring before shorter ones. Stable for equal lengths.
|
||||
return fields
|
||||
.map((field, index) => ({ field, index, length: (field.expected || '').length }))
|
||||
.sort((a, b) => b.length - a.length || a.index - b.index)
|
||||
.map(entry => entry.field)
|
||||
}
|
||||
|
||||
export function matchTranscriptionToFields(
|
||||
transcription: string,
|
||||
fields: SttFieldDef[],
|
||||
@@ -189,7 +212,8 @@ export function matchTranscriptionToFields(
|
||||
const denormalized = normalizeForMatch(denormalizeSpokenAtc(transcription))
|
||||
const matches: Record<string, string> = {}
|
||||
let filled = 0
|
||||
for (const field of fields) {
|
||||
|
||||
for (const field of pickLongestExpected(fields)) {
|
||||
const expectedRaw = (field.expected || '').trim()
|
||||
if (!expectedRaw) continue
|
||||
const altList = field.alternatives || []
|
||||
@@ -202,7 +226,10 @@ export function matchTranscriptionToFields(
|
||||
let matched = false
|
||||
for (const cand of candidates) {
|
||||
if (!cand) continue
|
||||
if (normalized.includes(cand) || denormalized.includes(cand)) { matched = true; break }
|
||||
if (candidateMatches(normalized, cand) || candidateMatches(denormalized, cand)) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
if (field.isCallsign && cand.length >= 4) {
|
||||
if (fuzzyContains(normalized, cand) || fuzzyContains(denormalized, cand)) {
|
||||
matched = true
|
||||
|
||||
Reference in New Issue
Block a user