speakText: port to Speex

This commit is contained in:
Enrico Ros
2025-11-30 12:51:55 -08:00
parent a1af51efcb
commit 423c2cce28
5 changed files with 16 additions and 35 deletions
-7
View File
@@ -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<void> => {
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}
/>
)}
+10 -9
View File
@@ -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<void>,
onTextSpeak: (selectedText: string) => Promise<void>,
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}
/>
);
@@ -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' });
}
}
-11
View File
@@ -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 {
+2 -3
View File
@@ -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,
});