moeru-ai/airi · error · Error

No voice selected. Pick a character in the speech settings.

Error message

No voice selected. Pick a character in the speech settings.

What it means

The VOICEVOX provider validates the incoming speech request body before proxying it to the engine. If the parsed JSON body has no `voice` field (empty string, missing, or null), readSpeechRequest throws this error because the engine needs a speaker/style id to synthesize audio. The library throws it early so the user gets an actionable message instead of an opaque engine-side 400.

Source

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

  })
}

/**
 * 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. Open the speech settings and select a character (speaker/style) so a voice id is persisted.
  2. Verify the settings store actually saves the selected voice id (check providers/voicevox settings state after selection).
  3. When calling the provider programmatically, always include `voice` in the JSON body: `{ input, voice }`.
  4. Add UI-level validation that disables the speak action until a voice is selected.

Example fix

// before
await fetch('/api/voicevox/speech', { method: 'POST', body: JSON.stringify({ input: 'hi' }) })
// after
await fetch('/api/voicevox/speech', { method: 'POST', body: JSON.stringify({ input: 'hi', voice: selectedVoiceId }) })
Defensive patterns

Strategy: validation

Validate before calling

if (!selectedVoiceId) {
  openSpeechSettings()
  return
}
await sendSpeech({ input, voice: selectedVoiceId })

Prevention

When it happens

Trigger: Calling the provider's speech endpoint (fetch to the provider route) with a JSON body where `voice` is absent, empty string, or null; e.g. `{ "input": "hello" }` with no voice field.

Common situations: The user never picked a character/style in the speech settings so the stored voice id is empty; a settings migration or fresh install left the voice field unset; custom UI code invoking the TTS route omits `voice`.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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