tinyhumansai/openhuman · error

audio blob is empty

Error message

audio blob is empty

What it means

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.

Solutions

  1. Guard at the call site: skip transcription entirely when blob.size === 0 (treat as 'no speech captured') rather than calling the API.
  2. Ensure MediaRecorder has received at least one dataavailable chunk before stop() — await the recorder's onstop or collect chunks until stop resolves.
  3. Log recorder state (error/mime/surface) when an empty blob is produced to find the underlying recorder failure.
  4. For push-to-talk, enforce a minimum hold time or discard sub-threshold recordings with a friendly hint.

Example fix

// before
const text = await transcribeCloud(blob);

// after
if (!blob || blob.size === 0) {
  showToast('No speech captured — hold the button a bit longer.');
  return;
}
const text = await transcribeCloud(blob);
Defensive patterns

Strategy: validation

Validate before calling

function hasAudio(blob: Blob | null | undefined): boolean {
  return !!blob && blob.size > 0;
}
if (!hasAudio(blob)) return notifyNoSpeech();

Type guard

function isNonEmptyBlob(b: unknown): b is Blob {
  return b instanceof Blob && b.size > 0;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/43ccb244a13c6ac0. Report an issue: GitHub.

Appendix: source

Thrown at app/src/features/human/voice/sttClient.ts:82

interface CloudTranscribeResult {
  text: string;
}

/**
 * Transcribe a recorded audio blob via the Rust core's cloud STT proxy.
 *
 * The blob is read into a base64 string and shipped over JSON-RPC; the core
 * decodes it and POSTs `multipart/form-data` to the hosted backend's
 * `/openai/v1/audio/transcriptions` endpoint. Going through the core keeps
 * the provider API key off the desktop app and reuses the same auth flow as
 * `synthesizeSpeech`.
 */
export async function transcribeCloud(
  blob: Blob,
  opts: CloudTranscribeOptions = {}
): Promise<string> {
  if (!blob || blob.size === 0) {
    throw new Error('audio blob is empty');
  }
  const encodeStart = Date.now();
  const audio_base64 = await blobToBase64(blob);
  const encodeMs = Math.round(Date.now() - encodeStart);

  const params: Record<string, unknown> = { audio_base64 };
  // MediaRecorder mime types include codec parameters (e.g. `audio/webm;codecs=opus`)
  // — the backend's allow-list expects the bare type, so strip the suffix.
  const mime = (opts.mimeType ?? blob.type ?? 'audio/webm').split(';')[0].trim() || 'audio/webm';
  params.mime_type = mime;
  params.file_name = opts.fileName ?? `audio.${guessExtension(mime)}`;
  if (opts.model) params.model = opts.model;
  if (opts.language) params.language = opts.language;

  sttLog(
    'transcribe bytes=%d mime=%s base64_ms=%d (b64_size=%d)',
    blob.size,
    mime,

View on GitHub (pinned to 7491200858)