diff --git a/src/common/components/icons/vendors/InworldIcon.tsx b/src/common/components/icons/vendors/InworldIcon.tsx new file mode 100644 index 000000000..8ea773f4b --- /dev/null +++ b/src/common/components/icons/vendors/InworldIcon.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; + +import { SvgIcon, SvgIconProps } from '@mui/joy'; + +export function InworldIcon(props: SvgIconProps) { + return + + + ; +} diff --git a/src/modules/speex/components/SpeexConfigureEngineFull.tsx b/src/modules/speex/components/SpeexConfigureEngineFull.tsx index ac30cd27e..1281b0f51 100644 --- a/src/modules/speex/components/SpeexConfigureEngineFull.tsx +++ b/src/modules/speex/components/SpeexConfigureEngineFull.tsx @@ -13,7 +13,7 @@ import { FormTextField } from '~/common/components/forms/FormTextField'; import { TooltipOutlined } from '~/common/components/TooltipOutlined'; import { useToggleableBoolean } from '~/common/util/hooks/useToggleableBoolean'; -import type { DCredentialsApiKey, DSpeexEngine, DSpeexEngineAny, DSpeexVendorType, DVoiceElevenLabs, DVoiceLocalAI, DVoiceOpenAI, DVoiceWebSpeech } from '../speex.types'; +import type { DCredentialsApiKey, DSpeexEngine, DSpeexEngineAny, DSpeexVendorType, DVoiceElevenLabs, DVoiceInworld, DVoiceLocalAI, DVoiceOpenAI, DVoiceWebSpeech } from '../speex.types'; import { SPEEX_DEFAULTS, SPEEX_PREVIEW_STREAM, SPEEX_PREVIEW_TEXT } from '../speex.config'; import { SpeexVoiceAutocomplete } from './SpeexVoiceAutocomplete'; import { SpeexVoiceSelect } from './SpeexVoiceSelect'; @@ -21,13 +21,14 @@ import { speakText } from '../speex.client'; import { speexVendorTypeLabel } from './SpeexEngineSelect'; -function CredentialsApiKeyInputs({ credentials, onUpdate, vendorType, showHost, hostRequired, hostPlaceholder }: { +function CredentialsApiKeyInputs({ credentials, onUpdate, vendorType, showHost, hostRequired, hostPlaceholder, keyPlaceholder }: { credentials: DCredentialsApiKey; onUpdate: (credentials: DCredentialsApiKey) => void; vendorType: DSpeexVendorType; showHost?: boolean; hostRequired?: boolean; hostPlaceholder?: string; + keyPlaceholder?: string; }) { return <> @@ -35,6 +36,7 @@ function CredentialsApiKeyInputs({ credentials, onUpdate, vendorType, showHost, autoCompleteId={`tts-${vendorType}-key`} title='API Key' description={hostRequired ? 'Optional' : speexVendorTypeLabel(vendorType)} + placeholder={keyPlaceholder} value={credentials.apiKey} onChange={value => onUpdate({ ...credentials, apiKey: value })} required={!hostRequired} @@ -115,6 +117,8 @@ export function SpeexConfigureEngineFull(props: { {/* Engine-Specific pane */} {engine.vendorType === 'elevenlabs' ? ( + ) : engine.vendorType === 'inworld' ? ( + ) : engine.vendorType === 'localai' ? ( ) : engine.vendorType === 'openai' ? ( @@ -219,6 +223,83 @@ function ElevenLabsConfig({ engine, onUpdate, mode, isMobile }: { } +function InworldConfig({ engine, onUpdate, mode, isMobile }: { + engine: DSpeexEngine<'inworld'>, + onUpdate: (updates: Partial>) => void; + isMobile: boolean; + mode: 'full' | 'voice-only'; +}) { + + const { credentials, voice } = engine; + const showCredentials = mode === 'full' && !engine.isAutoLinked && credentials.type === 'api-key'; + + const handleCredentialsUpdate = React.useCallback((newCredentials: DCredentialsApiKey) => { + onUpdate({ credentials: newCredentials }); + }, [onUpdate]); + + const handleVoiceChange = React.useCallback((ttsVoiceId: DVoiceInworld['ttsVoiceId']) => { + const { ttsVoiceId: _, ...restVoice } = voice; + onUpdate({ + voice: { + ...restVoice, + ...(ttsVoiceId && { ttsVoiceId }), + }, + }); + }, [onUpdate, voice]); + + const handleSpeedChange = React.useCallback((value: number) => { + onUpdate({ voice: { ...voice, ttsSpeakingRate: value } }); + }, [onUpdate, voice]); + + return <> + + {/* Credentials (only for manually added engines in full mode) */} + {showCredentials && ( + + )} + + > + title='Model' + alignEnd + options={[ + { value: 'inworld-tts-1.5-max', label: 'TTS 1.5 Max', description: 'Quality' }, + { value: 'inworld-tts-1.5-mini', label: 'TTS 1.5 Mini', description: 'Fast' }, + ]} + value={voice.ttsModel ?? SPEEX_DEFAULTS.INWORLD_MODEL} + onChange={(value) => onUpdate({ voice: { ...voice, ttsModel: value } })} + /> + + + + + + + + + ; +} + + function LocalAIConfig({ engine, onUpdate, mode, isMobile }: { engine: DSpeexEngine<'localai'>, onUpdate: (updates: Partial>) => void; diff --git a/src/modules/speex/components/SpeexConfigureEngines.tsx b/src/modules/speex/components/SpeexConfigureEngines.tsx index 737d88abe..44d1538b0 100644 --- a/src/modules/speex/components/SpeexConfigureEngines.tsx +++ b/src/modules/speex/components/SpeexConfigureEngines.tsx @@ -18,6 +18,7 @@ import LinkIcon from '@mui/icons-material/Link'; import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal'; import { ElevenLabsIcon } from '~/common/components/icons/vendors/ElevenLabsIcon'; import { FormLabelStart } from '~/common/components/forms/FormLabelStart'; +import { InworldIcon } from '~/common/components/icons/vendors/InworldIcon'; import { LocalAIIcon } from '~/common/components/icons/vendors/LocalAIIcon'; import { OpenAIIcon } from '~/common/components/icons/vendors/OpenAIIcon'; import { TooltipOutlined } from '~/common/components/TooltipOutlined'; @@ -101,6 +102,7 @@ const _styles = { const ADDABLE_VENDORS: { vendorType: DSpeexVendorType; label: string; description: string, icon?: React.FunctionComponent }[] = [ { vendorType: 'elevenlabs', label: 'ElevenLabs', description: 'Premium voices', icon: ElevenLabsIcon }, + { vendorType: 'inworld', label: 'Inworld', description: 'Expressive AI voices', icon: InworldIcon }, { vendorType: 'localai', label: 'LocalAI', description: 'Self-hosted TTS', icon: LocalAIIcon }, { vendorType: 'openai', label: 'OpenAI TTS', description: 'Reliable', icon: OpenAIIcon }, ] as const;