From c22bee48780fd0830651cb833b39ea7cba15d95f Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 18 May 2026 15:09:49 +0800 Subject: [PATCH] feat: move keyframes beside transcript timeline --- docs/source-analysis.html | 16 +- web/components/ad-recreation-board.tsx | 368 ++++++++++++++----------- 2 files changed, 222 insertions(+), 162 deletions(-) diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 5eeb8c9..34f1fb5 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -593,7 +593,7 @@ web/next.config.mjsNext.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator,并移除 Next 16 已不支持的 eslint 顶层配置,避免本地 dev 出现配置 Issue 提示。 web/app/globals.css全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 nextjs-portal 遮挡隐藏规则。 web/app/page.tsx产品工作台主状态:jobs、activeJobId、生成任务状态;主渲染为全屏素材输入列 + 信息流广告复刻工作表;“开始分析”会把 job 放入并行素材分析队列,下载完成后触发 triggerTranscribe 解析音频,并触发 analyzeJob 自动抽 12 张参考帧,形成“音频文案路 + 视频视觉路”同步推进;底部吸附音频条不再从主界面渲染。 - web/components/ad-recreation-board.tsx信息流广告复刻工作表:顶部由 buildWorkflowSteps 统一生成 01-09 流程顺序、状态和判定依据,WorkflowOrderBar 展示完整顺序,WorkflowStepBadge / PipelineLane / 分镜列标题共用同一套编号。左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧顶部用“音频文案、抽帧参考、相似主体、产品素材池”四个状态条显示后台并行进度。源视频工作区展示视频下载状态和默认折叠的文案依据。音频解析结果改成默认折叠的辅助信息,展开后同一行看讲话人/节奏/背景音;主工作区左侧放大为按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧;右侧上方是音频波形 / 切点参考,下方是逐句时间轴;下一行铺开“关键帧 / 相似主体”。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。关键帧区的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切并用更多列紧凑铺开,右侧直接显示“已选关键帧”窄列,鼠标停留会通过固定浮层放大展示完整帧。“生成 10 张高清图”放在相似主体白底视图区,不和抽参考按钮平齐;如果用户没有勾选帧,默认把全部关键帧作为主体参考,勾选后只传已选帧;生成区可在“透明骨架 / 普通真人”之间切换,可选择桌面导入的 5 套内置形象作为创意方向,并可填写统一主体方向,例如年轻女性、更运动、更高级。关键帧和相似主体白底视图都用更小的竖版缩略图密排;白底视图只展示每个 view 的最新一张,缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端调用 generateSubjectAssets 时按主体类型传 subject_style=transparent_humansource_actor,按需传 character_id,并使用 reconstruction_mode=similar;后端会把关键帧和内置形象视为同一个主体的创意证据,并锁定同一性别表现、年龄段、体型、材质、风格和视觉身份,同时生成全身多视角 + 肩颈正/左右近景 + 后颈肩背特写,避免整套图出现男女性别、老少年龄或样式混杂。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,新口播列进一步收窄,把横向空间留给画面规划和首尾帧。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和历史候选视频槽;画面规划区先选择镜头类型(人物/情绪、人物+产品、产品特写、场景过渡),再用人物/产品开关、首帧规划、尾帧规划和产品出现方式决定这一条到底需不需要产品图或相似主体参考。当前主流程暂停直接调用视频模型,不再提供“生成本条 · Seedance”或“一键提交全部”视频入口;行内新增“首尾帧闸门”,分别显示/生成首帧和尾帧,旧 keyframe 类型首尾帧会被忽略,只认真正的 asset 首尾帧。生成首尾帧时调用 generateSceneAsset,先按人物描述、镜头类型、首尾状态和产品佩戴需求,从相似主体 6/10 视图里自动挑选最多 5 张最相关主体视角,再传入 subject_images 和该行自动挑选的产品图 product_images;关键帧只作为前置主体重构证据和行数据承载位置,不再作为后续视频首尾帧参考。视频候选槽只展示历史候选和待生成占位,按钮改为“保存本条规划 / 保存全部规划”。只有该行勾选“产品”时,首尾帧生成才会从产品素材池按分镜角色、视角优先级、用途标签、置信度和风险自动挑选最多 6 张相关产品图;未勾选产品时不会把产品图提交给首尾帧/后续生视频模型。只有该行勾选“人物”时,才会传按需筛选后的相似主体参考图;否则 prompt 会明确禁止强行添加主角式透明骨架人,后端也不会再给产品特写强加透明骨架人约束。ModelTrace 会在音频解析、产品识别/补图、相似主体高清视图包、脚本改写等入口旁直接展示模型名;所有生图入口都显示并使用 gpt-image-2,没有其他图片模型 fallback;点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 + web/components/ad-recreation-board.tsx信息流广告复刻工作表:顶部由 buildWorkflowSteps 统一生成 01-09 流程顺序、状态和判定依据,WorkflowOrderBar 展示完整顺序,WorkflowStepBadge / PipelineLane / 分镜列标题共用同一套编号。左侧素材输入只负责链接/上传和任务切换,不再重复放横版原视频预览;右侧顶部用“音频文案、抽帧参考、相似主体、产品素材池”四个状态条显示后台并行进度。源视频工作区展示视频下载状态和默认折叠的文案依据。音频解析结果改成默认折叠的辅助信息,展开后同一行看讲话人/节奏/背景音;主工作区左侧放大为按 9:16 显示的竖版原视频播放器,播放器内覆盖“当前点抽帧”,按当前播放秒数手动补参考帧;右侧上方是音频波形 / 切点参考,下方左侧是关键帧与已选关键帧,右侧是逐句时间轴;下一行只保留“相似主体 / 主体模板”。音频波形用参考图式的连续灰色包络显示响度、停顿和密集爆点,顶部同时显示当前播放秒数、总时长和鼠标指针停点秒数。视频播放时通过 requestAnimationFrame 平滑驱动波形播放线,同时同步高亮并滚动当前句;点击音频波形或字幕行会跳转原视频时间。逐句时间轴左侧关键帧区的主入口是“自动抽帧 12 张”,一键按动作峰值目标重新抽取 12 张源视频参考帧,优先抓手势、表情变化、节奏点和镜头变化;缩略图按竖版完整比例显示不裁切并用更多列紧凑铺开,右侧直接显示“已选关键帧”窄列,鼠标停留会通过固定浮层放大展示完整帧。“生成 10 张高清图”放在下方相似主体白底视图区,不和抽参考按钮平齐;如果用户没有勾选帧,默认把全部关键帧作为主体参考,勾选后只传已选帧;生成区可在“透明骨架 / 普通真人”之间切换,可选择桌面导入的 5 套内置形象作为创意方向,并可填写统一主体方向,例如年轻女性、更运动、更高级。关键帧和相似主体白底视图都用更小的竖版缩略图密排;白底视图只展示每个 view 的最新一张,缩略图上提供“重新生成这一张”和“删除这一张”,单张重生会用 replace_views=true 替换同一视角。前端调用 generateSubjectAssets 时按主体类型传 subject_style=transparent_humansource_actor,按需传 character_id,并使用 reconstruction_mode=similar;后端会把关键帧和内置形象视为同一个主体的创意证据,并锁定同一性别表现、年龄段、体型、材质、风格和视觉身份,同时生成全身多视角 + 肩颈正/左右近景 + 后颈肩背特写,避免整套图出现男女性别、老少年龄或样式混杂。音频结果下方是信息流复刻分镜工作台:顶部产品参考区是“同一产品素材池”,不限量上传产品图,不做不同产品身份判断;上传原图推荐长边 1200-2000px、短边至少 600px,但后端会统一生成最长边 1600px、JPEG 92 的 AI 工作副本,并回显尺寸、自动转换和风险标注;上传后按“套在脖子上的 U 形肩颈按摩仪”进行同一产品批量识别,左/右按佩戴者身体左右、上/下按佩戴方向,额外标注内外侧、开口方向、局部结构点、背景类型、用途标签、生成风险和备注,用户只检查备注,鼠标悬停通过固定浮层显示大图预览,能盖过滚动容器和分镜框架;缺视角补图失败时保留重试入口。脚本区在分镜行上方提供“作者想法”和“整片改写”,每行新口播文案可直接编辑并可单段 AI 改写,分镜时间和原内容列压缩为窄摘要列,新口播列进一步收窄,把横向空间留给画面规划和首尾帧。每条音频分镜纵向排列,行内从左到右串起原内容、新口播文案、画面规划/产品融入和历史候选视频槽;画面规划区先选择镜头类型(人物/情绪、人物+产品、产品特写、场景过渡),再用人物/产品开关、首帧规划、尾帧规划和产品出现方式决定这一条到底需不需要产品图或相似主体参考。当前主流程暂停直接调用视频模型,不再提供“生成本条 · Seedance”或“一键提交全部”视频入口;行内新增“首尾帧闸门”,分别显示/生成首帧和尾帧,旧 keyframe 类型首尾帧会被忽略,只认真正的 asset 首尾帧。生成首尾帧时调用 generateSceneAsset,先按人物描述、镜头类型、首尾状态和产品佩戴需求,从相似主体 6/10 视图里自动挑选最多 5 张最相关主体视角,再传入 subject_images 和该行自动挑选的产品图 product_images;关键帧只作为前置主体重构证据和行数据承载位置,不再作为后续视频首尾帧参考。视频候选槽只展示历史候选和待生成占位,按钮改为“保存本条规划 / 保存全部规划”。只有该行勾选“产品”时,首尾帧生成才会从产品素材池按分镜角色、视角优先级、用途标签、置信度和风险自动挑选最多 6 张相关产品图;未勾选产品时不会把产品图提交给首尾帧/后续生视频模型。只有该行勾选“人物”时,才会传按需筛选后的相似主体参考图;否则 prompt 会明确禁止强行添加主角式透明骨架人,后端也不会再给产品特写强加透明骨架人约束。ModelTrace 会在音频解析、产品识别/补图、相似主体高清视图包、脚本改写等入口旁直接展示模型名;所有生图入口都显示并使用 gpt-image-2,没有其他图片模型 fallback;点击后用固定浮层展示模型链路、输入输出和回退逻辑。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。 web/components/media-asset-tile.tsx项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。 web/app/login/page.tsx生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。 web/app/login/layout.tsx登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 /login 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。 @@ -633,7 +633,7 @@ web/app/page.tsx -> 信息流广告复刻工作表:web/components/ad-recreation-board.tsx -> 开始分析:创建/激活 job → 下载完成后并行触发视频视觉路 analyzeJob 与音频文案路 triggerTranscribe -> WorkflowOrderBar:01 素材输入 → 02 源视频下载 → 03 音频文案 → 04 抽帧参考 → 05 相似主体 → 06 产品素材池 → 07 分镜文案 → 08 画面首尾帧 → 09 视频候选;每步从 buildWorkflowSteps 取判定依据和状态 - -> 左侧素材输入列 + 右侧 03-06 状态条 + 默认折叠的文案依据 + 源视频工作区(音频解析结果默认折叠,竖版 9:16 原视频播放器放大并内置当前点抽帧,右侧上方连续响度波形显示当前/总时长/指针停点,右侧下方逐句时间轴联动滚动,参考帧池在下方多列铺开且主入口为“自动抽帧 12 张”,已选关键帧直接显示在关键帧池右侧,相似主体高清视图包生成按钮放在视图区;不勾选帧则默认用全部帧,勾选后只用已选帧,可叠加 5 套内置形象) + -> 左侧素材输入列 + 右侧 03-06 状态条 + 默认折叠的文案依据 + 源视频工作区(音频解析结果默认折叠,竖版 9:16 原视频播放器放大并内置当前点抽帧,右侧上方连续响度波形显示当前/总时长/指针停点,右侧下方左侧是参考帧池 + 已选关键帧,右侧是逐句时间轴联动滚动;下方不再重复显示关键帧,只保留相似主体 / 主体模板和相似主体高清视图包;不勾选帧则默认用全部帧,勾选后只用已选帧,可叠加 5 套内置形象) -> 信息流复刻分镜工作台:06 同一产品素材池不限量上传 → 自动识别视角 / 背景 / 用途 / 风险 → 人工检查备注 → 07 逐句时间轴 / 原内容 / 新口播文案 → 08 画面规划与产品融入(镜头类型、人物描述、人物/产品开关、首帧、尾帧、产品出现方式)→ 首尾帧闸门:按需求选择主体视角 + 产品素材生成首帧/尾帧 → 保存规划 → 09 历史候选视频槽(当前不直接批量提交视频) -> 底部音频条:不再渲染,音频结果集中到右侧工作表 -> 旧节点/深度素材面板:web/components/nodes/index.tsx、web/components/lightbox.tsx、web/components/storyboard-workbench.tsx(底层保留,当前不作为主入口) @@ -1021,6 +1021,18 @@ ProductRefStateItem {

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-18 · 关键帧移到逐句时间轴左侧

+ UI + Workflow +
+
+

问题:关键帧和已选关键帧放在源视频工作区下方,用户看字幕时间轴时还要上下跳,下面主体生成区也被关键帧池占掉太多空间。

+

改动:AudioIntakePanel 在逐句时间轴左侧新增 SourceKeyframePicker,把参考帧池、已选关键帧、自动抽帧 12 张、删除关键帧和 hover 放大都放到时间轴旁。SourceReferenceBuildPanel 去掉下方重复关键帧池,改成只展示“相似主体 / 主体模板”、内置形象选择、主体类型、统一方向和生成后的相似主体白底视图。

+

影响:后续这里应理解为“上方完成源视频观看、字幕对齐和关键帧选择;下方只做主体模板和主体资产生成”。

+
+

2026-05-18 · 放大原版视频并右侧显示已选关键帧

diff --git a/web/components/ad-recreation-board.tsx b/web/components/ad-recreation-board.tsx index 5b9f725..3565097 100644 --- a/web/components/ad-recreation-board.tsx +++ b/web/components/ad-recreation-board.tsx @@ -1697,6 +1697,8 @@ function AudioIntakePanel({ const [audioFeatures, setAudioFeatures] = useState([]) const [audioFeatureStatus, setAudioFeatureStatus] = useState("idle") const [manualBusy, setManualBusy] = useState(false) + const [extracting, setExtracting] = useState(false) + const [deletingFrame, setDeletingFrame] = useState(null) const [waveHoverTime, setWaveHoverTime] = useState(null) const videoRef = useRef(null) const rowRefs = useRef>({}) @@ -1720,6 +1722,11 @@ function AudioIntakePanel({ ) }, [job, mediaDuration]) const activeSegment = job?.transcript.find((segment) => currentTime >= segment.start && currentTime <= Math.max(segment.end, segment.start + 0.2)) + const frames = useMemo(() => job ? [...job.frames].sort((a, b) => a.timestamp - b.timestamp) : [], [job]) + const selectedReferenceFrames = useMemo( + () => frames.filter((frame) => selectedFrames.has(frame.index)), + [frames, selectedFrames], + ) const waveTimeHint = waveHoverTime !== null ? `指针停点 ${waveHoverTime.toFixed(1)}s` : activeSegment @@ -1802,6 +1809,33 @@ function AudioIntakePanel({ } } + const extractKeyframes = async () => { + if (!job) return + setExtracting(true) + try { + for (const frame of job.frames) { + if (selectedFrames.has(frame.index)) onToggleFrame(frame.index) + } + const updated = await analyzeJob(job.id, 12, "motion", "replace", "accurate") + onJobUpdate(updated) + toast.info("已按动作峰值逻辑重新抽取 12 张参考帧,完成后在时间轴左侧选择主角参考。") + } catch (e) { + toast.error("12 张关键帧抽取失败:" + (e instanceof Error ? e.message : String(e))) + } finally { + setExtracting(false) + } + } + + const deleteReferenceFrame = async (idx: number) => { + if (!job || !onDeleteFrame) return + setDeletingFrame(idx) + try { + await onDeleteFrame(job.id, idx) + } finally { + setDeletingFrame(null) + } + } + if (!job) { return } @@ -1900,44 +1934,58 @@ function AudioIntakePanel({ />
-
-
- } title="逐句时间轴" /> - {job.transcript.length} 段 -
- {job.transcript.length ? ( -
-
-
时间
-
原文 / 中文
-
-
- {job.transcript.map((segment) => { - const active = activeSegment?.index === segment.index - return ( -
{ rowRefs.current[segment.index] = node }} - onClick={() => seekTo(segment.start)} - className={`grid cursor-pointer grid-cols-[76px_minmax(0,1fr)] gap-2 border-b px-3 py-1.5 text-[11.5px] leading-snug transition last:border-b-0 ${ - active - ? "border-emerald-300/18 bg-emerald-300/[0.12] text-white" - : "border-white/8 text-white/64 hover:bg-white/[0.045]" - }`} - > -
{segment.start.toFixed(1)}-{segment.end.toFixed(1)}s
-
-
{segment.en || -}
-
{segment.zh || 翻译中}
-
-
- ) - })} -
+
+ void extractKeyframes()} + onDeleteFrame={onDeleteFrame ? (idx) => void deleteReferenceFrame(idx) : undefined} + /> + +
+
+ } title="逐句时间轴" /> + {job.transcript.length} 段
- ) : ( - - )} + {job.transcript.length ? ( +
+
+
时间
+
原文 / 中文
+
+
+ {job.transcript.map((segment) => { + const active = activeSegment?.index === segment.index + return ( +
{ rowRefs.current[segment.index] = node }} + onClick={() => seekTo(segment.start)} + className={`grid cursor-pointer grid-cols-[76px_minmax(0,1fr)] gap-2 border-b px-3 py-1.5 text-[11.5px] leading-snug transition last:border-b-0 ${ + active + ? "border-emerald-300/18 bg-emerald-300/[0.12] text-white" + : "border-white/8 text-white/64 hover:bg-white/[0.045]" + }`} + > +
{segment.start.toFixed(1)}-{segment.end.toFixed(1)}s
+
+
{segment.en || -}
+
{segment.zh || 翻译中}
+
+
+ ) + })} +
+
+ ) : ( + + )} +
@@ -1945,9 +1993,7 @@ function AudioIntakePanel({ @@ -1956,25 +2002,137 @@ function AudioIntakePanel({ ) } +function SourceKeyframePicker({ + job, + frames, + selectedFrames, + selectedReferenceFrames, + extracting, + deletingFrame, + onToggleFrame, + onExtract, + onDeleteFrame, +}: { + job: Job + frames: KeyFrame[] + selectedFrames: Set + selectedReferenceFrames: KeyFrame[] + extracting: boolean + deletingFrame: number | null + onToggleFrame: (idx: number) => void + onExtract: () => void + onDeleteFrame?: (idx: number) => void +}) { + return ( +
+
+ } title="关键帧" /> +
+ + 已选 {selectedReferenceFrames.length || "全部"} + + +
+
+ +
+
+
+ 参考帧池 + 悬停放大 +
+
+ {frames.map((frame, index) => { + const selected = selectedFrames.has(frame.index) + return ( + onToggleFrame(frame.index)} + topLeft={{String(index + 1).padStart(2, "0")}} + topRight={{selected ? : }} + onDelete={onDeleteFrame ? () => onDeleteFrame(frame.index) : undefined} + deleting={deletingFrame === frame.index} + deleteLabel={`删除关键帧 ${index + 1}`} + /> + ) + })} + {!frames.length && ( +
+ 自动抽帧或在原版视频上用当前点抽帧。 +
+ )} +
+
+ + +
+
+ ) +} + function SourceReferenceBuildPanel({ job, selectedFrames, - onToggleFrame, onJobUpdate, - onDeleteFrame, runtimeModels, }: { job: Job selectedFrames: Set - onToggleFrame: (idx: number) => void onJobUpdate: (job: Job) => void - onDeleteFrame?: (jobId: string, idx: number) => Promise | void runtimeModels?: RuntimeModels }) { - const [extracting, setExtracting] = useState(false) const [subjectBusy, setSubjectBusy] = useState(false) const [subjectAssetBusy, setSubjectAssetBusy] = useState(null) - const [deletingFrame, setDeletingFrame] = useState(null) const [subjectStyle, setSubjectStyle] = useState("transparent_human") const [subjectDirection, setSubjectDirection] = useState("") const [characterLibrary, setCharacterLibrary] = useState([]) @@ -2011,9 +2169,9 @@ function SourceReferenceBuildPanel({ }) }, [actorAssets]) const referenceCountLabel = selectedReferenceFrames.length - ? `使用已选 ${selectedReferenceFrames.length} 张` + ? `已选 ${selectedReferenceFrames.length} 张参考帧` : frames.length - ? `默认使用全部 ${frames.length} 张` + ? `默认使用全部 ${frames.length} 张参考帧` : "待抽帧" useEffect(() => { @@ -2028,22 +2186,6 @@ function SourceReferenceBuildPanel({ return () => { cancelled = true } }, []) - const extractKeyframes = async () => { - setExtracting(true) - try { - for (const frame of job.frames) { - if (selectedFrames.has(frame.index)) onToggleFrame(frame.index) - } - const updated = await analyzeJob(job.id, 12, "motion", "replace", "accurate") - onJobUpdate(updated) - toast.info("已按动作峰值逻辑重新抽取 12 张参考帧,完成后在这里人工选择主角参考。") - } catch (e) { - toast.error("12 张关键帧抽取失败:" + (e instanceof Error ? e.message : String(e))) - } finally { - setExtracting(false) - } - } - const generateSimilarActor = async () => { if (!frames.length) { toast.warning("请先自动抽帧 12 张,或在原版视频上手动补帧。") @@ -2095,16 +2237,6 @@ function SourceReferenceBuildPanel({ } } - const deleteReferenceFrame = async (idx: number) => { - if (!onDeleteFrame) return - setDeletingFrame(idx) - try { - await onDeleteFrame(job.id, idx) - } finally { - setDeletingFrame(null) - } - } - const regenerateSubjectAsset = async (asset: SubjectAsset) => { if (!actorSource) return setSubjectAssetBusy(`regen:${asset.id}`) @@ -2150,99 +2282,15 @@ function SourceReferenceBuildPanel({ return (
- } title="关键帧 / 相似主体" /> + } title="相似主体 / 主体模板" />
{referenceCountLabel} -
-
-
-
-
- 缩略图完整显示,悬停看大图。 - 不勾选则默认用全部帧 -
- -
- {frames.map((frame, index) => { - const selected = selectedFrames.has(frame.index) - return ( - onToggleFrame(frame.index)} - topLeft={{String(index + 1).padStart(2, "0")}} - topRight={{selected ? : }} - onDelete={onDeleteFrame ? () => void deleteReferenceFrame(frame.index) : undefined} - deleting={deletingFrame === frame.index} - deleteLabel={`删除关键帧 ${index + 1}`} - /> - ) - })} - {!frames.length && ( -
- 点击“自动抽帧 12 张”,或在原版视频播放器上用“当前点抽帧”补充人物参考。 -
- )} -
-
- - -
- -
+
+
相似主体白底视图