perf: cap mobile dictation audio chunks (#4309)

This commit is contained in:
Neil 2026-05-31 11:55:22 -07:00 committed by GitHub
parent 873ec256da
commit ce552c4a26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 31 additions and 0 deletions

View File

@ -52,4 +52,23 @@ describe('speech RPC methods', () => {
expect(response).toMatchObject({ ok: false })
expect(runtime.feedMobileDictation).not.toHaveBeenCalled()
})
it('rejects oversized dictation chunks before decoding audio', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
feedMobileDictation: vi.fn().mockReturnValue({ dictationId: 'dict-1' })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SPEECH_METHODS })
const response = await dispatcher.dispatch(
makeRequest('speech.dictation.chunk', {
dictationId: 'dict-1',
audioBase64: 'A'.repeat(Math.ceil((16_000 * 2 * 5) / 3) * 4 + 1),
sampleRate: 16_000
})
)
expect(response).toMatchObject({ ok: false })
expect(runtime.feedMobileDictation).not.toHaveBeenCalled()
})
})

View File

@ -3,6 +3,12 @@ import { defineMethod, type RpcMethod } from '../core'
import { OptionalString, requiredString } from '../schemas'
const AUDIO_BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/
const DICTATION_SAMPLE_RATE = 16_000
const PCM_BYTES_PER_SAMPLE = 2
const MAX_DICTATION_AUDIO_SECONDS = 5
const MAX_DICTATION_AUDIO_CHUNK_BYTES =
DICTATION_SAMPLE_RATE * PCM_BYTES_PER_SAMPLE * MAX_DICTATION_AUDIO_SECONDS
const MAX_DICTATION_AUDIO_CHUNK_BASE64_LENGTH = Math.ceil(MAX_DICTATION_AUDIO_CHUNK_BYTES / 3) * 4
function isValidAudioBase64(value: string): boolean {
return value.length % 4 !== 1 && AUDIO_BASE64_PATTERN.test(value)
@ -16,6 +22,12 @@ const DictationStart = z.object({
const DictationChunk = z.object({
dictationId: requiredString('Missing dictation ID'),
audioBase64: requiredString('Missing audio chunk')
// Why: feedMobileDictation decodes into Buffer + Float32Array; reject
// oversized chunks before allocation. This mirrors the mobile pending-audio budget.
.refine(
(value) => value.length <= MAX_DICTATION_AUDIO_CHUNK_BASE64_LENGTH,
'Audio chunk is too large'
)
// Why: Buffer.from(..., 'base64') silently drops malformed bytes; reject
// bad mobile audio chunks instead of feeding empty/corrupt PCM.
.refine(isValidAudioBase64, 'Audio chunk must be base64'),