diff --git a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx index 2e2b3e293..264a8fe4f 100644 --- a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx +++ b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx @@ -61,6 +61,9 @@ const AiAvatarPage: React.FC = () => { "generating", ) const [lipsyncErrorMessage, setLipsyncErrorMessage] = useState("") + const [lipsyncElapsed, setLipsyncElapsed] = useState(0) + /* ── 对口型计时器 ── */ + const lipsyncElapsedTimerRef = useRef | null>(null) /* ── 渲染进度弹窗 ── */ const [showRenderModal, setShowRenderModal] = useState(false) const [renderStatus, setRenderStatus] = useState<"generating" | "completed" | "failed">( @@ -80,6 +83,30 @@ const AiAvatarPage: React.FC = () => { /* ── 渲染进度轮询 ── */ const renderTimerRef = useRef | null>(null) + /* ── 对口型计时器工具 ── */ + const _formatElapsed = useCallback((seconds: number) => { + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}` + }, []) + + const _startLipsyncElapsedTimer = useCallback(() => { + if (lipsyncElapsedTimerRef.current) { + clearInterval(lipsyncElapsedTimerRef.current) + } + setLipsyncElapsed(0) + lipsyncElapsedTimerRef.current = setInterval(() => { + setLipsyncElapsed((prev) => prev + 1) + }, 1000) + }, []) + + const _stopLipsyncElapsedTimer = useCallback(() => { + if (lipsyncElapsedTimerRef.current) { + clearInterval(lipsyncElapsedTimerRef.current) + lipsyncElapsedTimerRef.current = null + } + }, []) + const togglePanel = useCallback((key: PanelKey) => { setCollapsed((prev) => ({ ...prev, [key]: !prev[key] })) }, []) @@ -222,6 +249,7 @@ const AiAvatarPage: React.FC = () => { setShowLipsyncModal(true) setLipsyncStatus("generating") setLipsyncErrorMessage("") + _startLipsyncElapsedTimer() const asset = await getAssetById(video.id) const videoUrl = asset?.file_url @@ -265,6 +293,7 @@ const AiAvatarPage: React.FC = () => { state.setLipsyncJob(updated) if (updated.status === "completed") { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) + _stopLipsyncElapsedTimer() setLipsyncStatus("completed") setTimeout(() => { setShowLipsyncModal(false) @@ -272,6 +301,7 @@ const AiAvatarPage: React.FC = () => { }, 1000) } else if (updated.status === "failed") { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) + _stopLipsyncElapsedTimer() setLipsyncStatus("failed") setLipsyncErrorMessage(updated.error_message || "对口型生成失败") } @@ -285,6 +315,8 @@ const AiAvatarPage: React.FC = () => { data: (err as { response?: { data?: unknown } })?.response?.data, message: err instanceof Error ? err.message : String(err), }) + _stopLipsyncElapsedTimer() + setLipsyncElapsed(0) setShowLipsyncModal(false) message.error(err instanceof Error ? err.message : "对口型任务提交失败,请重试") } @@ -297,6 +329,8 @@ const AiAvatarPage: React.FC = () => { state.style, state.ttsPreview, + _startLipsyncElapsedTimer, + _stopLipsyncElapsedTimer, ]) // 取消对口型生成 @@ -305,16 +339,19 @@ const AiAvatarPage: React.FC = () => { clearInterval(lipsyncTimerRef.current) lipsyncTimerRef.current = null } + _stopLipsyncElapsedTimer() + setLipsyncElapsed(0) setShowLipsyncModal(false) setLipsyncStatus("generating") setLipsyncErrorMessage("") - }, []) + }, [_stopLipsyncElapsedTimer]) // 清理轮询 useEffect(() => { return () => { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) if (renderTimerRef.current) clearInterval(renderTimerRef.current) + if (lipsyncElapsedTimerRef.current) clearInterval(lipsyncElapsedTimerRef.current) } }, []) @@ -964,6 +1001,9 @@ const AiAvatarPage: React.FC = () => { 对口型视频生成中…
+ 已用时:{_formatElapsed(lipsyncElapsed)} +
+
请勿关闭页面,完成后将自动提示