{"record":{"id":"27542fac18a6e3b4","repo":"moeru-ai/airi","slug":"speech-pipeline-bidirectional-ws-provider-reache","errorCode":null,"errorMessage":"[Speech Pipeline] bidirectional-ws provider reached per-segment fallback","messagePattern":"\\[Speech Pipeline\\] bidirectional-ws provider reached per-segment fallback","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/components/scenes/Stage.vue","lineNumber":443,"sourceCode":"    if (speechMuted.value)\n      return null\n\n    if (activeSpeechProvider.value === 'speech-noop')\n      return null\n\n    if (!activeSpeechProvider.value)\n      return null\n\n    // Streaming provider must NEVER reach this per-segment callback. The\n    // streaming code path opens its own ws at `onBeforeMessageComposed`\n    // and bypasses speech-pipeline entirely. If we got here while the\n    // streaming provider is active, the open path failed (most often:\n    // voice catalog hadn't finished loading when the user sent the\n    // message). The old fallback would silently re-open a fresh ws per\n    // segment — exactly the behavior the refactor is meant to delete.\n    // Codex review MEDIUM #3: refuse loudly instead.\n    if (resolveSpeechTransport(activeSpeechProvider.value) === 'bidirectional-ws') {\n      console.warn('[Speech Pipeline] bidirectional-ws provider reached per-segment fallback', {\n        reason: 'streaming session was not opened at intent start (voice unset?)',\n        provider: activeSpeechProvider.value,\n        segment: request.text?.slice(0, 40),\n      })\n      return null\n    }\n\n    const provider = await providersStore.getProviderInstance(activeSpeechProvider.value) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>\n    if (!provider) {\n      console.error('Failed to initialize speech provider')\n      return null\n    }\n\n    if (!request.text && !request.special)\n      return null\n\n    const providerConfig = providerStore.getProviderConfig(activeSpeechProvider.value)\n","sourceCodeStart":425,"sourceCodeEnd":461,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/components/scenes/Stage.vue#L425-L461","documentation":"Stage.vue's per-segment speech callback refuses to synthesize for bidirectional-ws providers (streaming, e.g. ElevenLabs). The streaming path must open its own websocket at onBeforeMessageComposed and bypass speech-pipeline; reaching this per-segment fallback means that open never happened — most often the voice catalog had not finished loading when the message was sent. The segment's audio is deliberately dropped (returns null) instead of silently re-opening a ws per segment.","triggerScenarios":"The active speech provider resolves to 'bidirectional-ws' while no streaming session was opened at intent start: no voice selected or the voice catalog still loading when the user sent the message, or the open path having failed earlier.","commonSituations":"Sending the first message immediately after app start, before voices finish loading; a provider API key/voice config that makes catalog fetching fail; a network hiccup during session open.","solutions":["Wait for the voice catalog to load and a voice to be selected before sending the first message — subsequent messages open the streaming session correctly","Check the speech provider settings: an invalid API key or unreachable base URL keeps the catalog (and therefore the open path) from ever succeeding","If it fires on every message, use the warn payload (provider + segment text) and devtools to find why the onBeforeMessageComposed open path fails","Reload the page as a last resort to reinitialize the speech pipeline"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before sending a message with a streaming speech provider\nif (resolveSpeechTransport(provider) === 'bidirectional-ws') {\n  const voiceReady = voices.value.length > 0 && activeSpeechVoice.value\n  if (!voiceReady)\n    await until(() => voices.value.length > 0 && activeSpeechVoice.value).toBeTruthy({ timeout: 5000 })\n}","typeGuard":"function streamingSessionReady(provider: string, voiceId: string | undefined): boolean {\n  return resolveSpeechTransport(provider) !== 'bidirectional-ws' || typeof voiceId === 'string'\n}","tryCatchPattern":null,"preventionTips":["Block or defer message sending until the voice catalog has loaded for streaming providers","Validate speech provider credentials at settings-save time so catalog fetch cannot fail silently","Monitor this warn as an invariant violation: streaming providers must never hit the per-segment path"],"tags":["speech","streaming","websocket","race-condition","voice-catalog"],"backgroundTag":"streaming-session-not-opened","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}