feat: add regenerate all previews functionality and update localization strings

This commit is contained in:
2026-04-07 11:03:11 +08:00
parent 96b2eaf774
commit 2662b494c5
6 changed files with 58 additions and 4 deletions

View File

@@ -85,7 +85,12 @@
"voiceDesign": "Voice #{{id}}",
"noVoice": "Unassigned",
"editTitle": "Edit Character: {{name}}",
"savedSuccess": "Character saved"
"savedSuccess": "Character saved",
"regeneratingPreview": "Regenerating...",
"regeneratePreview": "Regenerate Preview",
"regenerateAll": "Regenerate All Previews",
"regenerateAllDone": "All previews regenerated",
"previewNotReady": "Collecting preview..."
},
"confirm": {

View File

@@ -84,7 +84,12 @@
"voiceDesign": "音声 #{{id}}",
"noVoice": "未割り当て",
"editTitle": "キャラクターを編集:{{name}}",
"savedSuccess": "キャラクターを保存しました"
"savedSuccess": "キャラクターを保存しました",
"regeneratingPreview": "試聴を再生成中...",
"regeneratePreview": "試聴を再生成",
"regenerateAll": "試聴を一括再生成",
"regenerateAllDone": "すべての試聴を再生成しました",
"previewNotReady": "試聴を収集中..."
},
"confirm": {

View File

@@ -84,7 +84,12 @@
"voiceDesign": "음성 #{{id}}",
"noVoice": "미할당",
"editTitle": "캐릭터 편집: {{name}}",
"savedSuccess": "캐릭터가 저장되었습니다"
"savedSuccess": "캐릭터가 저장되었습니다",
"regeneratingPreview": "미리듣기 재생성 중...",
"regeneratePreview": "미리듣기 재생성",
"regenerateAll": "미리듣기 일괄 재생성",
"regenerateAllDone": "모든 미리듣기가 재생성되었습니다",
"previewNotReady": "미리듣기 수집 중..."
},
"confirm": {

View File

@@ -88,6 +88,8 @@
"savedSuccess": "角色已保存",
"regeneratingPreview": "重新生成试听中...",
"regeneratePreview": "重生试听",
"regenerateAll": "一键重新生成试听",
"regenerateAllDone": "所有试听已重新生成",
"previewNotReady": "试听收集中..."
},

View File

@@ -84,7 +84,12 @@
"voiceDesign": "音色 #{{id}}",
"noVoice": "未分配",
"editTitle": "編輯角色:{{name}}",
"savedSuccess": "角色已儲存"
"savedSuccess": "角色已儲存",
"regeneratingPreview": "重新生成試聽中...",
"regeneratePreview": "重生試聽",
"regenerateAll": "一鍵重新生成試聽",
"regenerateAllDone": "所有試聽已重新生成",
"previewNotReady": "試聽收集中..."
},
"confirm": {

View File

@@ -1190,6 +1190,24 @@ function CharactersPanel({
}
}
const handleRegenerateAllPreviews = async () => {
const charsWithVoice = detail?.characters.filter(c => c.voice_design_id) ?? []
if (charsWithVoice.length === 0) return
const ids = charsWithVoice.map(c => c.id)
setRegeneratingVoices(new Set(ids))
for (const id of ids) {
try {
await audiobookApi.regenerateCharacterPreview(project.id, id)
setVoiceKeys(prev => ({ ...prev, [id]: (prev[id] || 0) + 1 }))
} catch (e: any) {
toast.error(`${detail?.characters.find(c => c.id === id)?.name}: ${formatApiError(e)}`)
} finally {
setRegeneratingVoices(prev => { const n = new Set(prev); n.delete(id); return n })
}
}
toast.success(t('projectCard.characters.regenerateAllDone'))
}
const charCount = detail?.characters.length ?? 0
const hasChaptersOutline = (detail?.chapters.length ?? 0) > 0
@@ -1237,6 +1255,20 @@ function CharactersPanel({
{t('projectCard.reanalyze')}
</Button>
)}
{!isActive && status === 'characters_ready' && (detail?.characters.some(c => c.voice_design_id) ?? false) && (
<Button
size="xs"
variant="ghost"
className="text-muted-foreground"
onClick={handleRegenerateAllPreviews}
disabled={regeneratingVoices.size > 0}
>
{regeneratingVoices.size > 0
? <><Loader2 className="h-3 w-3 mr-1 animate-spin" />{t('projectCard.characters.regeneratingPreview')}</>
: <><RefreshCw className="h-3 w-3 mr-1" />{t('projectCard.characters.regenerateAll')}</>
}
</Button>
)}
{hasChaptersOutline && (
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={onToggle}>
<PanelLeftClose className="h-3.5 w-3.5" />