moeru-ai/airi · warning

[Speech Pipeline] provider/voice/model changed mid-session,

Error message

[Speech Pipeline] provider/voice/model changed mid-session, tearing down

What it means

A watcher on [activeSpeechProvider, activeSpeechVoice.id, activeSpeechModel] cancels the current speech session whenever any of the three changes mid-session. The session is torn down via currentSession.cancel('provider-or-voice-changed') and in-flight LLM tokens for that turn are silently dropped rather than fork-replayed into the new provider (whose voice/model may differ).

Source

Thrown at packages/stage-ui/src/components/scenes/Stage.vue:899

  // })

  // await db.value?.execute(`INSERT INTO memory_test (vec) VALUES (${JSON.stringify(res.embedding)});`)
}))

// Mid-session provider / voice / model swaps would otherwise keep feeding
// tokens to the OLD adapter (segmenter for the new provider, or stale ws
// for the streaming provider). Cancel the active session so the next LLM
// token after the swap falls through `currentSession?.` cleanly (silent
// drop is acceptable — we don't try to fork-replay text into a new
// adapter with potentially different voice/model).
watch(
  [activeSpeechProvider, () => activeSpeechVoice.value?.id, activeSpeechModel],
  ([provider, voiceId, model], [prevProvider, prevVoiceId, prevModel]) => {
    if (!currentSession)
      return
    if (provider === prevProvider && voiceId === prevVoiceId && model === prevModel)
      return
    console.warn('[Speech Pipeline] provider/voice/model changed mid-session, tearing down', {
      provider,
      prevProvider,
      voiceId,
      prevVoiceId,
      model,
      prevModel,
    })
    currentSession.cancel('provider-or-voice-changed')
    currentSession = null
  },
)

// Resume audio context on first user interaction (browser requirement)
let audioContextResumed = false
function resumeAudioContextOnInteraction() {
  if (audioContextResumed || !audioContext)
    return
  audioContextResumed = true

View on GitHub (pinned to 677329427f)

Solutions

  1. Avoid mutating speech provider/voice/model while a reply is being voiced; apply changes between turns
  2. If teardown fires right after startup with no user action, make settings hydration write identical values without churn so the watcher does not trigger spuriously
  3. Re-send the message to hear it with the new provider/voice
  4. For smoother UX, defer applying speech settings until the current session finishes
Defensive patterns

Strategy: fallback

Validate before calling

// Freeze speech identity while a session is active
const speechIdentityFrozen = computed(() => currentSession !== null)
// In settings UI: disable voice/model/provider pickers while speechIdentityFrozen

Type guard

function speechIdentityChanged(
  cur: [string, string | undefined, string],
  prev: [string, string | undefined, string],
): boolean {
  return cur[0] !== prev[0] || cur[1] !== prev[1] || cur[2] !== prev[2]
}

Prevention

When it happens

Trigger: Any change to speech provider, selected voice id, or speech model while a TTS session is active: the user edits speech settings mid-conversation, or a store hydration/sync rewrites these refs after speech already started.

Common situations: Changing voice while the character is speaking; switching provider mid-turn; settings hydration firing after session open, producing a spurious teardown with identical values replaced by new references.

Related errors


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