feat: Add voice cloning support and prepare clone functionality in voice design

This commit is contained in:
2026-02-04 19:24:26 +08:00
parent d0b2cb29c4
commit dc5fd643e7
5 changed files with 59 additions and 15 deletions

View File

@@ -160,6 +160,20 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
let result
if (selectedItem?.source === 'saved-design') {
if (selectedItem.backendType === 'local') {
result = await ttsApi.createVoiceCloneJob({
text: data.text,
language: data.language,
ref_audio: null,
voice_design_id: selectedItem.designId,
max_new_tokens: data.max_new_tokens,
temperature: data.temperature,
top_k: data.top_k,
top_p: data.top_p,
repetition_penalty: data.repetition_penalty,
backend: 'local',
})
} else {
result = await ttsApi.createVoiceDesignJob({
text: data.text,
language: data.language,
@@ -171,6 +185,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
top_p: data.top_p,
repetition_penalty: data.repetition_penalty,
})
}
} else {
result = await ttsApi.createCustomVoiceJob(data)
}

View File

@@ -52,6 +52,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
})
const [showSaveDialog, setShowSaveDialog] = useState(false)
const [saveDesignName, setSaveDesignName] = useState('')
const [isPreparing, setIsPreparing] = useState(false)
const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling()
const { refresh } = useHistoryContext()
@@ -128,13 +129,29 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
toast.error('请输入设计名称')
return
}
try {
await voiceDesignApi.create({
const backend = preferences?.default_backend || 'local'
const design = await voiceDesignApi.create({
name: saveDesignName,
instruct: instruct,
backend_type: preferences?.default_backend || 'local'
backend_type: backend
})
toast.success('音色设计已保存')
if (backend === 'local') {
setIsPreparing(true)
try {
await voiceDesignApi.prepareClone(design.id)
toast.success('音色克隆准备完成')
} catch (error) {
toast.error('准备克隆失败,但设计已保存')
} finally {
setIsPreparing(false)
}
}
setShowSaveDialog(false)
setSaveDesignName('')
} catch (error) {
@@ -238,8 +255,8 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
}}>
</Button>
<Button type="button" onClick={handleSaveDesign}>
<Button type="button" onClick={handleSaveDesign} disabled={isPreparing}>
{isPreparing ? '准备中...' : '保存'}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -259,6 +259,9 @@ export const ttsApi = {
if (data.x_vector_only_mode !== undefined) {
formData.append('x_vector_only_mode', String(data.x_vector_only_mode))
}
if (data.voice_design_id !== undefined) {
formData.append('voice_design_id', String(data.voice_design_id))
}
if (data.max_new_tokens !== undefined) {
formData.append('max_new_tokens', String(data.max_new_tokens))
}
@@ -422,6 +425,13 @@ export const voiceDesignApi = {
delete: async (id: number): Promise<void> => {
await apiClient.delete(API_ENDPOINTS.VOICE_DESIGNS.DELETE(id))
},
prepareClone: async (id: number): Promise<{ message: string; cache_id: number; ref_text: string }> => {
const response = await apiClient.post<{ message: string; cache_id: number; ref_text: string }>(
API_ENDPOINTS.VOICE_DESIGNS.PREPARE_CLONE(id)
)
return response.data
},
}
export default apiClient

View File

@@ -33,6 +33,7 @@ export const API_ENDPOINTS = {
GET: (id: number) => `/voice-designs/${id}`,
UPDATE: (id: number) => `/voice-designs/${id}`,
DELETE: (id: number) => `/voice-designs/${id}`,
PREPARE_CLONE: (id: number) => `/voice-designs/${id}/prepare-clone`,
},
} as const

View File

@@ -47,6 +47,7 @@ export interface VoiceCloneForm {
top_p?: number
repetition_penalty?: number
backend?: string
voice_design_id?: number
}
export type SpeakerSource = 'builtin' | 'saved-design'