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 = trueView on GitHub (pinned to 677329427f)
Solutions
- Avoid mutating speech provider/voice/model while a reply is being voiced; apply changes between turns
- 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
- Re-send the message to hear it with the new provider/voice
- 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
- Defer speech settings changes until the current session finishes instead of cancelling mid-turn
- Ensure settings hydration writes identical values without object/string identity churn so the watcher does not fire spuriously
- After a teardown, re-send the message if the user needs to hear it with the new settings
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
- [Stage] Failed to post caption reset for ${source} (channel
- MiniMax TTS request failed: ${response.status} ${response.st
- OpenRouter audio request failed: ${response.status} ${await
- Invalid speech request body
- Failed to initialize speech provider
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/e0c0da86d6d3f66b.
Report an issue: GitHub.