feat(speech): refactor voice pack handling and remove synthetic model references

This commit is contained in:
RainbowBird
2026-07-01 02:47:31 +08:00
parent 4f80affcd3
commit 1f1ef49eed
3 changed files with 230 additions and 124 deletions
@@ -16,7 +16,7 @@ import {
import { useAnalytics } from '@proj-airi/stage-ui/composables'
import { OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID } from '@proj-airi/stage-ui/libs/providers/providers/official'
import { useAiriCardStore, useVoicePacksStore } from '@proj-airi/stage-ui/stores'
import { useSpeechStore, VOICE_PACK_MODEL_ID, voicePackForSpeechProvider } from '@proj-airi/stage-ui/stores/modules/speech'
import { useSpeechStore, voicePackForSpeechProvider } from '@proj-airi/stage-ui/stores/modules/speech'
import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers'
import {
FieldCheckbox,
@@ -72,43 +72,120 @@ const isGenerating = ref(false)
const audioUrl = ref('')
const audioPlayer = ref<HTMLAudioElement | null>(null)
const errorMessage = ref('')
const selectedSpeechSource = ref<string | null>(null)
const VOICE_PACK_MODEL_OPTION = {
id: VOICE_PACK_MODEL_ID,
name: 'Voice Pack',
description: 'Server-curated voices',
}
const VOICE_PACK_SOURCE_ID = 'voice-pack'
const STREAMING_MODEL_OPTION_PREFIX = 'streaming:'
const isOfficialSpeechProvider = computed(() => activeSpeechProvider.value === OFFICIAL_SPEECH_PROVIDER_ID)
const shouldShowVoicePackModel = computed(() => isOfficialSpeechProvider.value && voicePacks.value.length > 0)
const isVoicePackModelSelected = computed(() => shouldShowVoicePackModel.value && activeSpeechModel.value === VOICE_PACK_MODEL_ID)
const boundVoicePack = computed(() =>
voicePackForSpeechProvider(activeSpeechProvider.value, activeCard.value?.extensions.airi.modules.speech.voicePack),
)
const selectableSpeechProvidersMetadata = computed(() => {
const selectableSpeechSources = computed(() => {
const configuredSources = configuredSpeechProvidersMetadata.value
.filter(metadata =>
metadata.id !== 'speech-noop'
&& metadata.id !== OFFICIAL_SPEECH_STREAMING_PROVIDER_ID,
)
.map(metadata => ({
id: metadata.id,
providerId: metadata.id,
title: metadata.localizedName || 'Unknown',
description: metadata.localizedDescription,
}))
return [
...configuredSpeechProvidersMetadata.value.filter(metadata => metadata.id !== 'speech-noop'),
...allAudioSpeechProvidersMetadata.value.filter(metadata => metadata.id === 'speech-noop'),
{
id: VOICE_PACK_SOURCE_ID,
providerId: undefined,
title: 'Voice Pack',
description: 'Server-curated voices',
},
...configuredSources,
...allAudioSpeechProvidersMetadata.value
.filter(metadata => metadata.id === 'speech-noop')
.map(metadata => ({
id: metadata.id,
providerId: metadata.id,
title: metadata.localizedName || 'Unknown',
description: metadata.localizedDescription,
})),
]
})
const displayedSpeechSource = computed({
get: () => {
if (selectedSpeechSource.value === VOICE_PACK_SOURCE_ID)
return VOICE_PACK_SOURCE_ID
if (boundVoicePack.value && isOfficialSpeechProvider.value)
return VOICE_PACK_SOURCE_ID
if (activeSpeechProvider.value === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID)
return OFFICIAL_SPEECH_PROVIDER_ID
return activeSpeechProvider.value
},
set: (value: string) => {
selectSpeechSource(value)
},
})
const isVoicePackSourceSelected = computed(() => displayedSpeechSource.value === VOICE_PACK_SOURCE_ID)
const isOfficialSpeechSourceSelected = computed(() => displayedSpeechSource.value === OFFICIAL_SPEECH_PROVIDER_ID)
function streamingModelOptionId(modelId: string) {
return `${STREAMING_MODEL_OPTION_PREFIX}${modelId}`
}
function modelIdFromStreamingOptionId(optionId: string) {
return optionId.startsWith(STREAMING_MODEL_OPTION_PREFIX)
? optionId.slice(STREAMING_MODEL_OPTION_PREFIX.length)
: null
}
const displayedProviderModels = computed(() => {
if (!shouldShowVoicePackModel.value)
if (!isOfficialSpeechSourceSelected.value)
return providerModels.value
return [VOICE_PACK_MODEL_OPTION, ...providerModels.value]
const regularModels = providersStore.getModelsForProvider(OFFICIAL_SPEECH_PROVIDER_ID)
const streamingModels = providersStore.getModelsForProvider(OFFICIAL_SPEECH_STREAMING_PROVIDER_ID)
return [
...regularModels,
...streamingModels.map(model => ({
...model,
id: streamingModelOptionId(model.id),
name: model.name,
description: model.description || 'Low-latency streaming TTS',
})),
]
})
const displayedSpeechModel = computed({
get: () => activeSpeechModel.value === VOICE_PACK_MODEL_ID && !shouldShowVoicePackModel.value
? ''
get: () => activeSpeechProvider.value === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID && activeSpeechModel.value
? streamingModelOptionId(activeSpeechModel.value)
: activeSpeechModel.value,
set: (value: string) => {
activeSpeechModel.value = value
selectSpeechModel(value)
},
})
const currentSpeechModelId = computed(() => activeSpeechModel.value || '')
const displayedModelsLoading = computed(() => {
if (!isOfficialSpeechSourceSelected.value)
return isLoadingActiveProviderModels.value
return providersStore.isLoadingModels[OFFICIAL_SPEECH_PROVIDER_ID]
|| providersStore.isLoadingModels[OFFICIAL_SPEECH_STREAMING_PROVIDER_ID]
|| false
})
const displayedModelError = computed(() => {
if (!isOfficialSpeechSourceSelected.value)
return activeProviderModelError.value
return providersStore.modelLoadError[OFFICIAL_SPEECH_PROVIDER_ID]
|| providersStore.modelLoadError[OFFICIAL_SPEECH_STREAMING_PROVIDER_ID]
|| null
})
function createVoicePackVoice(voicePack: VoicePackSnapshot): VoiceInfo {
return {
id: voicePack.voiceId,
@@ -129,32 +206,8 @@ function packIdFromVoicePackVoiceId(voiceId: string) {
return voiceId.startsWith('voice-pack:') ? voiceId.slice('voice-pack:'.length) : null
}
function createVoicePackPickerVoice(pack: (typeof voicePacks.value)[number]): VoiceInfo {
return {
id: voicePackVoiceId(pack.id),
name: pack.name,
description: pack.description ?? pack.name,
previewURL: '',
languages: [{ code: 'en', title: 'English' }],
provider: activeSpeechProvider.value,
gender: 'neutral',
}
}
function createVoicePackSnapshotPickerVoice(voicePack: VoicePackSnapshot): VoiceInfo {
return {
id: voicePackVoiceId(voicePack.packId),
name: voicePack.name,
description: voicePack.name,
previewURL: '',
languages: [{ code: 'en', title: 'English' }],
provider: activeSpeechProvider.value,
gender: 'neutral',
}
}
const displayedVoiceOptions = computed(() => {
if (isVoicePackModelSelected.value) {
if (isVoicePackSourceSelected.value) {
const options = voicePacks.value.map(pack => ({
id: voicePackVoiceId(pack.id),
name: pack.name,
@@ -191,14 +244,37 @@ const displayedVoiceOptions = computed(() => {
}))
})
const displayedSpeechVoiceId = computed({
get: () => {
if (isVoicePackSourceSelected.value && boundVoicePack.value)
return voicePackVoiceId(boundVoicePack.value.packId)
return activeSpeechVoiceId.value
},
set: (value: string) => {
if (isVoicePackSourceSelected.value) {
void selectSpeechVoice(value)
return
}
activeSpeechVoiceId.value = value
},
})
const currentSpeechVoiceId = computed(() => {
if (isVoicePackSourceSelected.value && boundVoicePack.value)
return boundVoicePack.value.voiceId
return activeSpeechVoiceId.value || ''
})
function syncBoundVoicePackSelection() {
const voicePack = boundVoicePack.value
if (!shouldShowVoicePackModel.value || !voicePack)
if (!voicePack)
return false
activeSpeechModel.value = VOICE_PACK_MODEL_ID
activeSpeechVoiceId.value = voicePackVoiceId(voicePack.packId)
activeSpeechVoice.value = createVoicePackSnapshotPickerVoice(voicePack)
selectedSpeechSource.value = VOICE_PACK_SOURCE_ID
activeSpeechProvider.value = OFFICIAL_SPEECH_PROVIDER_ID
activeSpeechModel.value = voicePack.ttsModelId
activeSpeechVoiceId.value = voicePack.voiceId
activeSpeechVoice.value = createVoicePackVoice(voicePack)
return true
}
@@ -206,7 +282,7 @@ function syncBoundVoicePackSelection() {
* Resolves the current TTS model id for low-cardinality analytics payloads.
*/
function currentTtsModelId() {
if (isVoicePackModelSelected.value && boundVoicePack.value)
if (isVoicePackSourceSelected.value && boundVoicePack.value)
return boundVoicePack.value.ttsModelId
return activeSpeechModel.value || 'unknown'
}
@@ -296,7 +372,7 @@ async function selectSpeechVoice(voiceId: string | undefined) {
return
const voicePackId = packIdFromVoicePackVoiceId(voiceId)
if (isVoicePackModelSelected.value && voicePackId) {
if (isVoicePackSourceSelected.value && voicePackId) {
const pack = voicePacks.value.find(item => item.id === voicePackId)
if (!pack)
return
@@ -313,6 +389,53 @@ async function selectSpeechVoice(voiceId: string | undefined) {
})
}
function selectSpeechSource(sourceId: string) {
selectedSpeechSource.value = sourceId === VOICE_PACK_SOURCE_ID ? VOICE_PACK_SOURCE_ID : null
if (sourceId === VOICE_PACK_SOURCE_ID) {
activeSpeechProvider.value = OFFICIAL_SPEECH_PROVIDER_ID
const voicePack = boundVoicePack.value
if (voicePack) {
activeSpeechModel.value = voicePack.ttsModelId
activeSpeechVoiceId.value = voicePack.voiceId
activeSpeechVoice.value = createVoicePackVoice(voicePack)
return
}
activeSpeechModel.value = ''
activeSpeechVoiceId.value = ''
activeSpeechVoice.value = undefined
return
}
if (sourceId === OFFICIAL_SPEECH_PROVIDER_ID && boundVoicePack.value) {
airiCardStore.updateActiveCardSpeech({
provider: OFFICIAL_SPEECH_PROVIDER_ID,
model: '',
voice_id: '',
})
}
activeSpeechProvider.value = sourceId
}
function selectSpeechModel(modelOptionId: string) {
const streamingModelId = modelIdFromStreamingOptionId(modelOptionId)
const nextProvider = streamingModelId == null
? activeSpeechProvider.value === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
? OFFICIAL_SPEECH_PROVIDER_ID
: activeSpeechProvider.value
: OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
const nextModel = streamingModelId ?? modelOptionId
if (activeSpeechProvider.value !== nextProvider) {
activeSpeechProvider.value = nextProvider
activeSpeechVoiceId.value = ''
activeSpeechVoice.value = undefined
}
activeSpeechModel.value = nextModel
}
// Sync OpenAI Compatible model and voice from provider config
function syncOpenAICompatibleSettings() {
if (activeSpeechProvider.value !== 'openai-compatible-audio-speech')
@@ -343,9 +466,11 @@ function syncOpenAICompatibleSettings() {
onMounted(async () => {
await providersStore.loadModelsForConfiguredProviders()
await voicePacksStore.load()
syncBoundVoicePackSelection()
speechStore.ensureActiveSpeechModel()
await speechStore.loadVoicesForProvider(activeSpeechProvider.value, activeSpeechModel.value || undefined)
const syncedVoicePack = syncBoundVoicePackSelection()
if (!syncedVoicePack) {
speechStore.ensureActiveSpeechModel()
await speechStore.loadVoicesForProvider(activeSpeechProvider.value, activeSpeechModel.value || undefined)
}
syncOpenAICompatibleSettings()
})
@@ -354,9 +479,19 @@ async function bindVoicePack(pack: (typeof voicePacks.value)[number]) {
if (!bound)
return
activeSpeechModel.value = VOICE_PACK_MODEL_ID
activeSpeechVoiceId.value = voicePackVoiceId(pack.id)
activeSpeechVoice.value = createVoicePackPickerVoice(pack)
selectedSpeechSource.value = VOICE_PACK_SOURCE_ID
activeSpeechProvider.value = OFFICIAL_SPEECH_PROVIDER_ID
activeSpeechModel.value = pack.ttsModelId
activeSpeechVoiceId.value = pack.voiceId
activeSpeechVoice.value = {
id: pack.voiceId,
name: pack.name,
description: pack.description ?? pack.name,
previewURL: '',
languages: [{ code: 'en', title: 'English' }],
provider: activeSpeechProvider.value,
gender: 'neutral',
}
trackVoicePackBound({
tts_provider_id: activeSpeechProvider.value || 'unknown',
@@ -379,12 +514,22 @@ watch(activeSpeechProvider, async (newProvider, oldProvider) => {
await providersStore.loadModelsForConfiguredProviders()
// Reset model and voice when switching providers (but not on initial load)
if (oldProvider !== undefined && oldProvider !== newProvider) {
const isMergedOfficialSwitch = (
oldProvider === OFFICIAL_SPEECH_PROVIDER_ID
|| oldProvider === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
) && (
newProvider === OFFICIAL_SPEECH_PROVIDER_ID
|| newProvider === OFFICIAL_SPEECH_STREAMING_PROVIDER_ID
)
if (oldProvider !== undefined && oldProvider !== newProvider && !isMergedOfficialSwitch) {
activeSpeechModel.value = ''
activeSpeechVoiceId.value = ''
activeSpeechVoice.value = undefined
}
if (isVoicePackSourceSelected.value)
return
// Re-seed the streaming default model after the reset above so its voices
// load model-scoped (the server only returns recommended voices for an
// explicit ?model=). No-op for other providers / when a model is selected.
@@ -406,7 +551,7 @@ watch(activeSpeechModel, async (model) => {
if (!activeSpeechProvider.value)
return
if (model === VOICE_PACK_MODEL_ID)
if (isVoicePackSourceSelected.value)
return
activeSpeechVoiceId.value = ''
@@ -416,18 +561,6 @@ watch(activeSpeechModel, async (model) => {
})
watch([activeSpeechProvider, activeSpeechModel, activeSpeechVoiceId], ([provider, model, voiceId]) => {
if (provider === OFFICIAL_SPEECH_PROVIDER_ID && model === VOICE_PACK_MODEL_ID) {
const voicePack = boundVoicePack.value
if (voicePack) {
airiCardStore.updateActiveCardSpeech({
provider,
model: voicePack.ttsModelId,
voice_id: voicePack.voiceId,
})
}
return
}
airiCardStore.updateActiveCardSpeech({ provider, model, voice_id: voiceId })
})
@@ -636,26 +769,26 @@ function handleDeleteProvider(providerId: string) {
</div>
<div max-w-full>
<fieldset
v-if="selectableSpeechProvidersMetadata.length > 0" flex="~ row gap-4"
v-if="selectableSpeechSources.length > 0" flex="~ row gap-4"
min-w-0 overflow-x-auto scroll-smooth role="radiogroup"
>
<RadioCardSimple
v-for="metadata in selectableSpeechProvidersMetadata"
:id="metadata.id"
:key="metadata.id"
v-model="activeSpeechProvider"
v-for="source in selectableSpeechSources"
:id="source.id"
:key="source.id"
v-model="displayedSpeechSource"
name="speech-provider"
:value="metadata.id"
:title="metadata.localizedName || 'Unknown'"
:description="metadata.localizedDescription"
@click="selectSpeechProvider(metadata.id)"
:value="source.id"
:title="source.title"
:description="source.description"
@click="selectSpeechProvider(source.providerId || source.id)"
>
<template #topRight>
<button
v-if="metadata.id !== 'speech-noop' && !metadata.id.startsWith('official-provider')"
v-if="source.providerId && source.providerId !== 'speech-noop' && !source.providerId.startsWith('official-provider')"
type="button"
class="rounded bg-neutral-100 p-1 text-neutral-600 transition-colors dark:bg-neutral-800/60 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-700/60"
@click.stop.prevent="handleDeleteProvider(metadata.id)"
@click.stop.prevent="handleDeleteProvider(source.providerId)"
>
<div i-solar:trash-bin-trash-bold-duotone class="text-base" />
</button>
@@ -695,7 +828,7 @@ function handleDeleteProvider(providerId: string) {
</div>
<!-- Model selection section -->
<div v-if="activeSpeechProvider && activeSpeechProvider !== 'speech-noop'">
<div v-if="activeSpeechProvider && activeSpeechProvider !== 'speech-noop' && !isVoicePackSourceSelected">
<div flex="~ col gap-4">
<div>
<h2 class="text-lg md:text-2xl">
@@ -703,7 +836,7 @@ function handleDeleteProvider(providerId: string) {
</h2>
<div class="flex flex-col items-start gap-1 text-neutral-400 md:flex-row md:items-center md:justify-between dark:text-neutral-400">
<span>{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.subtitle') }}</span>
<span v-if="displayedSpeechModel" class="text-sm text-neutral-400 font-medium dark:text-neutral-400">{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.current_model_label') }} {{ displayedSpeechModel }}</span>
<span v-if="currentSpeechModelId" class="text-sm text-neutral-400 font-medium dark:text-neutral-400">{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.current_model_label') }} {{ currentSpeechModelId }}</span>
</div>
</div>
@@ -721,7 +854,7 @@ function handleDeleteProvider(providerId: string) {
<!-- Model listing for other providers -->
<div v-else-if="supportsModelListing" class="flex flex-col gap-4">
<!-- Loading state -->
<div v-if="isLoadingActiveProviderModels" class="flex items-center justify-center py-4">
<div v-if="displayedModelsLoading" class="flex items-center justify-center py-4">
<div class="mr-2 animate-spin">
<div i-solar:spinner-line-duotone text-xl />
</div>
@@ -729,10 +862,10 @@ function handleDeleteProvider(providerId: string) {
</div>
<!-- Error state -->
<template v-else-if="activeProviderModelError">
<template v-else-if="displayedModelError">
<ErrorContainer
:title="t('settings.pages.modules.consciousness.sections.section.provider-model-selection.error')"
:error="activeProviderModelError"
:error="displayedModelError"
/>
<FieldInput
@@ -745,7 +878,7 @@ function handleDeleteProvider(providerId: string) {
</template>
<!-- No models available -->
<template v-else-if="displayedProviderModels.length === 0 && !isLoadingActiveProviderModels">
<template v-else-if="displayedProviderModels.length === 0 && !displayedModelsLoading">
<Alert type="warning">
<template #title>
{{ t('settings.pages.modules.consciousness.sections.section.provider-model-selection.no_models') }}
@@ -793,13 +926,14 @@ function handleDeleteProvider(providerId: string) {
<h2 class="text-lg text-neutral-500 md:text-2xl dark:text-neutral-400">
Voice Configuration
</h2>
<div text="neutral-400 dark:neutral-500">
<div class="flex flex-col items-start gap-1 text-neutral-400 md:flex-row md:items-center md:justify-between dark:text-neutral-500">
<span>Customize how your AI assistant speaks</span>
<span v-if="currentSpeechVoiceId" class="text-sm text-neutral-400 font-medium dark:text-neutral-400">Current voice: {{ currentSpeechVoiceId }}</span>
</div>
</div>
<!-- Loading state -->
<div v-if="isLoadingSpeechProviderVoices || (isVoicePackModelSelected && isLoadingVoicePacks)">
<div v-if="isLoadingSpeechProviderVoices || (isVoicePackSourceSelected && isLoadingVoicePacks)">
<div class="flex flex-col gap-4">
<Skeleton class="w-full rounded-lg p-2.5 text-sm">
<div class="h-1lh" />
@@ -829,7 +963,7 @@ function handleDeleteProvider(providerId: string) {
>
<VoiceCardManySelect
v-model:search-query="voiceSearchQuery"
v-model:voice-id="activeSpeechVoiceId"
v-model:voice-id="displayedSpeechVoiceId"
:show-visualizer="false"
:voices="displayedVoiceOptions"
:searchable="true"
@@ -850,7 +984,7 @@ function handleDeleteProvider(providerId: string) {
</div>
<ErrorContainer
v-else-if="isVoicePackModelSelected && voicePacksError"
v-else-if="isVoicePackSourceSelected && voicePacksError"
class="mb-2"
:title="t('settings.pages.modules.speech.sections.section.voice-pack.error')"
:error="voicePacksError"
@@ -898,7 +1032,7 @@ function handleDeleteProvider(providerId: string) {
<!-- Manual voice input when no voices are available or for OpenAI Compatible -->
<div
v-if="!isVoicePackModelSelected && (activeSpeechProvider === 'openai-compatible-audio-speech' || !availableVoices[activeSpeechProvider] || availableVoices[activeSpeechProvider].length === 0)"
v-if="!isVoicePackSourceSelected && (activeSpeechProvider === 'openai-compatible-audio-speech' || !availableVoices[activeSpeechProvider] || availableVoices[activeSpeechProvider].length === 0)"
class="mt-2 space-y-6"
>
<FieldInput
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID, providerOfficialSpeech } from '../../libs/providers/providers/official'
import { useProvidersStore } from '../providers'
import { toSignedPercent, useSpeechStore, VOICE_PACK_MODEL_ID, voicePackForSpeechProvider } from './speech'
import { toSignedPercent, useSpeechStore, voicePackForSpeechProvider } from './speech'
const i18nState = vi.hoisted(() => ({
locale: { value: 'en-US' },
@@ -294,40 +294,21 @@ describe('speech store helpers', () => {
* @example
* speechStore.ensureActiveSpeechModel()
*/
it('keeps the synthetic Voice Pack model selected for the regular official provider', () => {
it('keeps a real Voice Pack TTS model selected for the regular official provider', () => {
const providersStore = useProvidersStore()
const speechStore = useSpeechStore()
speechStore.activeSpeechProvider = OFFICIAL_SPEECH_PROVIDER_ID
speechStore.activeSpeechModel = VOICE_PACK_MODEL_ID
speechStore.activeSpeechVoiceId = 'voice-pack:vp-1'
speechStore.activeSpeechModel = 'volcengine/pool-a'
speechStore.activeSpeechVoiceId = 'voice-a'
providersStore.providerRuntimeState[OFFICIAL_SPEECH_PROVIDER_ID].models = [
{ id: 'volcengine/pool-a', name: 'volcengine/pool-a', provider: OFFICIAL_SPEECH_PROVIDER_ID },
{ id: 'microsoft/v1', name: 'microsoft/v1', provider: OFFICIAL_SPEECH_PROVIDER_ID },
]
speechStore.ensureActiveSpeechModel()
expect(speechStore.activeSpeechModel).toBe(VOICE_PACK_MODEL_ID)
expect(speechStore.activeSpeechVoiceId).toBe('voice-pack:vp-1')
})
/**
* @example
* await speechStore.loadVoicesForProvider(OFFICIAL_SPEECH_PROVIDER_ID, VOICE_PACK_MODEL_ID)
*/
it('does not request raw official voices for the synthetic Voice Pack model', async () => {
const providersStore = useProvidersStore()
const speechStore = useSpeechStore()
const listVoices = vi.fn(async () => [{ id: 'raw-voice', name: 'Raw', provider: OFFICIAL_SPEECH_PROVIDER_ID, languages: [] }])
const metadata = providersStore.providerMetadata[OFFICIAL_SPEECH_PROVIDER_ID]
metadata.capabilities.listVoices = listVoices
const voices = await speechStore.loadVoicesForProvider(
OFFICIAL_SPEECH_PROVIDER_ID,
VOICE_PACK_MODEL_ID,
)
expect(voices).toEqual([])
expect(listVoices).not.toHaveBeenCalled()
expect(speechStore.activeSpeechModel).toBe('volcengine/pool-a')
expect(speechStore.activeSpeechVoiceId).toBe('voice-a')
})
/**
@@ -42,8 +42,6 @@ interface VoicePackSpeechInput {
const voicePackSupportedParams = new Set(['pitch', 'rate', 'volume'])
export const VOICE_PACK_MODEL_ID = 'voice-pack'
export function voicePackForSpeechProvider(
providerId: string | undefined,
voicePack: VoicePackSnapshot | undefined,
@@ -207,10 +205,6 @@ export const useSpeechStore = defineStore('speech', () => {
return []
}
if (provider === OFFICIAL_SPEECH_PROVIDER_ID && model === VOICE_PACK_MODEL_ID) {
return []
}
// Streaming provider visibility is server-driven and only confirmed after
// the auth probe force-configures it. Keep the gate at the public loader so
// pages cannot bypass it and issue `/voices/streaming` while unavailable.
@@ -288,9 +282,6 @@ export const useSpeechStore = defineStore('speech', () => {
if (!models.length)
return
if (activeSpeechModel.value === VOICE_PACK_MODEL_ID)
return
const hasValidSelection = !!activeSpeechModel.value && models.some(m => m.id === activeSpeechModel.value)
if (hasValidSelection)
return