{"record":{"id":"43ccb244a13c6ac0","repo":"tinyhumansai/openhuman","slug":"audio-blob-is-empty","errorCode":null,"errorMessage":"audio blob is empty","messagePattern":"audio blob is empty","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/features/human/voice/sttClient.ts","lineNumber":82,"sourceCode":"interface CloudTranscribeResult {\n  text: string;\n}\n\n/**\n * Transcribe a recorded audio blob via the Rust core's cloud STT proxy.\n *\n * The blob is read into a base64 string and shipped over JSON-RPC; the core\n * decodes it and POSTs `multipart/form-data` to the hosted backend's\n * `/openai/v1/audio/transcriptions` endpoint. Going through the core keeps\n * the provider API key off the desktop app and reuses the same auth flow as\n * `synthesizeSpeech`.\n */\nexport async function transcribeCloud(\n  blob: Blob,\n  opts: CloudTranscribeOptions = {}\n): Promise<string> {\n  if (!blob || blob.size === 0) {\n    throw new Error('audio blob is empty');\n  }\n  const encodeStart = Date.now();\n  const audio_base64 = await blobToBase64(blob);\n  const encodeMs = Math.round(Date.now() - encodeStart);\n\n  const params: Record<string, unknown> = { audio_base64 };\n  // MediaRecorder mime types include codec parameters (e.g. `audio/webm;codecs=opus`)\n  // — the backend's allow-list expects the bare type, so strip the suffix.\n  const mime = (opts.mimeType ?? blob.type ?? 'audio/webm').split(';')[0].trim() || 'audio/webm';\n  params.mime_type = mime;\n  params.file_name = opts.fileName ?? `audio.${guessExtension(mime)}`;\n  if (opts.model) params.model = opts.model;\n  if (opts.language) params.language = opts.language;\n\n  sttLog(\n    'transcribe bytes=%d mime=%s base64_ms=%d (b64_size=%d)',\n    blob.size,\n    mime,","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/app/src/features/human/voice/sttClient.ts#L64-L100","documentation":"transcribeCloud() sends a recorded audio blob to the core via JSON-RPC (base64-encoded) for hosted speech-to-text. Before encoding it guards that the blob exists and has size > 0, throwing 'audio blob is empty' otherwise. This is an input validation failure — no network or RPC has happened yet.","triggerScenarios":"Calling transcribeCloud(blob) with a zero-length Blob, a Blob constructed from an empty ArrayBuffer, or undefined/null. Happens when MediaRecorder.start() was called but stop() fired before any audio data was produced (instant tap-and-release on push-to-talk), or when a recording session failed silently and produced an empty blob.","commonSituations":"Push-to-talk UI where the user taps the button faster than MediaRecorder emits its first chunk; recorder error handler swallowed an error and handed an empty blob downstream; race between stop() and dataavailable event; testing with a mock blob of size 0.","solutions":["Guard at the call site: skip transcription entirely when blob.size === 0 (treat as 'no speech captured') rather than calling the API.","Ensure MediaRecorder has received at least one dataavailable chunk before stop() — await the recorder's onstop or collect chunks until stop resolves.","Log recorder state (error/mime/surface) when an empty blob is produced to find the underlying recorder failure.","For push-to-talk, enforce a minimum hold time or discard sub-threshold recordings with a friendly hint."],"exampleFix":"// before\nconst text = await transcribeCloud(blob);\n\n// after\nif (!blob || blob.size === 0) {\n  showToast('No speech captured — hold the button a bit longer.');\n  return;\n}\nconst text = await transcribeCloud(blob);","handlingStrategy":"validation","validationCode":"function hasAudio(blob: Blob | null | undefined): boolean {\n  return !!blob && blob.size > 0;\n}\nif (!hasAudio(blob)) return notifyNoSpeech();","typeGuard":"function isNonEmptyBlob(b: unknown): b is Blob {\n  return b instanceof Blob && b.size > 0;\n}","tryCatchPattern":null,"preventionTips":["Await the recorder's onstop and require chunks.length > 0 before building the Blob.","Guard at the UI boundary so empty captures get friendly feedback, not an exception.","Log blob.size/mime at capture end during development to catch recorder regressions."],"tags":["stt","voice","audio","blob","validation","recording"],"backgroundTag":"empty-request-payload","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}