diff --git a/qwen3-tts-frontend/src/locales/en-US/audiobook.json b/qwen3-tts-frontend/src/locales/en-US/audiobook.json
index 37a6ccf..1bc7bdb 100644
--- a/qwen3-tts-frontend/src/locales/en-US/audiobook.json
+++ b/qwen3-tts-frontend/src/locales/en-US/audiobook.json
@@ -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": {
diff --git a/qwen3-tts-frontend/src/locales/ja-JP/audiobook.json b/qwen3-tts-frontend/src/locales/ja-JP/audiobook.json
index d093977..9c6989f 100644
--- a/qwen3-tts-frontend/src/locales/ja-JP/audiobook.json
+++ b/qwen3-tts-frontend/src/locales/ja-JP/audiobook.json
@@ -84,7 +84,12 @@
"voiceDesign": "音声 #{{id}}",
"noVoice": "未割り当て",
"editTitle": "キャラクターを編集:{{name}}",
- "savedSuccess": "キャラクターを保存しました"
+ "savedSuccess": "キャラクターを保存しました",
+ "regeneratingPreview": "試聴を再生成中...",
+ "regeneratePreview": "試聴を再生成",
+ "regenerateAll": "試聴を一括再生成",
+ "regenerateAllDone": "すべての試聴を再生成しました",
+ "previewNotReady": "試聴を収集中..."
},
"confirm": {
diff --git a/qwen3-tts-frontend/src/locales/ko-KR/audiobook.json b/qwen3-tts-frontend/src/locales/ko-KR/audiobook.json
index 05eb5d1..0e75399 100644
--- a/qwen3-tts-frontend/src/locales/ko-KR/audiobook.json
+++ b/qwen3-tts-frontend/src/locales/ko-KR/audiobook.json
@@ -84,7 +84,12 @@
"voiceDesign": "음성 #{{id}}",
"noVoice": "미할당",
"editTitle": "캐릭터 편집: {{name}}",
- "savedSuccess": "캐릭터가 저장되었습니다"
+ "savedSuccess": "캐릭터가 저장되었습니다",
+ "regeneratingPreview": "미리듣기 재생성 중...",
+ "regeneratePreview": "미리듣기 재생성",
+ "regenerateAll": "미리듣기 일괄 재생성",
+ "regenerateAllDone": "모든 미리듣기가 재생성되었습니다",
+ "previewNotReady": "미리듣기 수집 중..."
},
"confirm": {
diff --git a/qwen3-tts-frontend/src/locales/zh-CN/audiobook.json b/qwen3-tts-frontend/src/locales/zh-CN/audiobook.json
index f6ed35a..d35e211 100644
--- a/qwen3-tts-frontend/src/locales/zh-CN/audiobook.json
+++ b/qwen3-tts-frontend/src/locales/zh-CN/audiobook.json
@@ -88,6 +88,8 @@
"savedSuccess": "角色已保存",
"regeneratingPreview": "重新生成试听中...",
"regeneratePreview": "重生试听",
+ "regenerateAll": "一键重新生成试听",
+ "regenerateAllDone": "所有试听已重新生成",
"previewNotReady": "试听收集中..."
},
diff --git a/qwen3-tts-frontend/src/locales/zh-TW/audiobook.json b/qwen3-tts-frontend/src/locales/zh-TW/audiobook.json
index 83466e9..9bac14c 100644
--- a/qwen3-tts-frontend/src/locales/zh-TW/audiobook.json
+++ b/qwen3-tts-frontend/src/locales/zh-TW/audiobook.json
@@ -84,7 +84,12 @@
"voiceDesign": "音色 #{{id}}",
"noVoice": "未分配",
"editTitle": "編輯角色:{{name}}",
- "savedSuccess": "角色已儲存"
+ "savedSuccess": "角色已儲存",
+ "regeneratingPreview": "重新生成試聽中...",
+ "regeneratePreview": "重生試聽",
+ "regenerateAll": "一鍵重新生成試聽",
+ "regenerateAllDone": "所有試聽已重新生成",
+ "previewNotReady": "試聽收集中..."
},
"confirm": {
diff --git a/qwen3-tts-frontend/src/pages/Audiobook.tsx b/qwen3-tts-frontend/src/pages/Audiobook.tsx
index cf08b39..d543fe7 100644
--- a/qwen3-tts-frontend/src/pages/Audiobook.tsx
+++ b/qwen3-tts-frontend/src/pages/Audiobook.tsx
@@ -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')}
)}
+ {!isActive && status === 'characters_ready' && (detail?.characters.some(c => c.voice_design_id) ?? false) && (
+
+ )}
{hasChaptersOutline && (