feat: add upstreamVoiceId to voice packs and related services
- Introduced upstreamVoiceId field in voice pack schema and database. - Updated voice pack service to handle upstreamVoiceId in CRUD operations. - Modified API routes and tests to accommodate upstreamVoiceId. - Enhanced UI components to include upstreamVoiceId in forms and displays. - Adjusted speech processing logic to utilize upstreamVoiceId where applicable. - Updated related tests to ensure proper functionality with new field.
This commit is contained in:
@@ -75,6 +75,8 @@ const errorMessage = ref('')
|
||||
const selectedSpeechSource = ref<string | null>(null)
|
||||
|
||||
const VOICE_PACK_SOURCE_ID = 'voice-pack'
|
||||
const VOICE_PACK_REQUEST_MODEL_ID = 'auto'
|
||||
const VOICE_PACK_ANALYTICS_MODEL_ID = 'voice_pack'
|
||||
const STREAMING_MODEL_OPTION_PREFIX = 'streaming:'
|
||||
|
||||
const isOfficialSpeechProvider = computed(() => activeSpeechProvider.value === OFFICIAL_SPEECH_PROVIDER_ID)
|
||||
@@ -198,6 +200,15 @@ function createVoicePackVoice(voicePack: VoicePackSnapshot): VoiceInfo {
|
||||
}
|
||||
}
|
||||
|
||||
function formatVoicePackCostMultiplier(costMultiplier: number) {
|
||||
return `Flux cost: ${costMultiplier}x`
|
||||
}
|
||||
|
||||
function voicePackDescription(description: string | null | undefined, costMultiplier: number) {
|
||||
const cost = formatVoicePackCostMultiplier(costMultiplier)
|
||||
return description ? `${description} · ${cost}` : cost
|
||||
}
|
||||
|
||||
function voicePackVoiceId(packId: string) {
|
||||
return `voice-pack:${packId}`
|
||||
}
|
||||
@@ -211,7 +222,7 @@ const displayedVoiceOptions = computed(() => {
|
||||
const options = voicePacks.value.map(pack => ({
|
||||
id: voicePackVoiceId(pack.id),
|
||||
name: pack.name,
|
||||
description: pack.description ?? undefined,
|
||||
description: voicePackDescription(pack.description, pack.costMultiplier),
|
||||
previewURL: '',
|
||||
customizable: false,
|
||||
}))
|
||||
@@ -221,7 +232,7 @@ const displayedVoiceOptions = computed(() => {
|
||||
options.unshift({
|
||||
id: frozenVoiceId,
|
||||
name: voicePack.name,
|
||||
description: voicePack.name,
|
||||
description: voicePackDescription(voicePack.name, voicePack.costMultiplier),
|
||||
previewURL: '',
|
||||
customizable: false,
|
||||
})
|
||||
@@ -265,6 +276,12 @@ const currentSpeechVoiceId = computed(() => {
|
||||
return activeSpeechVoiceId.value || ''
|
||||
})
|
||||
|
||||
const currentVoicePackCostMultiplier = computed(() => {
|
||||
if (isVoicePackSourceSelected.value && boundVoicePack.value)
|
||||
return formatVoicePackCostMultiplier(boundVoicePack.value.costMultiplier)
|
||||
return ''
|
||||
})
|
||||
|
||||
function syncBoundVoicePackSelection() {
|
||||
const voicePack = boundVoicePack.value
|
||||
if (!voicePack)
|
||||
@@ -272,7 +289,7 @@ function syncBoundVoicePackSelection() {
|
||||
|
||||
selectedSpeechSource.value = VOICE_PACK_SOURCE_ID
|
||||
activeSpeechProvider.value = OFFICIAL_SPEECH_PROVIDER_ID
|
||||
activeSpeechModel.value = voicePack.ttsModelId
|
||||
activeSpeechModel.value = VOICE_PACK_REQUEST_MODEL_ID
|
||||
activeSpeechVoiceId.value = voicePack.voiceId
|
||||
activeSpeechVoice.value = createVoicePackVoice(voicePack)
|
||||
return true
|
||||
@@ -283,7 +300,7 @@ function syncBoundVoicePackSelection() {
|
||||
*/
|
||||
function currentTtsModelId() {
|
||||
if (isVoicePackSourceSelected.value && boundVoicePack.value)
|
||||
return boundVoicePack.value.ttsModelId
|
||||
return VOICE_PACK_ANALYTICS_MODEL_ID
|
||||
return activeSpeechModel.value || 'unknown'
|
||||
}
|
||||
|
||||
@@ -396,7 +413,7 @@ function selectSpeechSource(sourceId: string) {
|
||||
activeSpeechProvider.value = OFFICIAL_SPEECH_PROVIDER_ID
|
||||
const voicePack = boundVoicePack.value
|
||||
if (voicePack) {
|
||||
activeSpeechModel.value = voicePack.ttsModelId
|
||||
activeSpeechModel.value = VOICE_PACK_REQUEST_MODEL_ID
|
||||
activeSpeechVoiceId.value = voicePack.voiceId
|
||||
activeSpeechVoice.value = createVoicePackVoice(voicePack)
|
||||
return
|
||||
@@ -481,12 +498,12 @@ async function bindVoicePack(pack: (typeof voicePacks.value)[number]) {
|
||||
|
||||
selectedSpeechSource.value = VOICE_PACK_SOURCE_ID
|
||||
activeSpeechProvider.value = OFFICIAL_SPEECH_PROVIDER_ID
|
||||
activeSpeechModel.value = pack.ttsModelId
|
||||
activeSpeechModel.value = VOICE_PACK_REQUEST_MODEL_ID
|
||||
activeSpeechVoiceId.value = pack.voiceId
|
||||
activeSpeechVoice.value = {
|
||||
id: pack.voiceId,
|
||||
name: pack.name,
|
||||
description: pack.description ?? pack.name,
|
||||
description: voicePackDescription(pack.description ?? pack.name, pack.costMultiplier),
|
||||
previewURL: '',
|
||||
languages: [{ code: 'en', title: 'English' }],
|
||||
provider: activeSpeechProvider.value,
|
||||
@@ -495,14 +512,14 @@ async function bindVoicePack(pack: (typeof voicePacks.value)[number]) {
|
||||
|
||||
trackVoicePackBound({
|
||||
tts_provider_id: activeSpeechProvider.value || 'unknown',
|
||||
tts_model_id: pack.ttsModelId,
|
||||
tts_model_id: VOICE_PACK_ANALYTICS_MODEL_ID,
|
||||
voice_id: pack.voiceId,
|
||||
voice_pack_id: pack.id,
|
||||
source: 'settings',
|
||||
})
|
||||
trackVoiceSelected({
|
||||
tts_provider_id: activeSpeechProvider.value || 'unknown',
|
||||
tts_model_id: pack.ttsModelId,
|
||||
tts_model_id: VOICE_PACK_ANALYTICS_MODEL_ID,
|
||||
voice_id: pack.voiceId,
|
||||
voice_type: 'voice_pack',
|
||||
voice_pack_id: pack.id,
|
||||
@@ -603,7 +620,7 @@ async function generateTestSpeech() {
|
||||
|
||||
const voicePack = boundVoicePack.value
|
||||
if (voicePack) {
|
||||
model = voicePack.ttsModelId
|
||||
model = VOICE_PACK_REQUEST_MODEL_ID
|
||||
if (!voice || voice.id !== voicePack.voiceId)
|
||||
voice = createVoicePackVoice(voicePack)
|
||||
}
|
||||
@@ -928,7 +945,10 @@ 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-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>
|
||||
<span v-if="currentSpeechVoiceId" class="text-sm text-neutral-400 font-medium dark:text-neutral-400">
|
||||
Current voice: {{ currentSpeechVoiceId }}
|
||||
<span v-if="currentVoicePackCostMultiplier">· {{ currentVoicePackCostMultiplier }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -964,7 +984,6 @@ function handleDeleteProvider(providerId: string) {
|
||||
<VoiceCardManySelect
|
||||
v-model:search-query="voiceSearchQuery"
|
||||
v-model:voice-id="displayedSpeechVoiceId"
|
||||
:show-visualizer="false"
|
||||
:voices="displayedVoiceOptions"
|
||||
:searchable="true"
|
||||
:search-placeholder="t('settings.pages.modules.speech.sections.section.provider-voice-selection.search_voices_placeholder')"
|
||||
|
||||
@@ -436,7 +436,7 @@ const speechPipeline = createSpeechPipeline<AudioBuffer>({
|
||||
|
||||
const voicePack = voicePackForSpeechProvider(activeSpeechProvider.value, activeCard.value?.extensions.airi.modules.speech.voicePack)
|
||||
if (voicePack) {
|
||||
model = voicePack.ttsModelId
|
||||
model = 'auto'
|
||||
if (!voice || voice.id !== voicePack.voiceId)
|
||||
voice = createVoicePackVoice(voicePack)
|
||||
}
|
||||
|
||||
@@ -117,10 +117,7 @@ describe('airi-card store', () => {
|
||||
const pack = {
|
||||
id: 'vp-1',
|
||||
name: 'Neuro Sama',
|
||||
provider: 'volcengine',
|
||||
model: 'seed-tts-2.0',
|
||||
voiceId: 'voice-neuro',
|
||||
ttsModelId: 'volcengine/neuro-pool',
|
||||
params: { pitch: '+20%', volume: '+5%' },
|
||||
costMultiplier: 1.5,
|
||||
}
|
||||
@@ -130,15 +127,12 @@ describe('airi-card store', () => {
|
||||
expect(bound).toBe(true)
|
||||
expect(cardStore.activeCard?.extensions.airi.modules.speech).toMatchObject({
|
||||
provider: OFFICIAL_SPEECH_PROVIDER_ID,
|
||||
model: 'volcengine/neuro-pool',
|
||||
model: 'auto',
|
||||
voice_id: 'voice-neuro',
|
||||
voicePack: {
|
||||
packId: 'vp-1',
|
||||
name: 'Neuro Sama',
|
||||
provider: 'volcengine',
|
||||
model: 'seed-tts-2.0',
|
||||
voiceId: 'voice-neuro',
|
||||
ttsModelId: 'volcengine/neuro-pool',
|
||||
params: { pitch: '+20%', volume: '+5%' },
|
||||
costMultiplier: 1.5,
|
||||
},
|
||||
@@ -157,12 +151,9 @@ describe('airi-card store', () => {
|
||||
cardStore.bindVoicePackToActiveCard({
|
||||
id: 'vp-1',
|
||||
name: 'Frozen',
|
||||
provider: 'volcengine',
|
||||
model: 'seed-tts-2.0',
|
||||
voiceId: 'voice-a',
|
||||
ttsModelId: 'volcengine/pool-a',
|
||||
params,
|
||||
costMultiplier: 1,
|
||||
costMultiplier: 2,
|
||||
})
|
||||
|
||||
params.pitch = '-10%'
|
||||
|
||||
@@ -23,10 +23,7 @@ export type VoicePackParams = Record<string, string | number | boolean | null>
|
||||
export interface VoicePackBindingInput {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
model: string
|
||||
voiceId: string
|
||||
ttsModelId: string
|
||||
params: VoicePackParams
|
||||
costMultiplier: number
|
||||
}
|
||||
@@ -34,10 +31,7 @@ export interface VoicePackBindingInput {
|
||||
export interface VoicePackSnapshot {
|
||||
packId: string
|
||||
name: string
|
||||
provider: string
|
||||
model: string
|
||||
voiceId: string
|
||||
ttsModelId: string
|
||||
params: VoicePackParams
|
||||
costMultiplier: number
|
||||
}
|
||||
@@ -210,8 +204,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
return updateActiveCardModules(({ modules }) => {
|
||||
const existingVoicePack = modules.speech.voicePack
|
||||
const shouldKeepVoicePack = speech.provider === OFFICIAL_SPEECH_PROVIDER_ID
|
||||
&& existingVoicePack?.ttsModelId === speech.model
|
||||
&& existingVoicePack.voiceId === speech.voice_id
|
||||
&& existingVoicePack?.voiceId === speech.voice_id
|
||||
|
||||
return {
|
||||
speech: {
|
||||
@@ -368,10 +361,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
const voicePack: VoicePackSnapshot = {
|
||||
packId: pack.id,
|
||||
name: pack.name,
|
||||
provider: pack.provider,
|
||||
model: pack.model,
|
||||
voiceId: pack.voiceId,
|
||||
ttsModelId: pack.ttsModelId,
|
||||
params: { ...pack.params },
|
||||
costMultiplier: pack.costMultiplier,
|
||||
}
|
||||
@@ -379,7 +369,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
const speech: AiriExtension['modules']['speech'] = {
|
||||
...extension.modules.speech,
|
||||
provider: OFFICIAL_SPEECH_PROVIDER_ID,
|
||||
model: pack.ttsModelId,
|
||||
model: 'auto',
|
||||
voice_id: pack.voiceId,
|
||||
voicePack,
|
||||
}
|
||||
@@ -399,7 +389,7 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
})
|
||||
|
||||
activeSpeechProvider.value = OFFICIAL_SPEECH_PROVIDER_ID
|
||||
activeSpeechModel.value = pack.ttsModelId
|
||||
activeSpeechModel.value = 'auto'
|
||||
activeSpeechVoiceId.value = pack.voiceId
|
||||
|
||||
return true
|
||||
|
||||
@@ -66,10 +66,7 @@ describe('speech store helpers', () => {
|
||||
const voicePack = {
|
||||
packId: 'vp-1',
|
||||
name: 'Frozen',
|
||||
provider: 'volcengine',
|
||||
model: 'seed-tts-2.0',
|
||||
voiceId: 'voice-a',
|
||||
ttsModelId: 'volcengine/pool-a',
|
||||
params: {},
|
||||
costMultiplier: 1,
|
||||
}
|
||||
@@ -198,9 +195,9 @@ describe('speech store helpers', () => {
|
||||
|
||||
/**
|
||||
* @example
|
||||
* speechStore.resolveVoicePackSpeechInput({ text, voice, voicePack: { packId: 'vp-1', costMultiplier: 1.5 } })
|
||||
* speechStore.resolveVoicePackSpeechInput({ text, voice, voicePack: { packId: 'vp-1' } })
|
||||
*/
|
||||
it('passes Voice Pack snapshot billing metadata through adapter options', () => {
|
||||
it('passes only Voice Pack identity through adapter options', () => {
|
||||
const speechStore = useSpeechStore()
|
||||
const voice = {
|
||||
id: 'voice-1',
|
||||
@@ -215,7 +212,6 @@ describe('speech store helpers', () => {
|
||||
params: {},
|
||||
voicePack: {
|
||||
packId: 'vp-1',
|
||||
costMultiplier: 1.5,
|
||||
},
|
||||
supportsAdapterProsody: true,
|
||||
})
|
||||
@@ -223,7 +219,6 @@ describe('speech store helpers', () => {
|
||||
expect(request.providerConfig.extraBody).toEqual({
|
||||
voice_pack: {
|
||||
pack_id: 'vp-1',
|
||||
cost_multiplier: 1.5,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ interface VoicePackSpeechInputOptions {
|
||||
voice: VoiceInfo
|
||||
providerConfig?: Record<string, unknown>
|
||||
params?: VoicePackParams
|
||||
voicePack?: Pick<VoicePackSnapshot, 'packId' | 'costMultiplier'>
|
||||
voicePack?: Pick<VoicePackSnapshot, 'packId'>
|
||||
forceSSML?: boolean
|
||||
supportsSSML?: boolean
|
||||
supportsAdapterProsody?: boolean
|
||||
@@ -517,7 +517,6 @@ export const useSpeechStore = defineStore('speech', () => {
|
||||
...(providerConfig.extraBody as Record<string, unknown> | undefined),
|
||||
voice_pack: {
|
||||
pack_id: options.voicePack.packId,
|
||||
cost_multiplier: options.voicePack.costMultiplier,
|
||||
...(needsProsody && options.supportsAdapterProsody
|
||||
? { pitch, volume }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user