fix(stage-pages): treat comet speech as openai-compatible as it doesn't support voice listing (#1060)

This commit is contained in:
Rin
2026-03-02 01:34:28 +08:00
committed by GitHub
parent 22423d4ef7
commit ea6f4dfb63
@@ -2,14 +2,16 @@
import type { SpeechProvider } from '@xsai-ext/providers/utils'
import {
SpeechPlayground,
Alert,
SpeechPlaygroundOpenAICompatible,
SpeechProviderSettings,
} from '@proj-airi/stage-ui/components'
import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation'
import { useSpeechStore } from '@proj-airi/stage-ui/stores/modules/speech'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import { FieldRange } from '@proj-airi/ui'
import { FieldInput, FieldRange } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { computed, ref, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const speechStore = useSpeechStore()
@@ -23,19 +25,77 @@ const defaultVoiceSettings = {
// Get provider metadata
const providerId = 'comet-api-speech'
const defaultModel = 'gpt-4o-mini-tts'
const defaultModel = 'tts' // https://www.cometapi.com/models/openai/tts/
const speed = ref<number>(1.0)
// Initialize speed from provider config or default
const speed = ref<number>(
(providers.value[providerId] as any)?.voiceSettings?.speed
|| (providers.value[providerId] as any)?.speed
|| defaultVoiceSettings.speed,
)
// Model selection
const model = computed({
get: () => providers.value[providerId]?.model as string | undefined || defaultModel,
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].model = value
},
})
const voice = computed({
get: () => providers.value[providerId]?.voice || 'alloy',
set: (value) => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].voice = value
},
})
// Watch provider config changes to sync local refs (for reset functionality)
watch(
() => providers.value[providerId],
(newConfig) => {
if (newConfig) {
const config = newConfig as any
const newSpeed = config.voiceSettings?.speed || config.speed || defaultVoiceSettings.speed
if (Math.abs(speed.value - newSpeed) > 0.001)
speed.value = newSpeed
if (!config.model && model.value !== defaultModel)
model.value = defaultModel
if (!config.voice && voice.value !== 'alloy')
voice.value = 'alloy'
}
else {
speed.value = defaultVoiceSettings.speed
model.value = defaultModel
voice.value = 'alloy'
}
},
{ deep: true, immediate: true },
)
// Check if API key is configured
const apiKeyConfigured = computed(() => !!providers.value[providerId]?.apiKey)
const availableVoices = computed(() => {
return speechStore.availableVoices[providerId] || []
// Ensure provider config is initialized on mount
onMounted(() => {
if (!providers.value[providerId]) {
providers.value[providerId] = {}
}
if (!providers.value[providerId].model) {
providers.value[providerId].model = defaultModel
}
if (!providers.value[providerId].voice) {
providers.value[providerId].voice = 'alloy'
}
})
// Generate speech with ElevenLabs-specific parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean) {
// Generate speech with OpenAI-compatible parameters
async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: boolean, modelId?: string) {
const provider = await providersStore.getProviderInstance<SpeechProvider<string>>(providerId)
if (!provider) {
throw new Error('Failed to initialize speech provider')
@@ -44,26 +104,47 @@ async function handleGenerateSpeech(input: string, voiceId: string, _useSSML: bo
// Get provider configuration
const providerConfig = providersStore.getProviderConfig(providerId)
// Get model from configuration or use default
const model = providerConfig.model as string | undefined || defaultModel
// Use the reactive model computed property (not a local variable)
const modelToUse = modelId || model.value || defaultModel
// ElevenLabs doesn't need SSML conversion, but if SSML is provided, use it directly
return await speechStore.speech(
provider,
model,
modelToUse,
input,
voiceId,
voiceId || (voice.value as string),
{
...providerConfig,
...defaultVoiceSettings,
speed: speed.value,
},
)
}
watch(speed, async () => {
const providerConfig = providersStore.getProviderConfig(providerId)
providerConfig.speed = speed.value
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].speed = speed.value
})
watch(model, () => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].model = model.value
})
watch(voice, () => {
if (!providers.value[providerId])
providers.value[providerId] = {}
providers.value[providerId].voice = voice.value
})
// Use the composable to get validation logic and state
const {
isValidating,
isValid,
validationMessage,
forceValid,
} = useProviderValidation(providerId)
</script>
<template>
@@ -72,8 +153,15 @@ watch(speed, async () => {
:default-model="defaultModel"
:additional-settings="defaultVoiceSettings"
>
<!-- Voice settings specific to ElevenLabs -->
<!-- Voice settings specific to Comet API -->
<template #voice-settings>
<!-- Model input -->
<FieldInput
v-model="model"
label="Model"
description="Enter the TTS model to use for speech generation"
placeholder="gpt-4o-mini-tts"
/>
<!-- Speed control - common to most providers -->
<FieldRange
v-model="speed"
@@ -85,13 +173,42 @@ watch(speed, async () => {
</template>
<template #playground>
<SpeechPlayground
:available-voices="availableVoices"
<SpeechPlaygroundOpenAICompatible
v-model:model-value="model"
v-model:voice="voice as any"
:generate-speech="handleGenerateSpeech"
:api-key-configured="apiKeyConfigured"
default-text="Hello! This is a test of the OpenAI Speech."
default-text="Hello! This is a test of the CometAPI Speech."
/>
</template>
<!-- Validation Status -->
<template #advanced-settings>
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
<template #title>
<div class="w-full flex items-center justify-between">
<span>{{ t('settings.dialogs.onboarding.validationFailed') }}</span>
<button
type="button"
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
@click="forceValid"
>
{{ t('settings.pages.providers.common.continueAnyway') }}
</button>
</div>
</template>
<template v-if="validationMessage" #content>
<div class="whitespace-pre-wrap break-all">
{{ validationMessage }}
</div>
</template>
</Alert>
<Alert v-if="isValid && isValidating === 0" type="success">
<template #title>
{{ t('settings.dialogs.onboarding.validationSuccess') }}
</template>
</Alert>
</template>
</SpeechProviderSettings>
</template>