fix: model card, make sure voice list working

This commit is contained in:
Neko Ayaka
2025-03-17 23:05:51 +08:00
parent 097633c707
commit e8b413111f
23 changed files with 255 additions and 392 deletions
@@ -51,9 +51,9 @@ async function extractColorsFromModel() {
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">{{ t('settings.title') }}</span>
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
</div>
<div text-3xl font-semibold>
<div text-nowrap text-3xl font-semibold>
{{ t('settings.pages.models.title') }}
</div>
</h1>
@@ -46,9 +46,9 @@ function updateCustomModelName(value: string) {
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">{{ t('settings.pages.modules.title') }}</span>
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.pages.modules.title') }}</span>
</div>
<div text-3xl font-semibold>
<div text-nowrap text-3xl font-semibold>
{{ t('settings.pages.modules.consciousness.title') }}
</div>
</h1>
@@ -109,9 +109,9 @@ const modulesList = computed<Module[]>(() => [
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">{{ t('settings.title') }}</span>
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
</div>
<div text-3xl font-semibold>
<div text-nowrap text-3xl font-semibold>
{{ t('settings.pages.modules.title') }}
</div>
</h1>
@@ -1,8 +1,13 @@
<script setup lang="ts">
import { RadioCardDetailManySelect, RadioCardSimple, Range } from '@proj-airi/stage-ui/components'
import {
FieldCheckbox,
FieldRange,
RadioCardDetailManySelect,
RadioCardSimple,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterLink, useRouter } from 'vue-router'
@@ -21,7 +26,6 @@ const {
rate,
isLoadingSpeechProviderVoices,
speechProviderError,
supportsSSML,
ssmlEnabled,
availableVoices,
} = storeToRefs(speechStore)
@@ -41,6 +45,10 @@ onMounted(async () => {
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
})
watch(activeSpeechProvider, async () => {
await speechStore.loadVoicesForProvider(activeSpeechProvider.value)
})
function updateVoiceName(value: string) {
voiceId.value = value
updateSSMLExample()
@@ -64,22 +72,18 @@ function updateSSMLExample() {
<template>
<div
v-motion
flex="~ row" items-center gap-2
:initial="{ opacity: 0, x: 10 }"
:enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }"
:duration="250"
v-motion flex="~ row" items-center gap-2 :initial="{ opacity: 0, x: 10 }" :enter="{ opacity: 1, x: 0 }"
:leave="{ opacity: 0, x: -10 }" :duration="250"
>
<button @click="router.back()">
<div i-solar:alt-arrow-left-line-duotone text-xl />
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">{{ t('settings.pages.modules.title') }}</span>
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.pages.modules.title') }}</span>
</div>
<div text-3xl font-semibold>
Speech
<div text-nowrap text-3xl font-semibold>
{{ t('settings.pages.modules.speech.title') }}
</div>
</h1>
</div>
@@ -88,37 +92,27 @@ function updateSSMLExample() {
<div flex="~ col gap-4">
<div>
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Provider
{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.title') }}
</h2>
<div text="neutral-400 dark:neutral-500">
<span>Select the suitable speech provider</span>
<span>{{ t('settings.pages.modules.speech.sections.section.provider-voice-selection.description') }}</span>
</div>
</div>
<div max-w-full>
<fieldset
v-if="availableProviders.length > 0"
flex="~ row gap-4"
:style="{ 'scrollbar-width': 'none' }"
min-w-0 of-x-scroll scroll-smooth
role="radiogroup"
v-if="availableProviders.length > 0" flex="~ row gap-4" :style="{ 'scrollbar-width': 'none' }"
min-w-0 of-x-scroll scroll-smooth role="radiogroup"
>
<RadioCardSimple
v-for="metadata in availableProvidersMetadata"
:id="metadata.id"
:key="metadata.id"
v-model="activeSpeechProvider"
name="speech-provider"
:value="metadata.id"
:title="metadata.localizedName"
v-for="metadata in availableProvidersMetadata" :id="metadata.id" :key="metadata.id"
v-model="activeSpeechProvider" name="speech-provider" :value="metadata.id" :title="metadata.localizedName"
:description="metadata.localizedDescription"
/>
</fieldset>
<div v-else>
<RouterLink
class="flex items-center gap-3 rounded-lg p-4"
border="2 dashed neutral-200 dark:neutral-800"
bg="neutral-50 dark:neutral-800"
transition="colors duration-200 ease-in-out" to="/settings/providers"
class="flex items-center gap-3 rounded-lg p-4" border="2 dashed neutral-200 dark:neutral-800"
bg="neutral-50 dark:neutral-800" transition="colors duration-200 ease-in-out" to="/settings/providers"
>
<div i-solar:warning-circle-line-duotone class="text-2xl text-amber-500 dark:text-amber-400" />
<div class="flex flex-col">
@@ -166,24 +160,25 @@ function updateSSMLExample() {
</div>
<!-- Voice selection with RadioCardDetailManySelect -->
<div v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0" class="space-y-6">
<div
v-else-if="availableVoices[activeSpeechProvider] && availableVoices[activeSpeechProvider].length > 0"
class="space-y-6"
>
<RadioCardDetailManySelect
v-model="voiceId"
v-model:search-query="voiceSearchQuery"
v-model="voiceId" v-model:search-query="voiceSearchQuery"
:items="availableVoices[activeSpeechProvider]?.map(voice => ({
id: voice.name,
name: voice.name,
description: voice.description || `${voice.labels?.gender || 'Neutral'} voice`,
description: voice.description,
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"
}))" :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')"
@update:custom-value="updateCustomVoiceName"
/>
@@ -209,66 +204,25 @@ function updateSSMLExample() {
</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 flex="~ col gap-4">
<FieldRange
v-model="pitch"
label="Pitch Adjustment (%)"
description="Tune the pitch of the speech"
:min="-100" :max="100" :step="0.1"
/>
<FieldRange
v-model="rate"
label="Speech Rate"
description="Adjust the speed of the speech"
:min="0.5" :max="2" :step="0.01"
/>
<!-- SSML Support -->
<FieldCheckbox
v-model="ssmlEnabled"
label="Enable SSML"
description="Enable Speech Synthesis Markup Language for more control over speech output"
/>
</div>
</div>
@@ -287,14 +241,16 @@ function updateSSMLExample() {
</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
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
</label>
<input
v-model="voiceId"
type="text"
v-model="voiceId" type="text"
class="w-full border border-neutral-300 rounded bg-white px-3 py-2 dark:border-neutral-700 dark:bg-neutral-900"
placeholder="Enter voice name (e.g., 'Rachel', 'Josh')"
@input="(event) => updateVoiceName((event.target as HTMLInputElement).value)"
@@ -325,74 +281,35 @@ function updateSSMLExample() {
</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 flex="~ col gap-4">
<FieldRange
v-model="pitch"
label="Pitch Adjustment (%)"
description="Tune the pitch of the speech"
:min="-100" :max="100" :step="0.1"
/>
<FieldRange
v-model="rate"
label="Speech Rate"
description="Adjust the speed of the speech"
:min="0.5" :max="2" :step="0.01"
/>
<!-- SSML Support -->
<FieldCheckbox
v-model="ssmlEnabled"
label="Enable SSML"
description="Enable Speech Synthesis Markup Language for more control over speech output"
/>
</div>
</div>
</div>
</div>
</div>
<div text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
<div
text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10
translate-y-10
>
<div text="40" i-lucide:volume-2 />
</div>
</template>
@@ -25,9 +25,9 @@ const { allProvidersMetadata } = storeToRefs(providersStore)
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">{{ t('settings.title') }}</span>
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
</div>
<div text-3xl font-semibold>
<div text-nowrap text-3xl font-semibold>
{{ t('settings.pages.providers.title') }}
</div>
</h1>
@@ -151,9 +151,9 @@ function isColorSelected(hexColor: string): boolean {
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">{{ t('settings.title') }}</span>
<span text="neutral-300 dark:neutral-500" text-nowrap>{{ t('settings.title') }}</span>
</div>
<div text-3xl font-semibold>
<div text-nowrap text-3xl font-semibold>
{{ t('settings.pages.themes.title') }}
</div>
</h1>