feat(server): add default TTS model handling and update related tests
This commit is contained in:
@@ -22,6 +22,15 @@ export const OFFICIAL_TRANSCRIPTION_PROVIDER_ID = 'official-provider-transcripti
|
||||
// first-voice matching when the server returns no recommendations.
|
||||
const recommendedVoicesByProvider: Record<string, Record<string, string>> = {}
|
||||
|
||||
// Server-curated default HTTP speech model id, populated by the HTTP speech
|
||||
// provider's listModels(). The speech store uses this when it needs to seed an
|
||||
// empty/stale model selection, so the UI mirrors `/audio/speech` `model: auto`.
|
||||
let defaultSpeechModelId: string | null = null
|
||||
|
||||
export function getDefaultSpeechModel(): string | null {
|
||||
return defaultSpeechModelId
|
||||
}
|
||||
|
||||
// Server-curated default streaming model id, populated by the streaming
|
||||
// provider's listModels(). Pages that need to seed an initial model selection
|
||||
// read this via getDefaultStreamingModel() instead of hardcoding an id.
|
||||
@@ -117,14 +126,17 @@ export const providerOfficialSpeech = defineProvider({
|
||||
validationRequiredWhen: () => false,
|
||||
extraMethods: {
|
||||
listModels: async (): Promise<ModelInfo[]> => {
|
||||
defaultSpeechModelId = null
|
||||
const res = await globalThis.fetch(`${SERVER_URL}/api/v1/audio/models`, { headers: authHeaders() })
|
||||
if (!res.ok)
|
||||
throw new Error(`audio models upstream ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 256))
|
||||
|
||||
const data = await res.json() as { models?: { id: string, name: string }[] }
|
||||
const data = await res.json() as { models?: { id: string, name: string }[], default?: string | null }
|
||||
if (!Array.isArray(data.models))
|
||||
throw new Error('audio models upstream returned malformed body')
|
||||
|
||||
defaultSpeechModelId = typeof data.default === 'string' && data.default.length > 0 ? data.default : null
|
||||
|
||||
return data.models.map(m => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID } from '../../libs/providers/providers/official'
|
||||
import { OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID, providerOfficialSpeech } from '../../libs/providers/providers/official'
|
||||
import { useProvidersStore } from '../providers'
|
||||
import { toSignedPercent, useSpeechStore, voicePackForSpeechProvider } from './speech'
|
||||
|
||||
@@ -289,7 +289,29 @@ describe('speech store helpers', () => {
|
||||
* @example
|
||||
* speechStore.ensureActiveSpeechModel()
|
||||
*/
|
||||
it('resets stale streaming model when the regular official speech provider is active', () => {
|
||||
it('resets stale streaming model to the server default when the regular official speech provider is active', async () => {
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: vi.fn(() => null),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
})
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = input.toString()
|
||||
if (url.includes('/api/v1/audio/models')) {
|
||||
return new Response(JSON.stringify({
|
||||
models: [
|
||||
{ id: 'alibaba/cosyvoice-v2', name: 'alibaba/cosyvoice-v2' },
|
||||
{ id: 'microsoft/v1', name: 'microsoft/v1' },
|
||||
],
|
||||
default: 'microsoft/v1',
|
||||
}), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
||||
}
|
||||
return new Response(JSON.stringify({ voices: [], recommended: {} }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}) as typeof fetch)
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const speechStore = useSpeechStore()
|
||||
speechStore.activeSpeechProvider = OFFICIAL_SPEECH_PROVIDER_ID
|
||||
@@ -301,15 +323,20 @@ describe('speech store helpers', () => {
|
||||
provider: OFFICIAL_SPEECH_STREAMING_PROVIDER_ID,
|
||||
languages: [],
|
||||
}
|
||||
providersStore.providerRuntimeState[OFFICIAL_SPEECH_PROVIDER_ID].models = [
|
||||
{ id: 'microsoft/v1', name: 'microsoft/v1', provider: OFFICIAL_SPEECH_PROVIDER_ID },
|
||||
{ id: 'alibaba/cosyvoice-v2', name: 'alibaba/cosyvoice-v2', provider: OFFICIAL_SPEECH_PROVIDER_ID },
|
||||
]
|
||||
try {
|
||||
providersStore.providerRuntimeState[OFFICIAL_SPEECH_PROVIDER_ID].models = await providerOfficialSpeech.extraMethods!.listModels!(
|
||||
{},
|
||||
providerOfficialSpeech.createProvider({}),
|
||||
)
|
||||
|
||||
speechStore.ensureActiveSpeechModel()
|
||||
speechStore.ensureActiveSpeechModel()
|
||||
|
||||
expect(speechStore.activeSpeechModel).toBe('microsoft/v1')
|
||||
expect(speechStore.activeSpeechVoiceId).toBe('')
|
||||
expect(speechStore.activeSpeechVoice).toBeUndefined()
|
||||
expect(speechStore.activeSpeechModel).toBe('microsoft/v1')
|
||||
expect(speechStore.activeSpeechVoiceId).toBe('')
|
||||
expect(speechStore.activeSpeechVoice).toBeUndefined()
|
||||
}
|
||||
finally {
|
||||
vi.unstubAllGlobals()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { toXml } from 'xast-util-to-xml'
|
||||
import { x } from 'xastscript'
|
||||
|
||||
import { getDefaultStreamingModel, OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID, setupOfficialSpeechAutoPick } from '../../libs/providers/providers/official'
|
||||
import { getDefaultSpeechModel, getDefaultStreamingModel, OFFICIAL_SPEECH_PROVIDER_ID, OFFICIAL_SPEECH_STREAMING_PROVIDER_ID, setupOfficialSpeechAutoPick } from '../../libs/providers/providers/official'
|
||||
import { useProvidersStore } from '../providers'
|
||||
|
||||
export function toSignedPercent(value: number): string {
|
||||
@@ -286,7 +286,10 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
if (hasValidSelection)
|
||||
return
|
||||
|
||||
activeSpeechModel.value = models[0]?.id ?? ''
|
||||
const defaultModel = getDefaultSpeechModel()
|
||||
activeSpeechModel.value = defaultModel && models.some(m => m.id === defaultModel)
|
||||
? defaultModel
|
||||
: models[0]?.id ?? ''
|
||||
clearVoiceSelection()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user