Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/stage-pocket/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function isEnvTruthy(value: string | undefined | null): boolean {

const stageUIAssetsRoot = resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src', 'assets'))
const sharedCacheDir = resolve(join(import.meta.dirname, '..', '..', '.cache'))
const FISH_AUDIO_PROXY_RE = /^\/fish-audio-api/

export default defineConfig({
optimizeDeps: {
Expand Down Expand Up @@ -83,6 +84,18 @@ export default defineConfig({
// See: https://vite.dev/config/server-options#server-fs-strict
strict: false,
},
// NOTICE: Fish Audio's API is server-to-server only and doesn't send
// Access-Control-Allow-Origin headers for browser origins, so direct
// fetch() calls from the browser are blocked by CORS. We proxy them
// through the local Vite dev server so they appear same-origin.
// See packages/stage-ui/src/stores/providers.ts for the matching client-side usage.
proxy: {
'/fish-audio-api': {
target: 'https://api.fish.audio',
changeOrigin: true,
rewrite: (path: string) => path.replace(FISH_AUDIO_PROXY_RE, ''),
},
},
warmup: {
clientFiles: [
`${resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src'))}/*.vue`,
Expand Down
13 changes: 13 additions & 0 deletions apps/stage-web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { VitePWA } from 'vite-plugin-pwa'

const stageUIAssetsRoot = resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src', 'assets'))
const sharedCacheDir = resolve(join(import.meta.dirname, '..', '..', '.cache'))
const FISH_AUDIO_PROXY_RE = /^\/fish-audio-api/

function hasFlagEnableMkcert(): boolean {
if (process.argv.includes('--mkcert')) {
Expand Down Expand Up @@ -83,6 +84,18 @@ export default defineConfig({
// See: https://vite.dev/config/server-options#server-fs-strict
strict: false,
},
// NOTICE: Fish Audio's API is server-to-server only and doesn't send
// Access-Control-Allow-Origin headers for browser origins, so direct
// fetch() calls from the browser are blocked by CORS. We proxy them
// through the local Vite dev server so they appear same-origin.
// See packages/stage-ui/src/stores/providers.ts for the matching client-side usage.
proxy: {
'/fish-audio-api': {
target: 'https://api.fish.audio',
changeOrigin: true,
rewrite: (path: string) => path.replace(FISH_AUDIO_PROXY_RE, ''),
},
},
warmup: {
clientFiles: [
`${resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'src'))}/*.vue`,
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/en/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,9 @@ pages:
default-text: Hello! This is a test of the Kokoro text-to-speech system.
title: Voice Playground
title: Kokoro TTS (Local)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: fireworks.ai
title: Fireworks.ai
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/es/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ pages:
default-text: '¡Hola! Esta es una prueba del sistema de texto a voz de Kokoro.'
title: Campo de voz
title: TTS de Kokoro (local)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: fireworks.ai
title: Fireworks.ai
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/fr/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ pages:
default-text: Bonjour ! Ceci est un test du système de synthèse vocale Kokoro.
title: Zone d'essai vocale
title: Kokoro TTS (Local)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: fireworks.ai
title: Fireworks.ai
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/ja/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ pages:
default-text: こんにちは!これは音声合成システム Kokoro のテストです。
title: 音声合成実験場
title: Kokoro TTS (ローカル)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: fireworks.ai
title: Fireworks.ai
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/ko/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ pages:
default-text: 안녕하세요! 이것은 Kokoro 문자 음성 변환 시스템의 테스트입니다.
title: 음성 플레이그라운드
title: Kokoro TTS (로컬)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: fireworks.ai
title: Fireworks.ai
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/ru/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ pages:
default-text: Привет! Это тест-системы синтеза речи.
title: Голосовая платформа
title: Kokoro TTS (Local)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: Fireworks.ai
title: Fireworks.ai
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/vi/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ pages:
default-text: Xin chào! Đây là bản thử nghiệm hệ thống giọng nói Kokoro.
title: Thử nghiệm giọng nói
title: Kokoro TTS (Cục bộ)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: fireworks.ai
title: Fireworks.ai
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/zh-Hans/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,9 @@ pages:
default-text: 您好!这是 Kokoro 文本转语音(TTS)系统的测试。
title: 实验平台
title: Kokoro TTS (本地)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: Fireworks.ai
title: Fireworks.ai
Expand Down
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/zh-Hant/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ pages:
default-text: 您好!這是 Kokoro 文字轉語音系統的測試。
title: 語音測試場
title: Kokoro TTS (本地)
fish-audio:
description: fish.audio
title: Fish Audio
fireworks:
description: Fireworks.ai
title: Fireworks.ai
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<script setup lang="ts">
import type { SpeechProviderWithExtraOptions } from '@xsai-ext/providers/utils'

import {
SpeechPlayground,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { useDebounceFn } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, watch } from 'vue'

const providerId = 'fish-audio'
const defaultModel = 's2-pro'

const speechStore = useSpeechStore()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore)

const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)

const availableVoices = computed(() => speechStore.availableVoices[providerId] || [])

async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
const provider = await providersStore.getProviderInstance(providerId) as SpeechProviderWithExtraOptions<string>
if (!provider) {
throw new Error('Failed to initialize speech provider')
}

const providerConfig = providersStore.getProviderConfig(providerId)
const model = providerConfig.model as string | undefined || defaultModel

return await speechStore.speech(
provider,
model,
input,
voiceId,
{ ...providerConfig },
)
}

async function tryLoadVoices() {
if (apiKeyConfigured.value) {
await speechStore.loadVoicesForProvider(providerId)
}
}

// Debounced so rapid keystrokes while editing API key / base URL don't fire
// repeated requests with partial/invalid credentials.
const debouncedLoadVoices = useDebounceFn(tryLoadVoices, 800)

onMounted(tryLoadVoices)

// Reload voices whenever the API key or base URL changes
watch(
() => [providers.value[providerId]?.apiKey, providers.value[providerId]?.baseUrl],
debouncedLoadVoices,
)
</script>

<template>
<SpeechProviderSettings
:provider-id="providerId"
:default-model="defaultModel"
>
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
:voices-loading="speechStore.isLoadingSpeechProviderVoices"
default-text="Hello! This is a test of the Fish Audio voice synthesis."
/>
</template>
</SpeechProviderSettings>
</template>

<route lang="yaml">
meta:
layout: settings
stageTransition:
name: slide
</route>
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ defineSlots<{
'advanced-settings': (props: any) => any
'playground': (props: any) => any
}>()
const { t } = useI18n()
const router = useRouter()
const providersStore = useProvidersStore()
Expand Down
8 changes: 7 additions & 1 deletion packages/stage-ui/src/components/scenes/Stage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,13 @@ const speechPipeline = createSpeechPipeline<AudioBuffer>({
const audioBuffer = await audioContext.decodeAudioData(res)
return audioBuffer
}
catch {
catch (error) {
console.error('[Speech Pipeline] TTS failed:', {
provider: activeSpeechProvider.value,
model: activeSpeechModel.value,
voice: activeSpeechVoice.value?.id,
error,
})
return null
}
},
Expand Down
Loading
Loading