feat(live-atc): route STT through local bridge with cloud fallback

This commit is contained in:
itsrubberduck
2026-07-22 20:30:24 +02:00
parent 0a51fe68ab
commit db984c22bb
3 changed files with 69 additions and 4 deletions

View File

@@ -3,6 +3,32 @@ import { ref } from 'vue'
const PORTS = [8765, 8766, 8767, 8768, 8769, 8770]
const HEALTH_TIMEOUT_MS = 1200
const RECHECK_MS = 15_000
const LOCAL_TIMEOUT_MS = 8000
export async function postWithLocalFallback(
localUrl: string | null,
body: any,
cloudPost: () => Promise<any>,
): Promise<any> {
if (localUrl) {
const ctrl = new AbortController()
const timeout = setTimeout(() => ctrl.abort(), LOCAL_TIMEOUT_MS)
try {
const response = await fetch(localUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: ctrl.signal,
})
if (response.ok) return await response.json()
} catch {
// Local Bridge is unavailable: continue with the existing cloud path.
} finally {
clearTimeout(timeout)
}
}
return cloudPost()
}
export async function probeLocalSpeech(ports: number[] = PORTS): Promise<string | null> {
for (const port of ports) {

View File

@@ -0,0 +1,27 @@
import { describe, it, expect, vi } from 'vitest'
import { postWithLocalFallback } from './useLocalSpeechBridge'
describe('postWithLocalFallback', () => {
it('uses local when it returns 2xx', async () => {
const local = vi.fn(async () => ({ ok: true, json: async () => ({ success: true, transcription: 'roger' }) }))
vi.stubGlobal('fetch', local as any)
const cloud = vi.fn()
const response = await postWithLocalFallback('http://127.0.0.1:8765/api/atc/ptt', { a: 1 }, cloud as any)
expect(response.transcription).toBe('roger')
expect(cloud).not.toHaveBeenCalled()
})
it('falls back to cloud when local throws', async () => {
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('refused') }) as any)
const cloud = vi.fn(async () => ({ success: true, transcription: 'cloud' }))
const response = await postWithLocalFallback('http://127.0.0.1:8765/api/atc/ptt', { a: 1 }, cloud as any)
expect(response.transcription).toBe('cloud')
expect(cloud).toHaveBeenCalledOnce()
})
it('falls back to cloud when localUrl is null', async () => {
const cloud = vi.fn(async () => ({ success: true, transcription: 'cloud' }))
const response = await postWithLocalFallback(null, { a: 1 }, cloud as any)
expect(response.transcription).toBe('cloud')
})
})

View File

@@ -3,6 +3,7 @@ import { useApi } from '~/composables/useApi'
import useCommunicationsEngine from '../../shared/utils/communicationsEngine'
import { encodeWav } from '../../shared/utils/wavEncoder'
import { pmLog } from '../../shared/utils/pmLog'
import { postWithLocalFallback, useLocalSpeechBridge } from './useLocalSpeechBridge'
export interface PttRecordingDeps {
stopCurrentSpeech: () => void
@@ -24,6 +25,7 @@ export function usePttRecording(
) {
const { currentState, variables: vars } = engine
const api = useApi()
const { localUrl } = useLocalSpeechBridge()
const {
stopCurrentSpeech, speakWithRadioEffects, radioEffectsEnabled, inputMode,
backendSessionId, backendExpectedPhrase, setLastTransmission, handlePilotTransmission,
@@ -278,13 +280,18 @@ export function usePttRecording(
const base64Audio = arrayBufferToBase64(arrayBuffer)
if (isIntercom) {
const result = await api.post('/api/atc/ptt', {
const payload = {
audio: base64Audio,
moduleId: 'pilot-monitoring-intercom',
lessonId: 'intercom',
format,
sessionId: backendSessionId.value || undefined,
})
}
const result = await postWithLocalFallback(
localUrl('/api/atc/ptt'),
payload,
() => api.post('/api/atc/ptt', payload),
)
if (result.success) {
pmLog.info('PTT ✓ INTERCOM transcription:', result.transcription)
@@ -300,14 +307,19 @@ export function usePttRecording(
}
}
} else {
const result = await api.post('/api/atc/ptt', {
const payload = {
audio: base64Audio,
moduleId: 'pilot-monitoring',
lessonId: currentState.value?.id || 'general',
format,
sessionId: backendSessionId.value || undefined,
expected: buildSttExpected(),
})
}
const result = await postWithLocalFallback(
localUrl('/api/atc/ptt'),
payload,
() => api.post('/api/atc/ptt', payload),
)
if (result.success) {
pmLog.info('PTT ✓ RADIO transcription:', result.transcription)