Feat: Support of Index-TTS, state-of-art opensourced TTS model - locally deployable. (#351)

This commit is contained in:
Ching Yang
2025-08-08 20:30:41 +08:00
committed by GitHub
parent c304558bf6
commit 389ee0a57c
9 changed files with 238 additions and 13 deletions
+1 -1
View File
@@ -105,7 +105,7 @@
"@iconify-json/vscode-icons": "^1.2.23",
"@iconify/utils": "^2.3.0",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@proj-airi/lobe-icons": "^1.0.8",
"@proj-airi/lobe-icons": "^1.0.10",
"@proj-airi/ui-transitions": "workspace:^",
"@proj-airi/unplugin-fetch": "^0.1.6",
"@proj-airi/unplugin-live2d-sdk": "^0.1.5",
@@ -0,0 +1,82 @@
<script setup lang="ts">
import type { SpeechProvider } from '@xsai-ext/shared-providers'
import {
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { computed, onMounted, watch } from 'vue'
// import { useI18n } from 'vue-i18n'
// const { t } = useI18n()
const providerId = 'index-tts-vllm'
const defaultModel = 'IndexTTS-1.5'
const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
// const { providers } = storeToRefs(providersStore)
// Check if API key is configured
// const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
const apiKeyConfigured = true // Assuming API key is always configured as its not required
// Get available voices for Index TTS provider
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
onMounted(async () => {
await speechStore.loadVoicesForProvider(providerId)
})
watch([apiKeyConfigured], async () => {
await speechStore.loadVoicesForProvider(providerId)
})
async function handleGenerateSpeech(input: string, voiceId: string) {
const provider = await providersStore.getProviderInstance(providerId) as SpeechProvider
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
const options = {
...providerConfig,
}
return await speechStore.speech(
provider,
model,
input,
voiceId,
options,
)
}
</script>
<template>
<SpeechProviderSettings :provider-id="providerId" :default-model="defaultModel">
<!-- Replace the default playground with our standalone component -->
<template #playground>
<SpeechPlayground
:available-voices="availableVoices" :generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured" :use-ssml="false"
default-text="Hello! This is a test of the Index TTS Speech synthesis?."
/>
</template>
</SpeechProviderSettings>
</template>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
+1 -1
View File
@@ -97,7 +97,7 @@
"@iconify-json/svg-spinners": "^1.2.2",
"@iconify-json/vscode-icons": "^1.2.23",
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@proj-airi/lobe-icons": "^1.0.8",
"@proj-airi/lobe-icons": "^1.0.10",
"@proj-airi/unplugin-fetch": "^0.1.6",
"@proj-airi/unplugin-live2d-sdk": "^0.1.5",
"@shikijs/markdown-it": "^3.9.1",
@@ -0,0 +1,91 @@
<script setup lang="ts">
import type { SpeechProvider } from '@xsai-ext/shared-providers'
import {
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProvidersStore, useSpeechStore } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { computed, watch } from 'vue'
// import { useI18n } from 'vue-i18n'
const providerId = 'index-tts-vllm'
const defaultModel = 'IndexTTS-1.5'
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)
const apiKeyConfigured = true // Assuming API key is always configured as its not required
// Get available voices for Index TTS
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
})
// Generate speech with IndexTTS-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = await providersStore.getProviderInstance(providerId) as SpeechProvider
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
// Index TTS doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
model,
input,
voiceId,
{
...providerConfig,
},
)
}
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"
>
<!-- 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 Index TTS voice synthesis."
/>
</template>
</SpeechProviderSettings>
</template>
<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
+1 -1
View File
@@ -26,7 +26,7 @@ function handleClose() {
<template>
<DialogRoot v-model:open="open">
<DialogTrigger class="text-muted-foreground border-muted text-md md:bg-card hover:bg-muted flex items-center rounded-lg px-3 py-[7px] transition-colors duration-200 ease-in-out space-x-2 md:border md:text-sm">
<DialogTrigger class="text-muted-foreground border-muted hover:bg-muted text-md md:bg-card flex items-center rounded-lg px-3 py-[7px] transition-colors duration-200 ease-in-out space-x-2 md:border md:text-sm">
<Icon icon="lucide:search" />
<span class="w-24 text-left hidden lg:w-40 md:inline-flex">{{ t('docs.theme.search.title') }}</span>
<span class="text-xs hidden prose md:inline-flex">
@@ -404,6 +404,9 @@ pages:
description: Speech Service region
label: Region
title: Microsoft / Azure Speech
index-tts-vllm:
description: https://index-tts.github.io/
title: Bilibili / IndexTTS
mistral:
description: mistral.ai
title: Mistral
+1 -1
View File
@@ -136,7 +136,7 @@
"@iconify-json/svg-spinners": "^1.2.2",
"@iconify-json/vscode-icons": "^1.2.23",
"@moeru/std": "catalog:",
"@proj-airi/lobe-icons": "^1.0.8",
"@proj-airi/lobe-icons": "^1.0.10",
"@types/culori": "^4.0.0",
"@types/three": "^0.178.1",
"@unocss/reset": "^66.3.3",
+49
View File
@@ -1063,6 +1063,55 @@ export const useProvidersStore = defineStore('providers', () => {
},
},
},
'index-tts-vllm': {
id: 'index-tts-vllm',
category: 'speech',
tasks: ['text-to-speech'],
nameKey: 'settings.pages.providers.provider.index-tts-vllm.title',
name: 'Index-TTS by Bilibili',
descriptionKey: 'settings.pages.providers.provider.index-tts-vllm.description',
description: 'index-tts.github.io',
iconColor: 'i-lobe-icons:bilibiliindex',
defaultOptions: () => ({
baseUrl: 'http://localhost:11996/tts',
}),
createProvider: async (config) => {
const provider: SpeechProvider = {
speech: () => {
const req = {
baseURL: config.baseUrl as string,
model: 'IndexTTS-1.5',
}
return req
},
}
return provider
},
capabilities: {
listVoices: async (config) => {
const voicesUrl = config.baseUrl as string
const response = await fetch(`${voicesUrl}/audio/voices`)
if (!response.ok) {
throw new Error(`Failed to fetch voices: ${response.statusText}`)
}
const voices = await response.json()
return Object.keys(voices).map((voice: any) => {
return {
id: voice,
name: voice,
provider: 'index-tts-vllm',
// previewURL: voice.preview_audio_url,
languages: [{ code: 'cn', title: 'Chinese' }, { code: 'en', title: 'English' }],
}
})
},
},
validators: {
validateProviderConfig: (config) => {
return !!config.baseUrl
},
},
},
'alibaba-cloud-model-studio': {
id: 'alibaba-cloud-model-studio',
category: 'speech',
+9 -9
View File
@@ -591,8 +591,8 @@ importers:
specifier: ^6.0.8
version: 6.0.8(@vue/compiler-dom@3.5.18)(eslint@9.32.0(jiti@2.5.1))(rollup@4.46.2)(typescript@5.9.2)(vue-i18n@11.1.11(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))
'@proj-airi/lobe-icons':
specifier: ^1.0.8
version: 1.0.8
specifier: ^1.0.10
version: 1.0.10
'@proj-airi/ui-transitions':
specifier: workspace:^
version: link:../../packages/ui-transitions
@@ -916,8 +916,8 @@ importers:
specifier: ^6.0.8
version: 6.0.8(@vue/compiler-dom@3.5.18)(eslint@9.32.0(jiti@2.5.1))(rollup@2.79.2)(typescript@5.9.2)(vue-i18n@11.1.11(vue@3.5.18(typescript@5.9.2)))(vue@3.5.18(typescript@5.9.2))
'@proj-airi/lobe-icons':
specifier: ^1.0.8
version: 1.0.8
specifier: ^1.0.10
version: 1.0.10
'@proj-airi/unplugin-fetch':
specifier: ^0.1.6
version: 0.1.6(@types/node@24.1.0)(jiti@2.5.1)(less@4.4.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
@@ -1511,8 +1511,8 @@ importers:
specifier: 'catalog:'
version: 0.1.0-beta.7
'@proj-airi/lobe-icons':
specifier: ^1.0.8
version: 1.0.8
specifier: ^1.0.10
version: 1.0.10
'@types/culori':
specifier: ^4.0.0
version: 4.0.0
@@ -4739,8 +4739,8 @@ packages:
web-worker:
optional: true
'@proj-airi/lobe-icons@1.0.8':
resolution: {integrity: sha512-28EtouwEwjwOuT4eI9iSrdTyZDWHBrg5eyrZIGF8oCYRwYVsZ1Q78M2cX9faJ3XB6r4+suEbheOHbP8zj649bA==}
'@proj-airi/lobe-icons@1.0.10':
resolution: {integrity: sha512-zzQkwup4YFwheqYq5zTCMyol/Er1yP1lxdDTvrjZP3WHXop1IkOe9Jw44IG3Hsx1h/XV5WVizfELHVRFsveWQw==}
'@proj-airi/server-sdk@0.7.0-beta.1':
resolution: {integrity: sha512-g4JtWrTDFA76hZIYHz5OMa5It/0LxOrBPvbVL9D3EWYeubjzGZoBHy8ZFYWq78r7yHQScTjLl1vtagLEMLQnyA==}
@@ -15893,7 +15893,7 @@ snapshots:
transitivePeerDependencies:
- '@75lb/nature'
'@proj-airi/lobe-icons@1.0.8': {}
'@proj-airi/lobe-icons@1.0.10': {}
'@proj-airi/server-sdk@0.7.0-beta.1':
dependencies: