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.
137 lines
5.2 KiB
TypeScript
137 lines
5.2 KiB
TypeScript
/**
|
|
* useMessageTTS Hook
|
|
*
|
|
* Hook for playing TTS on individual messages.
|
|
* Uses the configured voice provider (browser, OpenAI, or macOS Say).
|
|
*/
|
|
|
|
import { useCallback, useState } from 'react';
|
|
import { useConfigStore } from '@/stores/useConfigStore';
|
|
import { useServerTTS } from './useServerTTS';
|
|
import { useSayTTS } from './useSayTTS';
|
|
import { browserVoiceService } from '@/lib/voice/browserVoiceService';
|
|
import { sanitizeForTTS } from '@/lib/voice/summarize';
|
|
|
|
export interface UseMessageTTSReturn {
|
|
/** Whether TTS is currently playing for this message */
|
|
isPlaying: boolean;
|
|
/** Play the message text */
|
|
play: (text: string) => Promise<void>;
|
|
/** Stop playback */
|
|
stop: () => void;
|
|
}
|
|
|
|
export function useMessageTTS(): UseMessageTTSReturn {
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
|
const voiceProvider = useConfigStore((state) => state.voiceProvider);
|
|
const speechRate = useConfigStore((state) => state.speechRate);
|
|
const speechPitch = useConfigStore((state) => state.speechPitch);
|
|
const speechVolume = useConfigStore((state) => state.speechVolume);
|
|
const sayVoice = useConfigStore((state) => state.sayVoice);
|
|
const browserVoice = useConfigStore((state) => state.browserVoice);
|
|
const openaiVoice = useConfigStore((state) => state.openaiVoice);
|
|
const openaiCompatibleVoice = useConfigStore((state) => state.openaiCompatibleVoice);
|
|
const openaiCompatibleUrl = useConfigStore((state) => state.openaiCompatibleUrl);
|
|
const openaiCompatibleTtsModel = useConfigStore((state) => state.openaiCompatibleTtsModel);
|
|
const showMessageTTSButtons = useConfigStore((state) => state.showMessageTTSButtons);
|
|
|
|
const isServerProvider = voiceProvider === 'openai' || voiceProvider === 'openai-compatible';
|
|
const shouldCheckOpenAIAvailability = showMessageTTSButtons && isServerProvider;
|
|
const shouldCheckSayAvailability = showMessageTTSButtons && voiceProvider === 'say';
|
|
|
|
const { speak: speakServerTTS, stop: stopServerTTS, isAvailable: isServerTTSAvailable } = useServerTTS({
|
|
enabled: shouldCheckOpenAIAvailability,
|
|
availabilityMode: voiceProvider === 'openai-compatible' ? 'openai-compatible' : 'openai',
|
|
});
|
|
const { speak: speakSayTTS, stop: stopSayTTS, isAvailable: isSayTTSAvailable } = useSayTTS({
|
|
enabled: shouldCheckSayAvailability,
|
|
});
|
|
|
|
const stop = useCallback(() => {
|
|
setIsPlaying(false);
|
|
stopServerTTS();
|
|
stopSayTTS();
|
|
browserVoiceService.cancelSpeech();
|
|
}, [stopServerTTS, stopSayTTS]);
|
|
|
|
const play = useCallback(async (text: string) => {
|
|
if (!text.trim()) return;
|
|
|
|
// Stop any existing playback
|
|
stop();
|
|
|
|
setIsPlaying(true);
|
|
|
|
try {
|
|
const textToSpeak = sanitizeForTTS(text);
|
|
|
|
if (isServerProvider && isServerTTSAvailable) {
|
|
const voice = voiceProvider === 'openai-compatible' ? openaiCompatibleVoice : openaiVoice;
|
|
const baseURL = voiceProvider === 'openai-compatible' ? openaiCompatibleUrl : undefined;
|
|
const model = voiceProvider === 'openai-compatible' ? openaiCompatibleTtsModel : undefined;
|
|
await speakServerTTS(textToSpeak, {
|
|
voice,
|
|
model,
|
|
speed: speechRate,
|
|
pitch: speechPitch,
|
|
volume: speechVolume,
|
|
summarize: false,
|
|
baseURL,
|
|
onEnd: () => setIsPlaying(false),
|
|
onError: () => setIsPlaying(false),
|
|
});
|
|
} else if (voiceProvider === 'say' && isSayTTSAvailable) {
|
|
const wordsPerMinute = Math.round(100 + (speechRate - 0.5) * 200);
|
|
await speakSayTTS(textToSpeak, {
|
|
voice: sayVoice,
|
|
rate: wordsPerMinute,
|
|
onEnd: () => setIsPlaying(false),
|
|
onError: () => setIsPlaying(false),
|
|
});
|
|
} else {
|
|
// Browser TTS
|
|
await browserVoiceService.waitForVoices();
|
|
await browserVoiceService.resumeAudioContext();
|
|
await browserVoiceService.speakText(
|
|
textToSpeak,
|
|
navigator.language || 'en-US',
|
|
() => setIsPlaying(false),
|
|
{
|
|
rate: speechRate,
|
|
pitch: speechPitch,
|
|
volume: speechVolume,
|
|
voiceName: browserVoice || undefined,
|
|
}
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error('[useMessageTTS] Playback error:', err);
|
|
setIsPlaying(false);
|
|
}
|
|
}, [
|
|
voiceProvider,
|
|
isServerProvider,
|
|
speechRate,
|
|
speechPitch,
|
|
speechVolume,
|
|
sayVoice,
|
|
browserVoice,
|
|
openaiVoice,
|
|
openaiCompatibleVoice,
|
|
openaiCompatibleUrl,
|
|
openaiCompatibleTtsModel,
|
|
isServerTTSAvailable,
|
|
isSayTTSAvailable,
|
|
speakServerTTS,
|
|
speakSayTTS,
|
|
stop,
|
|
]);
|
|
|
|
return {
|
|
isPlaying,
|
|
play,
|
|
stop,
|
|
};
|
|
}
|