Speex: +Inworld Config

This commit is contained in:
Enrico Ros
2026-01-27 23:49:21 -08:00
parent bae691e33e
commit 3f9a419a19
3 changed files with 95 additions and 2 deletions
+10
View File
@@ -0,0 +1,10 @@
import * as React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/joy';
export function InworldIcon(props: SvgIconProps) {
return <SvgIcon viewBox='0 0 141 181' width='24' height='24' fill='currentColor' {...props}>
<path d='M48.2616 34.7993C47.9981 34.8585 47.9766 34.6058 48.1379 34.5144C53.3155 31.4874 60.6866 30.767 61.235 30.353C61.7297 29.6594 57.864 29.697 56.5199 29.7024C46.74 30.0411 38.2989 33.681 31.288 40.2941C20.5242 49.4664 16.9973 64.5582 18.4866 78.1285C20.2285 92.1611 27.9384 105.344 39.385 113.635C63.2405 129.673 96.9242 122.011 114.774 100.183C136.753 73.537 130.194 35.6004 107.215 18.2613C101.252 14.1214 94.7359 10.8524 87.7035 8.98143C84.4238 8.00828 79.8592 7.07277 77.7624 7.00288C74.5472 6.90073 75.6171 9.54596 75.0472 10.1965C74.4773 10.8471 68.5901 11.1105 75.2515 13.4493C79.1925 14.8311 76.7946 14.9117 73.7891 17.6322C71.1278 20.0409 73.4504 22.7399 72.531 23.6861C68.5847 27.7508 74.6279 29.5358 77.7462 31.31C92.8542 39.3747 101.177 53.8913 98.4242 70.9831C94.5585 93.5644 60.9286 103.317 46.0464 85.462C34.5999 71.924 38.928 49.3858 55.2241 42.0361C57.4822 40.9447 62.5792 39.3532 60.3909 39.536C51.9552 40.2403 45.369 44.7297 43.2399 46.3588C43.0571 46.4986 42.8152 46.2835 42.9281 46.0846C44.4711 43.3695 53.4122 37.036 71.0041 34.8101C74.1494 34.1327 62.3157 31.6541 48.2616 34.7993Z' />
<path d='M55.4704 170.577C56.4274 170.566 57.3791 170.706 58.2716 170.797C60.5673 170.867 63.148 170.604 65.6696 170.932C71.1536 171.244 76.6807 172.475 82.1808 172.609C85.9712 172.889 89.5896 172.717 93.4607 172.862C96.3801 173.238 99.848 173.007 102.466 173.4C104.101 173.588 105.601 173.507 107.241 173.749C111.579 174.367 115.902 174.459 120.295 174.4C121.967 174.453 123.682 174.486 125.349 174.378C127.128 174.276 129.069 174.351 130.499 173.212C131.349 172.711 131.306 171.695 131.483 170.996C131.704 170.615 132.166 170.41 132.37 170.023C132.596 169.357 132.741 168.663 132.752 167.98V167.937C132.8 165.615 131.688 163.33 130.838 161.174C129.424 158.05 127.763 155.13 125.967 152.206C123.962 149.372 122.956 145.506 119.547 143.974C113.402 141.281 107.085 141.565 100.283 140.743C87.482 139.608 73.8849 138.775 61.9867 138.614C50.8843 138.775 31.2708 139.501 21.1738 140.399C3.2378 141.996 11.5875 144.84 11.023 149.872C10.8187 151.727 8.79172 151.576 9.01753 153.394C9.39388 156.028 14.3349 159.324 16.8188 159.991C27.76 162.916 13.2435 163.351 14.8672 166.276C16.2166 168.711 22.2383 168.012 24.6954 168.98L55.4597 170.577H55.4704Z' />
</SvgIcon>;
}
@@ -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' ? (
<ElevenLabsConfig engine={engine} onUpdate={onUpdate} isMobile={isMobile} mode={mode} />
) : engine.vendorType === 'inworld' ? (
<InworldConfig engine={engine} onUpdate={onUpdate} isMobile={isMobile} mode={mode} />
) : engine.vendorType === 'localai' ? (
<LocalAIConfig engine={engine} onUpdate={onUpdate} isMobile={isMobile} mode={mode} />
) : 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<DSpeexEngine<'inworld'>>) => 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 && (
<CredentialsApiKeyInputs
credentials={credentials}
onUpdate={handleCredentialsUpdate}
vendorType='inworld'
keyPlaceholder='Base64-key'
/>
)}
<FormChipControl<Exclude<DVoiceInworld['ttsModel'], undefined>>
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 } })}
/>
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center', overflow: 'hidden' }}>
<FormLabelStart title='Voice' description={isMobile ? undefined : 'Inworld voice'} />
<SpeexVoiceSelect
autoPreview
engine={engine}
voiceId={voice.ttsVoiceId ?? null}
onVoiceChange={handleVoiceChange}
/>
</FormControl>
<FormSliderControl
title='Speed'
description={`${voice.ttsSpeakingRate ?? 1}x`}
min={0.5}
max={1.5}
step={0.1}
value={voice.ttsSpeakingRate ?? 1}
onChange={handleSpeedChange}
valueLabelDisplay={voice.ttsSpeakingRate && voice.ttsSpeakingRate !== 1 ? 'on' : 'auto'}
sliderSx={{ maxWidth: 220, my: -0.5 }}
/>
</>;
}
function LocalAIConfig({ engine, onUpdate, mode, isMobile }: {
engine: DSpeexEngine<'localai'>,
onUpdate: (updates: Partial<DSpeexEngine<'localai'>>) => void;
@@ -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<SvgIconProps> }[] = [
{ 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;