diff --git a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx index 2e2b3e293..b3c0537f4 100644 --- a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx +++ b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx @@ -61,6 +61,10 @@ const AiAvatarPage: React.FC = () => { "generating", ) const [lipsyncErrorMessage, setLipsyncErrorMessage] = useState("") + /* ── 对口型耗时计时(秒) ── */ + const [lipsyncElapsed, setLipsyncElapsed] = useState(0) + const lipsyncStartAtRef = useRef(0) + const lipsyncTickRef = useRef | null>(null) /* ── 渲染进度弹窗 ── */ const [showRenderModal, setShowRenderModal] = useState(false) const [renderStatus, setRenderStatus] = useState<"generating" | "completed" | "failed">( @@ -222,6 +226,13 @@ const AiAvatarPage: React.FC = () => { setShowLipsyncModal(true) setLipsyncStatus("generating") setLipsyncErrorMessage("") + // 启动计时器 + lipsyncStartAtRef.current = Date.now() + setLipsyncElapsed(0) + if (lipsyncTickRef.current) clearInterval(lipsyncTickRef.current) + lipsyncTickRef.current = setInterval(() => { + setLipsyncElapsed(Math.floor((Date.now() - lipsyncStartAtRef.current) / 1000)) + }, 1000) const asset = await getAssetById(video.id) const videoUrl = asset?.file_url @@ -265,6 +276,11 @@ const AiAvatarPage: React.FC = () => { state.setLipsyncJob(updated) if (updated.status === "completed") { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) + if (lipsyncTickRef.current) { + clearInterval(lipsyncTickRef.current) + lipsyncTickRef.current = null + } + setLipsyncElapsed(Math.floor((Date.now() - lipsyncStartAtRef.current) / 1000)) setLipsyncStatus("completed") setTimeout(() => { setShowLipsyncModal(false) @@ -272,6 +288,10 @@ const AiAvatarPage: React.FC = () => { }, 1000) } else if (updated.status === "failed") { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) + if (lipsyncTickRef.current) { + clearInterval(lipsyncTickRef.current) + lipsyncTickRef.current = null + } setLipsyncStatus("failed") setLipsyncErrorMessage(updated.error_message || "对口型生成失败") } @@ -285,6 +305,10 @@ const AiAvatarPage: React.FC = () => { data: (err as { response?: { data?: unknown } })?.response?.data, message: err instanceof Error ? err.message : String(err), }) + if (lipsyncTickRef.current) { + clearInterval(lipsyncTickRef.current) + lipsyncTickRef.current = null + } setShowLipsyncModal(false) message.error(err instanceof Error ? err.message : "对口型任务提交失败,请重试") } @@ -305,15 +329,21 @@ const AiAvatarPage: React.FC = () => { clearInterval(lipsyncTimerRef.current) lipsyncTimerRef.current = null } + if (lipsyncTickRef.current) { + clearInterval(lipsyncTickRef.current) + lipsyncTickRef.current = null + } setShowLipsyncModal(false) setLipsyncStatus("generating") setLipsyncErrorMessage("") + setLipsyncElapsed(0) }, []) // 清理轮询 useEffect(() => { return () => { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) + if (lipsyncTickRef.current) clearInterval(lipsyncTickRef.current) if (renderTimerRef.current) clearInterval(renderTimerRef.current) } }, []) @@ -963,6 +993,19 @@ const AiAvatarPage: React.FC = () => {
对口型视频生成中…
+
+ {`${Math.floor(lipsyncElapsed / 60) + .toString() + .padStart(2, "0")}:${(lipsyncElapsed % 60).toString().padStart(2, "0")}`} +
请勿关闭页面,完成后将自动提示
@@ -974,6 +1017,20 @@ const AiAvatarPage: React.FC = () => {
对口型视频生成完成
+
+ 总耗时{" "} + {Math.floor(lipsyncElapsed / 60) + .toString() + .padStart(2, "0")} + :{(lipsyncElapsed % 60).toString().padStart(2, "0")} +
)} {lipsyncStatus === "failed" && (