diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 8239487e7..aac471aa8 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -10,7 +10,6 @@ import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal'; import type { TradeConfig } from '~/modules/trade/TradeModal'; import { downloadSingleChat, importConversationsFromFilesAtRest, openConversationsAtRestPicker } from '~/modules/trade/trade.client'; import { imaginePromptFromTextOrThrow } from '~/modules/aifn/imagine/imaginePromptFromText'; -import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client'; import { useAreBeamsOpen } from '~/modules/beam/store-beam.hooks'; import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client'; @@ -346,11 +345,6 @@ export function AppChat() { }); }, [handleExecuteAndOutcome]); - const handleTextSpeak = React.useCallback(async (text: string): Promise => { - await elevenLabsSpeakText(text, undefined, true, true); - }, []); - - // Chat actions const handleConversationNewInFocusedPane = React.useCallback((forceNoRecycle: boolean, isIncognito: boolean) => { @@ -725,7 +719,6 @@ export function AppChat() { onConversationNew={handleConversationNewInFocusedPane} onTextDiagram={handleTextDiagram} onTextImagine={handleImagineFromText} - onTextSpeak={handleTextSpeak} sx={chatMessageListSx} /> )} diff --git a/src/apps/chat/components/ChatMessageList.tsx b/src/apps/chat/components/ChatMessageList.tsx index 28fc0557c..b83f82113 100644 --- a/src/apps/chat/components/ChatMessageList.tsx +++ b/src/apps/chat/components/ChatMessageList.tsx @@ -7,6 +7,7 @@ import { Box, List } from '@mui/joy'; import type { SystemPurposeExample } from '../../../data'; import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal'; +import { speakText } from '~/modules/speex/speex.client'; import type { ConversationHandler } from '~/common/chat-overlay/ConversationHandler'; import type { DLLMContextTokens } from '~/common/stores/llms/llms.types'; @@ -17,7 +18,6 @@ import { createDMessageFromFragments, createDMessageTextContent, DMessage, DMess import { createTextContentFragment, DMessageFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments'; import { openFileForAttaching } from '~/common/components/ButtonAttachFiles'; import { optimaOpenPreferences } from '~/common/layout/optima/useOptima'; -import { useCapabilityElevenLabs } from '~/common/components/useCapabilities'; import { useChatOverlayStore } from '~/common/chat-overlay/store-perchat_vanilla'; import { useChatStore } from '~/common/stores/chat/store-chats'; import { useScrollToBottom } from '~/common/scroll-to-bottom/useScrollToBottom'; @@ -50,7 +50,6 @@ export function ChatMessageList(props: { onConversationNew: (forceNoRecycle: boolean, isIncognito: boolean) => void, onTextDiagram: (diagramConfig: DiagramConfig | null) => void, onTextImagine: (conversationId: DConversationId, selectedText: string) => Promise, - onTextSpeak: (selectedText: string) => Promise, setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void, sx?: SxProps, }) { @@ -75,10 +74,9 @@ export function ChatMessageList(props: { _composerInReferenceToCount: state.inReferenceTo?.length ?? 0, ephemerals: state.ephemerals?.length ? state.ephemerals : null, }))); - const { mayWork: isSpeakable } = useCapabilityElevenLabs(); // derived state - const { conversationHandler, conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props; + const { conversationHandler, conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine } = props; const composerCanAddInReferenceTo = _composerInReferenceToCount < 5; const composerHasInReferenceto = _composerInReferenceToCount > 0; @@ -212,12 +210,15 @@ export function ChatMessageList(props: { }, [capabilityHasT2I, conversationId, onTextImagine]); const handleTextSpeak = React.useCallback(async (text: string) => { - if (!isSpeakable) - return optimaOpenPreferences('voice'); + // sandwich the speaking with the indicator setIsSpeaking(true); - await onTextSpeak(text); + const result = await speakText(text, undefined, { label: 'Chat speak' }); setIsSpeaking(false); - }, [isSpeakable, onTextSpeak]); + + // open voice preferences + if (!result.success && (result.errorType === 'tts-no-engine' || result.errorType === 'tts-unconfigured')) + optimaOpenPreferences('voice'); + }, []); // operate on the local selection set @@ -377,7 +378,7 @@ export function ChatMessageList(props: { onMessageTruncate={handleMessageTruncate} onTextDiagram={handleTextDiagram} onTextImagine={capabilityHasT2I ? handleTextImagine : undefined} - onTextSpeak={isSpeakable ? handleTextSpeak : undefined} + onTextSpeak={handleTextSpeak} /> ); diff --git a/src/apps/chat/editors/persona/PersonaChatMessageSpeak.ts b/src/apps/chat/editors/persona/PersonaChatMessageSpeak.ts index 8026ee757..615e00121 100644 --- a/src/apps/chat/editors/persona/PersonaChatMessageSpeak.ts +++ b/src/apps/chat/editors/persona/PersonaChatMessageSpeak.ts @@ -1,9 +1,8 @@ -import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client'; +import type { AixChatGenerateContent_DMessageGuts } from '~/modules/aix/client/aix.client'; +import { speakText } from '~/modules/speex/speex.client'; import { isTextContentFragment } from '~/common/stores/chat/chat.fragments'; -import type { AixChatGenerateContent_DMessageGuts } from '~/modules/aix/client/aix.client'; - import type { PersonaProcessorInterface } from '../chat-persona'; @@ -58,7 +57,7 @@ export class PersonaChatMessageSpeak implements PersonaProcessorInterface { #speak(text: string) { console.log('📢 TTS:', text); this.spokenLine = true; - // fire/forget: we don't want to stall this loop - void elevenLabsSpeakText(text, undefined, false, true); + // fire/forget: we don't want to stall streaming + void speakText(text, undefined, { label: 'Chat message' }); } } diff --git a/src/common/components/useCapabilities.ts b/src/common/components/useCapabilities.ts index 058e95902..46d0439ac 100644 --- a/src/common/components/useCapabilities.ts +++ b/src/common/components/useCapabilities.ts @@ -25,17 +25,6 @@ export interface CapabilityBrowserSpeechRecognition { export { browserSpeechRecognitionCapability as useCapabilityBrowserSpeechRecognition } from './speechrecognition/useSpeechRecognition'; -/// Speech Synthesis: ElevenLabs - -export interface CapabilityElevenLabsSpeechSynthesis { - mayWork: boolean; - isConfiguredServerSide: boolean; - isConfiguredClientSide: boolean; -} - -export { useCapability as useCapabilityElevenLabs } from '~/modules/elevenlabs/elevenlabs.client'; - - /// Image Generation export interface TextToImageProvider { diff --git a/src/server/trpc/trpc.router-edge.ts b/src/server/trpc/trpc.router-edge.ts index 59728efa8..4a29d1f5a 100644 --- a/src/server/trpc/trpc.router-edge.ts +++ b/src/server/trpc/trpc.router-edge.ts @@ -1,8 +1,8 @@ import { createTRPCRouter } from './trpc.server'; +// Edge routers import { aixRouter } from '~/modules/aix/server/api/aix.router'; import { backendRouter } from '~/modules/backend/backend.router'; -import { elevenlabsRouter } from '~/modules/elevenlabs/elevenlabs.router'; import { googleSearchRouter } from '~/modules/google/search.router'; import { llmAnthropicRouter } from '~/modules/llms/server/anthropic/anthropic.router'; import { llmGeminiRouter } from '~/modules/llms/server/gemini/gemini.router'; @@ -17,13 +17,12 @@ import { youtubeRouter } from '~/modules/youtube/youtube.router'; export const appRouterEdge = createTRPCRouter({ aix: aixRouter, backend: backendRouter, - elevenlabs: elevenlabsRouter, googleSearch: googleSearchRouter, llmAnthropic: llmAnthropicRouter, llmGemini: llmGeminiRouter, llmOllama: llmOllamaRouter, llmOpenAI: llmOpenAIRouter, - speex: speexRouter, + speex: speexRouter, // synthesize, listVoices (multi-provider TTS) youtube: youtubeRouter, });