{"record":{"id":"914e6df3621d2f8a","repo":"moeru-ai/airi","slug":"kokoro-worker-did-not-return-voice-metadata","errorCode":null,"errorMessage":"Kokoro worker did not return voice metadata","messagePattern":"Kokoro worker did not return voice metadata","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/inference/adapters/kokoro.ts","lineNumber":403,"sourceCode":"        voices = (response.metadata?.voices as Voices) ?? null\n\n        // Track GPU memory allocation\n        const coordinator = getGPUCoordinator()\n        if (allocationToken)\n          coordinator.release(allocationToken)\n        const estimateKey = `kokoro-${quantization}`\n        const estimated = MODEL_VRAM_ESTIMATES[estimateKey] ?? 165 * 1024 * 1024\n        allocationToken = coordinator.requestAllocation(`kokoro-${quantization}`, estimated)\n\n        // Record manifest so consumers can inspect how the adapter resolved\n        // device selection after fallback / WASM promotion.\n        lastManifest = { quantization, device: (response.device ?? effectiveDevice) as string }\n\n        state = 'ready'\n        updateInferenceStatus(modelStatusId, { state: 'ready', device: (response.device ?? effectiveDevice) as any })\n        onSuccess()\n        if (!voices)\n          throw new Error('Kokoro worker did not return voice metadata')\n        return voices\n      }, { signal: options?.signal })\n    }), { quantization, device: effectiveDevice }).catch((error) => {\n      // Don't route AbortError through handleWorkerError — cancellation is\n      // not a worker failure and shouldn't trigger restart logic.\n      if ((error as Error)?.name === 'AbortError')\n        throw error\n      handleWorkerError(error instanceof Error ? error : new Error(String(error)))\n      throw error\n    })\n  }\n\n  async function generate(\n    text: string,\n    voice: VoiceKey,\n    options?: { signal?: AbortSignal },\n  ): Promise<ArrayBuffer> {\n    throwIfAborted(options?.signal)","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui/src/libs/inference/adapters/kokoro.ts#L385-L421","documentation":"Thrown after a successful Kokoro model load when the worker response's metadata.voices field is null/undefined. The worker completed readiness (state set to 'ready', allocation token requested, manifest recorded) but did not return the voice catalog the adapter needs. This indicates the worker bundle/model is incomplete or a version skew between adapter and worker protocol.","triggerScenarios":"Inside loadModel's readyPromise, response.metadata?.voices resolves to null/undefined. The worker loaded weights and reported a device, but the voices manifest was stripped or never produced — e.g. a corrupted model fetch, a quantization variant without voice metadata, or a worker build that omits the metadata field.","commonSituations":"Model files partially downloaded or cached from a prior failed fetch. A custom Kokoro model without the voices.json manifest. Worker bundle downgraded/upgraded independently of the adapter so the message shape changed. WASM-fallback path that drops metadata.","solutions":["Clear the cached Kokoro model artifacts and reload so the worker re-fetches a complete bundle including voices metadata.","Ensure the worker build version matches the adapter version so the response.metadata.voices contract holds.","Try a different quantization to rule out a quantization-specific missing-manifest defect.","If running a custom model, confirm it ships the voice metadata the worker expects."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// after loadModel resolves, sanity-check voices before exposing the adapter\nif (!voices || Object.keys(voices).length === 0) {\n  // clear model cache and reload; do not surface 'ready' to callers\n}","typeGuard":"function hasVoices(v: unknown): v is Record<string, unknown> {\n  return !!v && typeof v === 'object' && Object.keys(v as object).length > 0\n}","tryCatchPattern":"try {\n  const voices = await adapter.loadModel()\n}\ncatch (err) {\n  if (err instanceof Error && err.message === 'Kokoro worker did not return voice metadata') {\n    // clear cached model artifacts and retry once; if it persists, report worker/model version skew\n  }\n  else throw err\n}","preventionTips":["Keep the Kokoro worker bundle and adapter versioned together so the metadata contract holds.","Use complete model artifacts that ship the voices manifest.","Clear the model cache after a failed load to avoid reusing an incomplete bundle."],"tags":["kokoro","tts","worker","web-worker","model-load","inference"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}