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>
+19 -2
View File
@@ -76,10 +76,9 @@ settings:
sections:
section:
provider-model-selection:
title: Model
description: Select the suitable LLM provider for consciousness
collapse: Collapse
custom_model_placeholder: Enter custom model name...
description: Select the suitable LLM provider for consciousness
error: Error loading models
expand: Expand
loading: Loading available models...
@@ -96,6 +95,7 @@ settings:
show_less: Show less
show_more: Show more
subtitle: Select a model from the provider
title: Model
title: Consciousness
description: Thinking, vision, speech synthesis, gaming, etc.
gaming-factorio:
@@ -118,6 +118,23 @@ settings:
title: Discord
speech:
description: Speech synthesis
sections:
section:
provider-voice-selection:
description: Select the suitable speech provider
title: Provider
custom_model_placeholder: Enter custom model name...
custom_voice_placeholder: Enter custom voice ID...
no_models: No models available
no_models_description: No models were found for this provider
no_voices: No voices available
no_voices_description: No voices were found for this provider
search_models_placeholder: Search models...
search_models_results: Found {count} of {total} models
search_voices_placeholder: Search voices...
search_voices_results: Found {count} of {total} voices
show_less: Show less
show_more: Show more
title: Speech
title: Modules
vision:
+17
View File
@@ -107,6 +107,23 @@ settings:
speech:
description: 语音合成
title: 发声
sections:
section:
provider-voice-selection:
description: 选择合适的语音提供商
title: 提供商
custom_model_placeholder: 输入指定模型名字...
custom_voice_placeholder: 输入指定声线 ID...
no_models: 没有任何可用的模型
no_models_description: 该提供商的模型列表没有返回任何的数据
no_voices: 没有任何可用的声线
no_voices_description: 该提供商的声线列表没有返回任何的数据
search_models_placeholder: 搜索模型...
search_models_results: 找到 {count} / {total} 个模型
search_voices_placeholder: 搜索声线...
search_voices_results: 找到 {count} / {total} 个声线
show_less: 显示更少
show_more: 显示更多
title: 机体模块
vision:
description: 视觉
@@ -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,26 @@ 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"
: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 +205,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 +242,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 +282,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>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import ProviderInput from './FieldInput.vue'
import FieldInput from './FieldInput.vue'
const textValue = ref('')
const passwordValue = ref('secret-password')
@@ -23,7 +23,7 @@ const emailValue = ref('user@example.com')
title="Text Input (Empty)"
>
<div>
<ProviderInput
<FieldInput
v-model="textValue"
label="Text Input"
description="A basic text input field"
@@ -37,7 +37,7 @@ const emailValue = ref('user@example.com')
title="Password Input"
>
<div>
<ProviderInput
<FieldInput
v-model="passwordValue"
label="Password"
description="A password input field"
@@ -52,7 +52,7 @@ const emailValue = ref('user@example.com')
title="Email Input"
>
<div>
<ProviderInput
<FieldInput
v-model="emailValue"
label="Email Address"
description="An email input field"
@@ -67,7 +67,7 @@ const emailValue = ref('user@example.com')
title="Required Input"
>
<div>
<ProviderInput
<FieldInput
v-model="textValue"
label="Required Field"
description="This field is required"
@@ -82,7 +82,7 @@ const emailValue = ref('user@example.com')
title="Optional Input"
>
<div>
<ProviderInput
<FieldInput
v-model="textValue"
label="Optional Field"
description="This field is optional"
@@ -39,11 +39,10 @@ watch([inputKey, inputValue], () => {
{{ props.description }}
</div>
</div>
<div flex="~ col gap-2">
<div v-auto-animate flex="~ col gap-2">
<div
v-for="(keyValue, index) in keyValues"
:key="index"
v-auto-animate
w-full flex items-center gap-2
>
<InputKeyValue
@@ -24,7 +24,7 @@ const modelValue = defineModel<number>({ required: true })
{{ description }}
</div>
</div>
<span font-mono>{{ props.formatValue?.(modelValue) }}</span>
<span font-mono>{{ props.formatValue?.(modelValue) || modelValue }}</span>
</div>
<div flex="~ row" items-center gap-2>
<Range
@@ -10,8 +10,8 @@ const modelValue = defineModel<string>({ required: true })
<input
v-model="modelValue"
:type="props.type || 'text'"
border="neutral-100 dark:neutral-800 solid 2 focus:neutral-200 dark:focus:neutral-700"
transition="all duration-250 ease-in-out"
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
transition="all duration-200 ease-in-out"
text="disabled:neutral-400 dark:disabled:neutral-600"
cursor="disabled:not-allowed"
w-full rounded-lg px-2 py-1 text-nowrap text-sm outline-none
@@ -138,7 +138,7 @@ function updateCustomInput(event: Event) {
<!-- Expand/collapse button for long descriptions -->
<button
v-if="showExpandCollapse && description.length > expandCollapseThreshold"
class="text-primary-500 dark:text-primary-400 mt-0.5 inline-flex items-center text-xs"
class="text-primary-500 dark:text-primary-600 mt-0.5 inline-flex items-center text-xs"
@click.prevent="toggleExpansion"
>
<span>{{ isExpanded ? 'Show less' : 'Show more' }}</span>
@@ -55,10 +55,6 @@ const filteredItems = computed(() => {
)
})
function clearSearch() {
searchQuery.value = ''
}
function updateCustomValue(value: string) {
customValue.value = value
emit('update:customValue', value)
@@ -68,7 +64,7 @@ function updateCustomValue(value: string) {
<template>
<div class="radio-card-detail-many-select">
<!-- Search bar -->
<div v-if="searchable" class="relative">
<div v-if="searchable" class="relative" inline-flex="~" w-full items-center>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<div i-solar:magnifer-line-duotone class="text-neutral-500 dark:text-neutral-400" />
</div>
@@ -76,23 +72,11 @@ function updateCustomValue(value: string) {
v-model="searchQuery"
type="search"
class="w-full rounded-xl p-2.5 pl-10 text-sm outline-none"
border="focus:primary-100 dark:focus:primary-900 ~ neutral-200 dark:neutral-800 2"
border="focus:primary-100 dark:focus:primary-400/50 2 solid neutral-200 dark:neutral-800"
transition="all duration-200 ease-in-out"
ring="focus:primary-500 dark:focus:primary-400 0 focus:2 focus:offset-0 focus:opacity-50"
bg="white dark:neutral-900"
:placeholder="searchPlaceholder"
>
<button
v-if="searchQuery"
type="button"
class="absolute inset-y-0 right-0 flex items-center pr-3"
@click="clearSearch"
>
<div
i-solar:close-circle-line-duotone
class="text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
/>
</button>
</div>
<!-- Items list with search results info -->
@@ -176,3 +160,9 @@ function updateCustomValue(value: string) {
</div>
</div>
</template>
<style scoped>
input[type='search']::-webkit-search-cancel-button {
display: none;
}
</style>
@@ -21,9 +21,9 @@ defineProps<{
</button>
<h1 relative>
<div absolute left-0 top-0 translate-y="[-80%]">
<span text="neutral-300 dark:neutral-500">Provider</span>
<span text="neutral-300 dark:neutral-500" text-nowrap>Provider</span>
</div>
<div text-3xl font-semibold>
<div text-nowrap text-3xl font-semibold>
{{ providerName }}
</div>
</h1>
@@ -1,4 +1,4 @@
import type { Voice } from '../fix/voice'
import type { VoiceInfo } from '../providers'
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
@@ -20,7 +20,7 @@ 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, Voice[]>>({})
const availableVoices = ref<Record<string, VoiceInfo[]>>({})
const selectedLanguage = useLocalStorage('settings/speech/language', 'en-US')
// Computed properties
@@ -77,7 +77,9 @@ export const useSpeechStore = defineStore('speech', () => {
speechProviderError.value = null
try {
return await providersStore.getProviderMetadata(provider).capabilities.listVoices?.(providersStore.getProviderConfig(provider)) || []
const voices = await providersStore.getProviderMetadata(provider).capabilities.listVoices?.(providersStore.getProviderConfig(provider)) || []
availableVoices.value[provider] = voices
return voices
}
catch (error) {
console.error(`Error fetching voices for ${provider}:`, error)
+4 -1
View File
@@ -623,7 +623,10 @@ export const useProvidersStore = defineStore('providers', () => {
// Transform and store the models
availableModels.value[providerId] = models.map(model => ({
id: model.id,
name: model.id,
name: model.name,
description: model.description,
contextLength: model.contextLength,
deprecated: model.deprecated,
provider: providerId,
}))