diff --git a/apps/stage-tamagotchi/src/renderer/src/pages/settings/providers/elevenlabs.vue b/apps/stage-tamagotchi/src/renderer/src/pages/settings/providers/elevenlabs.vue index 754b397b7..b991e51c9 100644 --- a/apps/stage-tamagotchi/src/renderer/src/pages/settings/providers/elevenlabs.vue +++ b/apps/stage-tamagotchi/src/renderer/src/pages/settings/providers/elevenlabs.vue @@ -3,419 +3,98 @@ import type { UnElevenLabsOptions } from '@xsai-ext/providers-local' import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers' import { - FieldCheckbox, - FieldRange, - ProviderAdvancedSettings, - ProviderApiKeyInput, - ProviderBaseUrlInput, - ProviderBasicSettings, - ProviderSettingsContainer, - ProviderSettingsLayout, - TestDummyMarker, + SpeechPlayground, + SpeechProviderSettings, + SpeechVoiceSettings, } from '@proj-airi/stage-ui/components' import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores' -import { useDebounceFn } from '@vueuse/core' -import { generateSpeech } from '@xsai/generate-speech' import { storeToRefs } from 'pinia' -import { computed, onMounted, onUnmounted, ref, watch } from 'vue' -import { useI18n } from 'vue-i18n' -import { useRouter } from 'vue-router' +import { computed } from 'vue' + +const providerId = 'elevenlabs' +const defaultModel = 'eleven_multilingual_v2' + +// Default voice settings specific to ElevenLabs +const defaultVoiceSettings = { + similarityBoost: 0.75, + stability: 0.5, + speed: 1.0, + style: 0, + useSpeakerBoost: true, +} -const { t } = useI18n() -const router = useRouter() -const providersStore = useProvidersStore() const speechStore = useSpeechStore() +const providersStore = useProvidersStore() const { providers } = storeToRefs(providersStore) -const selectedLanguage = ref('en-US') -const activeSpeechVoice = ref('') +// Check if API key is configured +const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey) -// For playground -const testText = ref('Hello! This is a test of the ElevenLabs voice synthesis.') -const isGenerating = ref(false) -const audioUrl = ref('') -const errorMessage = ref('') -const audioPlayer = ref(null) - -// Get provider metadata -const providerId = 'elevenlabs' -const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId)) - -const apiKey = computed({ - get: () => providers.value[providerId]?.apiKey as string | undefined || '', - set: (value) => { - if (!providers.value[providerId]) - providers.value[providerId] = {} - - providers.value[providerId].apiKey = value - }, +// Get available voices for ElevenLabs +const availableVoices = computed(() => { + return speechStore.availableVoices[providerId] || [] }) -const baseUrl = computed({ - get: () => providers.value[providerId]?.baseUrl as string | undefined || providerMetadata.value?.defaultOptions?.baseUrl as string | undefined || '', - set: (value) => { - if (!providers.value[providerId]) - providers.value[providerId] = {} - - providers.value[providerId].baseUrl = value - }, +// Get available languages +const availableLanguages = computed(() => { + return speechStore.availableLanguages }) -// Voice settings as individual computed properties -const similarityBoost = computed({ - get: () => (providers.value[providerId]?.voiceSettings as any)?.similarityBoost ?? 0.75, - set: (value) => { - if (!providers.value[providerId]) - providers.value[providerId] = {} - if (!providers.value[providerId].voiceSettings) - providers.value[providerId].voiceSettings = {} - - const voiceSettings = providers.value[providerId].voiceSettings as any - voiceSettings.similarityBoost = value - }, -}) - -const stability = computed({ - get: () => (providers.value[providerId]?.voiceSettings as any)?.stability ?? 0.5, - set: (value) => { - if (!providers.value[providerId]) - providers.value[providerId] = {} - if (!providers.value[providerId].voiceSettings) - providers.value[providerId].voiceSettings = {} - - const voiceSettings = providers.value[providerId].voiceSettings as any - voiceSettings.stability = value - }, -}) - -const speed = computed({ - get: () => (providers.value[providerId]?.voiceSettings as any)?.speed ?? 1.0, - set: (value) => { - if (!providers.value[providerId]) - providers.value[providerId] = {} - if (!providers.value[providerId].voiceSettings) - providers.value[providerId].voiceSettings = {} - - const voiceSettings = providers.value[providerId].voiceSettings as any - voiceSettings.speed = value - }, -}) - -const style = computed({ - get: () => (providers.value[providerId]?.voiceSettings as any)?.style ?? 0, - set: (value) => { - if (!providers.value[providerId]) - providers.value[providerId] = {} - if (!providers.value[providerId].voiceSettings) - providers.value[providerId].voiceSettings = {} - - const voiceSettings = providers.value[providerId].voiceSettings as any - voiceSettings.style = value - }, -}) - -const useSpeakerBoost = computed({ - get: () => (providers.value[providerId]?.voiceSettings as any)?.useSpeakerBoost !== false, - set: (value) => { - if (!providers.value[providerId]) - providers.value[providerId] = {} - if (!providers.value[providerId].voiceSettings) - providers.value[providerId].voiceSettings = {} - - const voiceSettings = providers.value[providerId].voiceSettings as any - voiceSettings.useSpeakerBoost = value - }, -}) - -// Speech settings -const availableVoices = computed(() => speechStore.availableVoicesForLanguage) - -onMounted(() => { - providersStore.initializeProvider(providerId) - - // Initialize refs with current values - apiKey.value = providers.value[providerId]?.apiKey as string | undefined || '' - baseUrl.value = providers.value[providerId]?.baseUrl as string | undefined || providerMetadata.value?.defaultOptions?.baseUrl as string | undefined || '' - - // Initialize voice settings refs - if (providers.value[providerId]?.voiceSettings) { - similarityBoost.value = (providers.value[providerId].voiceSettings as any)?.similarityBoost ?? 0.75 - stability.value = (providers.value[providerId].voiceSettings as any)?.stability ?? 0.5 - speed.value = (providers.value[providerId].voiceSettings as any)?.speed ?? 1.0 - style.value = (providers.value[providerId].voiceSettings as any)?.style ?? 0 - useSpeakerBoost.value = (providers.value[providerId].voiceSettings as any)?.useSpeakerBoost !== false - } - - // Load voices if provider is configured - if (providersStore.configuredProviders[providerId]) { - speechStore.loadVoicesForProvider(providerId) - } -}) - -const debouncedUpdate = useDebounceFn(() => { - providers.value[providerId] = { - ...providers.value[providerId], - apiKey: apiKey.value, - baseUrl: baseUrl.value || providerMetadata.value?.defaultOptions?.baseUrl || '', - voiceSettings: { - similarityBoost: similarityBoost.value, - stability: stability.value, - speed: speed.value, - style: style.value, - useSpeakerBoost: useSpeakerBoost.value, - }, - } -}, 1000) - -// Watch all settings and update the provider configuration -watch([apiKey, baseUrl, similarityBoost, stability, speed, style, useSpeakerBoost], debouncedUpdate) - -// Function to generate speech -async function generateTestSpeech() { - if (!testText.value.trim()) - return - +// Generate speech with ElevenLabs-specific parameters +async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) { const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions if (!provider) { - console.error('Failed to initialize speech provider') - return + throw new Error('Failed to initialize speech provider') } - if (!activeSpeechVoice.value) { - console.error('No active speech voice selected') - return - } + // Get provider configuration + const providerConfig = providersStore.getProviderConfig(providerId) - isGenerating.value = true - errorMessage.value = '' + // Get model from configuration or use default + const model = providerConfig.model as string | undefined || defaultModel - try { - // Stop any currently playing audio - if (audioUrl.value) { - stopTestAudio() - } - - const response = await generateSpeech({ - ...provider.speech('eleven_multilingual_v2', { - voiceSettings: { - stability: stability.value, - similarityBoost: similarityBoost.value, - // @ts-expect-error -- missing type - speed: speed.value, - style: style.value, - useSpeakerBoost: useSpeakerBoost.value, - }, - }), - input: testText.value, - voice: activeSpeechVoice.value, - }) - - // Convert the response to a blob and create an object URL - audioUrl.value = URL.createObjectURL(new Blob([response])) - - // Play the audio - setTimeout(() => { - if (audioPlayer.value) { - audioPlayer.value.play() - } - }, 100) - } - catch (error) { - console.error('Error generating speech:', error) - errorMessage.value = error instanceof Error ? error.message : 'An unknown error occurred' - } - finally { - isGenerating.value = false - } -} - -// Function to stop audio playback -function stopTestAudio() { - if (audioPlayer.value) { - audioPlayer.value.pause() - audioPlayer.value.currentTime = 0 - } - - // Clean up the object URL to prevent memory leaks - if (audioUrl.value) { - URL.revokeObjectURL(audioUrl.value) - audioUrl.value = '' - } -} - -// Clean up when component is unmounted -onUnmounted(() => { - if (audioUrl.value) { - URL.revokeObjectURL(audioUrl.value) - } -}) - -function handleResetVoiceSettings() { - providers.value[providerId] = { - ...(providerMetadata.value?.defaultOptions as any), - } + // ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly + return await speechStore.speech( + provider, + model, + input, + voiceId, + { + ...providerConfig, + ...defaultVoiceSettings, + }, + ) }