moeru-ai/airi · error · Error

Invalid speech request body

Error message

Invalid speech request body

What it means

readSpeechRequest extracts { input, voice } from a fetch-style RequestInit body routed to the VOICEVOX provider adapter. It throws this error when the request has no body or the body is not a string, because the adapter expects an init object (as produced by $useProvider speech requests) rather than a live Request. Only input and voice are read; other synthesis parameters come from provider configuration.

Source

Thrown at packages/stage-ui/src/libs/providers/providers/voicevox/define.ts:207

function createVoicevoxConfigSchema(defaultBaseUrl: string) {
  return z.object({
    baseUrl: z.string().default(defaultBaseUrl),
    voiceSettings: voicevoxVoiceSettingsSchema.default({ intonation: 1, pitch: 0, speed: 1, volume: 1 }),
  })
}

/**
 * Reads the segment text and the style id out of the OpenAI-shaped body that
 * `generateSpeech` builds.
 *
 * `requestBody` in `@xsai/shared` passes that object through `objCamelToSnake`.
 * Only single-word keys survive unchanged, and `input` and `voice` are two of
 * them. A key of more than one word arrives renamed. The synthesis parameters
 * therefore come from the provider configuration, not from this body.
 */
function readSpeechRequest(init: RequestInit | undefined): { input: string, voice: string } {
  if (!init?.body || typeof init.body !== 'string')
    throw new Error('Invalid speech request body')

  const body = JSON.parse(init.body) as { input?: string, voice?: string }
  if (!body.voice)
    throw new Error('No voice selected. Pick a character in the speech settings.')

  return { input: body.input ?? '', voice: body.voice }
}

function toVoiceInfo(providerId: string, speakerName: string, style: { id: number, name: string }): VoiceInfo {
  return {
    id: String(style.id),
    languages: [{ code: 'ja', title: 'Japanese' }],
    name: `${speakerName} / ${style.name}`,
    provider: providerId,
  }
}

View on GitHub (pinned to 9c213115f8)

Solutions

  1. Pass a RequestInit whose body is a JSON string containing at least { voice } and optionally { input }
  2. Ensure the calling hook passes the fetch init object, not a Request instance
  3. Use method POST with the serialized body instead of GET
  4. Stringify the payload: body: JSON.stringify({ input, voice })

Example fix

// before
provider.speech(new Request('/speech', { method: 'POST', body: JSON.stringify({ input, voice }) }))
// after
provider.speech({ method: 'POST', body: JSON.stringify({ input, voice }) })
Defensive patterns

Strategy: validation

Validate before calling

function hasJsonBody(init) {
  return typeof init?.body === 'string' && init.body.length > 0
}
if (!hasJsonBody(init))
  init = { method: 'POST', body: JSON.stringify({ input, voice }) }

Type guard

const isStringBodyInit = (init) => typeof init?.body === 'string'

Try / catch

try {
  const request = readSpeechRequest(init)
}
catch (error) {
  if (error.message === 'Invalid speech request body')
    console.error('Expected a RequestInit with a JSON string body containing { input, voice }, got:', typeof init?.body)
  else if (error.message.startsWith('No voice selected'))
    openSpeechSettings()
  else throw error
}

Prevention

When it happens

Trigger: Invoking the VOICEVOX speech adapter with undefined RequestInit, a Request object (whose body is a ReadableStream, not a string), a FormData/Blob body, or a fetch built with a GET method that has no body.

Common situations: Wiring the provider adapter to standard fetch onSend hooks that pass a Request instead of init; calling the adapter directly without a body; middleware that consumed or replaced the body stream.

Related errors


AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-09-02). Data as JSON: /api/errors/0aa37f714c09d11d. Report an issue: GitHub.