fix(stage-ui|stage-tamagotchi): microsoft / azure speech provider

This commit is contained in:
Neko Ayaka
2025-03-18 15:33:53 +08:00
parent 7a417fbae1
commit 76483a3740
7 changed files with 303 additions and 713 deletions
@@ -4,6 +4,7 @@ import {
FieldInput,
FieldRange,
RadioCardSimple,
Skeleton,
VoiceCardManySelect,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
@@ -149,53 +150,83 @@ watch(voiceId, updateSSMLExample)
</div>
<!-- Loading state -->
<div v-if="isLoadingSpeechProviderVoices" class="flex items-center justify-center py-4">
<div class="mr-2 animate-spin">
<div i-solar:spinner-line-duotone text-xl />
<TransitionGroup name="fade-slide-in-out">
<div v-if="isLoadingSpeechProviderVoices">
<div class="flex flex-col gap-4">
<Skeleton class="w-full rounded-lg p-2.5 text-sm">
<div class="h-1lh" />
</Skeleton>
<div flex="~ row gap-4">
<Skeleton class="w-full rounded-lg p-4 text-sm">
<div class="h-1lh" />
</Skeleton>
<Skeleton class="w-full rounded-lg p-4 text-sm">
<div class="h-1lh" />
</Skeleton>
<Skeleton class="w-full rounded-lg p-4 text-sm">
<div class="h-1lh" />
</Skeleton>
</div>
<Skeleton class="w-full rounded-lg p-3 text-sm">
<div class="h-1lh" />
</Skeleton>
</div>
</div>
<span>Loading available voices...</span>
</div>
<!-- Error state -->
<div
v-else-if="speechProviderError"
class="flex items-center gap-3 border border-2 border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
>
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
<div class="flex flex-col">
<span class="font-medium">Error loading voices</span>
<span class="text-sm text-red-600 dark:text-red-400">{{ speechProviderError }}</span>
<!-- Error state -->
<!-- Voice selection with RadioCardDetailManySelect -->
<div
v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0"
class="space-y-6"
>
<VoiceCardManySelect
v-model:search-query="voiceSearchQuery"
:voices="availableVoices[activeSpeechProvider]?.map(voice => ({
id: voice.name,
name: voice.name,
description: voice.description,
previewURL: voice.previewURL,
customizable: false,
}))"
:selected-voice-id="voiceId"
:searchable="true"
:search-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_placeholder')"
:search-no-results-title="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices')"
:search-no-results-description="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices_description')"
:search-results-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_results', { count: 0, total: 0 })"
:custom-input-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.custom_voice_placeholder')"
:expand-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_more')"
:collapse-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_less')"
:play-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.play_sample')"
:pause-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.pause')"
@update:selected-voice-id="handleVoiceSelection"
@update:custom-value="updateCustomVoiceName"
/>
</div>
</div>
<!-- Voice selection with RadioCardDetailManySelect -->
<div
v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0"
class="space-y-6"
>
<VoiceCardManySelect
v-model:search-query="voiceSearchQuery"
:voices="availableVoices[activeSpeechProvider]?.map(voice => ({
id: voice.name,
name: voice.name,
description: voice.description,
previewURL: voice.previewURL,
customizable: false,
}))"
:selected-voice-id="voiceId"
:searchable="true"
:search-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_placeholder')"
:search-no-results-title="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices')"
:search-no-results-description="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices_description')"
:search-results-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_results', { count: 0, total: 0 })"
:custom-input-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.custom_voice_placeholder')"
:expand-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_more')"
:collapse-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_less')"
:play-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.play_sample')"
:pause-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.pause')"
@update:selected-voice-id="handleVoiceSelection"
@update:custom-value="updateCustomVoiceName"
/>
<div
v-else-if="speechProviderError"
class="flex items-center gap-3 border border-2 border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
>
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
<div class="flex flex-col">
<span class="font-medium">Error loading voices</span>
<span class="text-sm text-red-600 dark:text-red-400">{{ speechProviderError }}</span>
</div>
</div>
<!-- No voices available -->
<div
v-else
class="flex items-center gap-3 border border-2 border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
>
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
<span class="font-medium">No voices available</span>
<span class="text-sm text-amber-600 dark:text-amber-400">
No voices were found for this provider. You can enter a custom voice name below.
</span>
</div>
</div>
<!-- Model selection for ElevenLabs -->
<div v-if="activeSpeechProvider === 'elevenlabs'">
@@ -239,21 +270,7 @@ watch(voiceId, updateSSMLExample)
description="Enable Speech Synthesis Markup Language for more control over speech output"
/>
</div>
</div>
<!-- No voices available -->
<div
v-else-if="!isLoadingSpeechProviderVoices && (!availableVoices[activeSpeechProvider] || availableVoices[activeSpeechProvider].length === 0)"
class="flex items-center gap-3 border border-2 border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
>
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
<span class="font-medium">No voices available</span>
<span class="text-sm text-amber-600 dark:text-amber-400">
No voices were found for this provider. You can enter a custom voice name below.
</span>
</div>
</div>
</TransitionGroup>
<!-- Manual voice input when no voices are available -->
<div
@@ -327,3 +344,24 @@ meta:
stageTransition:
name: slide
</route>
<style scoped>
.fade-slide-in-out-enter-active,
.fade-slide-in-out-leave-active {
transition: all 0.2s ease-in-out;
}
.fade-slide-in-out-enter-from,
.fade-slide-in-out-leave-to {
opacity: 0;
}
.fade-slide-in-out-leave-active {
transition: all 0.2s ease-in-out;
}
.fade-slide-in-out-leave-from,
.fade-slide-in-out-enter-to {
opacity: 1;
}
</style>
@@ -1,6 +1,5 @@
<script setup lang="ts">
import type { Voice } from '@proj-airi/stage-ui/constants'
import type { RemovableRef } from '@vueuse/core'
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
@@ -17,6 +16,7 @@ import {
} from '@proj-airi/stage-ui/components'
import { voiceMap } from '@proj-airi/stage-ui/constants'
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'
@@ -27,7 +27,7 @@ const { t } = useI18n()
const router = useRouter()
const providersStore = useProvidersStore()
const speechStore = useSpeechStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
const { providers } = storeToRefs(providersStore)
// For playground
const testText = ref('Hello! This is a test of the ElevenLabs voice synthesis.')
@@ -41,7 +41,7 @@ const providerId = 'elevenlabs'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
get: () => providers.value[providerId]?.apiKey as string | undefined || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
@@ -51,7 +51,7 @@ const apiKey = computed({
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || '',
get: () => providers.value[providerId]?.baseUrl as string | undefined || providerMetadata.value?.defaultOptions?.baseUrl as string | undefined || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
@@ -69,7 +69,8 @@ const similarityBoost = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].voiceSettings.similarityBoost = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.similarityBoost = value
},
})
@@ -81,7 +82,8 @@ const stability = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].voiceSettings.stability = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.stability = value
},
})
@@ -93,7 +95,8 @@ const speed = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].voiceSettings.speed = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.speed = value
},
})
@@ -105,7 +108,8 @@ const style = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].style = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.style = value
},
})
@@ -117,7 +121,8 @@ const useSpeakerBoost = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].voiceSettings.useSpeakerBoost = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.useSpeakerBoost = value
},
})
@@ -130,8 +135,8 @@ onMounted(() => {
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || ''
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) {
@@ -148,8 +153,7 @@ onMounted(() => {
}
})
// Watch all settings and update the provider configuration
watch([apiKey, baseUrl, similarityBoost, stability, speed, style, useSpeakerBoost], () => {
const debouncedUpdate = useDebounceFn(() => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
@@ -162,7 +166,10 @@ watch([apiKey, baseUrl, similarityBoost, stability, speed, style, useSpeakerBoos
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() {
@@ -1,413 +0,0 @@
<script setup lang="ts">
import type { Voice } from '@proj-airi/stage-ui/constants'
import type { RemovableRef } from '@vueuse/core'
import type { UnMicrosoftOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import {
FieldCheckbox,
FieldInput,
FieldRange,
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
ProviderBasicSettings,
ProviderSettingsContainer,
ProviderSettingsLayout,
TestDummyMarker,
} from '@proj-airi/stage-ui/components'
import { voiceMap } from '@proj-airi/stage-ui/constants'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
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'
const { t } = useI18n()
const router = useRouter()
const providersStore = useProvidersStore()
const speechStore = useSpeechStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
// For playground
const testText = ref('Hello! This is a test of the Microsoft Speech voice synthesis.')
const isGenerating = ref(false)
const audioUrl = ref('')
const errorMessage = ref('')
const audioPlayer = ref<HTMLAudioElement | null>(null)
// Get provider metadata
const providerId = 'microsoft-speech'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].apiKey = value
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
const region = computed({
get: () => providers.value[providerId]?.region || providerMetadata.value?.defaultOptions?.region || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].region = value
},
})
// 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 = {}
providers.value[providerId].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 = {}
providers.value[providerId].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 = {}
providers.value[providerId].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 = {}
providers.value[providerId].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 = {}
providers.value[providerId].voiceSettings.useSpeakerBoost = value
},
})
// Speech settings
const selectedLanguage = ref(speechStore.selectedLanguage)
const selectedVoice = ref(speechStore.voiceName)
const availableVoices = computed(() => speechStore.availableVoicesForLanguage)
onMounted(() => {
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || ''
// 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)
}
})
// Watch all settings and update the provider configuration
watch([apiKey, baseUrl, similarityBoost, stability, speed, style, useSpeakerBoost], () => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || providerMetadata.value?.defaultOptions?.baseUrl || '',
}
})
// Function to generate speech
async function generateTestSpeech() {
if (!testText.value.trim())
return
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnMicrosoftOptions>
if (!provider) {
console.error('Failed to initialize speech provider')
return
}
isGenerating.value = true
errorMessage.value = ''
try {
// Stop any currently playing audio
if (audioUrl.value) {
stopTestAudio()
}
const response = await generateSpeech({
...provider.speech('v1'),
input: testText.value,
voice: voiceMap[selectedVoice.value as Voice],
})
// 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),
}
}
</script>
<template>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName" :provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<div flex="~ col md:row gap-6">
<ProviderSettingsContainer class="w-full md:w-[40%]">
<ProviderBasicSettings
:title="t('settings.pages.providers.common.section.basic.title')"
:description="t('settings.pages.providers.common.section.basic.description')"
:on-reset="handleResetVoiceSettings"
>
<ProviderApiKeyInput v-model="apiKey" :provider-name="providerMetadata?.localizedName" placeholder="sk-" />
<FieldInput
v-model="region"
label="Region"
description="Speech Service region"
placeholder="eastasia"
required
type="text"
/>
</ProviderBasicSettings>
<div flex="~ col gap-6">
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
{{ t('settings.pages.providers.common.section.voice.title') }}
</h2>
<div flex="~ col gap-4">
<FieldRange
v-model="similarityBoost"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.simularity-boost.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.simularity-boost.description')"
:min="0" :max="1" :step="0.01"
/>
<FieldRange
v-model="stability"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.stability.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.stability.description')"
:min="0" :max="1" :step="0.01"
/>
<FieldRange
v-model="speed"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.speed.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.speed.description')" :min="0.7"
:max="1.2" :step="0.01"
/>
<FieldRange
v-model="style"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.style.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.style.description')" :min="0"
:max="1" :step="0.01"
/>
<FieldCheckbox
v-model="useSpeakerBoost"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.speaker-boost.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.speaker-boost.description')"
/>
</div>
</div>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.baseUrl as string || ''" required
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
<div flex="~ col gap-6" class="w-full md:w-[60%]">
<div w-full rounded-xl>
<h2 class="mb-4 text-lg text-neutral-500 md:text-2xl dark:text-neutral-400" w-full>
<div class="inline-flex items-center gap-4">
<TestDummyMarker />
<div>
{{ t('settings.pages.providers.provider.elevenlabs.playground.title') }}
</div>
</div>
</h2>
<div flex="~ col gap-4">
<textarea
v-model="testText"
:placeholder="t('settings.pages.providers.provider.elevenlabs.playground.fields.field.input.placeholder')"
border="neutral-100 dark:neutral-800 solid 2 focus:neutral-200 dark:focus:neutral-700"
transition="all duration-250 ease-in-out"
bg="neutral-100 dark:neutral-800 focus:neutral-50 dark:focus:neutral-900"
h-24 w-full rounded-lg px-3 py-2 text-sm outline-none
/>
<div flex="~ col gap-6">
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ t('settings.pages.providers.provider.elevenlabs.playground.fields.field.language.label') }}
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
{{ t('settings.pages.providers.provider.elevenlabs.playground.fields.field.language.description') }}
</div>
</div>
<select
v-model="selectedLanguage"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out" w-full rounded-lg px-2 py-1 text-nowrap text-sm
outline-none
>
<option v-for="language in speechStore.availableLanguages" :key="language" :value="language">
{{ language }}
</option>
</select>
</label>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ t('settings.pages.providers.provider.elevenlabs.playground.fields.field.voice.label') }}
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
{{ t('settings.pages.providers.provider.elevenlabs.playground.fields.field.voice.description') }}
</div>
</div>
<select
v-model="selectedVoice"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out" w-full rounded-lg px-2 py-1 text-nowrap text-sm
outline-none
>
<option v-for="voice in availableVoices" :key="voice.id" :value="voice.name">
{{ voice.name }}
</option>
</select>
</label>
</div>
<div flex="~ row" gap-4>
<button
border="neutral-800 dark:neutral-200 solid 2" transition="border duration-250 ease-in-out"
rounded-lg px-4 text="neutral-100 dark:neutral-900" py-2 text-sm
:disabled="isGenerating || !testText.trim() || !apiKey"
:class="{ 'opacity-50 cursor-not-allowed': isGenerating || !testText.trim() || !apiKey }"
bg="neutral-700 dark:neutral-300" @click="generateTestSpeech"
>
<div flex="~ row" items-center gap-2>
<div i-solar:play-circle-bold-duotone />
<span>{{ isGenerating ? t('settings.pages.providers.provider.elevenlabs.playground.buttons.button.test-voice.generating') : t('settings.pages.providers.provider.elevenlabs.playground.buttons.button.test-voice.label') }}</span>
</div>
</button>
<button
v-if="audioUrl" border="primary-300 dark:primary-800 solid 2"
transition="border duration-250 ease-in-out" rounded-lg px-4 py-2 text-sm @click="stopTestAudio"
>
<div flex="~ row" items-center gap-2>
<div i-solar:stop-circle-bold-duotone />
<span>Stop</span>
</div>
</button>
</div>
<div v-if="!apiKey" class="mt-2 text-sm text-red-500">
{{ t('settings.pages.providers.provider.elevenlabs.playground.validation.error-missing-api-key') }}
</div>
<div v-if="errorMessage" class="mt-2 text-sm text-red-500">
{{ errorMessage }}
</div>
<audio v-if="audioUrl" ref="audioPlayer" :src="audioUrl" controls class="mt-2 w-full" />
</div>
</div>
</div>
</div>
</ProviderSettingsLayout>
</template>
@@ -4,6 +4,7 @@ import {
FieldInput,
FieldRange,
RadioCardSimple,
Skeleton,
VoiceCardManySelect,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
@@ -149,53 +150,83 @@ watch(voiceId, updateSSMLExample)
</div>
<!-- Loading state -->
<div v-if="isLoadingSpeechProviderVoices" class="flex items-center justify-center py-4">
<div class="mr-2 animate-spin">
<div i-solar:spinner-line-duotone text-xl />
<TransitionGroup name="fade-slide-in-out">
<div v-if="isLoadingSpeechProviderVoices">
<div class="flex flex-col gap-4">
<Skeleton class="w-full rounded-lg p-2.5 text-sm">
<div class="h-1lh" />
</Skeleton>
<div flex="~ row gap-4">
<Skeleton class="w-full rounded-lg p-4 text-sm">
<div class="h-1lh" />
</Skeleton>
<Skeleton class="w-full rounded-lg p-4 text-sm">
<div class="h-1lh" />
</Skeleton>
<Skeleton class="w-full rounded-lg p-4 text-sm">
<div class="h-1lh" />
</Skeleton>
</div>
<Skeleton class="w-full rounded-lg p-3 text-sm">
<div class="h-1lh" />
</Skeleton>
</div>
</div>
<span>Loading available voices...</span>
</div>
<!-- Error state -->
<div
v-else-if="speechProviderError"
class="flex items-center gap-3 border border-2 border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
>
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
<div class="flex flex-col">
<span class="font-medium">Error loading voices</span>
<span class="text-sm text-red-600 dark:text-red-400">{{ speechProviderError }}</span>
<!-- Error state -->
<!-- Voice selection with RadioCardDetailManySelect -->
<div
v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0"
class="space-y-6"
>
<VoiceCardManySelect
v-model:search-query="voiceSearchQuery"
:voices="availableVoices[activeSpeechProvider]?.map(voice => ({
id: voice.name,
name: voice.name,
description: voice.description,
previewURL: voice.previewURL,
customizable: false,
}))"
:selected-voice-id="voiceId"
:searchable="true"
:search-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_placeholder')"
:search-no-results-title="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices')"
:search-no-results-description="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices_description')"
:search-results-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_results', { count: 0, total: 0 })"
:custom-input-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.custom_voice_placeholder')"
:expand-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_more')"
:collapse-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_less')"
:play-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.play_sample')"
:pause-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.pause')"
@update:selected-voice-id="handleVoiceSelection"
@update:custom-value="updateCustomVoiceName"
/>
</div>
</div>
<!-- Voice selection with RadioCardDetailManySelect -->
<div
v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0"
class="space-y-6"
>
<VoiceCardManySelect
v-model:search-query="voiceSearchQuery"
:voices="availableVoices[activeSpeechProvider]?.map(voice => ({
id: voice.name,
name: voice.name,
description: voice.description,
previewURL: voice.previewURL,
customizable: false,
}))"
:selected-voice-id="voiceId"
:searchable="true"
:search-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_placeholder')"
:search-no-results-title="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices')"
:search-no-results-description="t('settings.pages.modules.speech.sections.section.provider-voice-selection.no_voices_description')"
:search-results-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_results', { count: 0, total: 0 })"
:custom-input-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.custom_voice_placeholder')"
:expand-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_more')"
:collapse-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.show_less')"
:play-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.play_sample')"
:pause-button-text="t('settings.pages.modules.speech.sections.section.provider-voice-selection.pause')"
@update:selected-voice-id="handleVoiceSelection"
@update:custom-value="updateCustomVoiceName"
/>
<div
v-else-if="speechProviderError"
class="flex items-center gap-3 border border-2 border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
>
<div i-solar:close-circle-line-duotone class="text-2xl text-red-500 dark:text-red-400" />
<div class="flex flex-col">
<span class="font-medium">Error loading voices</span>
<span class="text-sm text-red-600 dark:text-red-400">{{ speechProviderError }}</span>
</div>
</div>
<!-- No voices available -->
<div
v-else
class="flex items-center gap-3 border border-2 border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
>
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
<span class="font-medium">No voices available</span>
<span class="text-sm text-amber-600 dark:text-amber-400">
No voices were found for this provider. You can enter a custom voice name below.
</span>
</div>
</div>
<!-- Model selection for ElevenLabs -->
<div v-if="activeSpeechProvider === 'elevenlabs'">
@@ -239,21 +270,7 @@ watch(voiceId, updateSSMLExample)
description="Enable Speech Synthesis Markup Language for more control over speech output"
/>
</div>
</div>
<!-- No voices available -->
<div
v-else-if="!isLoadingSpeechProviderVoices && (!availableVoices[activeSpeechProvider] || availableVoices[activeSpeechProvider].length === 0)"
class="flex items-center gap-3 border border-2 border-amber-200 rounded-lg bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
>
<div i-solar:info-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
<span class="font-medium">No voices available</span>
<span class="text-sm text-amber-600 dark:text-amber-400">
No voices were found for this provider. You can enter a custom voice name below.
</span>
</div>
</div>
</TransitionGroup>
<!-- Manual voice input when no voices are available -->
<div
@@ -327,3 +344,24 @@ meta:
stageTransition:
name: slide
</route>
<style scoped>
.fade-slide-in-out-enter-active,
.fade-slide-in-out-leave-active {
transition: all 0.2s ease-in-out;
}
.fade-slide-in-out-enter-from,
.fade-slide-in-out-leave-to {
opacity: 0;
}
.fade-slide-in-out-leave-active {
transition: all 0.2s ease-in-out;
}
.fade-slide-in-out-leave-from,
.fade-slide-in-out-enter-to {
opacity: 1;
}
</style>
@@ -1,6 +1,5 @@
<script setup lang="ts">
import type { Voice } from '@proj-airi/stage-ui/constants'
import type { RemovableRef } from '@vueuse/core'
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
@@ -17,6 +16,7 @@ import {
} from '@proj-airi/stage-ui/components'
import { voiceMap } from '@proj-airi/stage-ui/constants'
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'
@@ -27,7 +27,7 @@ const { t } = useI18n()
const router = useRouter()
const providersStore = useProvidersStore()
const speechStore = useSpeechStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
const { providers } = storeToRefs(providersStore)
// For playground
const testText = ref('Hello! This is a test of the ElevenLabs voice synthesis.')
@@ -41,7 +41,7 @@ const providerId = 'elevenlabs'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
get: () => providers.value[providerId]?.apiKey as string | undefined || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
@@ -51,7 +51,7 @@ const apiKey = computed({
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || '',
get: () => providers.value[providerId]?.baseUrl as string | undefined || providerMetadata.value?.defaultOptions?.baseUrl as string | undefined || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
@@ -69,7 +69,8 @@ const similarityBoost = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].voiceSettings.similarityBoost = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.similarityBoost = value
},
})
@@ -81,7 +82,8 @@ const stability = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].voiceSettings.stability = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.stability = value
},
})
@@ -93,7 +95,8 @@ const speed = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].voiceSettings.speed = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.speed = value
},
})
@@ -105,7 +108,8 @@ const style = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].style = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.style = value
},
})
@@ -117,7 +121,8 @@ const useSpeakerBoost = computed({
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].voiceSettings.useSpeakerBoost = value
const voiceSettings = providers.value[providerId].voiceSettings as any
voiceSettings.useSpeakerBoost = value
},
})
@@ -130,8 +135,8 @@ onMounted(() => {
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || ''
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) {
@@ -148,8 +153,7 @@ onMounted(() => {
}
})
// Watch all settings and update the provider configuration
watch([apiKey, baseUrl, similarityBoost, stability, speed, style, useSpeakerBoost], () => {
const debouncedUpdate = useDebounceFn(() => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
@@ -162,7 +166,10 @@ watch([apiKey, baseUrl, similarityBoost, stability, speed, style, useSpeakerBoos
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() {
@@ -1,13 +1,9 @@
<script setup lang="ts">
import type { Voice } from '@proj-airi/stage-ui/constants'
import type { RemovableRef } from '@vueuse/core'
import type { UnMicrosoftOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import {
FieldCheckbox,
FieldInput,
FieldRange,
ProviderAdvancedSettings,
ProviderApiKeyInput,
ProviderBaseUrlInput,
@@ -16,8 +12,8 @@ import {
ProviderSettingsLayout,
TestDummyMarker,
} from '@proj-airi/stage-ui/components'
import { voiceMap } from '@proj-airi/stage-ui/constants'
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'
@@ -28,10 +24,11 @@ const { t } = useI18n()
const router = useRouter()
const providersStore = useProvidersStore()
const speechStore = useSpeechStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
const { providers } = storeToRefs(providersStore)
const { availableVoices } = storeToRefs(speechStore)
// For playground
const testText = ref('Hello! This is a test of the Microsoft Speech voice synthesis.')
const testText = ref('Hello! This is a test of the Microsoft Speech synthesis.')
const isGenerating = ref(false)
const audioUrl = ref('')
const errorMessage = ref('')
@@ -42,7 +39,7 @@ const providerId = 'microsoft-speech'
const providerMetadata = computed(() => providersStore.getProviderMetadata(providerId))
const apiKey = computed({
get: () => providers.value[providerId]?.apiKey || '',
get: () => providers.value[providerId]?.apiKey as string | undefined || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
@@ -51,18 +48,8 @@ const apiKey = computed({
},
})
const baseUrl = computed({
get: () => providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].baseUrl = value
},
})
const region = computed({
get: () => providers.value[providerId]?.region || providerMetadata.value?.defaultOptions?.region || '',
get: () => providers.value[providerId]?.region as string | undefined || providerMetadata.value?.defaultOptions?.region as string | undefined || 'eastasia',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
@@ -71,87 +58,33 @@ const region = computed({
},
})
// Voice settings as individual computed properties
const similarityBoost = computed({
get: () => (providers.value[providerId]?.voiceSettings as any)?.similarityBoost ?? 0.75,
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] = {}
if (!providers.value[providerId].voiceSettings)
providers.value[providerId].voiceSettings = {}
providers.value[providerId].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 = {}
providers.value[providerId].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 = {}
providers.value[providerId].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 = {}
providers.value[providerId].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 = {}
providers.value[providerId].voiceSettings.useSpeakerBoost = value
providers.value[providerId].baseUrl = value
},
})
// Speech settings
const selectedLanguage = ref(speechStore.selectedLanguage)
const selectedVoice = ref(speechStore.voiceName)
const availableVoices = computed(() => speechStore.availableVoicesForLanguage)
const selectedVoice = ref('')
const availableVoicesForLanguage = computed(() => {
if (availableVoices.value[providerId] == null) {
return []
}
return availableVoices.value[providerId].filter(voice => voice.languages.filter(language => language.code === selectedLanguage.value).length > 0)
})
onMounted(() => {
providersStore.initializeProvider(providerId)
// Initialize refs with current values
apiKey.value = providers.value[providerId]?.apiKey || ''
baseUrl.value = providers.value[providerId]?.baseUrl || providerMetadata.value?.defaultOptions?.baseUrl || ''
// 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
}
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 || ''
// Load voices if provider is configured
if (providersStore.configuredProviders[providerId]) {
@@ -159,14 +92,18 @@ onMounted(() => {
}
})
// Watch all settings and update the provider configuration
watch([apiKey, baseUrl, similarityBoost, stability, speed, style, useSpeakerBoost], () => {
const debouncedUpdate = useDebounceFn(() => {
providers.value[providerId] = {
...providers.value[providerId],
apiKey: apiKey.value,
baseUrl: baseUrl.value || providerMetadata.value?.defaultOptions?.baseUrl || '',
baseUrl: baseUrl.value || providerMetadata.value?.defaultOptions?.baseUrl as string | undefined || '',
}
})
speechStore.loadVoicesForProvider(providerId)
}, 1000)
// Watch all settings and update the provider configuration
watch([apiKey, baseUrl], debouncedUpdate)
// Function to generate speech
async function generateTestSpeech() {
@@ -188,10 +125,12 @@ async function generateTestSpeech() {
stopTestAudio()
}
const voice = availableVoicesForLanguage.value.find(voice => voice.name === selectedVoice.value)
const response = await generateSpeech({
...provider.speech('v1'),
...provider.speech('v1', { region: region.value, gender: voice?.gender }),
input: testText.value,
voice: voiceMap[selectedVoice.value as Voice],
voice: voice!.id,
})
// Convert the response to a blob and create an object URL
@@ -268,41 +207,6 @@ function handleResetVoiceSettings() {
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
{{ t('settings.pages.providers.common.section.voice.title') }}
</h2>
<div flex="~ col gap-4">
<FieldRange
v-model="similarityBoost"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.simularity-boost.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.simularity-boost.description')"
:min="0" :max="1" :step="0.01"
/>
<FieldRange
v-model="stability"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.stability.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.stability.description')"
:min="0" :max="1" :step="0.01"
/>
<FieldRange
v-model="speed"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.speed.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.speed.description')" :min="0.7"
:max="1.2" :step="0.01"
/>
<FieldRange
v-model="style"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.style.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.style.description')" :min="0"
:max="1" :step="0.01"
/>
<FieldCheckbox
v-model="useSpeakerBoost"
:label="t('settings.pages.providers.provider.elevenlabs.fields.field.speaker-boost.label')"
:description="t('settings.pages.providers.provider.elevenlabs.fields.field.speaker-boost.description')"
/>
</div>
</div>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
@@ -369,7 +273,10 @@ function handleResetVoiceSettings() {
transition="border duration-250 ease-in-out" w-full rounded-lg px-2 py-1 text-nowrap text-sm
outline-none
>
<option v-for="voice in availableVoices" :key="voice.id" :value="voice.name">
<option value="">
Select a voice
</option>
<option v-for="voice in availableVoicesForLanguage" :key="voice.id" :value="voice.name">
{{ voice.name }}
</option>
</select>
+34 -28
View File
@@ -77,9 +77,12 @@ export interface VoiceInfo {
provider: string
description?: string
gender?: string
language?: string
deprecated?: boolean
previewURL?: string
languages: {
code: string
title: string
}[]
}
export const useProvidersStore = defineStore('providers', () => {
@@ -91,7 +94,7 @@ export const useProvidersStore = defineStore('providers', () => {
try {
const response = await fetch('https://openrouter.ai/api/v1/models', {
headers: {
'Authorization': `Bearer ${config.apiKey as string}`,
'Authorization': `Bearer ${(config.apiKey as string).trim()}`,
'Content-Type': 'application/json',
},
})
@@ -128,7 +131,7 @@ export const useProvidersStore = defineStore('providers', () => {
defaultOptions: {
baseUrl: 'https://openrouter.ai/api/v1/',
},
createProvider: config => createOpenRouter(config.apiKey as string, config.baseUrl as string),
createProvider: config => createOpenRouter((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return fetchOpenRouterModels(config)
@@ -145,11 +148,11 @@ export const useProvidersStore = defineStore('providers', () => {
defaultOptions: {
baseUrl: 'https://api.openai.com/v1/',
},
createProvider: config => createOpenAI(config.apiKey as string, config.baseUrl as string),
createProvider: config => createOpenAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createOpenAI(config.apiKey as string, config.baseUrl as string).model(),
...createOpenAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,
@@ -173,11 +176,11 @@ export const useProvidersStore = defineStore('providers', () => {
defaultOptions: {
baseUrl: 'http://localhost:11434/api/',
},
createProvider: config => createOllama(config.baseUrl as string),
createProvider: config => createOllama((config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createOllama(config.baseUrl as string).model(),
...createOllama((config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,
@@ -198,7 +201,7 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.vllm.description',
description: 'vllm.ai',
iconColor: 'i-lobe-icons:vllm-color',
createProvider: config => createOllama(config.baseUrl as string),
createProvider: config => createOllama((config.baseUrl as string).trim()),
capabilities: {
listModels: async () => {
return [
@@ -258,7 +261,7 @@ export const useProvidersStore = defineStore('providers', () => {
defaultOptions: {
baseUrl: 'https://api.perplexity.ai',
},
createProvider: config => createPerplexity(config.apiKey as string, config.baseUrl as string),
createProvider: config => createPerplexity((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async () => {
return [
@@ -315,13 +318,13 @@ export const useProvidersStore = defineStore('providers', () => {
stability: 0.5,
},
},
createProvider: config => createUnElevenLabs(config.apiKey as string, config.baseUrl as string) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>,
createProvider: config => createUnElevenLabs((config.apiKey as string).trim(), (config.baseUrl as string).trim()) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>,
capabilities: {
listModels: async () => {
return []
},
listVoices: async (config) => {
const provider = createUnElevenLabs(config.apiKey as string, config.baseUrl as string) as VoiceProviderWithExtraOptions<UnElevenLabsOptions>
const provider = createUnElevenLabs((config.apiKey as string).trim(), (config.baseUrl as string).trim()) as VoiceProviderWithExtraOptions<UnElevenLabsOptions>
const voices = await listVoices({
...provider.voice(),
@@ -350,6 +353,7 @@ export const useProvidersStore = defineStore('providers', () => {
name: voice.name,
provider: 'elevenlabs',
previewURL: voice.preview_audio_url,
languages: voice.languages,
}
})
},
@@ -362,11 +366,11 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.xai.description',
description: 'x.ai',
icon: 'i-lobe-icons:xai',
createProvider: config => createXAI(config.apiKey as string, config.baseUrl as string),
createProvider: config => createXAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createXAI(config.apiKey as string, config.baseUrl as string).model(),
...createXAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,
@@ -387,11 +391,11 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.deepseek.description',
description: 'deepseek.com',
iconColor: 'i-lobe-icons:deepseek-color',
createProvider: config => createDeepSeek(config.apiKey as string, config.baseUrl as string),
createProvider: config => createDeepSeek((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createDeepSeek(config.apiKey as string, config.baseUrl as string).model(),
...createDeepSeek((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,
@@ -412,11 +416,11 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.together.description',
description: 'together.ai',
iconColor: 'i-lobe-icons:together-color',
createProvider: config => createTogetherAI(config.apiKey as string, config.baseUrl as string),
createProvider: config => createTogetherAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createTogetherAI(config.apiKey as string, config.baseUrl as string).model(),
...createTogetherAI((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,
@@ -437,11 +441,11 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.novita.description',
description: 'novita.ai',
iconColor: 'i-lobe-icons:novita-color',
createProvider: config => createNovita(config.apiKey as string, config.baseUrl as string),
createProvider: config => createNovita((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createNovita(config.apiKey as string, config.baseUrl as string).model(),
...createNovita((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,
@@ -462,11 +466,11 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.fireworks.description',
description: 'fireworks.ai',
icon: 'i-lobe-icons:fireworks',
createProvider: config => createFireworks(config.apiKey as string, config.baseUrl as string),
createProvider: config => createFireworks((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createFireworks(config.apiKey as string, config.baseUrl as string).model(),
...createFireworks((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,
@@ -490,13 +494,13 @@ export const useProvidersStore = defineStore('providers', () => {
defaultOptions: {
baseUrl: 'https://unspeech.hyp3r.link/v1/',
},
createProvider: config => createUnMicrosoft(config.apiKey as string, config.baseUrl as string) as SpeechProviderWithExtraOptions<string, UnMicrosoftOptions>,
createProvider: config => createUnMicrosoft((config.apiKey as string).trim(), (config.baseUrl as string).trim()) as SpeechProviderWithExtraOptions<string, UnMicrosoftOptions>,
capabilities: {
listModels: async () => {
return []
},
listVoices: async (config) => {
const provider = createUnMicrosoft(config.apiKey as string, config.baseUrl as string) as VoiceProviderWithExtraOptions<UnMicrosoftOptions>
const provider = createUnMicrosoft((config.apiKey as string).trim(), (config.baseUrl as string).trim()) as VoiceProviderWithExtraOptions<UnMicrosoftOptions>
const voices = await listVoices({
...provider.voice({ region: config.region as string }),
@@ -508,6 +512,8 @@ export const useProvidersStore = defineStore('providers', () => {
name: voice.name,
provider: 'microsoft-speech',
previewURL: voice.preview_audio_url,
languages: voice.languages,
gender: voice.labels?.gender,
}
})
},
@@ -520,7 +526,7 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.cloudflare-workers-ai.description',
description: 'cloudflare.com',
iconColor: 'i-lobe-icons:cloudflare-color',
createProvider: config => createWorkersAI(config.apiKey as string, config.accountId as string),
createProvider: config => createWorkersAI((config.apiKey as string).trim(), config.accountId as string),
capabilities: {
listModels: async () => {
return []
@@ -534,11 +540,11 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.mistral.description',
description: 'mistral.ai',
iconColor: 'i-lobe-icons:mistral-color',
createProvider: config => createMistral(config.apiKey as string, config.baseUrl as string),
createProvider: config => createMistral((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createMistral(config.apiKey as string, config.baseUrl as string).model(),
...createMistral((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,
@@ -559,11 +565,11 @@ export const useProvidersStore = defineStore('providers', () => {
descriptionKey: 'settings.pages.providers.provider.moonshot.description',
description: 'moonshot.ai',
icon: 'i-lobe-icons:moonshot',
createProvider: config => createMoonshot(config.apiKey as string, config.baseUrl as string),
createProvider: config => createMoonshot((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listModels: async (config) => {
return (await listModels({
...createMoonshot(config.apiKey as string, config.baseUrl as string).model(),
...createMoonshot((config.apiKey as string).trim(), (config.baseUrl as string).trim()).model(),
})).map((model) => {
return {
id: model.id,