diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 687e531b4..94d5488c6 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -18,6 +18,10 @@ import { useCloneProgress } from "@/hooks/useCloneProgress" import { getAssetsByKind } from "@/api/assets" import CloneModal from "@/components/voice/CloneModal" import GenerateHeader from "./components/GenerateHeader" +import { + calculateTotalVideoDuration, + estimateTotalVideoDuration, +} from "./utils/calculateTotalVideoDuration" import GenerateStepsBar from "./components/GenerateStepsBar" import GenerateResultPanel from "./components/GenerateResultPanel" import PreviewVideoPanel from "./components/PreviewVideoPanel" @@ -109,16 +113,10 @@ const GeneratePage: React.FC = () => { /* ── 视频总时长计算(用于配音时长校验) ── */ const totalVideoDuration = useMemo(() => { - if (!previewAssets.length || !currentTemplate) return 0 - const templateSegments = currentTemplate.segments || [] - return previewAssets.reduce((sum, asset, i) => { - const assetDuration = asset.duration || asset.metadata?.duration || 30 - const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1] - const segDuration = tplSeg - ? Math.min(tplSeg.duration_max, Math.max(tplSeg.duration_min, assetDuration)) - : Math.min(assetDuration, 10) - return sum + segDuration - }, 0) + // 优先用素材精确时长;素材未加载时用模板 segments 的 duration_max 之和估算 + const exact = calculateTotalVideoDuration(previewAssets, currentTemplate ?? undefined) + if (exact > 0) return exact + return estimateTotalVideoDuration(currentTemplate ?? undefined) }, [previewAssets, currentTemplate]) /* ── 配音音频 URL ── */ diff --git a/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx b/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx index d799034bf..cd3f14bf7 100644 --- a/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx +++ b/apps/web/src/pages/generate/components/Step5VoiceSelect.tsx @@ -272,21 +272,22 @@ const Step5VoiceSelect: React.FC = ({ > {formatDuration(item.duration)} - {totalVideoDuration > 0 && (item.duration || 0) < totalVideoDuration && ( - - - 时长不足 - - )} + {totalVideoDuration > 0 && + (Number(item.duration) || 0) < Number(totalVideoDuration) && ( + + + 时长不足 + + )} {formatFileSize(item.file_size)} diff --git a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts index 315cd91bf..ba20f84ed 100644 --- a/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts +++ b/apps/web/src/pages/generate/hooks/useCanvasPlayer.ts @@ -428,7 +428,6 @@ export function useCanvasPlayer( if (frame) { const rect = computeDrawRect(canvas.width, canvas.height) ctx.drawImage(frame, rect.dx, rect.dy, rect.dw, rect.dh) - frame.close() } if (titleSettings?.text) { diff --git a/apps/web/src/pages/generate/utils/calculateTotalVideoDuration.ts b/apps/web/src/pages/generate/utils/calculateTotalVideoDuration.ts new file mode 100644 index 000000000..c91b235e3 --- /dev/null +++ b/apps/web/src/pages/generate/utils/calculateTotalVideoDuration.ts @@ -0,0 +1,54 @@ +/** + * 共享:根据素材列表和模板片段计算总视频时长 + * GeneratePage(配音校验)和 FrontendPreviewPlayer(播放控制)共用 + */ + +export interface DurationAsset { + id?: string + duration?: number + metadata?: { duration?: number } +} + +export interface DurationTemplateSegment { + duration_min?: number + duration_max?: number +} + +/** + * 计算总视频时长 + * @param assets 素材列表 + * @param template 模板(含 segments) + * @returns 总时长(秒),无有效数据时返回 0 + */ +export function calculateTotalVideoDuration( + assets: DurationAsset[] | undefined, + template: { segments?: DurationTemplateSegment[] } | undefined, +): number { + if (!assets || assets.length === 0 || !template) return 0 + + const templateSegments = template.segments || [] + + return assets.reduce((sum, asset, i) => { + const assetDuration = asset.duration || asset.metadata?.duration || 30 + const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1] + const segDuration = tplSeg + ? Math.min( + tplSeg.duration_max ?? assetDuration, + Math.max(tplSeg.duration_min ?? 0, assetDuration), + ) + : Math.min(assetDuration, 10) + return sum + segDuration + }, 0) +} + +/** + * 估算总视频时长(仅依赖模板 segments) + * 当素材未加载或加载失败时,用各片段 duration_max 之和作为估算值 + * 确保配音时长校验不会因素材未就绪而跳过 + */ +export function estimateTotalVideoDuration( + template: { segments?: DurationTemplateSegment[] } | undefined, +): number { + if (!template?.segments || template.segments.length === 0) return 0 + return template.segments.reduce((sum, seg) => sum + (seg.duration_max || 0), 0) +}