feat(stage-web|stage-tamagotchi): supports Alibaba Cloud Model Studio & Volcano Engine

This commit is contained in:
Neko Ayaka
2025-04-18 23:45:02 +08:00
parent d8c51e4b6b
commit d4a1e9f5f6
32 changed files with 1057 additions and 690 deletions
+11
View File
@@ -218,6 +218,9 @@ settings:
title: Voice Settings
description: LLMs, speech providers, etc.
provider:
alibaba-cloud-model-studio:
description: bailian.console.aliyun.com
title: Alibaba Cloud Model Studio
anthropic:
description: anthropic.com
title: Anthropic | Claude
@@ -326,6 +329,14 @@ settings:
vllm:
description: vllm.ai
title: vLLM
volcengine:
description: volcengine.com
fields:
field:
appId:
description: App ID of the project where you can obtain in Console
label: App ID
title: Volcano Engine
xai:
description: x.ai
title: xAI
+11
View File
@@ -207,6 +207,9 @@ settings:
title: 声音配置
description: LLM,语音合成,语音识别提供商等
provider:
alibaba-cloud-model-studio:
description: bailian.console.aliyun.com
title: 阿里百炼
anthropic:
description: anthropic.com
title: Anthropic | Claude
@@ -313,6 +316,14 @@ settings:
vllm:
description: vllm.ai
title: vLLM
volcengine:
description: volcengine.com
fields:
field:
appId:
description: 可在控制台获取的 App ID
label: App ID
title: 火山引擎
xai:
description: X.AI
title: xAI
@@ -0,0 +1,154 @@
<script setup lang="ts">
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import {
FieldRange,
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const providerId = 'alibaba-cloud-model-studio'
const defaultModel = 'cosyvoice-v1'
// Default voice settings specific to ElevenLabs
const defaultVoiceSettings = {
speed: 1.0,
}
const pitch = ref<number>(0)
const speed = ref<number>(1.0)
const volume = ref<number>(0)
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore)
const { t } = useI18n()
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
// Get available voices for ElevenLabs
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
if (!provider) {
throw new Error('Failed to initialize speech provider')
}
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
// Get model from configuration or use default
const model = providerConfig.model as string | undefined || defaultModel
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
model,
input,
voiceId,
{
...providerConfig,
...defaultVoiceSettings,
},
)
}
onMounted(async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
})
watch(pitch, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.pitch = pitch.value
})
watch(speed, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.speed = speed.value
})
watch(volume, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.volume = volume.value
})
watch(providers, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
}, {
immediate: true,
})
</script>
<template>
<SpeechProviderSettings
:provider-id="providerId"
:default-model="defaultModel"
:additional-settings="defaultVoiceSettings"
>
<!-- Voice settings specific to ElevenLabs -->
<template #voice-settings>
<div flex="~ col gap-4">
<!-- Pitch control - common to most providers -->
<FieldRange
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}%`"
/>
<!-- Speed control - common to most providers -->
<FieldRange
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"
/>
<!-- Volume control - available in some providers -->
<FieldRange
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}%`"
/>
</div>
</template>
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
/>
</template>
</SpeechProviderSettings>
</template>
@@ -3,13 +3,15 @@ import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import {
FieldCheckbox,
FieldRange,
SpeechPlayground,
SpeechProviderSettings,
SpeechVoiceSettings,
} 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, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const providerId = 'elevenlabs'
const defaultModel = 'eleven_multilingual_v2'
@@ -23,9 +25,18 @@ const defaultVoiceSettings = {
useSpeakerBoost: true,
}
const pitch = ref<number>(0)
const speed = ref<number>(1.0)
const volume = ref<number>(0)
const style = ref<number>(0)
const stability = ref<number>(0.5)
const similarityBoost = ref<number>(0.75)
const useSpeakerBoost = ref<boolean>(false)
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore)
const { t } = useI18n()
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
@@ -35,11 +46,6 @@ const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Get available languages
const availableLanguages = computed(() => {
return speechStore.availableLanguages
})
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
@@ -65,6 +71,65 @@ async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: bo
},
)
}
onMounted(async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
})
watch(pitch, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.pitch = pitch.value
})
watch(speed, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.speed = speed.value
})
watch(volume, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.volume = volume.value
})
watch(style, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.style = style.value
})
watch(stability, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.stability = stability.value
})
watch(similarityBoost, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.similarityBoost = similarityBoost.value
})
watch(useSpeakerBoost, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.useSpeakerBoost = useSpeakerBoost.value
})
watch(providers, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
}, {
immediate: true,
})
</script>
<template>
@@ -74,23 +139,75 @@ async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: bo
:additional-settings="defaultVoiceSettings"
>
<!-- Voice settings specific to ElevenLabs -->
<template #voice-settings="{ voiceSettings, updateVoiceSettings }">
<SpeechVoiceSettings
:settings="voiceSettings"
:show-similarity-boost="true"
:show-stability="true"
:show-speed="true"
:show-style="true"
:show-speaker-boost="true"
@update="updateVoiceSettings"
/>
<template #voice-settings>
<div flex="~ col gap-4">
<!-- Pitch control - common to most providers -->
<FieldRange
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}%`"
/>
<!-- Speed control - common to most providers -->
<FieldRange
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"
/>
<!-- Volume control - available in some providers -->
<FieldRange
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}%`"
/>
<!-- Style control - specific to ElevenLabs -->
<FieldRange
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"
/>
<!-- Stability control - specific to ElevenLabs -->
<FieldRange
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"
/>
<!-- Similarity Boost control - specific to ElevenLabs -->
<FieldRange
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"
/>
<!-- Speaker Boost checkbox - specific to ElevenLabs -->
<FieldCheckbox
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')"
/>
</div>
</template>
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:available-languages="availableLanguages"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
@@ -6,7 +6,6 @@ import {
FieldInput,
SpeechPlayground,
SpeechProviderSettings,
SpeechVoiceSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
@@ -52,11 +51,6 @@ const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Get available languages
const availableLanguages = computed(() => {
return speechStore.availableLanguages
})
// Generate speech with Microsoft-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnMicrosoftOptions>
@@ -143,7 +137,6 @@ async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boo
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:available-languages="availableLanguages"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the Microsoft Speech synthesis."
@@ -0,0 +1,144 @@
<script setup lang="ts">
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import {
FieldInput,
FieldRange,
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const providerId = 'volcengine'
const defaultModel = 'v1'
const speedRatio = ref<number>(1.0)
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore)
const { t } = useI18n()
// Additional settings specific to Volcengine (appId)
const appId = computed({
get: () => (providers.value[providerId]?.app as any)?.appId as string | undefined || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].app = {
appId: value,
}
},
})
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
// Get available voices for ElevenLabs
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
if (!provider) {
throw new Error('Failed to initialize speech provider')
}
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
// Get model from configuration or use default
const model = providerConfig.model as string | undefined || defaultModel
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
model,
input,
voiceId,
{
...providerConfig,
},
)
}
onMounted(async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
})
watch(speedRatio, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
if (!providerConfig.audio) {
providerConfig.audio = {}
}
(providerConfig.audio as any).speedRatio = speedRatio.value
})
watch([providers, appId], async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
}, {
immediate: true,
})
</script>
<template>
<SpeechProviderSettings
:provider-id="providerId"
:default-model="defaultModel"
>
<!-- Voice settings specific to ElevenLabs -->
<template #basic-settings>
<div flex="~ col gap-4">
<FieldInput
v-model="appId"
:label="t('settings.pages.providers.provider.volcengine.fields.field.appId.label')"
:description="t('settings.pages.providers.provider.volcengine.fields.field.appId.description')"
required
/>
</div>
</template>
<template #voice-settings>
<!-- Speed control - common to most providers -->
<FieldRange
v-model="speedRatio"
: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"
/>
</template>
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
/>
</template>
</SpeechProviderSettings>
</template>
+1 -1
View File
@@ -11,7 +11,7 @@
--airi-theme-primary-900: #8e0d3b;
--airi-theme-primary-950: #50011b;
--theme-colors-hue: 354.31;
--theme-colors-hue: 220.44;
--theme-colors-chroma: calc(0.18 + (cos(var(--theme-colors-hue) * 3.14159265 / 180) * 0.04));
--theme-colors-chroma-50: calc(var(--theme-colors-chroma) * 0.3);
--theme-colors-chroma-100: calc(var(--theme-colors-chroma) * 0.5);
+11
View File
@@ -227,6 +227,9 @@ settings:
title: Voice Settings
description: LLMs, speech providers, etc.
provider:
alibaba-cloud-model-studio:
description: bailian.console.aliyun.com
title: Alibaba Cloud Model Studio
anthropic:
description: anthropic.com
title: Anthropic | Claude
@@ -335,6 +338,14 @@ settings:
vllm:
description: vllm.ai
title: vLLM
volcengine:
description: volcengine.com
fields:
field:
appId:
description: App ID of the project where you can obtain in Console
label: App ID
title: Volcano Engine
xai:
description: x.ai
title: xAI
+11
View File
@@ -213,6 +213,9 @@ settings:
title: 声音配置
description: LLM,语音合成,语音识别提供商等
provider:
alibaba-cloud-model-studio:
description: bailian.console.aliyun.com
title: 阿里百炼
anthropic:
description: anthropic.com
title: Anthropic | Claude
@@ -319,6 +322,14 @@ settings:
vllm:
description: vllm.ai
title: vLLM
volcengine:
description: volcengine.com
fields:
field:
appId:
description: 可在控制台获取的 App ID
label: App ID
title: 火山引擎
xai:
description: X.AI
title: xAI
@@ -0,0 +1,154 @@
<script setup lang="ts">
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import {
FieldRange,
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const providerId = 'alibaba-cloud-model-studio'
const defaultModel = 'cosyvoice-v1'
// Default voice settings specific to ElevenLabs
const defaultVoiceSettings = {
speed: 1.0,
}
const pitch = ref<number>(0)
const speed = ref<number>(1.0)
const volume = ref<number>(0)
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore)
const { t } = useI18n()
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
// Get available voices for ElevenLabs
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
if (!provider) {
throw new Error('Failed to initialize speech provider')
}
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
// Get model from configuration or use default
const model = providerConfig.model as string | undefined || defaultModel
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
model,
input,
voiceId,
{
...providerConfig,
...defaultVoiceSettings,
},
)
}
onMounted(async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
})
watch(pitch, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.pitch = pitch.value
})
watch(speed, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.speed = speed.value
})
watch(volume, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.volume = volume.value
})
watch(providers, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
}, {
immediate: true,
})
</script>
<template>
<SpeechProviderSettings
:provider-id="providerId"
:default-model="defaultModel"
:additional-settings="defaultVoiceSettings"
>
<!-- Voice settings specific to ElevenLabs -->
<template #voice-settings>
<div flex="~ col gap-4">
<!-- Pitch control - common to most providers -->
<FieldRange
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}%`"
/>
<!-- Speed control - common to most providers -->
<FieldRange
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"
/>
<!-- Volume control - available in some providers -->
<FieldRange
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}%`"
/>
</div>
</template>
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
/>
</template>
</SpeechProviderSettings>
</template>
@@ -3,13 +3,15 @@ import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import {
FieldCheckbox,
FieldRange,
SpeechPlayground,
SpeechProviderSettings,
SpeechVoiceSettings,
} 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, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const providerId = 'elevenlabs'
const defaultModel = 'eleven_multilingual_v2'
@@ -23,9 +25,18 @@ const defaultVoiceSettings = {
useSpeakerBoost: true,
}
const pitch = ref<number>(0)
const speed = ref<number>(1.0)
const volume = ref<number>(0)
const style = ref<number>(0)
const stability = ref<number>(0.5)
const similarityBoost = ref<number>(0.75)
const useSpeakerBoost = ref<boolean>(false)
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore)
const { t } = useI18n()
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
@@ -35,11 +46,6 @@ const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Get available languages
const availableLanguages = computed(() => {
return speechStore.availableLanguages
})
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
@@ -65,6 +71,65 @@ async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: bo
},
)
}
onMounted(async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
})
watch(pitch, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.pitch = pitch.value
})
watch(speed, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.speed = speed.value
})
watch(volume, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.volume = volume.value
})
watch(style, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.style = style.value
})
watch(stability, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.stability = stability.value
})
watch(similarityBoost, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.similarityBoost = similarityBoost.value
})
watch(useSpeakerBoost, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.useSpeakerBoost = useSpeakerBoost.value
})
watch(providers, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
}, {
immediate: true,
})
</script>
<template>
@@ -74,23 +139,75 @@ async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: bo
:additional-settings="defaultVoiceSettings"
>
<!-- Voice settings specific to ElevenLabs -->
<template #voice-settings="{ voiceSettings, updateVoiceSettings }">
<SpeechVoiceSettings
:settings="voiceSettings"
:show-similarity-boost="true"
:show-stability="true"
:show-speed="true"
:show-style="true"
:show-speaker-boost="true"
@update="updateVoiceSettings"
/>
<template #voice-settings>
<div flex="~ col gap-4">
<!-- Pitch control - common to most providers -->
<FieldRange
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}%`"
/>
<!-- Speed control - common to most providers -->
<FieldRange
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"
/>
<!-- Volume control - available in some providers -->
<FieldRange
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}%`"
/>
<!-- Style control - specific to ElevenLabs -->
<FieldRange
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"
/>
<!-- Stability control - specific to ElevenLabs -->
<FieldRange
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"
/>
<!-- Similarity Boost control - specific to ElevenLabs -->
<FieldRange
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"
/>
<!-- Speaker Boost checkbox - specific to ElevenLabs -->
<FieldCheckbox
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')"
/>
</div>
</template>
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:available-languages="availableLanguages"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
@@ -6,7 +6,6 @@ import {
FieldInput,
SpeechPlayground,
SpeechProviderSettings,
SpeechVoiceSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
@@ -52,11 +51,6 @@ const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Get available languages
const availableLanguages = computed(() => {
return speechStore.availableLanguages
})
// Generate speech with Microsoft-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnMicrosoftOptions>
@@ -143,7 +137,6 @@ async function handleGenerateSpeech(input: string, voiceId: string, useSSML: boo
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:available-languages="availableLanguages"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the Microsoft Speech synthesis."
@@ -0,0 +1,144 @@
<script setup lang="ts">
import type { UnElevenLabsOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import {
FieldInput,
FieldRange,
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const providerId = 'volcengine'
const defaultModel = 'v1'
const speedRatio = ref<number>(1.0)
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore)
const { t } = useI18n()
// Additional settings specific to Volcengine (appId)
const appId = computed({
get: () => (providers.value[providerId]?.app as any)?.appId as string | undefined || '',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].app = {
appId: value,
}
},
})
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
// Get available voices for ElevenLabs
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string, UnElevenLabsOptions>
if (!provider) {
throw new Error('Failed to initialize speech provider')
}
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
// Get model from configuration or use default
const model = providerConfig.model as string | undefined || defaultModel
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
model,
input,
voiceId,
{
...providerConfig,
},
)
}
onMounted(async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
})
watch(speedRatio, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
if (!providerConfig.audio) {
providerConfig.audio = {}
}
(providerConfig.audio as any).speedRatio = speedRatio.value
})
watch([providers, appId], async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
const providerMetadata = providersStore.getProviderMetadata(providerId)
if (await providerMetadata.validators.validateProviderConfig(providerConfig)) {
await speechStore.loadVoicesForProvider(providerId)
}
else {
console.error('Failed to validate provider config', providerConfig)
}
}, {
immediate: true,
})
</script>
<template>
<SpeechProviderSettings
:provider-id="providerId"
:default-model="defaultModel"
>
<!-- Voice settings specific to ElevenLabs -->
<template #basic-settings>
<div flex="~ col gap-4">
<FieldInput
v-model="appId"
:label="t('settings.pages.providers.provider.volcengine.fields.field.appId.label')"
:description="t('settings.pages.providers.provider.volcengine.fields.field.appId.description')"
required
/>
</div>
</template>
<template #voice-settings>
<!-- Speed control - common to most providers -->
<FieldRange
v-model="speedRatio"
: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"
/>
</template>
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the ElevenLabs voice synthesis."
/>
</template>
</SpeechProviderSettings>
</template>
+1 -1
View File
@@ -11,7 +11,7 @@
--airi-theme-primary-900: #8e0d3b;
--airi-theme-primary-950: #50011b;
--theme-colors-hue: 354.31;
--theme-colors-hue: 220.44;
--theme-colors-chroma: calc(0.18 + (cos(var(--theme-colors-hue) * 3.14159265 / 180) * 0.04));
--theme-colors-chroma-50: calc(var(--theme-colors-chroma) * 0.3);
--theme-colors-chroma-100: calc(var(--theme-colors-chroma) * 0.5);
+7
View File
@@ -8,6 +8,8 @@ words:
- airi
- airi-vtuber
- Alaya
- alibabacloud
- aliyun
- APNG
- astrojs
- Attributify
@@ -16,6 +18,7 @@ words:
- Ayaka
- baichuan
- baiducloud
- bailian
- bigserial
- Bitstream
- browserbasehq
@@ -26,6 +29,8 @@ words:
- collectblock
- composables
- cooldown
- cosyvoice
- cozyvoice
- crossws
- csmmap
- csmvector
@@ -158,11 +163,13 @@ words:
- unhead
- unocss
- unplugin
- unspeech
- valibot
- vaul
- velin
- VITE
- vllm
- Volcengine
- vrma
- vueuse
- wavefile
+1
View File
@@ -65,6 +65,7 @@
"radix-vue": "^1.9.17",
"reka-ui": "^2.2.0",
"unist-builder": "^4.0.0",
"unspeech": "^0.1.7",
"xast-util-to-xml": "^4.0.0",
"xastscript": "^4.0.0"
},
@@ -10,7 +10,6 @@ const props = defineProps<{
// Input fields
defaultText?: string
availableVoices: VoiceInfo[]
availableLanguages: string[]
// Provider-specific handlers (provided from parent)
generateSpeech: (input: string, voice: string, useSSML: boolean) => Promise<ArrayBuffer>
@@ -29,7 +28,6 @@ const errorMessage = ref('')
const audioPlayer = ref<HTMLAudioElement | null>(null)
const useSSML = ref(false)
const ssmlText = ref('')
const selectedLanguage = ref(props.availableLanguages[0] || 'en-US')
const selectedVoice = ref('')
// Watch for changes in available voices
@@ -106,7 +104,6 @@ defineExpose({
testText,
ssmlText,
useSSML,
selectedLanguage,
selectedVoice,
isGenerating,
audioUrl,
@@ -156,27 +153,6 @@ defineExpose({
</template>
<div flex="~ col gap-6">
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ t('settings.pages.providers.provider.elevenlabs.playground.fields.field.language.label') }}
</div>
<div class="text-xs text-neutral-500 dark:text-neutral-400">
{{ t('settings.pages.providers.provider.elevenlabs.playground.fields.field.language.description') }}
</div>
</div>
<select
v-model="selectedLanguage"
border="neutral-300 dark:neutral-800 solid 2 focus:neutral-400 dark:focus:neutral-600"
transition="border duration-250 ease-in-out" w-full rounded-lg px-2 py-1 text-nowrap text-sm
outline-none
>
<option v-for="language in availableLanguages" :key="language" :value="language">
{{ language }}
</option>
</select>
</label>
<label grid="~ cols-2 gap-4">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
@@ -29,10 +29,7 @@ const props = defineProps<{
// Expose slots and emit events to allow customization
defineSlots<{
'basic-settings': (props: any) => any
'voice-settings': (props: {
voiceSettings: Record<string, any>
updateVoiceSettings: (key: string, value: any) => void
}) => any
'voice-settings': (props: any) => any
'advanced-settings': (props: any) => any
'playground': (props: {
isGenerating: boolean
@@ -1,102 +0,0 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { FieldCheckbox, FieldRange } from '../index'
defineProps<{
settings: Record<string, any>
// Which settings to show
showPitch?: boolean
showSpeed?: boolean
showStyle?: boolean
showStability?: boolean
showSimilarityBoost?: boolean
showVolume?: boolean
showSpeakerBoost?: boolean
}>()
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()
</script>
<template>
<div flex="~ col gap-4">
<!-- Pitch control - common to most providers -->
<FieldRange
v-if="showPitch"
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}%`"
/>
<!-- Speed control - common to most providers -->
<FieldRange
v-if="showSpeed"
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"
/>
<!-- Volume control - available in some providers -->
<FieldRange
v-if="showVolume"
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}%`"
/>
<!-- Style control - specific to ElevenLabs -->
<FieldRange
v-if="showStyle"
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"
/>
<!-- Stability control - specific to ElevenLabs -->
<FieldRange
v-if="showStability"
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"
/>
<!-- Similarity Boost control - specific to ElevenLabs -->
<FieldRange
v-if="showSimilarityBoost"
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"
/>
<!-- Speaker Boost checkbox - specific to ElevenLabs -->
<FieldCheckbox
v-if="showSpeakerBoost"
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')"
/>
<!-- Slot for additional provider-specific controls -->
<slot />
</div>
</template>
@@ -8,6 +8,4 @@ export { default as ProviderSettingsLayout2 } from './ProviderSettingsLayout2.vu
export { default as ProviderSettingsLayout } from './ProviderSettingsLayout.vue'
export { default as SpeechPlayground } from './SpeechPlayground.vue'
// New speech provider components
export { default as SpeechProviderSettings } from './SpeechProviderSettings.vue'
export { default as SpeechVoiceSettings } from './SpeechVoiceSettings.vue'
@@ -1,48 +0,0 @@
export enum Voice {
// English
Myriam = 'Myriam',
Beatrice = 'Beatrice',
Camilla_KM = 'Camilla_KM',
SallySunshine = 'Sally Sunshine',
Annie = 'Annie',
KawaiiAerisita = 'Kawaii Aerisita',
// Japanese
Morioki = 'Morioki',
}
export const voiceMap: Record<Voice, string> = {
// English
[Voice.Myriam]: 'lNxY9WuCBCZCISASyJ55',
// Beatrice is not 'childish' like the others
// voice: 'Beatrice',
[Voice.Beatrice]: 'KAsXoQDshjF6ehsWa1mF',
[Voice.Camilla_KM]: 'dLhSyo03JRp5WkGpUlz1',
[Voice.SallySunshine]: 'qswttdunP3b44zVZKMRB',
[Voice.Annie]: 'AfA1PA0ldViH0DA6pbml',
[Voice.KawaiiAerisita]: 'vGQNBgLaiM3EdZtxIiuY',
// Japanese
[Voice.Morioki]: '8EkOjt4xTPGMclNlh1pk',
}
// voice: 'ShanShan',
// Quite good for English
export const enVoiceList = [
Voice.Myriam,
Voice.Beatrice,
Voice.Camilla_KM,
Voice.SallySunshine,
Voice.Annie,
Voice.KawaiiAerisita,
]
export const jaVoiceList = [
Voice.Morioki,
]
export const voiceList: Record<string, Voice[]> = {
'en': enVoiceList,
'en-US': enVoiceList,
'ja': jaVoiceList,
'ja-JP': jaVoiceList,
}
-1
View File
@@ -1,5 +1,4 @@
export const llmInferenceEndToken = '<|llm_inference_end|>'
export * from './elevenlabs'
export * from './emotions'
export * from './prompts/system-v2'
@@ -1,186 +0,0 @@
import type { UnSpeechOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import type { VoiceProviderWithExtraOptions } from './voice'
import { merge } from '@xsai-ext/shared-providers'
import { objCamelToSnake } from '@xsai/shared'
/** @see {@link https://elevenlabs.io/docs/api-reference/text-to-speech/convert#request} */
export interface UnElevenLabsOptions {
/**
* This parameter controls text normalization with three modes: 'auto', 'on', and 'off'. When set to 'auto',
* the system will automatically decide whether to apply text normalization (e.g., spelling out numbers).
* With 'on', text normalization will always be applied, while with 'off', it will be skipped. Cannot be
* turned on for 'eleven_turbo_v2_5' model.
*/
applyTextNormalization?: 'auto' | 'off' | 'on'
/**
* Language code (ISO 639-1) used to enforce a language for the model. Currently only Turbo v2.5
* supports language enforcement. For other models, an error will be returned if language code is provided.
*/
languageCode?: string
/**
* A list of request_id of the samples that were generated before this generation. Can
* be used to improve the flow of prosody when splitting up a large task into multiple
* requests. The results will be best when the same model is used across the generations.
*
* In case both next_text and next_request_ids is send, next_text will be ignored.
* A maximum of 3 request_ids can be send.
*/
nextRequestIds?: string[]
/**
* The text that comes after the text of the current request. Can be used to improve
* the flow of prosody when concatenating together multiple generations or to influence
* the prosody in the current generation.
*/
nextText?: string
/**
* A list of request_id of the samples that were generated before this generation. Can be
* used to improve the flow of prosody when splitting up a large task into multiple requests.
* The results will be best when the same model is used across the generations. In case both
* previous_text and previous_request_ids is send, previous_text will be ignored. A maximum
* of 3 request_ids can be send.
*/
previousRequestIds?: string[]
/**
* The text that came before the text of the current request. Can be used to improve the
* flow of prosody when concatenating together multiple generations or to influence the
* prosody in the current generation.
*/
previousText?: string
/**
* A list of pronunciation dictionary locators (id, version_id) to be applied to the text.
* They will be applied in order. You may have up to 3 locators per request
*/
pronunciationDictionaryLocators?: {
pronunciationDictionaryId: string
versionId: string
}[]
/**
* If specified, our system will make a best effort to sample deterministically, such that
* repeated requests with the same seed and parameters should return the same result.
* Determinism is not guaranteed. Must be integer between 0 and 4294967295.
*/
seed?: number
/**
* Voice settings overriding stored settings for the given voice. They are applied only on the given request.
*/
voiceSettings?: {
/**
* Determines how closely the AI should adhere to the original voice when attempting to replicate it.
*/
similarityBoost: number
/**
* Controls the speed of the generated speech. Values range from 0.7 to 1.2, with 1.0 being the default
* speed. Lower values create slower, more deliberate speech while higher values produce faster-paced
* speech. Extreme values can impact the quality of the generated speech.
*
* @default 1.0
*/
speed?: number
/**
* Determines how stable the voice is and the randomness between each generation. Lower values introduce
* broader emotional range for the voice. Higher values can result in a monotonous voice with limited
* emotion.
*/
stability: number
/**
* Determines the style exaggeration of the voice. This setting attempts to amplify the style of the original
* speaker. It does consume additional computational resources and might increase latency if set to anything
* other than 0.
*
* @default 0
*/
style?: number
/**
* This setting boosts the similarity to the original speaker. Using this setting requires a slightly higher
* computational load, which in turn increases latency.
*
* @default true
*/
useSpeakerBoost?: boolean
}
}
/**
* [ElevenLabs](https://elevenlabs.io/) provider for [UnSpeech](https://github.com/moeru-ai/unspeech)
* only.
*
* [UnSpeech](https://github.com/moeru-ai/unspeech) is a open-source project that provides a
* OpenAI-compatible audio & speech related API that can be used with various providers such
* as ElevenLabs, Azure TTS, Google TTS, etc.
*
* @param apiKey - ElevenLabs API Key
* @param baseURL - UnSpeech Instance URL
* @returns SpeechProviderWithExtraOptions
*/
export function createUnElevenLabs(apiKey: string, baseURL = 'http://localhost:5933/v1/') {
const toUnSpeechOptions = ({
applyTextNormalization,
languageCode,
nextRequestIds,
nextText,
previousRequestIds,
previousText,
pronunciationDictionaryLocators,
seed,
voiceSettings,
}: UnElevenLabsOptions): UnSpeechOptions => ({
extraBody: objCamelToSnake({
applyTextNormalization,
languageCode,
nextRequestIds,
nextText,
previousRequestIds,
previousText,
pronunciationDictionaryLocators: pronunciationDictionaryLocators
? pronunciationDictionaryLocators.map(pdl => objCamelToSnake(pdl))
: undefined,
seed,
voiceSettings: voiceSettings != null
? objCamelToSnake(voiceSettings)
: {
similarityBoost: 0.75,
stability: 0.5,
},
}),
})
const speechProvider: SpeechProviderWithExtraOptions<
/** @see {@link https://elevenlabs.io/docs/developer-guides/models} */
'eleven_english_sts_v2' | 'eleven_flash_v2' | 'eleven_flash_v2_5' | 'eleven_multilingual_sts_v2' | 'eleven_multilingual_v2',
UnElevenLabsOptions
> = {
speech: (model, options) => ({
...(options ? toUnSpeechOptions(options) : {}),
apiKey,
baseURL,
model: `elevenlabs/${model}`,
}),
}
const voiceProvider: VoiceProviderWithExtraOptions<
UnElevenLabsOptions
> = {
voice: (options) => {
if (baseURL.endsWith('v1/')) {
baseURL = baseURL.slice(0, -3)
}
else if (baseURL.endsWith('v1')) {
baseURL = baseURL.slice(0, -2)
}
return {
query: `provider=elevenlabs`,
...(options ? toUnSpeechOptions(options) : {}),
apiKey,
baseURL,
}
},
}
return merge(
speechProvider,
voiceProvider,
)
}
@@ -1,22 +0,0 @@
import type { CommonRequestOptions } from '@xsai/shared'
import type { Voice } from './voice'
import { requestHeaders, requestURL, responseJSON } from '@xsai/shared'
export interface ListVoicesOptions extends Omit<CommonRequestOptions, 'model'> {
query?: string
}
export interface ListVoicesResponse {
voices: Voice[]
}
export async function listVoices(options: ListVoicesOptions): Promise<Voice[]> {
return (options.fetch ?? globalThis.fetch)(requestURL(options.query ? `api/voices?${options.query}` : 'api/voices', options.baseURL), {
headers: requestHeaders({ ...options.headers }, options.apiKey),
method: 'GET',
signal: options.abortSignal,
})
.then(responseJSON<ListVoicesResponse>)
.then(({ voices }) => voices)
}
@@ -1,174 +0,0 @@
import type { UnSpeechOptions } from '@xsai-ext/providers-local'
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/shared-providers'
import type { VoiceProviderWithExtraOptions } from './voice'
import { merge } from '@xsai-ext/shared-providers'
import { objCamelToSnake } from '@xsai/shared'
export type MicrosoftRegions =
| 'australiaeast'
| 'brazilsouth'
| 'canadacentral'
| 'centralindia'
| 'centralus'
| 'eastasia'
| 'eastus2'
| 'eastus'
| 'francecentral'
| 'germanywestcentral'
| 'japaneast'
| 'japanwest'
| 'jioindiawest'
| 'koreacentral'
| 'northcentralus'
| 'northeurope'
| 'norwayeast'
| 'southcentralus'
| 'southeastasia'
| 'swedencentral'
| 'switzerlandnorth'
| 'switzerlandwest'
| 'uaenorth'
| 'uksouth'
| 'usgovarizona'
| 'usgovvirginia'
| 'westcentralus'
| 'westeurope'
| 'westus2'
| 'westus3'
| 'westus'
export interface UnMicrosoftOptionAutoSSML {
gender:
| 'Female'
| 'Male'
| 'Neutral'
| string
lang:
| 'en-US'
| string
/**
* Speech Studio - Voice Gallery
* https://speech.microsoft.com/portal/018ba84135d64cf79106cc99c75ffa6a/voicegallery
*/
voice:
| 'en-US-AndrewMultilingualNeural'
| 'en-US-AriaNeural'
| 'en-US-AvaMultilingualNeural'
| 'en-US-BrianMultilingualNeural'
| 'en-US-ChristopherMultilingualNeural'
| 'en-US-EmmaMultilingualNeural'
| 'en-US-JaneNeural'
| string
}
export interface UnMicrosoftOptionCommon {
/**
* Text to speech API reference (REST) - Speech service - Azure AI services | Microsoft Learn
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#custom-neural-voices
*/
deploymentId?: string
/**
* Text to speech API reference (REST) - Speech service - Azure AI services | Microsoft Learn
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#prebuilt-neural-voices
*
* NOTICE: Voices in preview are available in only these three regions: East US, West Europe, and Southeast Asia.
*/
region: MicrosoftRegions | string
sampleRate?:
| 8000
| 16000
| 22050
| 24000
| 44100
| 48000
| number
}
export interface UnMicrosoftOptionCustomSSML {
/**
* By default, unspeech service will help you automatically convert OpenAI style plain text input
* into SSML with lang, gender, voice parameters, but if you ever wanted to provide your own SSML
* with all customizable parameters, you can set this option to `true` to disable the automatic
* conversion and use your own SSML instead.
*
* About SSML (Speech Synthesis Markup Language), @see {@link https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup}
*/
disableSsml?: boolean
}
/** @see {@link https://elevenlabs.io/docs/api-reference/text-to-speech/convert#request} */
export type UnMicrosoftOptions = (UnMicrosoftOptionAutoSSML | UnMicrosoftOptionCustomSSML) & UnMicrosoftOptionCommon
/**
* [Microsoft / Azure AI](https://speech.microsoft.com/portal) provider for [UnSpeech](https://github.com/moeru-ai/unspeech)
* only.
*
* [UnSpeech](https://github.com/moeru-ai/unspeech) is a open-source project that provides a
* OpenAI-compatible audio & speech related API that can be used with various providers such
* as ElevenLabs, Azure TTS, Google TTS, etc.
*
* @param apiKey - Microsoft / Azure AI subscription key
* @param baseURL - UnSpeech Instance URL
* @returns SpeechProviderWithExtraOptions
*/
export function createUnMicrosoft(apiKey: string, baseURL = 'http://localhost:5933/v1/') {
const toUnSpeechOptions = (options: UnMicrosoftOptions): UnSpeechOptions => {
const { deploymentId, region, sampleRate } = options
const extraBody: Record<string, unknown> = {
deploymentId,
region,
sampleRate,
}
if ('disableSsml' in options) {
extraBody.disableSsml = options.disableSsml
}
else if ('lang' in options) {
extraBody.lang = options.lang
extraBody.gender = options.gender
extraBody.voice = options.voice
}
return { extraBody: objCamelToSnake(extraBody) }
}
const speechProvider: SpeechProviderWithExtraOptions<
/** @see Currently, cognitive services are on v1 */
'microsoft/v1',
UnMicrosoftOptions
> = {
speech: (model, options) => ({
...(options ? toUnSpeechOptions(options) : {}),
apiKey,
baseURL,
model: `microsoft/${model}`,
}),
}
const voiceProvider: VoiceProviderWithExtraOptions<
UnMicrosoftOptions
> = {
voice: (options) => {
if (baseURL.endsWith('v1/')) {
baseURL = baseURL.slice(0, -3)
}
else if (baseURL.endsWith('v1')) {
baseURL = baseURL.slice(0, -2)
}
return {
query: `region=${options?.region}&provider=microsoft`,
...(options ? toUnSpeechOptions(options) : {}),
apiKey,
baseURL,
}
},
}
return merge(
speechProvider,
voiceProvider,
)
}
-41
View File
@@ -1,41 +0,0 @@
import type { CommonRequestOptions } from '@xsai/shared'
export interface Voice {
compatible_models: string[]
description: string
formats: VoiceFormat[]
id: string
labels: Record<string, any> & {
accent?: string
age?: string
gender?: string
type?: string
}
languages: VoiceLanguage[]
name: string
predefined_options?: Record<string, any>
preview_audio_url?: string
tags: string[]
}
export interface VoiceFormat {
bitrate: number
extension: string
format_code: string
mime_type: string
name: string
sample_rate: number
}
export interface VoiceLanguage {
code: string
title: string
}
export interface VoiceProvider {
voice: () => Omit<CommonRequestOptions, 'model'> & { query?: string }
}
export interface VoiceProviderWithExtraOptions<T = undefined> {
voice: (options?: T) => Omit<CommonRequestOptions, 'model'> & { query?: string } & Partial<T>
}
+2 -25
View File
@@ -8,7 +8,6 @@ import { computed, onMounted, ref, watch } from 'vue'
import { toXml } from 'xast-util-to-xml'
import { x } from 'xastscript'
import { voiceList, voiceMap } from '../../constants/elevenlabs'
import { useProvidersStore } from '../providers'
export const useSpeechStore = defineStore('speech', () => {
@@ -69,32 +68,12 @@ export const useSpeechStore = defineStore('speech', () => {
const supportsSSML = computed(() => {
// Currently only ElevenLabs and some other providers support SSML
return ['elevenlabs', 'microsoft-speech', 'azure-speech', 'google'].includes(activeSpeechProvider.value)
})
const availableLanguages = computed(() => {
return Object.keys(voiceList)
})
const availableVoicesForLanguage = computed(() => {
const language = selectedLanguage.value
if (!language || !voiceList[language]) {
return []
}
return voiceList[language].map(voiceEnum => ({
id: voiceMap[voiceEnum],
name: voiceEnum,
provider: 'elevenlabs',
language,
}))
return ['elevenlabs', 'microsoft-speech', 'azure-speech', 'google', 'alibaba-cloud-model-studio', 'volcengine'].includes(activeSpeechProvider.value)
})
// Helper function to determine if a provider is a speech provider
function isSpeechProvider(providerId: string): boolean {
// This is a simplified check - in a real implementation, you might have a more robust way
// to determine if a provider supports speech synthesis
return ['elevenlabs', 'microsoft-speech', 'azure-speech', 'google', 'amazon'].includes(providerId)
return ['elevenlabs', 'microsoft-speech', 'azure-speech', 'google', 'amazon', 'alibaba-cloud-model-studio', 'volcengine'].includes(providerId)
}
async function loadVoicesForProvider(provider: string) {
@@ -247,8 +226,6 @@ export const useSpeechStore = defineStore('speech', () => {
// Computed
availableSpeechProvidersMetadata,
supportsSSML,
availableLanguages,
availableVoicesForLanguage,
supportsModelListing,
providerModels,
isLoadingActiveProviderModels,
+120 -6
View File
@@ -8,9 +8,13 @@ import type {
TranscriptionProvider,
TranscriptionProviderWithExtraOptions,
} from '@xsai-ext/shared-providers'
import type { UnElevenLabsOptions } from './fix/elevenlabs'
import type { UnMicrosoftOptions } from './fix/microsoft'
import type { VoiceProviderWithExtraOptions } from './fix/voice'
import type {
UnAlibabaCloudOptions,
UnElevenLabsOptions,
UnMicrosoftOptions,
UnVolcengineOptions,
VoiceProviderWithExtraOptions,
} from 'unspeech'
import { useLocalStorage } from '@vueuse/core'
import {
@@ -31,12 +35,16 @@ import {
import { createOllama } from '@xsai-ext/providers-local'
import { listModels } from '@xsai/model'
import { defineStore } from 'pinia'
import {
createUnAlibabaCloud,
createUnElevenLabs,
createUnMicrosoft,
createUnVolcengine,
listVoices,
} from 'unspeech'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { createUnElevenLabs } from './fix/elevenlabs'
import { listVoices } from './fix/list-voices'
import { createUnMicrosoft } from './fix/microsoft'
import { models as elevenLabsModels } from './providers/elevenlabs/list-models'
export interface ProviderMetadata {
@@ -559,6 +567,112 @@ export const useProvidersStore = defineStore('providers', () => {
},
},
},
'alibaba-cloud-model-studio': {
id: 'alibaba-cloud-model-studio',
nameKey: 'settings.pages.providers.provider.alibaba-cloud-model-studio.title',
name: 'Alibaba Cloud Model Studio',
descriptionKey: 'settings.pages.providers.provider.alibaba-cloud-model-studio.description',
description: 'bailian.console.aliyun.com',
iconColor: 'i-lobe-icons:alibabacloud',
defaultOptions: {
baseUrl: 'https://unspeech.hyp3r.link/v1/',
},
createProvider: config => createUnAlibabaCloud((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listVoices: async (config) => {
const provider = createUnAlibabaCloud((config.apiKey as string).trim(), (config.baseUrl as string).trim()) as VoiceProviderWithExtraOptions<UnAlibabaCloudOptions>
const voices = await listVoices({
...provider.voice(),
})
return voices.map((voice) => {
return {
id: voice.id,
name: voice.name,
provider: 'alibaba-cloud-model-studio',
previewURL: voice.preview_audio_url,
languages: voice.languages,
gender: voice.labels?.gender,
}
})
},
listModels: async () => {
return [
{
id: 'cozyvoice-v1',
name: 'CozyVoice',
provider: 'alibaba-cloud-model-studio',
description: '',
contextLength: 0,
deprecated: false,
},
{
id: 'cozyvoice-v2',
name: 'CozyVoice (New)',
provider: 'alibaba-cloud-model-studio',
description: '',
contextLength: 0,
deprecated: false,
},
]
},
},
validators: {
validateProviderConfig: (config) => {
return !!config.apiKey && !!config.baseUrl
},
},
},
'volcengine': {
id: 'volcengine',
nameKey: 'settings.pages.providers.provider.volcengine.title',
name: 'settings.pages.providers.provider.volcengine.title',
descriptionKey: 'settings.pages.providers.provider.volcengine.description',
description: 'volcengine.com',
iconColor: 'i-lobe-icons:volcengine',
defaultOptions: {
baseUrl: 'https://unspeech.hyp3r.link/v1/',
},
createProvider: config => createUnVolcengine((config.apiKey as string).trim(), (config.baseUrl as string).trim()),
capabilities: {
listVoices: async (config) => {
const provider = createUnVolcengine((config.apiKey as string).trim(), (config.baseUrl as string).trim()) as VoiceProviderWithExtraOptions<UnVolcengineOptions>
const voices = await listVoices({
...provider.voice(),
})
return voices.map((voice) => {
return {
id: voice.id,
name: voice.name,
provider: 'volcano-engine',
previewURL: voice.preview_audio_url,
languages: voice.languages,
gender: voice.labels?.gender,
}
})
},
listModels: async () => {
return [
{
id: 'v1',
name: 'v1',
provider: 'volcano-engine',
description: '',
contextLength: 0,
deprecated: false,
},
]
},
},
validators: {
validateProviderConfig: (config) => {
return !!config.apiKey && !!config.baseUrl && !!config.app && !!(config.app as any).appId
},
},
},
'together-ai': {
id: 'together-ai',
nameKey: 'settings.pages.providers.provider.together.title',
+1 -1
View File
@@ -3,7 +3,7 @@ import { converter } from 'culori'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
export const DEFAULT_THEME_COLORS_HUE = 178.17
export const DEFAULT_THEME_COLORS_HUE = 220.44
const convert = converter('oklch')
const getHueFrom = (color?: string) => color ? convert(color)?.h : DEFAULT_THEME_COLORS_HUE
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
const DEFAULT_THEME_COLORS_HUE = 178.17
const DEFAULT_THEME_COLORS_HUE = 220.44
const themeColorsHue = ref(DEFAULT_THEME_COLORS_HUE)
+1 -1
View File
@@ -11,7 +11,7 @@
--airi-theme-primary-900: #8e0d3b;
--airi-theme-primary-950: #50011b;
--theme-colors-hue: 354.31;
--theme-colors-hue: 220.44;
--theme-colors-chroma: calc(0.18 + (cos(var(--theme-colors-hue) * 3.14159265 / 180) * 0.04));
--theme-colors-chroma-50: calc(var(--theme-colors-chroma) * 0.3);
--theme-colors-chroma-100: calc(var(--theme-colors-chroma) * 0.5);
+11
View File
@@ -1084,6 +1084,9 @@ importers:
unist-builder:
specifier: ^4.0.0
version: 4.0.0
unspeech:
specifier: ^0.1.7
version: 0.1.7
xast-util-to-xml:
specifier: ^4.0.0
version: 4.0.0
@@ -11151,6 +11154,9 @@ packages:
unrs-resolver@1.3.2:
resolution: {integrity: sha512-ZKQBC351Ubw0PY8xWhneIfb6dygTQeUHtCcNGd0QB618zabD/WbFMYdRyJ7xeVT+6G82K5v/oyZO0QSHFtbIuw==}
unspeech@0.1.7:
resolution: {integrity: sha512-HQ49mqEfvkMIKsbYDfHXDxbpUuNnb+gJdLO6wjQ/iPP1VuUfXs5Ky20attrRr1Qp2S0FHWsuz7/nlv9CAHHuhg==}
unstorage@1.15.0:
resolution: {integrity: sha512-m40eHdGY/gA6xAPqo8eaxqXgBuzQTlAKfmB1iF7oCKXE1HfwHwzDJBywK+qQGn52dta+bPlZluPF7++yR3p/bg==}
peerDependencies:
@@ -23885,6 +23891,11 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.3.2
'@unrs/resolver-binding-win32-x64-msvc': 1.3.2
unspeech@0.1.7:
dependencies:
'@xsai-ext/shared-providers': 0.2.0-beta.3
'@xsai/shared': 0.2.0-beta.3
unstorage@1.15.0:
dependencies:
anymatch: 3.1.3