fix(stage-web|stage-tamagotchi): speech
This commit is contained in:
@@ -47,18 +47,22 @@ settings:
|
||||
chinese: 简体中文
|
||||
english: English
|
||||
title: Language
|
||||
live2d:
|
||||
change-model:
|
||||
from-file: Load from File
|
||||
from-file-select: Select
|
||||
from-url: Load from URL
|
||||
from-url-confirm: Load
|
||||
from-url-placeholder: Enter Live2D model URL
|
||||
title: Change Model
|
||||
edit-motion-map:
|
||||
title: Edit motion map
|
||||
map-motions:
|
||||
play: Play Motion
|
||||
title: Map Motions
|
||||
title: Live2D Settings
|
||||
microphone: Microphone
|
||||
models: Model
|
||||
quit: Quit
|
||||
viewer: Viewer
|
||||
shortcuts:
|
||||
title: Shortcuts
|
||||
window:
|
||||
move: Move the window
|
||||
resize: Resize the window
|
||||
debug: Toggle developer tools
|
||||
press_keys: Press keys...
|
||||
other: Other
|
||||
pages:
|
||||
models:
|
||||
description: Live2D, VRM, etc.
|
||||
@@ -69,10 +73,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...
|
||||
@@ -89,6 +92,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:
|
||||
@@ -111,13 +115,49 @@ settings:
|
||||
title: Discord
|
||||
speech:
|
||||
description: Speech synthesis
|
||||
sections:
|
||||
section:
|
||||
playground:
|
||||
buttons:
|
||||
stop:
|
||||
label: Stop
|
||||
select-voice:
|
||||
required: Please select a voice
|
||||
provider-voice-selection:
|
||||
custom_model_placeholder: Enter custom model name...
|
||||
custom_voice_placeholder: Enter custom voice ID...
|
||||
description: Select the suitable speech provider
|
||||
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: Provider
|
||||
voice-settings:
|
||||
input-ssml:
|
||||
placeholder: Enter SSML text...
|
||||
use-ssml:
|
||||
description: Enable to input raw SSML instead of plain text
|
||||
label: Use Custom SSML
|
||||
title: Speech
|
||||
title: Modules
|
||||
vision:
|
||||
description: Vision
|
||||
title: Vision
|
||||
x:
|
||||
description: X / Twitter browsing and usage
|
||||
title: X / Twitter
|
||||
providers:
|
||||
common:
|
||||
fields:
|
||||
field:
|
||||
api-key:
|
||||
label: API Key Input
|
||||
section:
|
||||
advanced:
|
||||
fields:
|
||||
@@ -148,6 +188,20 @@ settings:
|
||||
api-key:
|
||||
placeholder: Input Cloudflare Account ID
|
||||
title: Cloudflare Workers AI
|
||||
common:
|
||||
fields:
|
||||
field:
|
||||
pitch:
|
||||
description: >-
|
||||
Tune the pitch of synthesized speech (e.g., sharper or
|
||||
coarser)
|
||||
label: Pitch
|
||||
speed:
|
||||
description: Adjust the speed of speech
|
||||
label: Speed
|
||||
volume:
|
||||
description: Adjust the volume of speech
|
||||
label: Volume
|
||||
deepseek:
|
||||
description: deepseek.com
|
||||
title: DeepSeek
|
||||
@@ -193,6 +247,12 @@ settings:
|
||||
fireworks:
|
||||
description: fireworks.ai
|
||||
title: Fireworks.ai
|
||||
microsoft-speech:
|
||||
fields:
|
||||
field:
|
||||
region:
|
||||
description: Speech Service region
|
||||
label: Region
|
||||
mistral:
|
||||
description: mistral.ai
|
||||
title: Mistral
|
||||
@@ -246,10 +306,10 @@ settings:
|
||||
description: Traditional Japanese color palette
|
||||
title: Japanese Colors
|
||||
monet:
|
||||
description: Impressionist palette inspired by Claude Monet\'s works
|
||||
description: Impressionist palette inspired by Claude Monet's works
|
||||
title: Monet Colors
|
||||
morandi:
|
||||
description: Soft, muted tones inspired by Giorgio Morandi\'s paintings
|
||||
description: Soft, muted tones inspired by Giorgio Morandi's paintings
|
||||
title: Morandi Colors
|
||||
nordic:
|
||||
description: Scandinavian minimalist color scheme
|
||||
|
||||
@@ -35,17 +35,20 @@ settings:
|
||||
chinese: 简体中文
|
||||
english: English
|
||||
title: 语言
|
||||
live2d:
|
||||
change-model:
|
||||
from-file: 从文件加载
|
||||
from-file-select: 选择
|
||||
from-url: 从 URL 加载
|
||||
from-url-confirm: 加载
|
||||
from-url-placeholder: 输入 Live2D 模型 URL
|
||||
title: 更换模型
|
||||
map-motions:
|
||||
play: 播放动作
|
||||
title: 映射动作
|
||||
title: Live2D 设置
|
||||
microphone: 麦克风
|
||||
models: 模型
|
||||
quit: 退出
|
||||
viewer: 查看器
|
||||
shortcuts:
|
||||
title: 快捷键
|
||||
window:
|
||||
move: 移动窗口
|
||||
resize: 调整窗口大小
|
||||
debug: 切换开发者模式
|
||||
press_keys: 按下快捷键...
|
||||
other: 其他
|
||||
pages:
|
||||
models:
|
||||
description: 切换角色的 Live2D,VRM 模型
|
||||
@@ -53,14 +56,12 @@ settings:
|
||||
modules:
|
||||
consciousness:
|
||||
description: 人格设定、使用的模型等配置
|
||||
title: 意识
|
||||
sections:
|
||||
section:
|
||||
provider-model-selection:
|
||||
title: 模型
|
||||
description: 为意识选择合适的 LLM 提供商
|
||||
collapse: 折叠
|
||||
custom_model_placeholder: 输入自订模型名字
|
||||
description: 为意识选择合适的 LLM 提供商
|
||||
error: 获取出错啦
|
||||
expand: 展开
|
||||
loading: 获取模型中...
|
||||
@@ -77,6 +78,8 @@ settings:
|
||||
show_less: 显示更多
|
||||
show_more: 收起
|
||||
subtitle: 选择一个模型
|
||||
title: 模型
|
||||
title: 意识
|
||||
description: 思维,视觉,言语综合,游戏等
|
||||
gaming-factorio:
|
||||
description: 一起玩 Factorio!
|
||||
@@ -98,19 +101,57 @@ settings:
|
||||
title: Discord
|
||||
speech:
|
||||
description: 语音合成
|
||||
sections:
|
||||
section:
|
||||
playground:
|
||||
buttons:
|
||||
stop:
|
||||
label: 停止
|
||||
select-voice:
|
||||
required: 请选择声线
|
||||
provider-voice-selection:
|
||||
custom_model_placeholder: 输入指定模型名字...
|
||||
custom_voice_placeholder: 输入指定声线 ID...
|
||||
description: 选择合适的语音提供商
|
||||
no_models: 没有任何可用的模型
|
||||
no_models_description: 该提供商的模型列表没有返回任何的数据
|
||||
no_voices: 没有任何可用的声线
|
||||
no_voices_description: 该提供商的声线列表没有返回任何的数据
|
||||
pause: Pause
|
||||
play_sample: Play
|
||||
search_models_placeholder: 搜索模型...
|
||||
search_models_results: 找到 {count} / {total} 个模型
|
||||
search_voices_placeholder: 搜索声线...
|
||||
search_voices_results: 找到 {count} / {total} 个声线
|
||||
show_less: 显示更少
|
||||
show_more: 显示更多
|
||||
title: 选择语音合成提供商
|
||||
voice-settings:
|
||||
input-ssml:
|
||||
placeholder: 请输入 SSML 文本
|
||||
use-ssml:
|
||||
description: 启用后可以输入原始 SSML 而不是纯文本
|
||||
label: 选用自定义 SSML
|
||||
title: 发声
|
||||
title: 机体模块
|
||||
vision:
|
||||
description: 视觉
|
||||
title: 视觉
|
||||
x:
|
||||
description: X / Twitter 的浏览和使用
|
||||
title: X / Twitter
|
||||
providers:
|
||||
common:
|
||||
fields:
|
||||
field:
|
||||
api-key:
|
||||
label: API 密钥
|
||||
section:
|
||||
advanced:
|
||||
fields:
|
||||
field:
|
||||
headers:
|
||||
description: 添加自定义HTTP标头
|
||||
description: 添加自定义 HTTP 标头
|
||||
key:
|
||||
placeholder: 请求头名字
|
||||
label: HTTP 请求头
|
||||
@@ -129,12 +170,24 @@ settings:
|
||||
fields:
|
||||
field:
|
||||
account-id:
|
||||
description: Cloudflare帐户ID
|
||||
description: Cloudflare 帐户 ID
|
||||
label: Cloudflare Account ID
|
||||
placeholder: 请输入 Cloudflare Account ID
|
||||
api-key:
|
||||
placeholder: 请输入 Cloudflare Workers AI 的 API Key
|
||||
title: Workers AI
|
||||
common:
|
||||
fields:
|
||||
field:
|
||||
pitch:
|
||||
description: 调节合成语音的音高(比如更尖锐或更粗旷)
|
||||
label: 音高
|
||||
speed:
|
||||
description: 调节说话的语速
|
||||
label: 语速
|
||||
volume:
|
||||
description: 调节语音的响度
|
||||
label: 响度
|
||||
deepseek:
|
||||
description: DeepSeek.com
|
||||
title: 深度求索 DeepSeek
|
||||
@@ -180,6 +233,12 @@ settings:
|
||||
fireworks:
|
||||
description: Fireworks.ai
|
||||
title: Fireworks.ai
|
||||
microsoft-speech:
|
||||
fields:
|
||||
field:
|
||||
region:
|
||||
description: 服务 Endpoint 地区(比如亚太 eastasia 区域)
|
||||
label: Endpoint 地区
|
||||
mistral:
|
||||
description: mistral.ai
|
||||
title: Mistral
|
||||
@@ -231,7 +290,7 @@ settings:
|
||||
monet:
|
||||
title: 莫奈颜色
|
||||
morandi:
|
||||
title: Morandi颜色
|
||||
title: Morandi 颜色
|
||||
nordic:
|
||||
title: 北欧颜色
|
||||
title: 主题预设
|
||||
|
||||
@@ -29,6 +29,7 @@ const {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
activeSpeechVoice,
|
||||
activeSpeechVoiceId,
|
||||
pitch,
|
||||
isLoadingSpeechProviderVoices,
|
||||
supportsModelListing,
|
||||
@@ -43,7 +44,6 @@ const {
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const voiceId = ref('')
|
||||
const voiceSearchQuery = ref('')
|
||||
const useSSML = ref(false)
|
||||
const testText = ref('Hello, my name is AI Assistant')
|
||||
@@ -100,7 +100,7 @@ async function generateTestSpeech() {
|
||||
|
||||
const input = useSSML.value
|
||||
? ssmlText.value
|
||||
: speechStore.generateSSML(testText.value, activeSpeechVoice.value)
|
||||
: speechStore.generateSSML(testText.value, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value })
|
||||
|
||||
const response = await generateSpeech({
|
||||
...provider.speech(activeSpeechModel.value, providerConfig),
|
||||
@@ -148,10 +148,6 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function handleVoiceSelection(value: string) {
|
||||
activeSpeechVoice.value = availableVoices.value[activeSpeechProvider.value].find(voice => voice.id === value)
|
||||
}
|
||||
|
||||
function updateCustomVoiceName(value: string) {
|
||||
activeSpeechVoice.value = {
|
||||
id: value,
|
||||
@@ -167,16 +163,6 @@ function updateCustomVoiceName(value: string) {
|
||||
function updateCustomModelName(value: string) {
|
||||
activeSpeechModel.value = value
|
||||
}
|
||||
|
||||
watch(voiceId, (newVoice) => {
|
||||
const foundVoice = availableVoices.value[activeSpeechProvider.value].find(voice => voice.id === newVoice)
|
||||
if (foundVoice) {
|
||||
activeSpeechVoice.value = foundVoice
|
||||
}
|
||||
else {
|
||||
updateCustomVoiceName(newVoice)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -353,6 +339,7 @@ watch(voiceId, (newVoice) => {
|
||||
>
|
||||
<VoiceCardManySelect
|
||||
v-model:search-query="voiceSearchQuery"
|
||||
v-model:voice-id="activeSpeechVoiceId"
|
||||
:voices="availableVoices[activeSpeechProvider]?.map(voice => ({
|
||||
id: voice.id,
|
||||
name: voice.name,
|
||||
@@ -360,7 +347,6 @@ watch(voiceId, (newVoice) => {
|
||||
previewURL: voice.previewURL,
|
||||
customizable: false,
|
||||
}))"
|
||||
:selected-voice-id="activeSpeechVoice?.id"
|
||||
: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')"
|
||||
@@ -371,7 +357,6 @@ watch(voiceId, (newVoice) => {
|
||||
: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>
|
||||
@@ -423,7 +408,7 @@ watch(voiceId, (newVoice) => {
|
||||
class="mt-2 space-y-6"
|
||||
>
|
||||
<FieldInput
|
||||
v-model="voiceId"
|
||||
v-model="activeSpeechVoiceId"
|
||||
type="text"
|
||||
label="Voice ID"
|
||||
description="Enter the voice ID for your custom voice"
|
||||
|
||||
+9
-1
@@ -10,7 +10,7 @@ import {
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -29,6 +29,10 @@ const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
|
||||
const pitch = ref(0)
|
||||
const speed = ref(1.0)
|
||||
const volume = ref(0)
|
||||
|
||||
// Additional settings specific to Microsoft Speech (region)
|
||||
const region = computed({
|
||||
get: () => providers.value[providerId]?.region as string | undefined || 'eastasia',
|
||||
@@ -80,6 +84,7 @@ async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boo
|
||||
const ssml = speechStore.generateSSML(
|
||||
input,
|
||||
voice,
|
||||
{ ...providerConfig, pitch: pitch.value },
|
||||
)
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
@@ -123,6 +128,9 @@ async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boo
|
||||
<!-- Voice settings specific to Microsoft Speech -->
|
||||
<template #voice-settings="{ voiceSettings, updateVoiceSettings }">
|
||||
<SpeechVoiceSettings
|
||||
v-model:pitch="pitch"
|
||||
v-model:speed="speed"
|
||||
v-model:volume="volume"
|
||||
:settings="voiceSettings"
|
||||
:show-pitch="true"
|
||||
:show-speed="true"
|
||||
|
||||
@@ -29,6 +29,7 @@ const {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
activeSpeechVoice,
|
||||
activeSpeechVoiceId,
|
||||
pitch,
|
||||
isLoadingSpeechProviderVoices,
|
||||
supportsModelListing,
|
||||
@@ -43,7 +44,6 @@ const {
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const voiceId = ref('')
|
||||
const voiceSearchQuery = ref('')
|
||||
const useSSML = ref(false)
|
||||
const testText = ref('Hello, my name is AI Assistant')
|
||||
@@ -100,7 +100,7 @@ async function generateTestSpeech() {
|
||||
|
||||
const input = useSSML.value
|
||||
? ssmlText.value
|
||||
: speechStore.generateSSML(testText.value, activeSpeechVoice.value)
|
||||
: speechStore.generateSSML(testText.value, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value })
|
||||
|
||||
const response = await generateSpeech({
|
||||
...provider.speech(activeSpeechModel.value, providerConfig),
|
||||
@@ -148,10 +148,6 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
function handleVoiceSelection(value: string) {
|
||||
activeSpeechVoice.value = availableVoices.value[activeSpeechProvider.value].find(voice => voice.id === value)
|
||||
}
|
||||
|
||||
function updateCustomVoiceName(value: string) {
|
||||
activeSpeechVoice.value = {
|
||||
id: value,
|
||||
@@ -167,16 +163,6 @@ function updateCustomVoiceName(value: string) {
|
||||
function updateCustomModelName(value: string) {
|
||||
activeSpeechModel.value = value
|
||||
}
|
||||
|
||||
watch(voiceId, (newVoice) => {
|
||||
const foundVoice = availableVoices.value[activeSpeechProvider.value].find(voice => voice.id === newVoice)
|
||||
if (foundVoice) {
|
||||
activeSpeechVoice.value = foundVoice
|
||||
}
|
||||
else {
|
||||
updateCustomVoiceName(newVoice)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -353,6 +339,7 @@ watch(voiceId, (newVoice) => {
|
||||
>
|
||||
<VoiceCardManySelect
|
||||
v-model:search-query="voiceSearchQuery"
|
||||
v-model:voice-id="activeSpeechVoiceId"
|
||||
:voices="availableVoices[activeSpeechProvider]?.map(voice => ({
|
||||
id: voice.id,
|
||||
name: voice.name,
|
||||
@@ -360,7 +347,6 @@ watch(voiceId, (newVoice) => {
|
||||
previewURL: voice.previewURL,
|
||||
customizable: false,
|
||||
}))"
|
||||
:selected-voice-id="activeSpeechVoice?.id"
|
||||
: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')"
|
||||
@@ -371,7 +357,6 @@ watch(voiceId, (newVoice) => {
|
||||
: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>
|
||||
@@ -423,7 +408,7 @@ watch(voiceId, (newVoice) => {
|
||||
class="mt-2 space-y-6"
|
||||
>
|
||||
<FieldInput
|
||||
v-model="voiceId"
|
||||
v-model="activeSpeechVoiceId"
|
||||
type="text"
|
||||
label="Voice ID"
|
||||
description="Enter the voice ID for your custom voice"
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -29,6 +29,10 @@ const speechStore = useSpeechStore()
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers } = storeToRefs(providersStore)
|
||||
|
||||
const pitch = ref(0)
|
||||
const speed = ref(1.0)
|
||||
const volume = ref(0)
|
||||
|
||||
// Additional settings specific to Microsoft Speech (region)
|
||||
const region = computed({
|
||||
get: () => providers.value[providerId]?.region as string | undefined || 'eastasia',
|
||||
@@ -80,6 +84,7 @@ async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boo
|
||||
const ssml = speechStore.generateSSML(
|
||||
input,
|
||||
voice,
|
||||
{ ...providerConfig, pitch: pitch.value },
|
||||
)
|
||||
return await speechStore.speech(
|
||||
provider,
|
||||
@@ -123,6 +128,9 @@ async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boo
|
||||
<!-- Voice settings specific to Microsoft Speech -->
|
||||
<template #voice-settings="{ voiceSettings, updateVoiceSettings }">
|
||||
<SpeechVoiceSettings
|
||||
v-model:pitch="pitch"
|
||||
v-model:speed="speed"
|
||||
v-model:volume="volume"
|
||||
:settings="voiceSettings"
|
||||
:show-pitch="true"
|
||||
:show-speed="true"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import AudioSpectrum from '../Gadgets/AudioSpectrum.vue'
|
||||
import AudioSpectrumVisualizer from '../Gadgets/AudioSpectrumVisualizer.vue'
|
||||
import TransitionVertical from '../TransitionVertical.vue'
|
||||
@@ -30,8 +28,8 @@ interface Voice {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
voice: Voice
|
||||
selectedVoiceId?: string
|
||||
currentlyPlayingId?: string
|
||||
customInputPlaceholder?: string
|
||||
showVisualizer?: boolean
|
||||
@@ -45,15 +43,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select': [voiceId: string]
|
||||
'togglePlayback': [voice: Voice]
|
||||
'update:modelValue': [value: string]
|
||||
togglePlayback: [voice: Voice]
|
||||
}>()
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => '',
|
||||
set: value => emit('update:modelValue', value),
|
||||
})
|
||||
const voiceId = defineModel<string>('voice-id', { required: false, default: '' })
|
||||
const customVoiceName = defineModel<string>('custom-voice-name', { required: false, default: '' })
|
||||
|
||||
// Get preview URL from either field
|
||||
function getPreviewUrl(voice: Voice): string | undefined {
|
||||
@@ -88,36 +82,42 @@ function formatVoiceAttributes(voice: Voice): string[] {
|
||||
return attributes
|
||||
}
|
||||
|
||||
function selectVoice() {
|
||||
emit('select', props.voice.id)
|
||||
}
|
||||
|
||||
function togglePlayback() {
|
||||
emit('togglePlayback', props.voice)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="scroll-snap-align-start voice-card relative flex flex-col overflow-hidden rounded-xl"
|
||||
<label
|
||||
border="2px solid"
|
||||
class="scroll-snap-align-start form_voice-card relative flex flex-col overflow-hidden rounded-xl"
|
||||
transition="all duration-200 ease-in-out"
|
||||
border="2 solid"
|
||||
:class="[
|
||||
selectedVoiceId === voice.id
|
||||
voiceId === props.voice.id
|
||||
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-100 dark:border-primary-900 hover:border-primary-500/30 dark:hover:border-primary-400/30'
|
||||
: 'bg-white dark:bg-neutral-900/20 border-neutral-100 dark:border-neutral-700 hover:border-primary-500/30 dark:hover:border-primary-400/30',
|
||||
voice.deprecated ? 'opacity-60' : '',
|
||||
voiceId === props.voice.id
|
||||
? 'form_voice-card-active'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<!-- Voice info section -->
|
||||
<div class="p-3">
|
||||
<div class="flex items-start">
|
||||
<!-- Radio button -->
|
||||
<div class="relative mr-3 mt-0.5 flex-shrink-0" @click="selectVoice">
|
||||
<input
|
||||
v-model="voiceId"
|
||||
:checked="voiceId === props.voice.id"
|
||||
type="radio"
|
||||
:name="name"
|
||||
:value="props.voice.id"
|
||||
class="absolute cursor-pointer opacity-0"
|
||||
>
|
||||
<div class="relative mr-3 mt-0.5 flex-shrink-0">
|
||||
<div
|
||||
class="size-5 cursor-pointer border-2 rounded-full transition-colors duration-200"
|
||||
class="size-5 border-2 rounded-full transition-colors duration-200"
|
||||
:class="[
|
||||
selectedVoiceId === voice.id
|
||||
voiceId === props.voice.id
|
||||
? 'border-primary-500 dark:border-primary-400'
|
||||
: 'border-neutral-300 dark:border-neutral-600',
|
||||
]"
|
||||
@@ -125,7 +125,7 @@ function togglePlayback() {
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 size-3 rounded-full transition-opacity duration-200 -translate-x-1/2 -translate-y-1/2"
|
||||
:class="[
|
||||
selectedVoiceId === voice.id
|
||||
voiceId === props.voice.id
|
||||
? 'opacity-100 bg-primary-500 dark:bg-primary-400'
|
||||
: 'opacity-0',
|
||||
]"
|
||||
@@ -156,12 +156,12 @@ function togglePlayback() {
|
||||
</div>
|
||||
|
||||
<!-- Voice info -->
|
||||
<div class="flex-1 cursor-pointer" @click="selectVoice">
|
||||
<div class="flex-1 cursor-pointer">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class="line-clamp-1 font-medium"
|
||||
:class="[
|
||||
selectedVoiceId === voice.id
|
||||
voiceId === voice.id
|
||||
? 'text-neutral-700 dark:text-neutral-300'
|
||||
: 'text-neutral-700 dark:text-neutral-400',
|
||||
]"
|
||||
@@ -177,7 +177,7 @@ function togglePlayback() {
|
||||
:key="attrIndex"
|
||||
class="inline-flex items-center rounded-md px-1.5 py-0.5 text-xs"
|
||||
:class="[
|
||||
selectedVoiceId === voice.id
|
||||
voiceId === voice.id
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-400'
|
||||
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300',
|
||||
]"
|
||||
@@ -187,9 +187,9 @@ function togglePlayback() {
|
||||
</div>
|
||||
|
||||
<!-- Custom input field for selected voice -->
|
||||
<div v-if="voice.customizable && selectedVoiceId === voice.id" class="mt-3">
|
||||
<div v-if="voice.customizable && voiceId === voice.id" class="mt-3">
|
||||
<Input
|
||||
v-model="modelValue"
|
||||
v-model="customVoiceName"
|
||||
type="text"
|
||||
class="w-full border border-neutral-300 rounded bg-white px-2 py-1 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||
:placeholder="customInputPlaceholder"
|
||||
@@ -200,7 +200,7 @@ function togglePlayback() {
|
||||
<div
|
||||
v-if="voice.description" class="line-clamp-2 mt-1 text-xs"
|
||||
:class="[
|
||||
selectedVoiceId === voice.id
|
||||
voiceId === voice.id
|
||||
? 'text-neutral-600 dark:text-neutral-400'
|
||||
: 'text-neutral-500 dark:text-neutral-500',
|
||||
]"
|
||||
@@ -227,7 +227,7 @@ function togglePlayback() {
|
||||
>
|
||||
<AudioSpectrumVisualizer
|
||||
:frequencies="frequencies"
|
||||
:bars-class="selectedVoiceId === voice.id
|
||||
:bars-class="voiceId === voice.id
|
||||
? 'bg-primary-500 dark:bg-primary-400'
|
||||
: 'bg-neutral-400 dark:bg-neutral-600'"
|
||||
/>
|
||||
@@ -235,18 +235,18 @@ function togglePlayback() {
|
||||
</div>
|
||||
</TransitionVertical>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.voice-card {
|
||||
.form_voice-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.voice-card::before {
|
||||
.form_voice-card::before {
|
||||
pointer-events: none;
|
||||
--at-apply: 'bg-gradient-to-r from-primary-500/0 to-primary-500/0 dark:from-primary-400/0 dark:to-primary-400/0';
|
||||
content: '';
|
||||
@@ -260,7 +260,7 @@ function togglePlayback() {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.voice-card:hover::before {
|
||||
.form_voice-card:hover::before {
|
||||
--at-apply: 'bg-gradient-to-r from-primary-500/20 via-primary-500/10 to-transparent dark:from-primary-400/20 dark:via-primary-400/10 dark:to-transparent';
|
||||
width: 85%;
|
||||
opacity: 1;
|
||||
|
||||
@@ -28,7 +28,6 @@ interface Voice {
|
||||
|
||||
interface Props {
|
||||
voices: Voice[]
|
||||
selectedVoiceId?: string
|
||||
searchable?: boolean
|
||||
searchPlaceholder?: string
|
||||
searchNoResultsTitle?: string
|
||||
@@ -56,12 +55,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
showVisualizer: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:selectedVoiceId': [value: string]
|
||||
'update:searchQuery': [value: string]
|
||||
}>()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const isListExpanded = ref(false)
|
||||
const currentlyPlayingId = ref<string>()
|
||||
const audioElements = ref<Map<string, HTMLAudioElement>>(new Map())
|
||||
@@ -80,10 +73,8 @@ function initAudioContext() {
|
||||
return sharedAudioContext.value
|
||||
}
|
||||
|
||||
// Watch for search query changes and emit the value
|
||||
watch(searchQuery, (value) => {
|
||||
emit('update:searchQuery', value)
|
||||
})
|
||||
const searchQuery = defineModel<string>('search-query', { required: false, default: '' })
|
||||
const voiceId = defineModel<string>('voice-id', { required: false, default: '' })
|
||||
|
||||
// Filter voices based on search query
|
||||
const filteredVoices = computed(() => {
|
||||
@@ -251,10 +242,6 @@ function togglePlayback(voice: Voice) {
|
||||
}
|
||||
}
|
||||
|
||||
function selectVoice(voiceId: string) {
|
||||
emit('update:selectedVoiceId', voiceId)
|
||||
}
|
||||
|
||||
// Clean up audio elements when component is unmounted
|
||||
function cleanup() {
|
||||
audioElements.value.forEach((audio) => {
|
||||
@@ -350,7 +337,7 @@ const customVoiceName = ref('')
|
||||
<div class="relative">
|
||||
<!-- Horizontally scrollable container -->
|
||||
<div
|
||||
class="scrollbar-hide grid auto-cols-[350px] grid-flow-col max-h-[calc(100dvh-7lh)] gap-4 overflow-x-auto pb-4"
|
||||
class="scrollbar-hide grid auto-cols-[350px] grid-flow-col max-h-[calc(100dvh-7lh)] gap-4 overflow-x-auto"
|
||||
:class="[
|
||||
isListExpanded ? 'grid-cols-1 md:grid-cols-2 grid-flow-row auto-cols-auto' : '',
|
||||
]"
|
||||
@@ -361,15 +348,15 @@ const customVoiceName = ref('')
|
||||
<VoiceCard
|
||||
v-for="voice in filteredVoices"
|
||||
:key="voice.id"
|
||||
v-model:voice-id="voiceId"
|
||||
v-model:custom-voice-name="customVoiceName"
|
||||
name="voice"
|
||||
:voice="voice"
|
||||
:selected-voice-id="selectedVoiceId"
|
||||
:currently-playing-id="currentlyPlayingId"
|
||||
:custom-input-placeholder="customInputPlaceholder"
|
||||
:show-visualizer="showVisualizer"
|
||||
:audio-stream="audioStreams.get(voice.id)"
|
||||
@select="selectVoice"
|
||||
@toggle-playback="togglePlayback"
|
||||
@update:model-value="customVoiceName = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,16 +15,15 @@ defineProps<{
|
||||
showSpeakerBoost?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [key: string, value: any]
|
||||
}>()
|
||||
const pitch = defineModel<number>('pitch', { required: false, default: 0 })
|
||||
const speed = defineModel<number>('speed', { required: false, default: 1.0 })
|
||||
const volume = defineModel<number>('volume', { required: false, default: 0 })
|
||||
const style = defineModel<number>('style', { required: false, default: 0 })
|
||||
const stability = defineModel<number>('stability', { required: false, default: 0.5 })
|
||||
const similarityBoost = defineModel<number>('similarityBoost', { required: false, default: 0.75 })
|
||||
const useSpeakerBoost = defineModel<boolean>('useSpeakerBoost', { required: false, default: false })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Define a function to update settings
|
||||
function updateSetting(key: string, value: any) {
|
||||
emit('update', key, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -32,72 +31,69 @@ function updateSetting(key: string, value: any) {
|
||||
<!-- Pitch control - common to most providers -->
|
||||
<FieldRange
|
||||
v-if="showPitch"
|
||||
:model-value="settings.pitch ?? 0"
|
||||
v-model="pitch"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.pitch.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.pitch.description')"
|
||||
:min="-100"
|
||||
:max="100" :step="1" :format-value="value => `${value}%`"
|
||||
@update:model-value="value => updateSetting('pitch', value)"
|
||||
/>
|
||||
|
||||
<!-- Speed control - common to most providers -->
|
||||
<FieldRange
|
||||
v-if="showSpeed"
|
||||
:model-value="settings.speed ?? 1.0"
|
||||
v-model="speed"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.speed.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.speed.description')"
|
||||
:min="0.5"
|
||||
:max="2.0" :step="0.01" @update:model-value="value => updateSetting('speed', value)"
|
||||
:max="2.0" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Volume control - available in some providers -->
|
||||
<FieldRange
|
||||
v-if="showVolume"
|
||||
:model-value="settings.volume ?? 0"
|
||||
v-model="volume"
|
||||
:label="t('settings.pages.providers.provider.common.fields.field.volume.label')"
|
||||
:description="t('settings.pages.providers.provider.common.fields.field.volume.description')"
|
||||
:min="-100"
|
||||
:max="100" :step="1" :format-value="value => `${value}%`"
|
||||
@update:model-value="value => updateSetting('volume', value)"
|
||||
/>
|
||||
|
||||
<!-- Style control - specific to ElevenLabs -->
|
||||
<FieldRange
|
||||
v-if="showStyle"
|
||||
:model-value="settings.style ?? 0"
|
||||
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" @update:model-value="value => updateSetting('style', value)"
|
||||
:max="1" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Stability control - specific to ElevenLabs -->
|
||||
<FieldRange
|
||||
v-if="showStability"
|
||||
:model-value="settings.stability ?? 0.5"
|
||||
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" @update:model-value="value => updateSetting('stability', value)"
|
||||
:max="1" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Similarity Boost control - specific to ElevenLabs -->
|
||||
<FieldRange
|
||||
v-if="showSimilarityBoost"
|
||||
:model-value="settings.similarityBoost ?? 0.75"
|
||||
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" @update:model-value="value => updateSetting('similarityBoost', value)"
|
||||
:max="1" :step="0.01"
|
||||
/>
|
||||
|
||||
<!-- Speaker Boost checkbox - specific to ElevenLabs -->
|
||||
<FieldCheckbox
|
||||
v-if="showSpeakerBoost"
|
||||
:model-value="settings.useSpeakerBoost !== false"
|
||||
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')"
|
||||
@update:model-value="value => updateSetting('useSpeakerBoost', value)"
|
||||
/>
|
||||
|
||||
<!-- Slot for additional provider-specific controls -->
|
||||
|
||||
@@ -72,7 +72,7 @@ const audioQueue = useQueue<{ audioBuffer: AudioBuffer, text: string }>({
|
||||
})
|
||||
|
||||
const speechStore = useSpeechStore()
|
||||
const { ssmlEnabled, activeSpeechProvider, activeSpeechModel, activeSpeechVoice } = storeToRefs(speechStore)
|
||||
const { ssmlEnabled, activeSpeechProvider, activeSpeechModel, activeSpeechVoice, pitch } = storeToRefs(speechStore)
|
||||
|
||||
async function handleSpeechGeneration(ctx: { data: string }) {
|
||||
try {
|
||||
@@ -96,7 +96,7 @@ async function handleSpeechGeneration(ctx: { data: string }) {
|
||||
const providerConfig = providersStore.getProviderConfig(activeSpeechProvider.value)
|
||||
|
||||
const input = ssmlEnabled.value
|
||||
? speechStore.generateSSML(ctx.data, activeSpeechVoice.value)
|
||||
? speechStore.generateSSML(ctx.data, activeSpeechVoice.value, { ...providerConfig, pitch: pitch.value })
|
||||
: ctx.data
|
||||
|
||||
const res = await generateSpeech({
|
||||
|
||||
@@ -134,15 +134,27 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (activeSpeechVoiceId.value) {
|
||||
activeSpeechVoice.value = availableVoices.value[activeSpeechProvider.value]?.find(voice => voice.id === activeSpeechVoiceId.value)
|
||||
}
|
||||
loadVoicesForProvider(activeSpeechProvider.value).then(() => {
|
||||
if (activeSpeechVoiceId.value) {
|
||||
activeSpeechVoice.value = availableVoices.value[activeSpeechProvider.value]?.find(voice => voice.id === activeSpeechVoiceId.value)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(activeSpeechVoice, (voice) => {
|
||||
if (voice) {
|
||||
activeSpeechVoiceId.value = voice.id
|
||||
watch(activeSpeechVoiceId, (voiceId) => {
|
||||
if (voiceId) {
|
||||
activeSpeechVoice.value = availableVoices.value[activeSpeechProvider.value]?.find(voice => voice.id === voiceId)
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(availableVoices, (voices) => {
|
||||
if (activeSpeechVoiceId.value) {
|
||||
activeSpeechVoice.value = voices[activeSpeechProvider.value]?.find(voice => voice.id === activeSpeechVoiceId.value)
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -176,19 +188,33 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
function generateSSML(
|
||||
text: string,
|
||||
voice: VoiceInfo,
|
||||
pitch?: number,
|
||||
speed?: number,
|
||||
volume?: number,
|
||||
providerConfig?: Record<string, any>,
|
||||
): string {
|
||||
const pitch = providerConfig?.pitch
|
||||
const speed = providerConfig?.speed
|
||||
const volume = providerConfig?.volume
|
||||
|
||||
const prosody = {
|
||||
pitch: pitch != null ? pitch > 0 ? `+${pitch}%` : `-${pitch}%` : undefined,
|
||||
rate: speed != null ? speed !== 1.0 ? `${speed}` : '1' : undefined,
|
||||
volume: volume != null ? volume > 0 ? `+${volume}%` : `${volume}%` : undefined,
|
||||
pitch: pitch != null
|
||||
? pitch > 0
|
||||
? `+${pitch}%`
|
||||
: `-${pitch}%`
|
||||
: undefined,
|
||||
rate: speed != null
|
||||
? speed !== 1.0
|
||||
? `${speed}`
|
||||
: '1'
|
||||
: undefined,
|
||||
volume: volume != null
|
||||
? volume > 0
|
||||
? `+${volume}%`
|
||||
: `${volume}%`
|
||||
: undefined,
|
||||
}
|
||||
|
||||
const ssmlXast = x('speak', { 'version': '1.0', 'xmlns': 'http://www.w3.org/2001/10/synthesis', 'xml:lang': voice.languages[0]?.code || 'en-US' }, [
|
||||
x('voice', { name: voice.id, gender: voice.gender || 'neutral' }, [
|
||||
Object.entries(prosody).filter(([_, value]) => value !== undefined).length > 0
|
||||
Object.entries(prosody).filter(([_, value]) => value != null).length > 0
|
||||
? x('prosody', {
|
||||
pitch: pitch != null ? pitch > 0 ? `+${pitch}%` : `-${pitch}%` : undefined,
|
||||
rate: speed != null ? speed !== 1.0 ? `${speed}` : '1' : undefined,
|
||||
@@ -208,6 +234,7 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
activeSpeechProvider,
|
||||
activeSpeechModel,
|
||||
activeSpeechVoice,
|
||||
activeSpeechVoiceId,
|
||||
pitch,
|
||||
rate,
|
||||
ssmlEnabled,
|
||||
|
||||
Reference in New Issue
Block a user