mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-04 16:22:48 +08:00
feat(live-atc): route STT through local bridge with cloud fallback
This commit is contained in:
@@ -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) {
|
||||
|
||||
27
app/composables/usePttRecording.localRouting.test.ts
Normal file
27
app/composables/usePttRecording.localRouting.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user