fix(stage-web|stage-tamagotchi|stage-ui): voice & models
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { RadioCardSimple, Range } from '@proj-airi/stage-ui/components'
|
||||
import { RadioCardDetailManySelect, RadioCardSimple, Range } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref } from 'vue'
|
||||
@@ -7,7 +7,10 @@ import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const { availableProviders, availableProvidersMetadata } = storeToRefs(providersStore)
|
||||
const {
|
||||
availableProviders,
|
||||
availableProvidersMetadata,
|
||||
} = storeToRefs(providersStore)
|
||||
const {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
@@ -18,6 +21,7 @@ const {
|
||||
speechProviderError,
|
||||
supportsSSML,
|
||||
ssmlEnabled,
|
||||
availableVoices,
|
||||
} = storeToRefs(speechStore)
|
||||
|
||||
const router = useRouter()
|
||||
@@ -29,6 +33,8 @@ const ssmlExample = ref(`<speak>
|
||||
</voice>
|
||||
</speak>`)
|
||||
|
||||
const voiceSearchQuery = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
|
||||
})
|
||||
@@ -38,6 +44,11 @@ function updateVoiceName(value: string) {
|
||||
updateSSMLExample()
|
||||
}
|
||||
|
||||
function updateCustomVoiceName(value: string) {
|
||||
voiceName.value = value
|
||||
updateSSMLExample()
|
||||
}
|
||||
|
||||
function updateSSMLExample() {
|
||||
ssmlExample.value = `<speak>
|
||||
Hello, my name is <voice name="${voiceName.value || 'Default'}">
|
||||
@@ -136,7 +147,7 @@ function updateSSMLExample() {
|
||||
<!-- Error state -->
|
||||
<div
|
||||
v-else-if="speechProviderError"
|
||||
class="flex items-center gap-3 border border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
|
||||
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">
|
||||
@@ -145,9 +156,129 @@ function updateSSMLExample() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voice configuration form -->
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Voice selection -->
|
||||
<!-- Voice selection with RadioCardDetailManySelect -->
|
||||
<div v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0" class="space-y-6">
|
||||
<RadioCardDetailManySelect
|
||||
v-model="voiceName"
|
||||
v-model:search-query="voiceSearchQuery"
|
||||
:items="availableVoices[activeSpeechProvider]?.map(voice => ({
|
||||
id: voice.name,
|
||||
name: voice.name,
|
||||
description: voice.description || `${voice.labels?.gender || 'Neutral'} voice`,
|
||||
customizable: true,
|
||||
}))"
|
||||
:searchable="true"
|
||||
search-placeholder="Search voices..."
|
||||
search-no-results-title="No voices found"
|
||||
search-no-results-description="Try a different search term or enter a custom voice name"
|
||||
search-results-text="{count} of {total} voices"
|
||||
custom-input-placeholder="Enter custom voice name"
|
||||
expand-button-text="Show more voices"
|
||||
collapse-button-text="Show fewer voices"
|
||||
@update:custom-value="updateCustomVoiceName"
|
||||
/>
|
||||
|
||||
<!-- Model selection for ElevenLabs -->
|
||||
<div v-if="activeSpeechProvider === 'elevenlabs'">
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
v-model="activeSpeechModel"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
>
|
||||
<option value="eleven_monolingual_v1">
|
||||
Monolingual v1
|
||||
</option>
|
||||
<option value="eleven_multilingual_v1">
|
||||
Multilingual v1
|
||||
</option>
|
||||
<option value="eleven_multilingual_v2">
|
||||
Multilingual v2
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Voice parameters -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Pitch Adjustment (%)
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<Range
|
||||
v-model="pitch" :min="-50" :max="50" :step="5"
|
||||
class="w-full"
|
||||
@change="updateSSMLExample"
|
||||
/>
|
||||
<span class="w-12 text-center">{{ pitch }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Speech Rate
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<Range
|
||||
v-model="rate" :min="0.5" :max="2" :step="0.1"
|
||||
class="w-full"
|
||||
@change="updateSSMLExample"
|
||||
/>
|
||||
<span class="w-12 text-center">{{ rate }}x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSML Support -->
|
||||
<div v-if="supportsSSML" class="border border-2 border-neutral-200 rounded-lg p-4 dark:border-neutral-700">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="font-medium">SSML Support</label>
|
||||
<div class="relative mr-2 inline-block w-10 select-none align-middle">
|
||||
<input
|
||||
id="ssml-toggle"
|
||||
v-model="ssmlEnabled"
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
>
|
||||
<label
|
||||
for="ssml-toggle"
|
||||
class="block h-6 cursor-pointer overflow-hidden rounded-full bg-neutral-300 dark:bg-neutral-700"
|
||||
>
|
||||
<span
|
||||
:class="{ 'translate-x-4': ssmlEnabled, 'translate-x-0': !ssmlEnabled }"
|
||||
class="block h-6 w-6 transform rounded-full bg-white shadow transition-transform duration-200 ease-in-out"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-3 text-sm text-neutral-500">
|
||||
Enable Speech Synthesis Markup Language for more control over speech output
|
||||
</p>
|
||||
<div v-if="ssmlEnabled" class="mt-3">
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
SSML Example
|
||||
</label>
|
||||
<pre class="overflow-auto rounded bg-neutral-50 p-3 text-xs dark:bg-neutral-800">{{ ssmlExample }}</pre>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Manual voice input when no voices are available -->
|
||||
<div v-if="!availableVoices[activeSpeechProvider] || availableVoices[activeSpeechProvider].length === 0" class="mt-2 space-y-6">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Voice Name
|
||||
@@ -194,6 +325,7 @@ function updateSSMLExample() {
|
||||
<Range
|
||||
v-model="pitch" :min="-50" :max="50" :step="5"
|
||||
class="w-full"
|
||||
@change="updateSSMLExample"
|
||||
/>
|
||||
<span class="w-12 text-center">{{ pitch }}%</span>
|
||||
</div>
|
||||
@@ -206,6 +338,7 @@ function updateSSMLExample() {
|
||||
<Range
|
||||
v-model="rate" :min="0.5" :max="2" :step="0.1"
|
||||
class="w-full"
|
||||
@change="updateSSMLExample"
|
||||
/>
|
||||
<span class="w-12 text-center">{{ rate }}x</span>
|
||||
</div>
|
||||
@@ -213,7 +346,7 @@ function updateSSMLExample() {
|
||||
</div>
|
||||
|
||||
<!-- SSML Support -->
|
||||
<div v-if="supportsSSML" class="border border-neutral-200 rounded-lg p-4 dark:border-neutral-700">
|
||||
<div v-if="supportsSSML" class="border border-2 border-neutral-200 rounded-lg p-4 dark:border-neutral-700">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="font-medium">SSML Support</label>
|
||||
<div class="relative mr-2 inline-block w-10 select-none align-middle">
|
||||
@@ -253,3 +386,9 @@ function updateSSMLExample() {
|
||||
<div text="40" i-lucide:volume-2 />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { RadioCardSimple, Range } from '@proj-airi/stage-ui/components'
|
||||
import { RadioCardDetailManySelect, RadioCardSimple, Range } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref } from 'vue'
|
||||
@@ -7,7 +7,10 @@ import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const speechStore = useSpeechStore()
|
||||
const { availableProviders, availableProvidersMetadata } = storeToRefs(providersStore)
|
||||
const {
|
||||
availableProviders,
|
||||
availableProvidersMetadata,
|
||||
} = storeToRefs(providersStore)
|
||||
const {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
@@ -18,6 +21,7 @@ const {
|
||||
speechProviderError,
|
||||
supportsSSML,
|
||||
ssmlEnabled,
|
||||
availableVoices,
|
||||
} = storeToRefs(speechStore)
|
||||
|
||||
const router = useRouter()
|
||||
@@ -29,6 +33,8 @@ const ssmlExample = ref(`<speak>
|
||||
</voice>
|
||||
</speak>`)
|
||||
|
||||
const voiceSearchQuery = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
|
||||
})
|
||||
@@ -38,6 +44,11 @@ function updateVoiceName(value: string) {
|
||||
updateSSMLExample()
|
||||
}
|
||||
|
||||
function updateCustomVoiceName(value: string) {
|
||||
voiceName.value = value
|
||||
updateSSMLExample()
|
||||
}
|
||||
|
||||
function updateSSMLExample() {
|
||||
ssmlExample.value = `<speak>
|
||||
Hello, my name is <voice name="${voiceName.value || 'Default'}">
|
||||
@@ -136,7 +147,7 @@ function updateSSMLExample() {
|
||||
<!-- Error state -->
|
||||
<div
|
||||
v-else-if="speechProviderError"
|
||||
class="flex items-center gap-3 border border-red-200 rounded-lg bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20"
|
||||
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">
|
||||
@@ -145,9 +156,129 @@ function updateSSMLExample() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voice configuration form -->
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Voice selection -->
|
||||
<!-- Voice selection with RadioCardDetailManySelect -->
|
||||
<div v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0" class="space-y-6">
|
||||
<RadioCardDetailManySelect
|
||||
v-model="voiceName"
|
||||
v-model:search-query="voiceSearchQuery"
|
||||
:items="availableVoices[activeSpeechProvider]?.map(voice => ({
|
||||
id: voice.name,
|
||||
name: voice.name,
|
||||
description: voice.description || `${voice.labels?.gender || 'Neutral'} voice`,
|
||||
customizable: true,
|
||||
}))"
|
||||
:searchable="true"
|
||||
search-placeholder="Search voices..."
|
||||
search-no-results-title="No voices found"
|
||||
search-no-results-description="Try a different search term or enter a custom voice name"
|
||||
search-results-text="{count} of {total} voices"
|
||||
custom-input-placeholder="Enter custom voice name"
|
||||
expand-button-text="Show more voices"
|
||||
collapse-button-text="Show fewer voices"
|
||||
@update:custom-value="updateCustomVoiceName"
|
||||
/>
|
||||
|
||||
<!-- Model selection for ElevenLabs -->
|
||||
<div v-if="activeSpeechProvider === 'elevenlabs'">
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
v-model="activeSpeechModel"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
>
|
||||
<option value="eleven_monolingual_v1">
|
||||
Monolingual v1
|
||||
</option>
|
||||
<option value="eleven_multilingual_v1">
|
||||
Multilingual v1
|
||||
</option>
|
||||
<option value="eleven_multilingual_v2">
|
||||
Multilingual v2
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Voice parameters -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Pitch Adjustment (%)
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<Range
|
||||
v-model="pitch" :min="-50" :max="50" :step="5"
|
||||
class="w-full"
|
||||
@change="updateSSMLExample"
|
||||
/>
|
||||
<span class="w-12 text-center">{{ pitch }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Speech Rate
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<Range
|
||||
v-model="rate" :min="0.5" :max="2" :step="0.1"
|
||||
class="w-full"
|
||||
@change="updateSSMLExample"
|
||||
/>
|
||||
<span class="w-12 text-center">{{ rate }}x</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSML Support -->
|
||||
<div v-if="supportsSSML" class="border border-2 border-neutral-200 rounded-lg p-4 dark:border-neutral-700">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="font-medium">SSML Support</label>
|
||||
<div class="relative mr-2 inline-block w-10 select-none align-middle">
|
||||
<input
|
||||
id="ssml-toggle"
|
||||
v-model="ssmlEnabled"
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
>
|
||||
<label
|
||||
for="ssml-toggle"
|
||||
class="block h-6 cursor-pointer overflow-hidden rounded-full bg-neutral-300 dark:bg-neutral-700"
|
||||
>
|
||||
<span
|
||||
:class="{ 'translate-x-4': ssmlEnabled, 'translate-x-0': !ssmlEnabled }"
|
||||
class="block h-6 w-6 transform rounded-full bg-white shadow transition-transform duration-200 ease-in-out"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-3 text-sm text-neutral-500">
|
||||
Enable Speech Synthesis Markup Language for more control over speech output
|
||||
</p>
|
||||
<div v-if="ssmlEnabled" class="mt-3">
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
SSML Example
|
||||
</label>
|
||||
<pre class="overflow-auto rounded bg-neutral-50 p-3 text-xs dark:bg-neutral-800">{{ ssmlExample }}</pre>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Manual voice input when no voices are available -->
|
||||
<div v-if="!availableVoices[activeSpeechProvider] || availableVoices[activeSpeechProvider].length === 0" class="mt-2 space-y-6">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">
|
||||
Voice Name
|
||||
@@ -194,6 +325,7 @@ function updateSSMLExample() {
|
||||
<Range
|
||||
v-model="pitch" :min="-50" :max="50" :step="5"
|
||||
class="w-full"
|
||||
@change="updateSSMLExample"
|
||||
/>
|
||||
<span class="w-12 text-center">{{ pitch }}%</span>
|
||||
</div>
|
||||
@@ -206,6 +338,7 @@ function updateSSMLExample() {
|
||||
<Range
|
||||
v-model="rate" :min="0.5" :max="2" :step="0.1"
|
||||
class="w-full"
|
||||
@change="updateSSMLExample"
|
||||
/>
|
||||
<span class="w-12 text-center">{{ rate }}x</span>
|
||||
</div>
|
||||
@@ -213,7 +346,7 @@ function updateSSMLExample() {
|
||||
</div>
|
||||
|
||||
<!-- SSML Support -->
|
||||
<div v-if="supportsSSML" class="border border-neutral-200 rounded-lg p-4 dark:border-neutral-700">
|
||||
<div v-if="supportsSSML" class="border border-2 border-neutral-200 rounded-lg p-4 dark:border-neutral-700">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<label class="font-medium">SSML Support</label>
|
||||
<div class="relative mr-2 inline-block w-10 select-none align-middle">
|
||||
@@ -253,3 +386,9 @@ function updateSSMLExample() {
|
||||
<div text="40" i-lucide:volume-2 />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
</route>
|
||||
|
||||
@@ -105,7 +105,7 @@ function updateCustomValue(value: string) {
|
||||
<!-- No search results -->
|
||||
<div
|
||||
v-if="searchQuery && filteredItems.length === 0"
|
||||
class="flex items-center gap-3 border border-amber-200 rounded-xl bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-900/20"
|
||||
class="flex items-center gap-3 border border-2 border-amber-200 rounded-xl 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">
|
||||
@@ -122,7 +122,7 @@ function updateCustomValue(value: string) {
|
||||
<div
|
||||
class="scrollbar-hide grid auto-cols-[350px] grid-flow-col gap-4 overflow-x-auto pb-4"
|
||||
:class="[
|
||||
isListExpanded ? 'md:grid-cols-2 md:grid-flow-row md:auto-cols-auto' : '',
|
||||
isListExpanded ? 'grid-cols-1 md:grid-cols-2 grid-flow-row auto-cols-auto' : '',
|
||||
]"
|
||||
transition="all duration-200 ease-in-out"
|
||||
style="scroll-snap-type: x mandatory;"
|
||||
@@ -153,7 +153,7 @@ function updateCustomValue(value: string) {
|
||||
bg="neutral-100 dark:[rgba(0,0,0,0.3)]"
|
||||
rounded-xl
|
||||
:class="[
|
||||
isListExpanded ? 'fixed bottom-4 left-1/2 translate-x--1/2 z-10 w-[calc(100%-16px-40px-16px)]' : 'mt-0 w-full rounded-lg',
|
||||
isListExpanded ? 'fixed bottom-4 left-1/2 translate-x--1/2 z-10 w-full px-9 max-w-screen-lg' : 'mt-0 w-full rounded-lg',
|
||||
]"
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Voice } from '../fix/voice'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { Voice, voiceList, voiceMap } from '../../constants/elevenlabs'
|
||||
import { voiceList, voiceMap } from '../../constants/elevenlabs'
|
||||
import { useProvidersStore } from '../providers'
|
||||
|
||||
export const useSpeechStore = defineStore('speech', () => {
|
||||
@@ -17,20 +19,9 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
const ssmlEnabled = useLocalStorage('settings/speech/ssml-enabled', false)
|
||||
const isLoadingSpeechProviderVoices = ref(false)
|
||||
const speechProviderError = ref<string | null>(null)
|
||||
const availableVoices = ref<Record<string, VoiceInfo[]>>({})
|
||||
const availableVoices = ref<Record<string, Voice[]>>({})
|
||||
const selectedLanguage = useLocalStorage('settings/speech/language', 'en-US')
|
||||
|
||||
// Voice info interface
|
||||
interface VoiceInfo {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
description?: string
|
||||
gender?: string
|
||||
previewUrl?: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const availableSpeechProvidersMetadata = computed(() => {
|
||||
// Filter only speech providers from all available providers
|
||||
@@ -69,31 +60,6 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
return ['elevenlabs', 'microsoft', 'google', 'amazon'].includes(providerId)
|
||||
}
|
||||
|
||||
// Actions
|
||||
function setActiveSpeechProvider(provider: string) {
|
||||
activeSpeechProvider.value = provider
|
||||
}
|
||||
|
||||
function setActiveSpeechModel(model: string) {
|
||||
activeSpeechModel.value = model
|
||||
}
|
||||
|
||||
function setVoiceName(name: string) {
|
||||
voiceName.value = name
|
||||
}
|
||||
|
||||
const voiceId = computed(() => {
|
||||
return voiceMap[voiceName.value as Voice]
|
||||
})
|
||||
|
||||
function setSSMLEnabled(enabled: boolean) {
|
||||
ssmlEnabled.value = enabled
|
||||
}
|
||||
|
||||
function setLanguage(language: string) {
|
||||
selectedLanguage.value = language
|
||||
}
|
||||
|
||||
function resetVoiceSettings() {
|
||||
voiceName.value = ''
|
||||
pitch.value = 0
|
||||
@@ -110,56 +76,7 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
speechProviderError.value = null
|
||||
|
||||
try {
|
||||
// In a real implementation, you would fetch voices from the provider
|
||||
// For now, we'll use our predefined voices for ElevenLabs
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
if (provider === 'elevenlabs') {
|
||||
// Use the predefined voices from elevenlabs.ts
|
||||
const allVoices: VoiceInfo[] = []
|
||||
|
||||
for (const language in voiceList) {
|
||||
const voices = voiceList[language]
|
||||
for (const voice of voices) {
|
||||
allVoices.push({
|
||||
id: voiceMap[voice],
|
||||
name: voice,
|
||||
provider: 'elevenlabs',
|
||||
language,
|
||||
description: getVoiceDescription(voice),
|
||||
gender: getVoiceGender(voice),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
availableVoices.value[provider] = allVoices
|
||||
}
|
||||
else if (provider === 'microsoft') {
|
||||
availableVoices.value[provider] = [
|
||||
{
|
||||
id: 'en-US-AriaNeural',
|
||||
name: 'Aria',
|
||||
provider: 'microsoft',
|
||||
description: 'Microsoft neural voice (female)',
|
||||
gender: 'female',
|
||||
language: 'en-US',
|
||||
},
|
||||
{
|
||||
id: 'en-US-GuyNeural',
|
||||
name: 'Guy',
|
||||
provider: 'microsoft',
|
||||
description: 'Microsoft neural voice (male)',
|
||||
gender: 'male',
|
||||
language: 'en-US',
|
||||
},
|
||||
]
|
||||
}
|
||||
else {
|
||||
// For other providers, return an empty array for now
|
||||
availableVoices.value[provider] = []
|
||||
}
|
||||
|
||||
return availableVoices.value[provider]
|
||||
return await providersStore.getProviderMetadata(provider).capabilities.listVoices?.(providersStore.getProviderConfig(provider)) || []
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error fetching voices for ${provider}:`, error)
|
||||
@@ -171,27 +88,6 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get voice descriptions
|
||||
function getVoiceDescription(voice: string): string {
|
||||
const descriptions: Record<string, string> = {
|
||||
[Voice.Myriam]: 'Professional female voice with clear articulation',
|
||||
[Voice.Beatrice]: 'Mature and sophisticated female voice',
|
||||
[Voice.Camilla_KM]: 'Friendly and approachable female voice',
|
||||
[Voice.SallySunshine]: 'Cheerful and upbeat female voice',
|
||||
[Voice.Annie]: 'Young and energetic female voice',
|
||||
[Voice.KawaiiAerisita]: 'Cute and playful female voice',
|
||||
[Voice.Morioki]: 'Deep and authoritative Japanese male voice',
|
||||
}
|
||||
|
||||
return descriptions[voice as Voice] || 'ElevenLabs voice'
|
||||
}
|
||||
|
||||
// Helper function to get voice gender
|
||||
function getVoiceGender(voice: string): string {
|
||||
const maleVoices = [Voice.Morioki]
|
||||
return maleVoices.includes(voice as Voice) ? 'male' : 'female'
|
||||
}
|
||||
|
||||
// Get voices for a specific provider
|
||||
function getVoicesForProvider(provider: string) {
|
||||
return availableVoices.value[provider] || []
|
||||
@@ -241,7 +137,6 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
voiceName,
|
||||
voiceId,
|
||||
pitch,
|
||||
rate,
|
||||
ssmlEnabled,
|
||||
@@ -256,12 +151,6 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
availableLanguages,
|
||||
availableVoicesForLanguage,
|
||||
|
||||
// Actions
|
||||
setActiveSpeechProvider,
|
||||
setActiveSpeechModel,
|
||||
setVoiceName,
|
||||
setSSMLEnabled,
|
||||
setLanguage,
|
||||
resetVoiceSettings,
|
||||
loadVoicesForProvider,
|
||||
getVoicesForProvider,
|
||||
|
||||
@@ -709,8 +709,13 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
return availableProviders.value.map(id => getProviderMetadata(id))
|
||||
})
|
||||
|
||||
function getProviderConfig(providerId: string) {
|
||||
return providerCredentials.value[providerId]
|
||||
}
|
||||
|
||||
return {
|
||||
providers: providerCredentials,
|
||||
getProviderConfig,
|
||||
availableProviders,
|
||||
configuredProviders,
|
||||
providerMetadata,
|
||||
|
||||
Reference in New Issue
Block a user