From 4cac97937fa577ae3cee95d85120b8706843abd5 Mon Sep 17 00:00:00 2001 From: lockrush-dev Date: Mon, 26 Jan 2026 01:59:50 -0500 Subject: [PATCH] fix(stage-ui): fix transcription session callback reuse and auto-send issues (#996) --- apps/stage-pocket/src/pages/index.vue | 6 +- .../src/renderer/pages/index.vue | 1 + .../src/components/Widgets/ChatArea.vue | 42 ++++++++-- .../src/pages/settings/modules/hearing.vue | 81 ++++++++++--------- .../stage-ui/src/stores/modules/hearing.ts | 46 ++++++++--- 5 files changed, 122 insertions(+), 54 deletions(-) diff --git a/apps/stage-pocket/src/pages/index.vue b/apps/stage-pocket/src/pages/index.vue index 7a39a84a5..ce302e0dd 100644 --- a/apps/stage-pocket/src/pages/index.vue +++ b/apps/stage-pocket/src/pages/index.vue @@ -46,7 +46,7 @@ const settingsAudioDeviceStore = useSettingsAudioDevice() const { stream, enabled } = storeToRefs(settingsAudioDeviceStore) const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream) const hearingPipeline = useHearingSpeechInputPipeline() -const { transcribeForRecording, transcribeForMediaStream } = hearingPipeline +const { transcribeForRecording, transcribeForMediaStream, stopStreamingTranscription } = hearingPipeline const { supportsStreamInput } = storeToRefs(hearingPipeline) const providersStore = useProvidersStore() const consciousnessStore = useConsciousnessStore() @@ -99,6 +99,8 @@ async function startAudioInteraction() { async function handleSpeechStart() { if (shouldUseStreamInput.value && stream.value) { + // Use both callbacks to support incremental updates and final transcript replacement. + // ChatArea uses only onSentenceEnd to avoid re-adding deleted text. await transcribeForMediaStream(stream.value, { onSentenceEnd: (delta) => { const finalText = delta @@ -139,6 +141,8 @@ function stopAudioInteraction() { try { stopOnStopRecord?.() stopOnStopRecord = undefined + // Stop any active streaming transcription sessions to prevent session leakage + void stopStreamingTranscription(true) disposeVAD() } catch {} diff --git a/apps/stage-tamagotchi/src/renderer/pages/index.vue b/apps/stage-tamagotchi/src/renderer/pages/index.vue index 513393cce..7087a3f96 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/index.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/index.vue @@ -203,6 +203,7 @@ async function startAudioInteraction() { return } + // Use sentence deltas for live captions and speech end for final text. await transcribeForMediaStream(stream.value, { onSentenceEnd: (delta) => { console.info('[Main Page] Received transcription delta:', delta) diff --git a/packages/stage-layouts/src/components/Widgets/ChatArea.vue b/packages/stage-layouts/src/components/Widgets/ChatArea.vue index c17128cc5..7c2229e4e 100644 --- a/packages/stage-layouts/src/components/Widgets/ChatArea.vue +++ b/packages/stage-layouts/src/components/Widgets/ChatArea.vue @@ -49,8 +49,18 @@ const shouldUseStreamInput = computed(() => supportsStreamInput.value && !!strea let autoSendTimeout: ReturnType | undefined const pendingAutoSendText = ref('') +function clearPendingAutoSend() { + if (autoSendTimeout) { + clearTimeout(autoSendTimeout) + autoSendTimeout = undefined + } + pendingAutoSendText.value = '' +} + async function debouncedAutoSend(text: string) { + // Double-check auto-send is enabled before proceeding if (!autoSendEnabled.value) { + clearPendingAutoSend() return } @@ -64,6 +74,12 @@ async function debouncedAutoSend(text: string) { // Set new timeout autoSendTimeout = setTimeout(async () => { + // Final check before sending - auto-send might have been disabled while waiting + if (!autoSendEnabled.value) { + clearPendingAutoSend() + return + } + const textToSend = pendingAutoSendText.value.trim() if (textToSend && autoSendEnabled.value) { try { @@ -259,6 +275,8 @@ async function startListening() { if (!shouldUseStreamInput.value) { const errorMsg = 'Streaming input not supported by the selected transcription provider. Please select a provider that supports streaming (e.g., Web Speech API).' console.warn('[ChatArea]', errorMsg) + // Clean up any existing sessions from other pages (e.g., test page) that might interfere + await stopStreamingTranscription(true) isListening.value = false return } @@ -276,14 +294,18 @@ async function startListening() { messageInput.value = currentText ? `${currentText} ${delta}` : delta console.info('[ChatArea] Received transcription delta:', delta) - // Auto-send if enabled + // Auto-send if enabled - check the current value (not captured in closure) + // This ensures we always respect the current setting, even if callbacks are reused if (autoSendEnabled.value) { debouncedAutoSend(delta) } + else { + // If auto-send is disabled, clear any pending auto-send text to prevent accidental sends + clearPendingAutoSend() + } } }, - // Don't use onSpeechEnd - it re-adds text that users may have deleted - // onSentenceEnd handles all text updates as transcription happens + // Omit onSpeechEnd to avoid re-adding user-deleted text; use sentence deltas only. }) // Only set listening to true if transcription started successfully @@ -311,10 +333,7 @@ async function stopListening() { console.info('[ChatArea] Stopping transcription...') // Clear auto-send timeout - if (autoSendTimeout) { - clearTimeout(autoSendTimeout) - autoSendTimeout = undefined - } + clearPendingAutoSend() // Send any pending text immediately if auto-send is enabled if (autoSendEnabled.value && pendingAutoSendText.value.trim()) { @@ -367,6 +386,15 @@ watch(stream, async (val) => { await stopListening() } }) + +// Watch for auto-send setting changes and clear pending sends if disabled +watch(autoSendEnabled, (enabled) => { + if (!enabled) { + // Auto-send was disabled - clear any pending auto-send + clearPendingAutoSend() + console.info('[ChatArea] Auto-send disabled, cleared pending text') + } +})