{"record":{"id":"b40a7e23f18bb68a","repo":"moeru-ai/airi","slug":"unknown-kokoro-voice-body-voice","errorCode":null,"errorMessage":"Unknown Kokoro voice: ${body.voice}","messagePattern":"Unknown Kokoro voice: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/providers/providers/kokoro-local/index.ts","lineNumber":99,"sourceCode":"  },\n  createProvider() {\n    const adapterPromise = getKokoroAdapter()\n    return {\n      speech: () => ({\n        baseURL: 'http://kokoro-local/v1/',\n        model: 'kokoro-82m',\n        fetch: async (_input: RequestInfo | URL, init?: RequestInit) => {\n          if (!init?.body || typeof init.body !== 'string')\n            throw new Error('Invalid request body')\n\n          const body = JSON.parse(init.body) as { input?: string, voice?: string }\n          if (!body.voice)\n            throw new Error('Voice parameter is required')\n\n          try {\n            const adapter = await adapterPromise\n            if (!(body.voice in adapter.getVoices()))\n              throw new Error(`Unknown Kokoro voice: ${body.voice}`)\n            const buffer = await adapter.generate(body.input ?? '', body.voice as VoiceKey)\n            return new Response(buffer, {\n              status: 200,\n              headers: { 'Content-Type': 'audio/wav' },\n            })\n          }\n          catch (error) {\n            console.error('Kokoro TTS generation failed:', error)\n            throw error\n          }\n        },\n      }),\n    }\n  },\n  validationRequiredWhen: () => false,\n  validators: {\n    validateConfig: [\n      ({ t }) => ({","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/libs/providers/providers/kokoro-local/index.ts#L81-L117","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use a voice id from adapter.getVoices() — e.g. af_heart, af_bella, am_michael","Populate the voice picker from the adapter's actual voices, not a static list","Update the kokoro voice data files to the version matching the requested ids","Re-select the voice in the UI after upgrading the provider"],"exampleFix":"// before\nbody: JSON.stringify({ input: text, voice: 'alloy' }) // OpenAI voice id → throw\n\n// after\nbody: JSON.stringify({ input: text, voice: 'af_heart' }) // key of adapter.getVoices()","handlingStrategy":"validation","validationCode":"const voices = adapter.getVoices()\nconst voice = voiceId in voices ? voiceId : (Object.keys(voices)[0] as string)","typeGuard":"function isKokoroVoice(voice: string, voices: Record<string, unknown>): voice is VoiceKey {\n  return voice in voices\n}","tryCatchPattern":"try {\n  await generate(text, voice)\n}\ncatch (err) {\n  if (err.message.startsWith('Unknown Kokoro voice:'))\n    await generate(text, 'af_heart') // fall back to a known voice\n}","preventionTips":["Source voice dropdowns from the loaded adapter's getVoices()","Map OpenAI voice names to kokoro equivalents in compatibility layers","Validate persisted voice ids against the adapter after upgrades"],"tags":["kokoro","tts","voice","validation"],"backgroundTag":"invalid-parameter-value","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}