fix(stage-web|stage-tamagotchi|stage-ui): voice & models

This commit is contained in:
Neko Ayaka
2025-03-15 22:05:11 +08:00
parent e764d33404
commit b8d93b6d7d
5 changed files with 305 additions and 133 deletions
@@ -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>