decolua/9router · error · Error

Inworld TTS returned no audio

Error message

Inworld TTS returned no audio

What it means

The Inworld TTS handler POSTs `{ input, modelId, audioConfig:{audioEncoding:'MP3'} }` to Inworld's endpoint. After a 2xx response it requires the `audioContent` field (base64 MP3) in the JSON body; if absent it throws this error. It means Inworld acknowledged the request but did not return audio in the expected field.

Source

Thrown at open-sse/handlers/ttsProviders/genericFormats.js:87

  if (!res.ok) await throwUpstreamError(res);
  return responseToBase64(res, "mp3");
}

// Inworld: Basic auth, JSON { audioContent }
async function inworld({ baseUrl, apiKey, text, modelId, voiceId }) {
  const res = await fetch(baseUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json", "Authorization": `Basic ${apiKey}` },
    body: JSON.stringify({
      text,
      voiceId: voiceId || "Alex",
      modelId: modelId || "inworld-tts-1.5-mini",
      audioConfig: { audioEncoding: "MP3" },
    }),
  });
  if (!res.ok) await throwUpstreamError(res);
  const data = await res.json();
  if (!data.audioContent) throw new Error("Inworld TTS returned no audio");
  return { base64: data.audioContent, format: "mp3" };
}

// Cartesia: X-API-Key header
async function cartesia({ baseUrl, apiKey, text, modelId, voiceId }) {
  const res = await fetch(baseUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": apiKey,
      "Cartesia-Version": "2024-06-10",
    },
    body: JSON.stringify({
      model_id: modelId || "sonic-2",
      transcript: text,
      ...(voiceId ? { voice: { mode: "id", id: voiceId } } : {}),
      output_format: { container: "mp3", bit_rate: 128000, sample_rate: 44100 },
    }),

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Log the raw response body at this point to see what Inworld actually returned
  2. Verify the Inworld TTS endpoint URL and payload schema against current docs (field may now be nested, e.g. `result.audioContent`)
  3. Check the voice ID / API key / quota — some Inworld errors come back as 200 with a message field instead of audio
  4. Pin/upgrade to the current Inworld API version and update the handler if the field moved

Example fix

// before
const data = await res.json();
if (!data.audioContent) throw new Error('Inworld TTS returned no audio');
// after (tolerate nested shape)
const data = await res.json();
const b64 = data.audioContent ?? data.result?.audioContent ?? data.audio?.audioContent;
if (!b64) throw new Error('Inworld TTS returned no audio: ' + JSON.stringify(data).slice(0, 200));
Defensive patterns

Strategy: try-catch

Validate before calling

if (!text || typeof text !== 'string') throw new Error('text required for Inworld TTS');

Type guard

const hasAudio = (d) => d != null && typeof d === 'object' && typeof d.audioContent === 'string' && d.audioContent.length > 0;

Try / catch

try { return await inworldTts(...); } catch (e) { if (e.message.includes('no audio')) { logger.warn('inworld empty payload'); return fallbackTts(...); } throw e; }

Prevention

When it happens

Trigger: Upstream returns 200 with a JSON body lacking `audioContent` — e.g. response shape changed (API version drift), or an error/muted/empty payload embedded in a 200 response.

Common situations: Inworld API contract change (field renamed or nested differently); account/voice misconfiguration returning a silent empty response; proxy/gateway returning a 200 HTML or JSON error envelope instead of the TTS payload.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/84d615c697817773. Report an issue: GitHub.