Adds an option to transcribe server STT audio when stopping voice input. (#1219)
* feat(voice): add transcribe-on-stop recognition option * fix(voice): tighten transcribe-on-stop cleanup * fix(voice): address transcribe-on-stop review feedback * fix(voice): scope transcribe-on-stop to server STT --------- Co-authored-by: Konstantin Zolin <kzolin@alfabank.ru> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Konstantin Zolin
Bohdan Triapitsyn
parent
9347f97a98
commit
c1948fe691
@@ -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' && (
|
||||
<div className="py-1.5 space-y-2">
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={sttTranscribeOnStop}
|
||||
onClick={() => setSttTranscribeOnStop(!sttTranscribeOnStop)}
|
||||
onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setSttTranscribeOnStop(!sttTranscribeOnStop); } }}
|
||||
>
|
||||
<Checkbox checked={sttTranscribeOnStop} onChange={setSttTranscribeOnStop} ariaLabel={t('settings.voice.page.field.transcribeOnStopAria')} />
|
||||
<span className="typography-ui-label text-foreground">{t('settings.voice.page.field.transcribeOnStop')}</span>
|
||||
</div>
|
||||
|
||||
{!audioStreamService.isSupported() && (
|
||||
<p className="typography-meta text-[var(--status-error)]">
|
||||
{t('settings.voice.page.field.sttBrowserSupportError')}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user