From 445375e1cb9ee3ff22509a2ccb96e181d21eb2f4 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 24 Sep 2026 12:40:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai-avatar):=20=E5=AF=B9=E5=8F=A3=E5=9E=8B?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=BC=B9=E7=AA=97=E5=8A=A0=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E8=AE=A1=E6=97=B6=E5=99=A8=20+=20=E5=AE=8C=E6=88=90=E6=80=BB?= =?UTF-8?q?=E8=80=97=E6=97=B6=E6=98=BE=E7=A4=BA=20(#2027)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: xiaoxia Co-committed-by: xiaoxia --- apps/web/src/pages/ai-avatar/AiAvatarPage.tsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) 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" && (