From 13d552bb57a1816f563081c811e68d86cda32ea4 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Sun, 5 Oct 2025 22:40:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0=E6=92=AD?= =?UTF-8?q?=E5=AE=A2=E7=94=9F=E6=88=90=E5=A4=B1=E8=B4=A5=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加了播客生成失败后的重试功能,包括: - 在播客卡片中显示失败状态和重试按钮 - 保存失败任务的输入文本内容以便重试 - 实现重试事件系统,将失败内容回填到创建组件 - 更新多语言支持中的失败和重试文本 同时修复了TTS配置中的语音名称错误,调整了遮罩层透明度, 并改进了路径获取逻辑以处理根路径情况。 --- config/edge-tts.json | 6 +-- server/main.py | 4 +- server/podcast_generator.py | 4 +- web/public/locales/en/components.json | 4 +- web/public/locales/ja/components.json | 4 +- web/public/locales/zh-CN/components.json | 4 +- web/src/app/[lang]/layout.tsx | 2 +- web/src/app/[lang]/page.tsx | 23 ++++++++++ web/src/components/PodcastCard.tsx | 57 +++++++++++++++++++++++- web/src/components/PodcastCreator.tsx | 29 ++++++++++++ web/src/lib/utils.ts | 5 ++- web/src/types/index.ts | 1 + 12 files changed, 130 insertions(+), 13 deletions(-) diff --git a/config/edge-tts.json b/config/edge-tts.json index 9588237..aeab871 100644 --- a/config/edge-tts.json +++ b/config/edge-tts.json @@ -45,15 +45,15 @@ "audio": "https://podcasts.hubtoday.app/podcast/example/edgetts/zh-CN-Xiaoxiao:DragonHDFlashLatestNeural.wav" }, { - "name": "Xiaoxiao2Flash", + "name": "XiaoxiaoFlash2", "alias": "小笑-flash", - "code": "zh-CN-Xiaoxiao2:DragonHDFlashLatestNeural", + "code": "zh-CN-Xiaoxiao:DragonHDFlashLatestNeural2", "locale": "zh-CN", "gender": "Female", "usedname": "小笑", "volume_adjustment": 0, "speed_adjustment": 0, - "audio": "https://podcasts.hubtoday.app/podcast/example/edgetts/zh-CN-Xiaoxiao2:DragonHDFlashLatestNeural.wav" + "audio": "https://podcasts.hubtoday.app/podcast/example/edgetts/zh-CN-Xiaoxiao:DragonHDFlashLatestNeural.wav" }, { "name": "YunxiaoFlash", diff --git a/server/main.py b/server/main.py index 59dd967..f464157 100644 --- a/server/main.py +++ b/server/main.py @@ -256,6 +256,7 @@ async def _generate_podcast_task( except Exception as e: task_results[auth_id][task_id]["status"] = TaskStatus.FAILED task_results[auth_id][task_id]["result"] = str(e) + task_results[auth_id][task_id]["input_txt_content"] = input_txt_content # 失败时保存输入文本 print(f"\nPodcast generation failed for task {task_id}: {e}") finally: # 无论成功或失败,都尝试调用回调 if callback_url: @@ -374,7 +375,8 @@ async def get_podcast_status( "title": task_info.get("title"), "tags": task_info.get("tags"), "error": task_info["result"] if task_info["status"] == TaskStatus.FAILED else None, - "timestamp": task_info["timestamp"] + "timestamp": task_info["timestamp"], + "input_txt_content": task_info.get("input_txt_content"), # 添加输入文本内容 }) return {"message": "Tasks retrieved successfully.", "tasks": all_tasks_for_auth_id} diff --git a/server/podcast_generator.py b/server/podcast_generator.py index 5791151..747842b 100644 --- a/server/podcast_generator.py +++ b/server/podcast_generator.py @@ -340,8 +340,8 @@ def _parse_arguments(): parser.add_argument("--base-url", default="https://api.openai.com/v1", help="OpenAI API base URL (default: https://api.openai.com/v1).") parser.add_argument("--model", default="gpt-3.5-turbo", help="OpenAI model to use (default: gpt-3.5-turbo).") parser.add_argument("--threads", type=int, default=1, help="Number of threads to use for audio generation (default: 1).") - parser.add_argument("--output-language", type=str, default=None, help="Language for the podcast overview and script (default: Chinese).") - parser.add_argument("--usetime", type=str, default=None, help="Specific time to be mentioned in the podcast script, e.g., 10 minutes, 1 hour.") + parser.add_argument("--output-language", type=str, default="Chinese", help="Language for the podcast overview and script (default: Chinese).") + parser.add_argument("--usetime", type=str, default="Under 5 minutes", help="Specific time to be mentioned in the podcast script, e.g., 10 minutes, 1 hour.") return parser.parse_args() diff --git a/web/public/locales/en/components.json b/web/public/locales/en/components.json index 734de58..4bb34c6 100644 --- a/web/public/locales/en/components.json +++ b/web/public/locales/en/components.json @@ -53,8 +53,10 @@ "podcastCard": { "podcastGenerationQueued": "Podcast generation queued...", "podcastGenerating": "Podcast generating...", + "podcastGenerationFailed": "Podcast generation failed", "moreOperations": "More operations", - "mostPopular": "Most Popular" + "mostPopular": "Most Popular", + "retry": "Retry" }, "podcastContent": { "speaker": "Speaker", diff --git a/web/public/locales/ja/components.json b/web/public/locales/ja/components.json index 433190f..36688db 100644 --- a/web/public/locales/ja/components.json +++ b/web/public/locales/ja/components.json @@ -53,8 +53,10 @@ "podcastCard": { "podcastGenerationQueued": "ポッドキャスト生成キューに追加されました...", "podcastGenerating": "ポッドキャスト生成中...", + "podcastGenerationFailed": "ポッドキャスト生成に失敗しました", "moreOperations": "その他の操作", - "mostPopular": "最も人気" + "mostPopular": "最も人気", + "retry": "再試行" }, "podcastContent": { "speaker": "スピーカー", diff --git a/web/public/locales/zh-CN/components.json b/web/public/locales/zh-CN/components.json index e938a74..b323764 100644 --- a/web/public/locales/zh-CN/components.json +++ b/web/public/locales/zh-CN/components.json @@ -53,8 +53,10 @@ "podcastCard": { "podcastGenerationQueued": "播客生成排队中...", "podcastGenerating": "播客生成中...", + "podcastGenerationFailed": "播客生成失败", "moreOperations": "更多操作", - "mostPopular": "最受欢迎" + "mostPopular": "最受欢迎", + "retry": "重试" }, "podcastContent": { "speaker": "说话人", diff --git a/web/src/app/[lang]/layout.tsx b/web/src/app/[lang]/layout.tsx index 8170cb4..fb61845 100644 --- a/web/src/app/[lang]/layout.tsx +++ b/web/src/app/[lang]/layout.tsx @@ -18,7 +18,7 @@ const inter = Inter({ export async function generateMetadata({ params }: { params: { lang: string } }): Promise { const { lang } = await params; const { t } = await getTranslation(lang, 'layout'); - const truePath = await getTruePathFromHeaders(await headers(), lang); + const truePath = await getTruePathFromHeaders(await headers(), lang, true); return { metadataBase: new URL('https://www.podcasthub.com'), title: t('title'), diff --git a/web/src/app/[lang]/page.tsx b/web/src/app/[lang]/page.tsx index 94e8afd..596e319 100644 --- a/web/src/app/[lang]/page.tsx +++ b/web/src/app/[lang]/page.tsx @@ -77,6 +77,7 @@ export default function HomePage({ params }: { params: Promise<{ lang: string }> tags: task.tags ? task.tags.split('#').map((tag: string) => tag.trim()).filter((tag: string) => !!tag) : task.status === 'failed' ? [task.error] : [t('podcastTagsPlaceholder')], status: task.status, file_name: task.output_audio_filepath || '', + input_txt_content: task.input_txt_content || '', })); }; @@ -227,6 +228,28 @@ export default function HomePage({ params }: { params: Promise<{ lang: string }> router.push(`${truePath}/podcast/${podcast.file_name.split(".")[0]}`); }; + // 处理重试播客生成 + const handleRetryPodcastGeneration = (input_txt_content: string) => { + // 在这里将 input_txt_content 回填到输入框 + // 通过事件系统通知 PodcastCreator 组件 + const retryEvent = new CustomEvent('retryWithContent', { detail: { input_txt_content } }); + window.dispatchEvent(retryEvent); + }; + + // 监听重试事件 + useEffect(() => { + const handleRetryEvent = (e: Event) => { + const customEvent = e as CustomEvent; + handleRetryPodcastGeneration(customEvent.detail.input_txt_content); + }; + + window.addEventListener('retryPodcastGeneration', handleRetryEvent); + + return () => { + window.removeEventListener('retryPodcastGeneration', handleRetryEvent); + }; + }, []); + const handlePlayPodcast = (podcast: PodcastItem) => { if (currentPodcast?.id === podcast.id) { setIsPlaying(prev => !prev); diff --git a/web/src/components/PodcastCard.tsx b/web/src/components/PodcastCard.tsx index 0b13578..aed1a63 100644 --- a/web/src/components/PodcastCard.tsx +++ b/web/src/components/PodcastCard.tsx @@ -79,7 +79,7 @@ const PodcastCard: React.FC = ({
)} - + {/* 播放按钮覆盖层 */}
{/* 遮罩层 */} {(podcast.status === 'pending' || podcast.status === 'running') && ( -
+

{podcast.status === 'pending' ? t('podcastCard.podcastGenerationQueued') : t('podcastCard.podcastGenerating')}

)} + {/* 失败状态的重试按钮 */} + {podcast.status === 'failed' && podcast.input_txt_content && ( +
+

{t('podcastCard.podcastGenerationFailed')}

+ +
+ )}
); } @@ -270,6 +296,33 @@ const PodcastCard: React.FC = ({ )} + + {/* 失败状态的重试按钮 - 仅在大卡片模式下显示 */} + {podcast.status === 'failed' && podcast.input_txt_content && ( +
+

{t('podcastCard.podcastGenerationFailed')}

+ +
+ )} ); }; diff --git a/web/src/components/PodcastCreator.tsx b/web/src/components/PodcastCreator.tsx index fa3b8ff..8204bc5 100644 --- a/web/src/components/PodcastCreator.tsx +++ b/web/src/components/PodcastCreator.tsx @@ -80,6 +80,35 @@ const PodcastCreator: React.FC = ({ } }, []); + // 监听重试事件以回填输入框内容 + useEffect(() => { + const handleRetryEvent = (e: Event) => { + const customEvent = e as CustomEvent; + const content = customEvent.detail.input_txt_content; + + // 如果内容包含自定义指令的标记,将其分离 + if (content.includes('```custom-begin') && content.includes('```custom-end')) { + const customBeginIndex = content.indexOf('```custom-begin'); + const customEndIndex = content.indexOf('```custom-end') + 13; // 13 is the length of '```custom-end' + + const customInstruction = content.substring(customBeginIndex + 15, customEndIndex - 13); // 15 is the length of '```custom-begin' + const topicContent = content.substring(customEndIndex + 1).trim(); // +1 to remove the newline after custom-end + + setTopic(topicContent); + setCustomInstructions(customInstruction); + } else { + // 如果没有自定义指令标记,整个内容都是主题 + setTopic(content); + } + }; + + window.addEventListener('retryWithContent', handleRetryEvent); + + return () => { + window.removeEventListener('retryWithContent', handleRetryEvent); + }; + }, []); + const getInitialLanguage = (currentLang: string) => { if (currentLang.startsWith('zh')) { return 'Chinese'; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index f8ddfa0..78fd3fc 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -195,11 +195,14 @@ import path from 'path'; /** * 从请求头和参数中获取真实的路径 */ -export async function getTruePathFromHeaders(headersList: ReadonlyHeaders, langParam: string): Promise { +export async function getTruePathFromHeaders(headersList: ReadonlyHeaders, langParam: string, isRoot: boolean = false): Promise { const pathname = headersList.get('x-next-pathname') || ''; // console.log('Current pathname (from server):', pathname); // console.log('langParam:', langParam); if (pathname === '' || !languages.includes(pathname)) { + if (isRoot) { + return '/'; + } return ''; } return pathname !== langParam ? "/" : "/"+langParam; diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 1374ff6..0bb766c 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -132,6 +132,7 @@ export interface PodcastItem { tags: string[]; status: 'pending' | 'running' | 'completed' | 'failed'; // 添加status属性 file_name: string; + input_txt_content?: string; // 新增 input_txt_content 字段 } // 设置表单数据类型 - 从 SettingsForm.tsx 复制过来并导出