完成视频复刻和优化

This commit is contained in:
Azmat@qq.com
2026-08-27 11:54:47 +08:00
parent c25060c6c6
commit 51620bf25b
46 changed files with 2575 additions and 663 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ const SHELL_COMMANDS: Command[] = [
{ id: "projects", group: "导航", label: "视频创作", sub: "从商品或参考视频出发,选择生产方式", page: "projects", icon: "clapperboard", key: "V" },
{ id: "asset-factory", group: "导航", label: "图片生成", sub: "模特上身图、平台套图、图片创作", page: "assetFactory", icon: "sparkles", key: "I" },
{ id: "free-create", group: "导航", label: "自由创作", sub: "AI 视频生成 · 全能参考 / 首尾帧", page: "freeCreate", icon: "film", key: "F" },
{ id: "quick-create", group: "导航", label: "极速成片", sub: "上传商品图片,自动完成整条视频", page: "quickCreate", icon: "wand", key: "Q" },
{ id: "quick-create", group: "导航", label: "一键成片", sub: "上传商品图片,自动完成整条视频", page: "quickCreate", icon: "wand", key: "Q" },
{ id: "video-remix", group: "导航", label: "提炼提示词", sub: "上传参考视频,提炼可编辑提示词", page: "videoRemix", icon: "scan", key: "R" },
{ id: "video-replace", group: "导航", label: "视频复刻", sub: "保留参考视频的人物、场景与节奏,复刻出自己的带货内容", page: "videoReplace", icon: "replace", key: "E" },
{ id: "library", group: "导航", label: "成品库", sub: "素材、人物、场景、成片统一管理", page: "library", icon: "folder", key: "A" },
@@ -26,7 +26,7 @@ const SHELL_COMMANDS: Command[] = [
{ id: "messages", group: "常用动作", label: "消息中心", sub: "任务提醒、协作评论、系统通知", page: "messages", icon: "bell", key: "M" },
{ id: "new-product", group: "常用动作", label: "新建商品", sub: "从商品信息开始生成素材与视频", page: "productCreateUpload", icon: "productPlus" },
{ id: "new-project", group: "常用动作", label: "新建视频项目", sub: "选择商品并进入脚本配置", page: "projectWizard", icon: "clapperboard" },
{ id: "quick-create-action", group: "常用动作", label: "极速成片", sub: "输入商品名称并上传图片,自动生成视频", page: "quickCreate", icon: "wand" },
{ id: "quick-create-action", group: "常用动作", label: "一键成片", sub: "输入商品名称并上传图片,自动生成视频", page: "quickCreate", icon: "wand" },
{ id: "model-photo", group: "常用动作", label: "生成模特上身图", sub: "快速生成 3:4 商品展示素材", page: "modelPhoto", icon: "users" },
{ id: "platform-cover", group: "常用动作", label: "生成平台套图", sub: "适配电商平台封面与详情图", page: "platformCover", icon: "images" },
{ id: "image-optimize", group: "常用动作", label: "图片创作", sub: "对话式生成、编辑", page: "imageOptimize", icon: "images" }
@@ -33,6 +33,7 @@ export const MAX_IMAGES = 9;
export const MAX_VIDEOS = 3;
export const MAX_AUDIOS = 3;
export const MAX_VIDEO_TOTAL_SECONDS = 15;
export const VIDEO_DURATION_SLACK = 0.5;
// 任务在途状态(继续轮询);终态 = succeeded / failed / cancelled / compensating
export const IN_FLIGHT_STATUSES = ["created", "reserved", "submitted", "polling", "postprocessing"];
@@ -145,8 +146,9 @@ function probeMedia(file: File, kind: "video" | "audio"): Promise<FileCheck> {
URL.revokeObjectURL(url);
const duration = el.duration;
if (!isFinite(duration)) resolve({ ok: true, type: kind });
else if (duration < 2 || duration > 15) resolve({ ok: false, error: `${kind === "video" ? "视频" : "音频"}时长需在 2-15 秒之间` });
else resolve({ ok: true, type: kind, duration: Math.round(duration * 10) / 10 });
else if (duration < 2 - 0.05 || duration > MAX_VIDEO_TOTAL_SECONDS + VIDEO_DURATION_SLACK) {
resolve({ ok: false, error: `${kind === "video" ? "视频" : "音频"}时长需在 2-15 秒之间` });
} else resolve({ ok: true, type: kind, duration: Math.min(MAX_VIDEO_TOTAL_SECONDS, Math.round(duration * 10) / 10) });
};
el.onerror = () => { URL.revokeObjectURL(url); resolve({ ok: false, error: "媒体文件解析失败,请更换文件" }); };
el.src = url;
+9 -4
View File
@@ -2,6 +2,12 @@ import { useEffect, useState, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { CheckCircle2, Inbox, Music, Shield, Trash2, X } from "lucide-react";
// 抽屉 / 弹窗 / 全屏播放器必须挂到 document.body。
// 写在页面树里会被顶栏(sticky + z-index)盖住:搜索、余额、铃铛会浮在抽屉上。
export function OverlayPortal({ children }: { children: ReactNode }) {
return createPortal(children, document.body);
}
// 浮层打开时锁住 body 滚动(否则滚轮会滚动遮罩后面的页面,体感"遮罩没盖住")。
// 多浮层叠开用计数,最后一个关闭才解锁。
let scrollLockCount = 0;
@@ -64,7 +70,7 @@ export function MediaLightbox({ open, src, kind, name, close }: {
return () => document.removeEventListener("keydown", onKey);
}, [open, close]);
if (!open || !src) return null;
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
// 挂到 body:浮层才能盖住顶栏(搜索 / 余额 / 铃铛)和侧栏
return createPortal(
<div className="np-lightbox show" onClick={close}>
<button className="lb-x" type="button" aria-label="关闭" onClick={close}><X /></button>
@@ -114,7 +120,7 @@ export function TeamModal({ open, title, subtitle = "", icon, close, children, f
useBodyScrollLock(open);
const { mounted, show } = useOverlayTransition(open, close);
if (!mounted) return null;
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
// 挂到 body:浮层才能盖住顶栏(搜索 / 余额 / 铃铛)和侧栏
return createPortal(
<div className={`modal-bg${show ? " show" : ""}`} onClick={dismissable ? close : undefined}>
<div className="modal invite-modal" onClick={(event) => event.stopPropagation()}>
@@ -143,7 +149,7 @@ export function ConfirmModal({ open, title, detail, confirmText, subtitle = "",
useBodyScrollLock(open);
const { mounted, show } = useOverlayTransition(open, onCancel);
if (!mounted) return null;
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
// 挂到 body:浮层才能盖住顶栏(搜索 / 余额 / 铃铛)和侧栏
return createPortal(
<div className={`modal-bg${show ? " show" : ""}`} onClick={dismissable ? onCancel : undefined}>
<div className="modal" onClick={(event) => event.stopPropagation()}>
@@ -185,7 +191,6 @@ export function Drawer({ title, open, close, children, className }: { title: str
useBodyScrollLock(open);
const { mounted, show } = useOverlayTransition(open, close);
if (!mounted) return null;
// 挂到 body:脱离 .content(z-index:1)层叠上下文,遮罩才能盖住头部/侧栏
return createPortal(
<>
<div className={`drawer-bg${show ? " show" : ""}`} onClick={close} />