feat(ui): 添加播客生成失败重试功能
添加了播客生成失败后的重试功能,包括: - 在播客卡片中显示失败状态和重试按钮 - 保存失败任务的输入文本内容以便重试 - 实现重试事件系统,将失败内容回填到创建组件 - 更新多语言支持中的失败和重试文本 同时修复了TTS配置中的语音名称错误,调整了遮罩层透明度, 并改进了路径获取逻辑以处理根路径情况。
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -53,8 +53,10 @@
|
||||
"podcastCard": {
|
||||
"podcastGenerationQueued": "ポッドキャスト生成キューに追加されました...",
|
||||
"podcastGenerating": "ポッドキャスト生成中...",
|
||||
"podcastGenerationFailed": "ポッドキャスト生成に失敗しました",
|
||||
"moreOperations": "その他の操作",
|
||||
"mostPopular": "最も人気"
|
||||
"mostPopular": "最も人気",
|
||||
"retry": "再試行"
|
||||
},
|
||||
"podcastContent": {
|
||||
"speaker": "スピーカー",
|
||||
|
||||
@@ -53,8 +53,10 @@
|
||||
"podcastCard": {
|
||||
"podcastGenerationQueued": "播客生成排队中...",
|
||||
"podcastGenerating": "播客生成中...",
|
||||
"podcastGenerationFailed": "播客生成失败",
|
||||
"moreOperations": "更多操作",
|
||||
"mostPopular": "最受欢迎"
|
||||
"mostPopular": "最受欢迎",
|
||||
"retry": "重试"
|
||||
},
|
||||
"podcastContent": {
|
||||
"speaker": "说话人",
|
||||
|
||||
@@ -18,7 +18,7 @@ const inter = Inter({
|
||||
export async function generateMetadata({ params }: { params: { lang: string } }): Promise<Metadata> {
|
||||
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'),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -79,7 +79,7 @@ const PodcastCard: React.FC<PodcastCardProps> = ({
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* 播放按钮覆盖层 */}
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all duration-200 flex items-center justify-center opacity-0 group-hover:opacity-100">
|
||||
<button
|
||||
@@ -120,12 +120,38 @@ const PodcastCard: React.FC<PodcastCardProps> = ({
|
||||
</div>
|
||||
{/* 遮罩层 */}
|
||||
{(podcast.status === 'pending' || podcast.status === 'running') && (
|
||||
<div className="absolute inset-0 bg-black/100 z-10 flex flex-col items-center justify-center text-white text-lg font-semibold p-4 text-center">
|
||||
<div className="absolute inset-0 bg-black/70 z-10 flex flex-col items-center justify-center text-white text-lg font-semibold p-4 text-center backdrop-blur-sm">
|
||||
<p className="mb-2">
|
||||
{podcast.status === 'pending' ? t('podcastCard.podcastGenerationQueued') : t('podcastCard.podcastGenerating')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* 失败状态的重试按钮 */}
|
||||
{podcast.status === 'failed' && podcast.input_txt_content && (
|
||||
<div className="absolute inset-0 bg-black/70 z-10 flex flex-col items-center justify-center text-white text-center p-4 backdrop-blur-sm font-semibold">
|
||||
<p className="mb-2 text-sm">{t('podcastCard.podcastGenerationFailed')}</p>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const event = new CustomEvent('retryPodcastGeneration', {
|
||||
detail: {
|
||||
input_txt_content: podcast.input_txt_content || ''
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}}
|
||||
className="px-4 py-2 hover:bg-brand-purple rounded-md text-sm transition-colors flex items-center gap-2 text-white hover:border-brand-purple"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-refresh-cw">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
|
||||
<path d="M21 3v5h-5"/>
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
|
||||
<path d="M8 16H3v5"/>
|
||||
</svg>
|
||||
{t('podcastCard.retry')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -270,6 +296,33 @@ const PodcastCard: React.FC<PodcastCardProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 失败状态的重试按钮 - 仅在大卡片模式下显示 */}
|
||||
{podcast.status === 'failed' && podcast.input_txt_content && (
|
||||
<div className="absolute inset-0 bg-black/70 z-10 flex flex-col items-center justify-center text-white text-center p-4 backdrop-blur-sm font-semibold">
|
||||
<p className="mb-2 text-sm">{t('podcastCard.podcastGenerationFailed')}</p>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const event = new CustomEvent('retryPodcastGeneration', {
|
||||
detail: {
|
||||
input_txt_content: podcast.input_txt_content || ''
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}}
|
||||
className="px-4 py-2 hover:bg-brand-purple rounded-md text-sm transition-colors flex items-center gap-2 text-white hover:border-brand-purple"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-refresh-cw">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
|
||||
<path d="M21 3v5h-5"/>
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
|
||||
<path d="M8 16H3v5"/>
|
||||
</svg>
|
||||
{t('podcastCard.retry')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,6 +80,35 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 监听重试事件以回填输入框内容
|
||||
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';
|
||||
|
||||
@@ -195,11 +195,14 @@ import path from 'path';
|
||||
/**
|
||||
* 从请求头和参数中获取真实的路径
|
||||
*/
|
||||
export async function getTruePathFromHeaders(headersList: ReadonlyHeaders, langParam: string): Promise<string> {
|
||||
export async function getTruePathFromHeaders(headersList: ReadonlyHeaders, langParam: string, isRoot: boolean = false): Promise<string> {
|
||||
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;
|
||||
|
||||
@@ -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 复制过来并导出
|
||||
|
||||
Reference in New Issue
Block a user