diff --git a/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx b/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx
index 455774cb..fb8619af 100644
--- a/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx
+++ b/packages/ui/src/components/sections/openchamber/VoiceSettings.tsx
@@ -92,6 +92,8 @@ export const VoiceSettings: React.FC = () => {
const setSttSilenceThresholdDb = useConfigStore((state) => state.setSttSilenceThresholdDb);
const sttSilenceHoldMs = useConfigStore((state) => state.sttSilenceHoldMs);
const setSttSilenceHoldMs = useConfigStore((state) => state.setSttSilenceHoldMs);
+ const sttTranscribeOnStop = useConfigStore((state) => state.sttTranscribeOnStop);
+ const setSttTranscribeOnStop = useConfigStore((state) => state.setSttTranscribeOnStop);
const setShowMessageTTSButtons = useConfigStore((state) => state.setShowMessageTTSButtons);
const voiceModeEnabled = useConfigStore((state) => state.voiceModeEnabled);
const setVoiceModeEnabled = useConfigStore((state) => state.setVoiceModeEnabled);
@@ -769,6 +771,18 @@ export const VoiceSettings: React.FC = () => {
{sttProvider === 'server' && (
+
setSttTranscribeOnStop(!sttTranscribeOnStop)}
+ onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setSttTranscribeOnStop(!sttTranscribeOnStop); } }}
+ >
+
+ {t('settings.voice.page.field.transcribeOnStop')}
+
+
{!audioStreamService.isSupported() && (
{t('settings.voice.page.field.sttBrowserSupportError')}
diff --git a/packages/ui/src/components/voice/BrowserVoiceButton.tsx b/packages/ui/src/components/voice/BrowserVoiceButton.tsx
index 70b17431..d72abb4b 100644
--- a/packages/ui/src/components/voice/BrowserVoiceButton.tsx
+++ b/packages/ui/src/components/voice/BrowserVoiceButton.tsx
@@ -25,6 +25,7 @@ import {
import { VoiceStatusIndicator } from './VoiceStatusIndicator';
import { toast } from '@/components/ui/toast';
import { Icon } from "@/components/icon/Icon";
+import { useI18n } from '@/lib/i18n';
// Status text for accessibility and labels
const statusLabels: Record = {
@@ -65,7 +66,10 @@ const normalizeVoiceErrorMessage = (error: string): string => {
* Browser Voice Button with language selection
*/
export function BrowserVoiceButton() {
+ const { t } = useI18n();
const voiceModeEnabled = useConfigStore((s) => s.voiceModeEnabled);
+ const sttProvider = useConfigStore((s) => s.sttProvider);
+ const sttTranscribeOnStop = useConfigStore((s) => s.sttTranscribeOnStop);
const {
status,
@@ -74,6 +78,7 @@ export function BrowserVoiceButton() {
startVoice,
stopVoice,
+ finishVoiceInput,
conversationMode,
toggleConversationMode,
isMobile,
@@ -108,6 +113,8 @@ export function BrowserVoiceButton() {
const isIdle = status === 'idle';
const isSpeaking = status === 'speaking';
+ const canTranscribeOnStop = sttProvider === 'server' && sttTranscribeOnStop;
+ const isListeningWithTranscribeOnStop = status === 'listening' && canTranscribeOnStop;
// Show toast notification when voice error occurs
useEffect(() => {
@@ -131,6 +138,8 @@ export function BrowserVoiceButton() {
// Status text for accessibility
const statusText = isError
? error || 'Voice Error'
+ : isListeningWithTranscribeOnStop
+ ? t('voice.action.finishAndTranscribe')
: conversationMode && status === 'idle'
? 'Start Voice (Continuous mode on)'
: statusLabels[status] || 'Start Voice';
@@ -140,6 +149,9 @@ export function BrowserVoiceButton() {
if (isError && error) {
return normalizeVoiceErrorMessage(error);
}
+ if (isListeningWithTranscribeOnStop) {
+ return t('voice.action.finishAndTranscribe');
+ }
if (isActive) {
return 'Stop voice conversation';
}
@@ -152,6 +164,10 @@ export function BrowserVoiceButton() {
// Handle voice activation (used by both click and touch)
const activateVoice = useCallback(async () => {
if (isActive) {
+ if (status === 'listening' && canTranscribeOnStop) {
+ finishVoiceInput();
+ return;
+ }
stopVoice();
} else if (status !== 'error') {
// On mobile, we must NOT do any async operations before calling startVoice()
@@ -181,7 +197,7 @@ export function BrowserVoiceButton() {
}
}
}
- }, [isActive, status, startVoice, stopVoice, isMobile]);
+ }, [isActive, status, canTranscribeOnStop, finishVoiceInput, startVoice, stopVoice, isMobile]);
// Handle Shift+Click to toggle conversation mode
const handleClick = useCallback(async (e: React.MouseEvent) => {
diff --git a/packages/ui/src/hooks/useBrowserVoice.ts b/packages/ui/src/hooks/useBrowserVoice.ts
index c641816c..783c9c2f 100644
--- a/packages/ui/src/hooks/useBrowserVoice.ts
+++ b/packages/ui/src/hooks/useBrowserVoice.ts
@@ -53,6 +53,8 @@ export interface UseBrowserVoiceReturn {
startVoice: () => void;
/** Stop voice mode */
stopVoice: () => void;
+ /** Finish current voice input and process it */
+ finishVoiceInput: () => void;
/** Whether conversation mode is active */
conversationMode: boolean;
/** Toggle conversation mode */
@@ -834,6 +836,55 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
setStatus('idle');
setError(null);
}, [stopServerTTS, stopSayTTS]);
+
+ const finishVoiceInput = useCallback(() => {
+ if (!isActiveRef.current) {
+ return;
+ }
+
+ pendingResumeOnVisibleRef.current = false;
+ if (deviceChangeRestartTimerRef.current) {
+ clearTimeout(deviceChangeRestartTimerRef.current);
+ deviceChangeRestartTimerRef.current = null;
+ }
+ setStatus('processing');
+
+ if (sttProvider === 'server') {
+ void audioStreamService.finishListening().then(() => {
+ window.setTimeout(() => {
+ if (!isActiveRef.current) {
+ return;
+ }
+ if (pendingFinalTranscriptRef.current || finalTranscriptTimerRef.current) {
+ return;
+ }
+ if (processingMessageRef.current) {
+ return;
+ }
+ isActiveRef.current = false;
+ processingMessageRef.current = false;
+ setStatus('idle');
+ }, FINAL_TRANSCRIPT_SETTLE_MS + 200);
+ });
+ return;
+ }
+
+ browserVoiceService.stopListening();
+ window.setTimeout(() => {
+ if (!isActiveRef.current) {
+ return;
+ }
+ if (pendingFinalTranscriptRef.current || finalTranscriptTimerRef.current) {
+ return;
+ }
+ if (processingMessageRef.current) {
+ return;
+ }
+ isActiveRef.current = false;
+ processingMessageRef.current = false;
+ setStatus('idle');
+ }, FINAL_TRANSCRIPT_SETTLE_MS + 300);
+ }, [sttProvider]);
// Cleanup on unmount
useEffect(() => {
@@ -863,6 +914,7 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
setLanguage,
startVoice,
stopVoice,
+ finishVoiceInput,
conversationMode,
toggleConversationMode,
prepareVoice,
diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts
index 5c80ca14..49724cbc 100644
--- a/packages/ui/src/lib/i18n/messages/en.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/en.settings.ts
@@ -1330,6 +1330,8 @@ export const settingsDict = {
'settings.voice.page.field.sttLanguageHint': 'BCP-47 code (for example en, fr). Leave blank for auto-detect.',
'settings.voice.page.field.silenceThreshold': 'Silence Threshold',
'settings.voice.page.field.silenceHold': 'Silence Hold',
+ 'settings.voice.page.field.transcribeOnStopAria': 'Transcribe when stopping recording',
+ 'settings.voice.page.field.transcribeOnStop': 'Transcribe on Stop',
'settings.voice.page.field.millisecondsUnit': 'ms',
'settings.voice.page.field.messageReadAloudButtonAria': 'Message read aloud button',
'settings.voice.page.field.messageReadAloudButton': 'Message Read Aloud Button',
diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts
index 9da22acc..c11cb6b1 100644
--- a/packages/ui/src/lib/i18n/messages/en.ts
+++ b/packages/ui/src/lib/i18n/messages/en.ts
@@ -2021,6 +2021,7 @@ export const dict = {
'voice.status.speaking': 'Speaking...',
'voice.status.error': 'Voice Error',
'voice.status.conversationModeActiveAria': 'Conversation mode active',
+ 'voice.action.finishAndTranscribe': 'Finish and transcribe voice input',
'onboarding.common.actions.back': 'Back',
'onboarding.common.copyToClipboard': 'Copy to clipboard',
'onboarding.common.status.copiedToClipboard': 'Copied to clipboard',
diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts
index c2fd1527..188ff9d9 100644
--- a/packages/ui/src/lib/i18n/messages/es.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/es.settings.ts
@@ -1330,6 +1330,8 @@ export const settingsDict = {
"settings.voice.page.field.sttLanguageHint": "Código BCP-47 (por ejemplo en, fr). Dejar en blanco para detección automática.",
"settings.voice.page.field.silenceThreshold": "Umbral de silencio",
"settings.voice.page.field.silenceHold": "Retención de silencio",
+ "settings.voice.page.field.transcribeOnStopAria": "Transcribir al detener la grabación",
+ "settings.voice.page.field.transcribeOnStop": "Transcribir al detener",
"settings.voice.page.field.millisecondsUnit": "ms",
"settings.voice.page.field.messageReadAloudButtonAria": "Botón de lectura en voz alta del mensaje",
"settings.voice.page.field.messageReadAloudButton": "Botón de lectura en voz alta del mensaje",
diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts
index 4c883b1b..6afb330c 100644
--- a/packages/ui/src/lib/i18n/messages/es.ts
+++ b/packages/ui/src/lib/i18n/messages/es.ts
@@ -1987,6 +1987,7 @@ export const dict: Record = {
"voice.status.speaking": "Hablando...",
"voice.status.error": "Error de voz",
"voice.status.conversationModeActiveAria": "Modo de conversación activo",
+ "voice.action.finishAndTranscribe": "Finalizar y transcribir la entrada de voz",
"onboarding.common.actions.back": "Atrás",
"onboarding.common.copyToClipboard": "Copiar al portapapeles",
"onboarding.common.status.copiedToClipboard": "Copiado al portapapeles",
diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts
index ddcede71..f87baebd 100644
--- a/packages/ui/src/lib/i18n/messages/ko.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts
@@ -1330,6 +1330,8 @@ export const settingsDict = {
'settings.voice.page.field.sttLanguageHint': 'BCP-47 코드(예: en, fr). 자동 감지하려면 비워 두세요.',
'settings.voice.page.field.silenceThreshold': '무음 기준',
'settings.voice.page.field.silenceHold': '무음 유지',
+ 'settings.voice.page.field.transcribeOnStopAria': '녹음을 중지할 때 받아쓰기',
+ 'settings.voice.page.field.transcribeOnStop': '중지 시 받아쓰기',
'settings.voice.page.field.millisecondsUnit': 'ms',
'settings.voice.page.field.messageReadAloudButtonAria': '메시지 읽어주기 버튼',
'settings.voice.page.field.messageReadAloudButton': '메시지 읽어주기 버튼',
diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts
index 7680f558..943886aa 100644
--- a/packages/ui/src/lib/i18n/messages/ko.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.ts
@@ -2021,6 +2021,7 @@ export const dict: Record = {
'voice.status.speaking': '말하는 중…',
'voice.status.error': '음성 오류',
'voice.status.conversationModeActiveAria': '대화 모드 활성 상태',
+ 'voice.action.finishAndTranscribe': '음성 입력을 완료하고 받아쓰기',
'onboarding.common.actions.back': '뒤로',
'onboarding.common.copyToClipboard': '클립보드에 복사',
'onboarding.common.status.copiedToClipboard': '클립보드에 복사했습니다',
diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts
index 50d82870..5e48e5b2 100644
--- a/packages/ui/src/lib/i18n/messages/pl.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts
@@ -1502,6 +1502,8 @@ export const settingsDict = {
'settings.voice.page.field.serverUrlHint': 'Bazowy URL serwera TTS zgodnego z OpenAI',
'settings.voice.page.field.silenceHold': 'Podtrzymanie ciszy',
'settings.voice.page.field.silenceThreshold': 'Próg ciszy',
+ 'settings.voice.page.field.transcribeOnStopAria': 'Transkrybuj po zatrzymaniu nagrania',
+ 'settings.voice.page.field.transcribeOnStop': 'Transkrybuj po zatrzymaniu',
'settings.voice.page.field.speechPitch': 'Wysokość głosu',
'settings.voice.page.field.speechRate': 'Tempo mowy',
'settings.voice.page.field.speechVolume': 'Głośność mowy',
diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts
index 418d3c14..58f81033 100644
--- a/packages/ui/src/lib/i18n/messages/pl.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.ts
@@ -582,6 +582,7 @@ export const dict: Record = {
'voice.status.speaking': 'Mówię...',
'voice.status.error': 'Błąd głosu',
'voice.status.conversationModeActiveAria': 'Tryb rozmowy aktywny',
+ 'voice.action.finishAndTranscribe': 'Zakończ i transkrybuj wejście głosowe',
'onboarding.common.actions.back': 'Wstecz',
'onboarding.common.copyToClipboard': 'Kopiuj do schowka',
'onboarding.common.status.copiedToClipboard': 'Skopiowano do schowka',
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
index c5f37d7f..4157aab4 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
@@ -1330,6 +1330,8 @@ export const settingsDict = {
"settings.voice.page.field.sttLanguageHint": "Código BCP-47 (por exemplo en, fr). Deixe em branco para detecção automática.",
"settings.voice.page.field.silenceThreshold": "Limite de silêncio",
"settings.voice.page.field.silenceHold": "Retenção de silêncio",
+ "settings.voice.page.field.transcribeOnStopAria": "Transcrever ao parar a gravação",
+ "settings.voice.page.field.transcribeOnStop": "Transcrever ao parar",
"settings.voice.page.field.millisecondsUnit": "ms",
"settings.voice.page.field.messageReadAloudButtonAria": "Botão de leitura em voz alta da mensagem",
"settings.voice.page.field.messageReadAloudButton": "Botão de leitura em voz alta da mensagem",
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts
index f514e13b..9e1ddc00 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts
@@ -1987,6 +1987,7 @@ export const dict: Record = {
"voice.status.speaking": "Falando...",
"voice.status.error": "Erro de voz",
"voice.status.conversationModeActiveAria": "Modo de conversa ativo",
+ "voice.action.finishAndTranscribe": "Finalizar e transcrever a entrada de voz",
"onboarding.common.actions.back": "Voltar",
"onboarding.common.copyToClipboard": "Copiar para a área de transferência",
"onboarding.common.status.copiedToClipboard": "Copiado para a área de transferência",
diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts
index 5655c138..8e3a7669 100644
--- a/packages/ui/src/lib/i18n/messages/uk.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts
@@ -1330,6 +1330,8 @@ export const settingsDict = {
"settings.voice.page.field.sttLanguageHint": "BCP-47 код (наприклад en, fr). Залиште поле порожнім для автоматичного визначення.",
"settings.voice.page.field.silenceThreshold": "Поріг тиші",
"settings.voice.page.field.silenceHold": "Утримання тиші",
+ "settings.voice.page.field.transcribeOnStopAria": "Розпізнавати після зупинки запису",
+ "settings.voice.page.field.transcribeOnStop": "Розпізнавати при зупинці",
"settings.voice.page.field.millisecondsUnit": "мс",
"settings.voice.page.field.messageReadAloudButtonAria": "Кнопка «Прочитати повідомлення вголос»",
"settings.voice.page.field.messageReadAloudButton": "Кнопка «Прочитати вголос» для повідомлень",
diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts
index 813cf9b7..9170a8a9 100644
--- a/packages/ui/src/lib/i18n/messages/uk.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.ts
@@ -1987,6 +1987,7 @@ export const dict: Record = {
"voice.status.speaking": "Говорить...",
"voice.status.error": "Голосова помилка",
"voice.status.conversationModeActiveAria": "Активний режим розмови",
+ "voice.action.finishAndTranscribe": "Завершити й розпізнати голосове введення",
"onboarding.common.actions.back": "Назад",
"onboarding.common.copyToClipboard": "Копіювати в буфер обміну",
"onboarding.common.status.copiedToClipboard": "Скопійовано в буфер обміну",
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
index 34bdc06f..4d94a006 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
@@ -1330,6 +1330,8 @@ export const settingsDict = {
'settings.voice.page.field.sttLanguageHint': 'BCP-47 代码(例如 en、fr)。留空为自动检测。',
'settings.voice.page.field.silenceThreshold': '静音阈值',
'settings.voice.page.field.silenceHold': '静音保持时长',
+ 'settings.voice.page.field.transcribeOnStopAria': '停止录音时转写',
+ 'settings.voice.page.field.transcribeOnStop': '停止时转写',
'settings.voice.page.field.millisecondsUnit': '毫秒',
'settings.voice.page.field.messageReadAloudButtonAria': '消息朗读按钮',
'settings.voice.page.field.messageReadAloudButton': '消息朗读按钮',
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts
index bd879f48..d3f03260 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts
@@ -1987,6 +1987,7 @@ export const dict: Record = {
'voice.status.speaking': '播放中...',
'voice.status.error': '语音错误',
'voice.status.conversationModeActiveAria': '会话模式已启用',
+ 'voice.action.finishAndTranscribe': '结束并转写语音输入',
'onboarding.common.actions.back': '返回',
'onboarding.common.copyToClipboard': '复制到剪贴板',
'onboarding.common.status.copiedToClipboard': '已复制到剪贴板',
diff --git a/packages/ui/src/lib/voice/audioStreamService.ts b/packages/ui/src/lib/voice/audioStreamService.ts
index fe1d5f61..1e26ceb6 100644
--- a/packages/ui/src/lib/voice/audioStreamService.ts
+++ b/packages/ui/src/lib/voice/audioStreamService.ts
@@ -59,6 +59,7 @@ class AudioStreamService {
private silenceSince: number | null = null;
private onResult: SpeechResultCallback | null = null;
private onError: ErrorCallback | null = null;
+ private finishResolver: (() => void) | null = null;
private lang = 'en';
// Configurable parameters
@@ -125,16 +126,29 @@ class AudioStreamService {
/** Stop listening and clean up all resources. */
stopListening(): void {
- this.isActive = false;
this._stopVAD();
this._stopRecorder();
- this._teardownAudioContext();
- this._releaseStream();
- this.chunks = [];
+ this._cleanupAfterStop(true);
+ }
+
+ async finishListening(): Promise {
+ if (!this.isActive) return;
+
+ this._stopVAD();
this.isSpeaking = false;
this.silenceSince = null;
- this.onResult = null;
- this.onError = null;
+
+ if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive') {
+ this._cleanupAfterStop(true);
+ return;
+ }
+
+ await new Promise((resolve) => {
+ this.finishResolver = resolve;
+ this._finaliseUtterance(false);
+ });
+
+ this._cleanupAfterStop(true);
}
/** Whether currently listening. */
@@ -186,11 +200,18 @@ class AudioStreamService {
this.mediaRecorder.onstop = () => {
const blobs = this.chunks.splice(0);
const durationMs = Date.now() - this.recordingStartMs;
- if (blobs.length === 0 || durationMs < MIN_UTTERANCE_MS) return;
+ if (blobs.length === 0 || durationMs < MIN_UTTERANCE_MS) {
+ this.finishResolver?.();
+ this.finishResolver = null;
+ return;
+ }
const mType = blobs[0].type || mimeType || 'audio/webm';
const blob = new Blob(blobs, { type: mType });
- void this._upload(blob, mType);
+ void this._upload(blob, mType).finally(() => {
+ this.finishResolver?.();
+ this.finishResolver = null;
+ });
};
// Collect data every 250 ms so we don't lose the tail on stop()
@@ -202,6 +223,8 @@ class AudioStreamService {
try {
this.mediaRecorder.stop();
} catch {
+ this.finishResolver?.();
+ this.finishResolver = null;
// ignore
}
}
@@ -244,7 +267,7 @@ class AudioStreamService {
// End of utterance — stop recorder (triggers onstop → upload)
this.isSpeaking = false;
this.silenceSince = null;
- this._finaliseUtterance();
+ this._finaliseUtterance(true);
}
}
}
@@ -258,12 +281,31 @@ class AudioStreamService {
}
}
- /** Stop the current recorder to flush the utterance, then restart it. */
- private _finaliseUtterance(): void {
+ private _cleanupAfterStop(clearChunks: boolean): void {
+ const pendingResolver = this.finishResolver;
+ this.isActive = false;
+ this.finishResolver = null;
+ this.mediaRecorder = null;
+ this._teardownAudioContext();
+ this._releaseStream();
+ if (clearChunks) {
+ this.chunks = [];
+ }
+ this.isSpeaking = false;
+ this.silenceSince = null;
+ this.onResult = null;
+ this.onError = null;
+ pendingResolver?.();
+ }
+
+ /** Stop the current recorder to flush the utterance, optionally restarting for the next one. */
+ private _finaliseUtterance(restart: boolean): void {
if (!this.isActive) return;
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop();
}
+ if (!restart) return;
+
// Restart recorder for the next utterance after a short delay
// (MediaRecorder.onstop fires asynchronously; we wait for it to complete)
setTimeout(() => {
diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts
index f65e1d9b..68e32ea6 100644
--- a/packages/ui/src/stores/useConfigStore.ts
+++ b/packages/ui/src/stores/useConfigStore.ts
@@ -510,6 +510,7 @@ interface ConfigStore {
sttLanguage: string;
sttSilenceThresholdDb: number;
sttSilenceHoldMs: number;
+ sttTranscribeOnStop: boolean;
showMessageTTSButtons: boolean;
voiceModeEnabled: boolean;
// Summarization settings
@@ -533,6 +534,7 @@ interface ConfigStore {
setSttLanguage: (lang: string) => void;
setSttSilenceThresholdDb: (db: number) => void;
setSttSilenceHoldMs: (ms: number) => void;
+ setSttTranscribeOnStop: (enabled: boolean) => void;
setShowMessageTTSButtons: (show: boolean) => void;
setVoiceModeEnabled: (enabled: boolean) => void;
setSummarizeMessageTTS: (enabled: boolean) => void;
@@ -760,6 +762,13 @@ export const useConfigStore = create()(
}
return 1500;
})(),
+ sttTranscribeOnStop: (() => {
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('sttTranscribeOnStop');
+ if (saved === 'true') return true;
+ }
+ return false;
+ })(),
// Show TTS buttons on messages - disabled by default until user enables it
showMessageTTSButtons: (() => {
if (typeof window !== 'undefined') {
@@ -1865,6 +1874,13 @@ export const useConfigStore = create()(
}
},
+ setSttTranscribeOnStop: (enabled: boolean) => {
+ set({ sttTranscribeOnStop: enabled });
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('sttTranscribeOnStop', String(enabled));
+ }
+ },
+
setShowMessageTTSButtons: (show: boolean) => {
set({ showMessageTTSButtons: show });
if (typeof window !== 'undefined') {