chore: retire zen-backed summarization

Disable the active Zen summarization flow because the unauthenticated/free Zen provider is no longer available and now returns usage-limit errors for this feature.

Keep /api/text/summarize as an API-compatible stub that returns local sanitized or distilled fallback text with summarized=false, rather than attempting external model calls.

Remove notification and voice playback summary behavior from runtime paths. Notification {last_message} now always uses normalized truncated text, and TTS playback ignores historical summarize request fields.

Hide the notification summary settings and voice summarize-before-playback controls while preserving legacy persisted settings for compatibility. Also disable Zen model startup validation and make Zen model list routes return empty results.

Update module documentation and tests to describe the retired provider behavior and the remaining compatibility stubs.
This commit is contained in:
Bohdan Triapitsyn
2026-05-19 02:06:52 +03:00
parent a57b02a308
commit 174fa4e96d
27 changed files with 137 additions and 1136 deletions
@@ -1,18 +1,11 @@
import React from 'react';
import { useUIStore } from '@/stores/useUIStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
import { useDeviceInfo } from '@/lib/device';
import { updateDesktopSettings } from '@/lib/persistence';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from '@/components/ui';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { Input } from '@/components/ui/input';
import { NumberInput } from '@/components/ui/number-input';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Icon } from "@/components/icon/Icon";
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
@@ -42,16 +35,8 @@ const TEMPLATE_EVENT_LABEL_KEYS = {
question: 'settings.notifications.page.template.event.question',
} as const satisfies Record<NotificationTemplateEvent, string>;
const UTILITY_PREFERRED_MODEL_ID = 'big-pickle';
const UTILITY_NOT_SELECTED_VALUE = '__not_selected__';
const DEFAULT_SUMMARY_THRESHOLD = 200;
const DEFAULT_SUMMARY_LENGTH = 100;
const DEFAULT_MAX_LAST_MESSAGE_LENGTH = 250;
export const NotificationSettings: React.FC = () => {
const { t } = useI18n();
const { isMobile } = useDeviceInfo();
const isDesktop = React.useMemo(() => isDesktopShell(), []);
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
const isBrowser = !isDesktop && !isVSCode;
@@ -69,92 +54,11 @@ export const NotificationSettings: React.FC = () => {
const setNotifyOnQuestion = useUIStore(state => state.setNotifyOnQuestion);
const notificationTemplates = useUIStore(state => state.notificationTemplates);
const setNotificationTemplates = useUIStore(state => state.setNotificationTemplates);
const summarizeLastMessage = useUIStore(state => state.summarizeLastMessage);
const setSummarizeLastMessage = useUIStore(state => state.setSummarizeLastMessage);
const summaryThreshold = useUIStore(state => state.summaryThreshold);
const setSummaryThreshold = useUIStore(state => state.setSummaryThreshold);
const summaryLength = useUIStore(state => state.summaryLength);
const setSummaryLength = useUIStore(state => state.setSummaryLength);
const maxLastMessageLength = useUIStore(state => state.maxLastMessageLength);
const setMaxLastMessageLength = useUIStore(state => state.setMaxLastMessageLength);
const settingsZenModel = useConfigStore((state) => state.settingsZenModel);
const setSettingsZenModel = useConfigStore((state) => state.setSettingsZenModel);
const [notificationPermission, setNotificationPermission] = React.useState<NotificationPermission>('default');
const [pushSupported, setPushSupported] = React.useState(false);
const [pushSubscribed, setPushSubscribed] = React.useState(false);
const [pushBusy, setPushBusy] = React.useState(false);
const [fetchedZenModels, setFetchedZenModels] = React.useState<Array<{ id: string; name: string }>>([]);
React.useEffect(() => {
const controller = new AbortController();
void fetch('/api/zen/models', {
method: 'GET',
headers: { Accept: 'application/json' },
signal: controller.signal,
})
.then(async (response) => {
if (!response.ok) {
return [] as Array<{ id: string; name: string }>;
}
const payload = await response.json().catch(() => ({}));
const models = Array.isArray(payload?.models) ? payload.models : [];
return models
.map((entry: unknown) => {
const id = typeof (entry as { id?: unknown })?.id === 'string'
? (entry as { id: string }).id.trim()
: '';
if (!id) {
return null;
}
return { id, name: id };
})
.filter((entry: { id: string; name: string } | null): entry is { id: string; name: string } => entry !== null);
})
.then((models) => {
setFetchedZenModels(models);
})
.catch((error) => {
if (error?.name !== 'AbortError') {
console.warn('Failed to load zen utility models:', error);
}
});
return () => {
controller.abort();
};
}, []);
const utilityModelOptions = React.useMemo(() => {
return fetchedZenModels;
}, [fetchedZenModels]);
const utilitySelectedModelId = React.useMemo(() => {
if (settingsZenModel && utilityModelOptions.some((model) => model.id === settingsZenModel)) {
return settingsZenModel;
}
if (utilityModelOptions.some((model) => model.id === UTILITY_PREFERRED_MODEL_ID)) {
return UTILITY_PREFERRED_MODEL_ID;
}
return utilityModelOptions[0]?.id ?? '';
}, [settingsZenModel, utilityModelOptions]);
const handleUtilityModelChange = React.useCallback(
async (value: string) => {
const modelId = value === UTILITY_NOT_SELECTED_VALUE ? undefined : value;
setSettingsZenModel(modelId);
try {
await updateDesktopSettings({
zenModel: modelId ?? '',
gitProviderId: '',
gitModelId: '',
});
} catch (error) {
console.warn('Failed to save utility model setting:', error);
}
},
[setSettingsZenModel]
);
React.useEffect(() => {
if (!isBrowser) {
@@ -763,165 +667,6 @@ export const NotificationSettings: React.FC = () => {
</div>
</div>
{/* --- Summarization --- */}
<div className="mb-8">
<div className="mb-1 px-1">
<h3 className="typography-ui-header font-medium text-foreground">
{t('settings.notifications.page.summary.title')}
</h3>
</div>
<section className="px-2 pb-2 pt-0 space-y-0.5">
<div
className="group flex cursor-pointer items-center gap-2 py-1.5"
role="button"
tabIndex={0}
aria-pressed={summarizeLastMessage}
onClick={() => setSummarizeLastMessage(!summarizeLastMessage)}
onKeyDown={(event) => {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
setSummarizeLastMessage(!summarizeLastMessage);
}
}}
>
<Checkbox
checked={summarizeLastMessage}
onChange={setSummarizeLastMessage}
ariaLabel={t('settings.notifications.page.summary.toggleAria')}
/>
<span className="typography-ui-label text-foreground">{t('settings.notifications.page.summary.toggleLabel')}</span>
</div>
<div className="pl-6 pb-1">
<span className="typography-meta text-muted-foreground">
{t('settings.notifications.page.summary.requiresTemplateVariable')}
{' '}
<code className="text-[var(--primary-base)]">{'{last_message}'}</code>.
</span>
</div>
<div className={cn("flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8")}>
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
<div className="flex items-center gap-2">
<span className="typography-ui-label text-foreground">{t('settings.notifications.page.summary.modelLabel')}</span>
<Tooltip>
<TooltipTrigger asChild>
<Icon name="information" className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
{t('settings.notifications.page.summary.modelTooltip')}
</TooltipContent>
</Tooltip>
</div>
</div>
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
<Select
value={utilitySelectedModelId || UTILITY_NOT_SELECTED_VALUE}
onValueChange={handleUtilityModelChange}
>
<SelectTrigger className="w-fit min-w-[220px]">
<SelectValue placeholder={t('settings.notifications.page.summary.notSelected')} />
</SelectTrigger>
<SelectContent>
<SelectItem value={UTILITY_NOT_SELECTED_VALUE}>{t('settings.notifications.page.summary.notSelected')}</SelectItem>
{utilityModelOptions.map((model) => (
<SelectItem key={model.id} value={model.id}>
{model.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{summarizeLastMessage ? (
<>
<div className="flex items-center gap-8 py-1.5 mt-1 border-t border-[var(--surface-subtle)]">
<div className="flex min-w-0 flex-col w-56 shrink-0">
<span className="typography-ui-label text-foreground">{t('settings.notifications.page.summary.thresholdLabel')}</span>
<span className="typography-meta text-muted-foreground">{t('settings.notifications.page.summary.thresholdHint')}</span>
</div>
<div className="flex items-center gap-2 w-fit">
<NumberInput
value={summaryThreshold}
onValueChange={setSummaryThreshold}
min={50}
max={2000}
step={50}
className="w-20 tabular-nums"
/>
<Button size="sm"
type="button"
variant="ghost"
onClick={() => setSummaryThreshold(DEFAULT_SUMMARY_THRESHOLD)}
disabled={summaryThreshold === DEFAULT_SUMMARY_THRESHOLD}
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
aria-label={t('settings.notifications.page.summary.resetThresholdAria')}
title={t('settings.common.actions.reset')}
>
<Icon name="restart" className="h-3.5 w-3.5" />
</Button>
</div>
</div>
<div className="flex items-center gap-8 py-1.5">
<div className="flex min-w-0 flex-col w-56 shrink-0">
<span className="typography-ui-label text-foreground">{t('settings.notifications.page.summary.lengthLabel')}</span>
<span className="typography-meta text-muted-foreground">{t('settings.notifications.page.summary.lengthHint')}</span>
</div>
<div className="flex items-center gap-2 w-fit">
<NumberInput
value={summaryLength}
onValueChange={setSummaryLength}
min={20}
max={500}
step={10}
className="w-20 tabular-nums"
/>
<Button size="sm"
type="button"
variant="ghost"
onClick={() => setSummaryLength(DEFAULT_SUMMARY_LENGTH)}
disabled={summaryLength === DEFAULT_SUMMARY_LENGTH}
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
aria-label={t('settings.notifications.page.summary.resetLengthAria')}
title={t('settings.common.actions.reset')}
>
<Icon name="restart" className="h-3.5 w-3.5" />
</Button>
</div>
</div>
</>
) : (
<div className={cn("py-1.5 mt-1 border-t border-[var(--surface-subtle)]", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
<span className="typography-ui-label text-foreground">{t('settings.notifications.page.summary.maxLengthLabel')}</span>
<span className="typography-meta text-muted-foreground">{t('settings.notifications.page.summary.maxLengthHint')}</span>
</div>
<div className={cn("flex items-center gap-2", isMobile ? "w-full" : "w-fit")}>
<NumberInput
value={maxLastMessageLength}
onValueChange={setMaxLastMessageLength}
min={50}
max={1000}
step={10}
className="w-20 tabular-nums"
/>
<Button size="sm"
type="button"
variant="ghost"
onClick={() => setMaxLastMessageLength(DEFAULT_MAX_LAST_MESSAGE_LENGTH)}
disabled={maxLastMessageLength === DEFAULT_MAX_LAST_MESSAGE_LENGTH}
className="h-7 w-7 px-0 text-muted-foreground hover:text-foreground"
aria-label={t('settings.notifications.page.summary.resetMaxLengthAria')}
title={t('settings.common.actions.reset')}
>
<Icon name="restart" className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)}
</section>
</div>
</>
)}
@@ -177,14 +177,6 @@ export const VoiceSettings: React.FC = () => {
const setShowMessageTTSButtons = useConfigStore((state) => state.setShowMessageTTSButtons);
const voiceModeEnabled = useConfigStore((state) => state.voiceModeEnabled);
const setVoiceModeEnabled = useConfigStore((state) => state.setVoiceModeEnabled);
const summarizeMessageTTS = useConfigStore((state) => state.summarizeMessageTTS);
const setSummarizeMessageTTS = useConfigStore((state) => state.setSummarizeMessageTTS);
const summarizeVoiceConversation = useConfigStore((state) => state.summarizeVoiceConversation);
const setSummarizeVoiceConversation = useConfigStore((state) => state.setSummarizeVoiceConversation);
const summarizeCharacterThreshold = useConfigStore((state) => state.summarizeCharacterThreshold);
const setSummarizeCharacterThreshold = useConfigStore((state) => state.setSummarizeCharacterThreshold);
const summarizeMaxLength = useConfigStore((state) => state.summarizeMaxLength);
const setSummarizeMaxLength = useConfigStore((state) => state.setSummarizeMaxLength);
const [isSayAvailable, setIsSayAvailable] = useState(false);
const [sayVoices, setSayVoices] = useState<Array<{ name: string; locale: string }>>([]);
@@ -1006,51 +998,6 @@ export const VoiceSettings: React.FC = () => {
<span className="typography-ui-label text-foreground">{t('settings.voice.page.field.messageReadAloudButton')}</span>
</div>
<div
className="group flex cursor-pointer items-center gap-2 py-1.5"
role="button"
tabIndex={0}
aria-pressed={summarizeMessageTTS}
onClick={() => setSummarizeMessageTTS(!summarizeMessageTTS)}
onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setSummarizeMessageTTS(!summarizeMessageTTS); } }}
>
<Checkbox checked={summarizeMessageTTS} onChange={setSummarizeMessageTTS} ariaLabel={t('settings.voice.page.field.summarizeBeforePlaybackAria')} />
<span className="typography-ui-label text-foreground">{t('settings.voice.page.field.summarizeBeforePlayback')}</span>
</div>
{voiceModeEnabled && (
<div
className="group flex cursor-pointer items-center gap-2 py-1.5"
role="button"
tabIndex={0}
aria-pressed={summarizeVoiceConversation}
onClick={() => setSummarizeVoiceConversation(!summarizeVoiceConversation)}
onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setSummarizeVoiceConversation(!summarizeVoiceConversation); } }}
>
<Checkbox checked={summarizeVoiceConversation} onChange={setSummarizeVoiceConversation} ariaLabel={t('settings.voice.page.field.summarizeVoiceModeResponsesAria')} />
<span className="typography-ui-label text-foreground">{t('settings.voice.page.field.summarizeVoiceModeResponses')}</span>
</div>
)}
{(summarizeMessageTTS || summarizeVoiceConversation) && (
<>
<div className="flex items-center gap-8 py-1.5">
<span className="typography-ui-label text-foreground sm:w-56 shrink-0">{t('settings.voice.page.field.summarizationThreshold')}</span>
<div className="flex items-center gap-2 w-fit">
{!isMobile && <input type="range" min={50} max={2000} step={50} value={summarizeCharacterThreshold} onChange={(e) => setSummarizeCharacterThreshold(Number(e.target.value))} className={sliderClass} />}
<NumberInput value={summarizeCharacterThreshold} onValueChange={setSummarizeCharacterThreshold} min={50} max={2000} step={50} className="w-16 tabular-nums" />
</div>
</div>
<div className="flex items-center gap-8 py-1.5">
<span className="typography-ui-label text-foreground sm:w-56 shrink-0">{t('settings.voice.page.field.summaryMaxLength')}</span>
<div className="flex items-center gap-2 w-fit">
{!isMobile && <input type="range" min={50} max={2000} step={50} value={summarizeMaxLength} onChange={(e) => setSummarizeMaxLength(Number(e.target.value))} className={sliderClass} />}
<NumberInput value={summarizeMaxLength} onValueChange={setSummarizeMaxLength} min={50} max={2000} step={50} className="w-16 tabular-nums" />
</div>
</div>
</>
)}
</section>
{voiceModeEnabled && isSupported && (
+3 -15
View File
@@ -35,7 +35,7 @@ import { getSyncMessages, getSyncParts } from '@/sync/sync-refs';
import { useConfigStore } from '@/stores/useConfigStore';
import { useServerTTS } from './useServerTTS';
import { useSayTTS } from './useSayTTS';
import { summarizeText, shouldSummarize, sanitizeForTTS } from '@/lib/voice/summarize';
import { sanitizeForTTS } from '@/lib/voice/summarize';
export type BrowserVoiceStatus = 'idle' | 'listening' | 'processing' | 'speaking' | 'error';
@@ -151,8 +151,6 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
const openaiCompatibleVoice = useConfigStore((state) => state.openaiCompatibleVoice);
const openaiCompatibleUrl = useConfigStore((state) => state.openaiCompatibleUrl);
const openaiCompatibleTtsModel = useConfigStore((state) => state.openaiCompatibleTtsModel);
const summarizeVoiceConversation = useConfigStore((state) => state.summarizeVoiceConversation);
const summarizeCharacterThreshold = useConfigStore((state) => state.summarizeCharacterThreshold);
const shouldCheckOpenAIAvailability = voiceModeEnabled && (voiceProvider === 'openai' || voiceProvider === 'openai-compatible');
const shouldCheckSayAvailability = voiceModeEnabled && voiceProvider === 'say';
@@ -476,17 +474,7 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
// Speak the response
setStatus('speaking');
try {
// Summarize text if enabled and over threshold
let textToSpeak = textParts;
if (summarizeVoiceConversation && shouldSummarize(textParts, 'voice')) {
console.log('[useBrowserVoice] Summarizing AI response before speaking...');
textToSpeak = await summarizeText(textParts, {
threshold: summarizeCharacterThreshold,
});
} else {
// Still sanitize for TTS even when not summarizing
textToSpeak = sanitizeForTTS(textParts);
}
const textToSpeak = sanitizeForTTS(textParts);
// Helper to restart listening after speech ends
// Only auto-restart if conversation mode is enabled
@@ -613,7 +601,7 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
setStatus('error');
processingMessageRef.current = false;
}
}, [currentSessionId, currentProviderId, currentModelId, currentAgentName, language, sendMessage, setPendingInputText, createSession, speechRate, speechPitch, speechVolume, isServerTTSAvailable, speakServerTTS, isSayTTSAvailable, speakSayTTS, voiceProvider, sayVoice, browserVoice, openaiVoice, openaiCompatibleVoice, openaiCompatibleUrl, openaiCompatibleTtsModel, summarizeVoiceConversation, summarizeCharacterThreshold, conversationMode, startCurrentSTT]);
}, [currentSessionId, currentProviderId, currentModelId, currentAgentName, language, sendMessage, setPendingInputText, createSession, speechRate, speechPitch, speechVolume, isServerTTSAvailable, speakServerTTS, isSayTTSAvailable, speakSayTTS, voiceProvider, sayVoice, browserVoice, openaiVoice, openaiCompatibleVoice, openaiCompatibleUrl, openaiCompatibleTtsModel, conversationMode, startCurrentSTT]);
// Handle speech recognition result
const handleSpeechResult = useCallback(async (text: string, isFinal: boolean) => {
+3 -16
View File
@@ -10,7 +10,7 @@ import { useConfigStore } from '@/stores/useConfigStore';
import { useServerTTS } from './useServerTTS';
import { useSayTTS } from './useSayTTS';
import { browserVoiceService } from '@/lib/voice/browserVoiceService';
import { summarizeText, shouldSummarize, sanitizeForTTS } from '@/lib/voice/summarize';
import { sanitizeForTTS } from '@/lib/voice/summarize';
export interface UseMessageTTSReturn {
/** Whether TTS is currently playing for this message */
@@ -34,8 +34,6 @@ export function useMessageTTS(): UseMessageTTSReturn {
const openaiCompatibleVoice = useConfigStore((state) => state.openaiCompatibleVoice);
const openaiCompatibleUrl = useConfigStore((state) => state.openaiCompatibleUrl);
const openaiCompatibleTtsModel = useConfigStore((state) => state.openaiCompatibleTtsModel);
const summarizeMessageTTS = useConfigStore((state) => state.summarizeMessageTTS);
const summarizeCharacterThreshold = useConfigStore((state) => state.summarizeCharacterThreshold);
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
const isServerProvider = voiceProvider === 'openai' || voiceProvider === 'openai-compatible';
@@ -66,16 +64,7 @@ export function useMessageTTS(): UseMessageTTSReturn {
setIsPlaying(true);
try {
// Summarize text if enabled and over threshold
let textToSpeak = text;
if (summarizeMessageTTS && shouldSummarize(text, 'message')) {
textToSpeak = await summarizeText(text, {
threshold: summarizeCharacterThreshold,
});
} else {
// Still sanitize for TTS even when not summarizing
textToSpeak = sanitizeForTTS(text);
}
const textToSpeak = sanitizeForTTS(text);
if (isServerProvider && isServerTTSAvailable) {
const voice = voiceProvider === 'openai-compatible' ? openaiCompatibleVoice : openaiVoice;
@@ -87,7 +76,7 @@ export function useMessageTTS(): UseMessageTTSReturn {
speed: speechRate,
pitch: speechPitch,
volume: speechVolume,
summarize: false, // We already summarized client-side
summarize: false,
baseURL,
onEnd: () => setIsPlaying(false),
onError: () => setIsPlaying(false),
@@ -132,8 +121,6 @@ export function useMessageTTS(): UseMessageTTSReturn {
openaiCompatibleVoice,
openaiCompatibleUrl,
openaiCompatibleTtsModel,
summarizeMessageTTS,
summarizeCharacterThreshold,
isServerTTSAvailable,
isSayTTSAvailable,
speakServerTTS,
+3 -11
View File
@@ -134,14 +134,11 @@ export function useServerTTS(options: UseServerTTSOptions = {}): UseServerTTSRet
const audioSourceRef = useRef<AudioBufferSourceNode | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
// Get current model, threshold, and max length from config store for summarization
// Get current model and API settings from config store.
const currentProviderId = useConfigStore((state) => state.currentProviderId);
const currentModelId = useConfigStore((state) => state.currentModelId);
const summarizeCharacterThreshold = useConfigStore((state) => state.summarizeCharacterThreshold);
const summarizeMaxLength = useConfigStore((state) => state.summarizeMaxLength);
const openaiApiKey = useConfigStore((state) => state.openaiApiKey);
const openaiCompatibleUrl = useConfigStore((state) => state.openaiCompatibleUrl);
const settingsZenModel = useConfigStore((state) => state.settingsZenModel);
// Check if server TTS is available
const checkAvailability = useCallback(async (): Promise<boolean> => {
@@ -275,19 +272,14 @@ export function useServerTTS(options: UseServerTTSOptions = {}): UseServerTTSRet
model: options?.model || undefined,
speed: options?.speed || 0.9,
instructions: options?.instructions,
summarize: options?.summarize ?? true, // Summarize by default for voice output
summarize: false,
// Use provided provider/model, or fall back to current chat model
providerId: options?.providerId || currentProviderId || undefined,
modelId: options?.modelId || currentModelId || undefined,
// Use provided threshold, or fall back to user setting, or default to 200
threshold: options?.threshold ?? summarizeCharacterThreshold ?? 200,
// Max character length for summaries
maxLength: summarizeMaxLength ?? 500,
// Send API key from settings if available
apiKey: openaiApiKey || undefined,
// Send custom base URL for OpenAI-compatible servers
baseURL: options?.baseURL || undefined,
...(settingsZenModel ? { zenModel: settingsZenModel } : {}),
}),
signal: abortControllerRef.current.signal,
});
@@ -350,7 +342,7 @@ export function useServerTTS(options: UseServerTTSOptions = {}): UseServerTTSRet
options?.onError?.(errorMsg);
setIsPlaying(false);
}
}, [stop, currentProviderId, currentModelId, summarizeCharacterThreshold, summarizeMaxLength, openaiApiKey, settingsZenModel]);
}, [stop, currentProviderId, currentModelId, openaiApiKey]);
// Cleanup on unmount
useEffect(() => {
@@ -1302,7 +1302,7 @@ export const settingsDict = {
'settings.notifications.page.testNotification.body': 'This is a test notification from OpenChamber.',
'settings.voice.page.section.voiceSetup': 'Voice Setup',
'settings.voice.page.section.speechRecognition': 'Speech Recognition',
'settings.voice.page.section.playbackAndSummary': 'Playback & Summarization',
'settings.voice.page.section.playbackAndSummary': 'Playback',
'settings.voice.page.field.enableVoiceModeAria': 'Enable voice mode',
'settings.voice.page.field.enableVoiceMode': 'Enable Voice Mode',
'settings.voice.page.field.provider': 'Provider',
@@ -1302,7 +1302,7 @@ export const settingsDict = {
"settings.notifications.page.testNotification.body": "Esta es una notificación de prueba de OpenChamber.",
"settings.voice.page.section.voiceSetup": "Configuración de voz",
"settings.voice.page.section.speechRecognition": "Reconocimiento de voz",
"settings.voice.page.section.playbackAndSummary": "Reproducción y resumen",
"settings.voice.page.section.playbackAndSummary": "Reproducción",
"settings.voice.page.field.enableVoiceModeAria": "Habilitar modo de voz",
"settings.voice.page.field.enableVoiceMode": "Habilitar modo de voz",
"settings.voice.page.field.provider": "Proveedor",
@@ -1302,7 +1302,7 @@ export const settingsDict = {
'settings.notifications.page.testNotification.body': 'OpenChamber의 테스트 알림입니다.',
'settings.voice.page.section.voiceSetup': '음성',
'settings.voice.page.section.speechRecognition': '음성 인식',
'settings.voice.page.section.playbackAndSummary': '재생 및 요약',
'settings.voice.page.section.playbackAndSummary': '재생',
'settings.voice.page.field.enableVoiceModeAria': '음성 모드 활성화',
'settings.voice.page.field.enableVoiceMode': '음성 모드 활성화',
'settings.voice.page.field.provider': '프로바이더',
@@ -1548,7 +1548,7 @@ export const settingsDict = {
'settings.voice.page.stt.wasmNotLoaded': 'Model zostanie pobrany przy pierwszym użyciu',
'settings.voice.page.stt.wasmDownload': 'Pobierz',
'settings.voice.page.stt.wasmRetry': 'Spróbuj ponownie',
'settings.voice.page.section.playbackAndSummary': 'Odtwarzanie i podsumowanie',
'settings.voice.page.section.playbackAndSummary': 'Odtwarzanie',
'settings.voice.page.section.speechRecognition': 'Rozpoznawanie mowy',
'settings.voice.page.section.voiceSetup': 'Konfiguracja głosu',
'settings.voice.page.tooltip.browser': 'Darmowe, offline, ograniczone wsparcie mobilne.',
@@ -1302,7 +1302,7 @@ export const settingsDict = {
"settings.notifications.page.testNotification.body": "Esta é uma notificação de teste do OpenChamber.",
"settings.voice.page.section.voiceSetup": "Configurações de voz",
"settings.voice.page.section.speechRecognition": "Reconhecimento de voz",
"settings.voice.page.section.playbackAndSummary": "Reprodução e resumo",
"settings.voice.page.section.playbackAndSummary": "Reprodução",
"settings.voice.page.field.enableVoiceModeAria": "Ativar modo de voz",
"settings.voice.page.field.enableVoiceMode": "Ativar modo de voz",
"settings.voice.page.field.provider": "Provedor",
@@ -1302,7 +1302,7 @@ export const settingsDict = {
"settings.notifications.page.testNotification.body": "Це тестове сповіщення від OpenChamber.",
"settings.voice.page.section.voiceSetup": "Налаштування голосу",
"settings.voice.page.section.speechRecognition": "Розпізнавання мовлення",
"settings.voice.page.section.playbackAndSummary": "Відтворення та підсумовування",
"settings.voice.page.section.playbackAndSummary": "Відтворення",
"settings.voice.page.field.enableVoiceModeAria": "Увімкнути голосовий режим",
"settings.voice.page.field.enableVoiceMode": "Увімкнути голосовий режим",
"settings.voice.page.field.provider": "Провайдер",
@@ -1302,7 +1302,7 @@ export const settingsDict = {
'settings.notifications.page.testNotification.body': '这是来自 OpenChamber 的测试通知。',
'settings.voice.page.section.voiceSetup': '语音设置',
'settings.voice.page.section.speechRecognition': '语音识别',
'settings.voice.page.section.playbackAndSummary': '播放与摘要',
'settings.voice.page.section.playbackAndSummary': '播放',
'settings.voice.page.field.enableVoiceModeAria': '启用语音模式',
'settings.voice.page.field.enableVoiceMode': '启用语音模式',
'settings.voice.page.field.provider': '提供方',