{"record":{"id":"e661503516d13e1f","repo":"moeru-ai/airi","slug":"kokoro-tts-generation-failed-no-model-loaded","errorCode":null,"errorMessage":"Kokoro TTS generation failed: No model loaded.","messagePattern":"Kokoro TTS generation failed: No model loaded\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/workers/kokoro/worker.ts","lineNumber":251,"sourceCode":"        throw new Error('Model not loaded. Send load-model first.')\n\n      if (isCancelled(requestId)) {\n        clearCancelled(requestId)\n        return\n      }\n\n      const result: InferenceResultResponse<KokoroVoicesOutput> = {\n        type: 'inference-result',\n        requestId,\n        output: { action: 'getVoices', voices: ttsModel.voices },\n      }\n      globalThis.postMessage(result)\n      return\n    }\n\n    // action === 'generate'\n    if (!ttsModel)\n      throw new Error('Kokoro TTS generation failed: No model loaded.')\n\n    const { text, voice } = input\n    const audioResult = await ttsModel.generate(text, { voice })\n\n    if (isCancelled(requestId)) {\n      clearCancelled(requestId)\n      return\n    }\n\n    // Transfer raw PCM Float32Array directly — avoids WAV blob encode/decode overhead.\n    const samples = audioResult.audio\n    const result: InferenceResultResponse<KokoroGenerateOutput> = {\n      type: 'inference-result',\n      requestId,\n      output: { action: 'generate', samples, samplingRate: audioResult.sampling_rate },\n    }\n    ;(globalThis as any).postMessage(result, [samples.buffer])\n  }","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/workers/kokoro/worker.ts#L233-L269","documentation":"The Kokoro TTS web worker keeps ttsModel null until its async init (weights download plus WASM/ONNX setup) completes; a 'generate' action arriving before that hits this guard instead of crashing on ttsModel.generate. Note that getVoices answers from the static voice list, so a successful voices response does not imply the model is loaded.","triggerScenarios":"UI posts an inference 'generate' immediately after spawning the worker; an earlier init failed (its error response was missed) and a later generate is sent; the worker was restarted by page reload or HMR while UI state still says ready.","commonSituations":"First-use race on slow networks; HMR during development resetting worker state; init error handling not wired so failures stay silent until generate.","solutions":["Await the worker's init/ready response before enabling generation.","Treat any init error response as fatal for the session and re-init before the next generate.","Queue generate requests until ready instead of posting them eagerly.","If the weights download failed, clear the model cache and re-init."],"exampleFix":"// before\nworker.postMessage({ type: 'inference', requestId, input: { action: 'generate', text, voice } })\n\n// after\nawait kokoroReady // promise resolved by the init-result message\nworker.postMessage({ type: 'inference', requestId, input: { action: 'generate', text, voice } })","handlingStrategy":"validation","validationCode":"let ready = false\nworker.addEventListener('message', (e) => {\n  if (e.data?.type === 'inference-result' && e.data.output?.action === 'init')\n    ready = true\n})\nif (!ready)\n  await initPromise","typeGuard":null,"tryCatchPattern":"try {\n  await generate(text, voice)\n}\ncatch (e) {\n  if (errorMessageFrom(e)?.includes('No model loaded')) {\n    await initWorker()\n    return await generate(text, voice) // single retry after re-init\n  }\n  throw e\n}","preventionTips":["Disable speak buttons until the init-result message arrives.","Track worker lifecycle: on replace or reload, reset the ready state.","Surface init errors in the same UI that shows generate errors."],"tags":["tts","kokoro","web-worker","race-condition","model-loading"],"backgroundTag":"model-not-loaded","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T16:17:53.355Z"}