moeru-ai/airi · error · Error

Unknown Kokoro voice: ${body.voice}

Error message

Unknown Kokoro voice: ${body.voice}

What it means

Inside the provider's fetch shim, after the kokoro adapter loads, the requested body.voice must be a key of adapter.getVoices(); otherwise the request is rejected before synthesis runs. The available set is defined by the adapter's voice data, so validity depends on which voices that adapter version ships.

Source

Thrown at packages/stage-ui/src/libs/providers/providers/kokoro-local/index.ts:99

  },
  createProvider() {
    const adapterPromise = getKokoroAdapter()
    return {
      speech: () => ({
        baseURL: 'http://kokoro-local/v1/',
        model: 'kokoro-82m',
        fetch: async (_input: RequestInfo | URL, init?: RequestInit) => {
          if (!init?.body || typeof init.body !== 'string')
            throw new Error('Invalid request body')

          const body = JSON.parse(init.body) as { input?: string, voice?: string }
          if (!body.voice)
            throw new Error('Voice parameter is required')

          try {
            const adapter = await adapterPromise
            if (!(body.voice in adapter.getVoices()))
              throw new Error(`Unknown Kokoro voice: ${body.voice}`)
            const buffer = await adapter.generate(body.input ?? '', body.voice as VoiceKey)
            return new Response(buffer, {
              status: 200,
              headers: { 'Content-Type': 'audio/wav' },
            })
          }
          catch (error) {
            console.error('Kokoro TTS generation failed:', error)
            throw error
          }
        },
      }),
    }
  },
  validationRequiredWhen: () => false,
  validators: {
    validateConfig: [
      ({ t }) => ({

View on GitHub (pinned to 677329427f)

Solutions

  1. Use a voice id from adapter.getVoices() — e.g. af_heart, af_bella, am_michael
  2. Populate the voice picker from the adapter's actual voices, not a static list
  3. Update the kokoro voice data files to the version matching the requested ids
  4. Re-select the voice in the UI after upgrading the provider

Example fix

// before
body: JSON.stringify({ input: text, voice: 'alloy' }) // OpenAI voice id → throw

// after
body: JSON.stringify({ input: text, voice: 'af_heart' }) // key of adapter.getVoices()
Defensive patterns

Strategy: validation

Validate before calling

const voices = adapter.getVoices()
const voice = voiceId in voices ? voiceId : (Object.keys(voices)[0] as string)

Type guard

function isKokoroVoice(voice: string, voices: Record<string, unknown>): voice is VoiceKey {
  return voice in voices
}

Try / catch

try {
  await generate(text, voice)
}
catch (err) {
  if (err.message.startsWith('Unknown Kokoro voice:'))
    await generate(text, 'af_heart') // fall back to a known voice
}

Prevention

When it happens

Trigger: Voice name typo (kokoro uses snake_case ids like af_heart); passing OpenAI TTS voice names (alloy, echo) through a compatibility layer; a persisted voice removed or renamed in a newer voices data file; the adapter loaded with a reduced voice bundle.

Common situations: Client code written against the OpenAI speech API reused for kokoro; voice list from a different kokoro version; user config carrying an outdated voice id.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/b40a7e23f18bb68a. Report an issue: GitHub.