From f3636a5ec7de5ae31aad3c11d9971e67fb01f8da Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 05:32:54 +0800 Subject: [PATCH] auto-save 2026-05-14 05:32 (~4) --- .memory/worklog.json | 13 ++++++ docs/source-analysis.html | 38 +++++++++++------ web/components/lightbox.tsx | 32 ++++++--------- web/components/nodes/index.tsx | 74 +++++----------------------------- 4 files changed, 60 insertions(+), 97 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index f98dab1..d3669ca 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -3348,6 +3348,19 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 05:21 (~3)", "files_changed": 1 + }, + { + "ts": "2026-05-14T05:27:24+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 05:27 (~3)", + "hash": "2c19b52", + "files_changed": 3 + }, + { + "ts": "2026-05-13T21:28:50Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 05:27 (~3)", + "files_changed": 2 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index ae6423a..5426efe 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -554,9 +554,9 @@
1

输入

TK 链接或本地上传,后端下载/保存源视频。

2

镜头拆解

拆轨、抽关键帧、手动加帧,形成参考分镜池。

3

清洗水印

对关键帧做全图或区域清洗,必要时应用为当前参考图。

-
4

Vision 识别

识别场景和候选元素,只是候选,不应锁死。

-
5

元素提取

编辑/新增/删除元素,对元素反复生成提取图。

-
6

元素改造

把参考主体、场景、动作和 SKG 产品放入分镜结构。

+
4

主体识别

识别场景和主体候选,只是候选,不应锁死。

+
5

素材准备

清洗关键帧,生成场景图和主体多视角/动作/表情资产包。

+
6

分镜改造

把参考主体、场景、动作和 SKG 产品放入分镜结构。

7

生成视频

用分镜 4 图槽、改造目标和时长调用 Seedance / Kling / Veo 3 生视频 API,结果回写到画面工作台节点。

8

合成成品

片段、字幕、配音、转场合成最终 mp4。当前未实现。

@@ -571,7 +571,7 @@ web/app/page.tsx产品工作台主状态:jobs、activeJobId、selectedFrames、clipboard、ReactFlow 节点和边;负责打开/找回画布工作面板。 web/components/nodes/index.tsxDAG 节点定义:Input、VisualLab、Audio、Compose,以及画布工作面板 KeyframePanel / VideoFramePanel;旧 Keyframe/Storyboard/VideoGen 组件保留但不再挂主画布。 - web/components/lightbox.tsx镜头拆解和元素提取的主工作面板:清洗、识别、元素编辑、区域提取、抠图。 + web/components/lightbox.tsx关键帧素材准备面板:清洗、场景图、主体候选、主体资产包和审核。 web/components/storyboard-bar.tsx顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。 web/components/storyboard-workbench.tsx顶部分镜编排条下方的明细区:4 图槽、改造目标、时长、自动保存。 web/lib/api.ts前端类型和 API client,是前后端数据契约镜像。 @@ -618,12 +618,12 @@ api/main.py
你看到的区域画面工作台 · Visual Lab
-
主要源码VisualLabNode in web/components/nodes/index.tsx;它现在是素材准备看板,汇总关键帧、场景图、主体资产包、普通抠图和视频任务。
+
主要源码VisualLabNode in web/components/nodes/index.tsx;它现在是素材准备看板,汇总关键帧、场景图、主体资产包和视频任务。
适合怎么描述“画面工作台的素材准备进度、分组缩略图、关键帧审核入口和后续分镜入口应该如何组织”。
你看到的区域关键帧素材审核面板
-
主要源码FrameLightbox;按“原图/清洗、场景图、主体包、审核”四个页签组织;相关接口包括 cleanupFramegenerateSceneAssetgenerateSubjectAssetscutoutElement
+
主要源码FrameLightbox;按“原图/清洗、场景图、主体资产、审核”四个页签组织;相关接口包括 cleanupFrameaddElementgenerateSceneAssetgenerateSubjectAssets
适合怎么描述“某张关键帧的水印、场景图、主体多视角/动作/表情图和质量风险应该如何审核”。
@@ -664,7 +664,7 @@ api/main.py

KeyElement

-

从关键帧里识别或手动添加的可借鉴元素。Vision 给的是候选,用户可编辑,并可多次生成提取图。

+

从关键帧里识别或手动添加的主体候选。Vision 给的是候选,用户可编辑、删除,并可基于它生成主体资产包。

KeyElement {
   id,
   name_zh, name_en, position,
@@ -751,7 +751,7 @@ SubjectAsset {
             
             
               画面工作台 Visual Lab
-              作为素材准备看板:显示准备进度、质量风险、关键帧 / 场景图 / 主体包 / 分镜视频四个入口;上方缩略图按关键帧、场景图、主体包、普通抠图、视频任务分组。点击关键帧进入素材审核面板,点击资产图复制到分镜编排。
+              作为素材准备看板:显示准备进度、质量风险、关键帧 / 场景图 / 主体包 / 分镜视频四个入口;上方缩略图按关键帧、场景图、主体包、视频任务分组。点击关键帧进入素材审核面板,点击资产图复制到分镜编排。
               不要在主卡片里堆复杂表单;主卡片只做状态总览和入口。
               VisualLabNodeFrameLightboxgenerateSceneAssetgenerateSubjectAssets、视频任务接口
             
@@ -787,8 +787,8 @@ SubjectAsset {
               
  • 视频下载或本地保存,ffmpeg 抽关键帧。
  • 手动按时间戳加关键帧。
  • 关键帧清洗水印,全图或区域清洗。
  • -
  • Vision 识别关键帧,输出 scene、objects、style、suggested_prompt。
  • -
  • 元素增改删、区域元素添加、元素多次提取图。
  • +
  • Vision 识别关键帧,输出 scene、objects、style、suggested_prompt,并作为主体候选来源。
  • +
  • 主体候选增改删、区域主体添加、主体资产包生成。
  • 分镜工作台 4 图槽和改造说明自动保存。
  • nano-banana-pro image-to-image 生图。
  • @@ -813,8 +813,8 @@ SubjectAsset {

    需求描述模板

    -

    改镜头拆解 / 元素提取

    -

    “我在关键帧 lightbox 里,Vision 识别后的元素列表应该怎么编辑/重提取/删除;点击元素不要跳转;提取图怎么预览和复制。”

    +

    改关键帧素材准备

    +

    “我在关键帧素材准备面板里,主体候选应该怎么编辑/删除;场景图和主体资产包怎么生成、审核、复制到分镜。”

    改 Storyboard 节点

    @@ -839,6 +839,18 @@ SubjectAsset {

    变更记录

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

    +
    +
    +

    2026-05-14 · 关键帧详情改为素材准备面板

    + FrameLightbox + UX +
    +
    +

    问题:面板标题仍叫“关键帧详情 · 元素提取”,里面还露出普通 cutout 抠图、AI 提取、元素清单等旧流程入口;“选用此帧”也无法说明它其实是在维护目标关键帧集合。

    +

    改动:KeyframePanelNode 标题改为“关键帧素材准备”;FrameLightbox 的主体页改成“主体识别 / 主体清单 / 主体资产包”,移除普通抠图列表和 AI 提取按钮;“选用此帧”改为“加入目标帧 / 目标帧 · 点击移出”。VisualLabNode 上方缩略图也不再展示普通抠图分组,只展示关键帧、场景图、主体包和视频任务。

    +

    影响:web/components/lightbox.tsxweb/components/nodes/index.tsxdocs/source-analysis.html。底层旧 cutout 数据和接口暂保留兼容历史任务,但不再作为新素材准备流程的可见入口。

    +
    +

    2026-05-14 · 画面工作台改为素材准备看板

    @@ -847,7 +859,7 @@ SubjectAsset {

    问题:画面工作台从展示缩略图扩展为素材生产中枢后,关键帧、场景图、主体资产包和视频任务继续混在一个列表里会让流程不清晰;关键帧详情面板也把清洗、识别、场景和主体生成都堆在一屏。

    -

    改动:VisualLabNode 改成素材准备进度看板,显示目标关键帧、场景图、主体资产和分镜/视频四个入口,并在上方缩略图中按关键帧、场景图、主体包、普通抠图、视频任务分组。FrameLightbox 新增“原图/清洗、场景图、主体包、审核”四个页签,素材审核信息从普通元素列表中拆出来。

    +

    改动:VisualLabNode 改成素材准备进度看板,显示目标关键帧、场景图、主体资产和分镜/视频四个入口。FrameLightbox 新增“原图/清洗、场景图、主体资产、审核”四个页签,素材审核信息从普通列表中拆出来。

    影响:web/components/nodes/index.tsxweb/components/lightbox.tsxdocs/source-analysis.html。这轮只重排工作台信息架构,批量自动准备队列仍留到下一阶段。

    diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index b81372f..7d9ab54 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -107,21 +107,16 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const pos = frames.findIndex((x) => x.index === activeIndex) if (e.key === "ArrowLeft" && pos > 0) onChange(frames[pos - 1].index) if (e.key === "ArrowRight" && pos < frames.length - 1) onChange(frames[pos + 1].index) - if (e.key === " " || e.key === "Enter") { - e.preventDefault() - onToggleSelect(activeIndex) - } } } window.addEventListener("keydown", onKey) return () => window.removeEventListener("keydown", onKey) - }, [activeIndex, frames.length, onClose, onChange, onToggleSelect]) + }, [activeIndex, frames.length, onClose, onChange]) const f = activeIndex !== null ? frames.find((x) => x.index === activeIndex) : undefined const arrayPos = f ? frames.findIndex((x) => x.index === f.index) : -1 if (activeIndex === null || !f || !mounted) return null - const isSelected = selected.has(f.index) const desc = f.description const elements = f.elements ?? [] const hasCleaned = !!f.cleaned_url @@ -412,7 +407,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
    - {/* 主体 — 左:大图 + 清洗 / 目标帧;右:主体识别 + 主体资产 */} + {/* 主体 — 左:大图 + 清洗状态;右:主体识别 + 主体资产 */}
    {/* 左侧大图区 */}
    )} - +
    +
    + + 已在素材准备流程 +
    +
    + 抽帧留下的关键帧默认都会参与清洗、场景图和主体资产准备,不需要在这里单独选用。 +
    +
    {/* 右侧主体识别 + 主体资产 */} @@ -1092,7 +1084,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
    - ←/→ 切换 · Space 切换目标帧 · ESC 关闭 + ←/→ 切换关键帧 · ESC 关闭
    ) diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index d9db426..4ea3cda 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -1212,11 +1212,9 @@ export function VisualLabNode({ data, selected }: any) { const aspect = job && (job.width ?? 0) > 0 && (job.height ?? 0) > 0 ? `${job.width}/${job.height}` : "9/16" - const elementCrops = collectElementCrops(job) const sceneAssets = collectSceneAssets(job) const subjectAssets = collectSubjectAssets(job) const cleanedCount = frames.filter((x) => x.cleaned_url).length - const cutoutCount = frames.reduce((s, x) => s + (x.elements?.filter((e) => hasCutout(e)).length ?? 0), 0) const sceneAssetCount = sceneAssets.length const subjectAssetCount = subjectAssets.length const selectedFrameCount = frames.filter((f) => d.selectedFrames.has(f.index)).length @@ -1232,7 +1230,7 @@ export function VisualLabNode({ data, selected }: any) { ? "running" : failedVideo ? "failed" - : frames.length > 0 || elementCrops.length > 0 || completedVideos.length > 0 + : frames.length > 0 || subjectAssets.length > 0 || completedVideos.length > 0 ? "done" : keyframeStatus(job) @@ -1240,7 +1238,6 @@ export function VisualLabNode({ data, selected }: any) { | { id: string; kind: "frame"; group: string; frameIdx: number; src: string; label: string; caption: string; borderClass: string; aspect: string } | { id: string; kind: "scene"; group: string; frameIdx: number; assetId: string; src: string; label: string; caption: string; borderClass: string; aspect: string } | { id: string; kind: "subject"; group: string; frameIdx: number; assetId: string; src: string; label: string; caption: string; borderClass: string; aspect: string } - | { id: string; kind: "cutout"; group: string; frameIdx: number; elementId: string; cutoutId: string; src: string; label: string; caption: string; borderClass: string; aspect: string } | { id: string; kind: "video"; group: string; videoId: string; videoSrc?: string; posterSrc?: string; label: string; caption: string; borderClass: string; aspect: string } const [hoverPreview, setHoverPreview] = useState | null>(null) @@ -1284,19 +1281,6 @@ export function VisualLabNode({ data, selected }: any) { borderClass: "border-violet-300/65", aspect: p.width && p.height ? `${p.width}/${p.height}` : "1/1", })), - ...elementCrops.map((p) => ({ - id: `cutout:${p.frameIdx}:${p.elementId}:${p.cid}`, - kind: "cutout" as const, - group: "普通抠图", - frameIdx: p.frameIdx, - elementId: p.elementId, - cutoutId: p.cid, - src: p.src, - label: p.name, - caption: `分镜 ${p.frameIdx + 1}`, - borderClass: "border-violet-300/60", - aspect: "1/1", - })), ...videos.map((v, i) => { const videoSrc = apiAssetUrl(v.url) const posterSrc = apiAssetUrl(v.poster_url) @@ -1344,7 +1328,7 @@ export function VisualLabNode({ data, selected }: any) { p.kind === "frame" ? isSelected ? "border-emerald-400 ring-2 ring-emerald-400/60" : "border-white/30 dark:border-white/20" : p.borderClass - } ${p.kind === "cutout" || p.kind === "subject" ? "bg-white" : "bg-black"}`} + } ${p.kind === "subject" ? "bg-white" : "bg-black"}`} style={{ height: THUMBNAIL_HEIGHT, aspectRatio: p.aspect }} onMouseEnter={(e) => setHoverPreview({ id: p.id, ...canvasThumbnailAnchor(rootRef.current, e.currentTarget) })} onMouseLeave={() => setHoverPreview(null)} @@ -1370,10 +1354,6 @@ export function VisualLabNode({ data, selected }: any) { }) if (!d.selectedFrames.has(p.frameIdx)) d.onToggleFrame(p.frameIdx) d.onOpenWorkbench?.(p.frameIdx) - } else if (p.kind === "cutout") { - if (!d.selectedFrames.has(p.frameIdx)) d.onToggleFrame(p.frameIdx) - d.onOpenStoryboard?.(p.frameIdx) - d.onOpenWorkbench?.(p.frameIdx) } else { const video = videos.find((v) => v.id === p.videoId) if (video) { @@ -1400,7 +1380,7 @@ export function VisualLabNode({ data, selected }: any) {
    )}
    - {p.kind === "frame" ? p.caption.replace("s", "") + "s" : p.kind === "scene" ? "场景" : p.kind === "subject" ? "主体" : p.kind === "cutout" ? "抠图" : "视频"} + {p.kind === "frame" ? p.caption.replace("s", "") + "s" : p.kind === "scene" ? "场景" : p.kind === "subject" ? "主体" : "视频"}
    @@ -1438,26 +1418,6 @@ export function VisualLabNode({ data, selected }: any) { )} - {p.kind === "cutout" && d.onCopyImage && ( - - )} - {p.kind === "video" && ( - )} - {p.kind === "video" && d.onDeleteVideo && (
    - {targetFrameCount} 张目标帧 + {targetFrameCount} 张素材帧 {qualityRiskCount > 0 ? ( @@ -1606,9 +1552,9 @@ export function VisualLabNode({ data, selected }: any) { >
    - {targetFrameCount}/{frames.length} + {frames.length}
    -
    目标关键帧
    +
    关键帧素材
    @@ -1653,7 +1599,7 @@ export function VisualLabNode({ data, selected }: any) {
    {frames.length > 0 ? ( <> - {cleanedCount} 已清洗 · {sceneAssetCount} 场景图 · {subjectAssetCount || cutoutCount} 主体素材 · {selectedFrameCount}/{frames.length} 入编排 · {completedVideos.length} 已完成 + {cleanedCount} 已清洗 · {sceneAssetCount} 场景图 · {subjectAssetCount} 主体资产 · {targetFrameCount} 素材帧 · {completedVideos.length} 已完成 ) : ( "解析后这里变成素材准备看板:先审关键帧,再生成场景图和主体资产包。" @@ -1808,7 +1754,7 @@ export function KeyframeNode({ data, selected }: any) { type="process" status={st} icon={} title="镜头拆解 · 素材准备" - subtitle={`STEP 2 · ${frames.length ? `${d.selectedFrames.size}/${frames.length} 目标帧` : "等待抽取"}`} + subtitle={`STEP 2 · ${frames.length ? `${frames.length} 素材帧` : "等待抽取"}`} selected={selected} pinned={d.pinnedNodes?.has("keyframe")} onTogglePin={() => d.onToggleNodePin?.("keyframe")} @@ -2342,7 +2288,7 @@ export function StoryboardNode({ data, selected }: any) { onClick={(e) => { e.stopPropagation(); d.onOpenWorkbench?.() }} disabled={!job || storyboardCount === 0} className="mt-2 w-full rounded-md bg-gradient-to-r from-violet-500 to-pink-500 px-3 py-2 text-[12px] font-semibold text-white shadow-lg shadow-violet-500/25 transition hover:opacity-95 disabled:cursor-not-allowed disabled:opacity-35" - title={storyboardCount === 0 ? "先在关键帧节点选用分镜" : "进入 4 图槽分镜编排"} + title={storyboardCount === 0 ? "先准备关键帧素材" : "进入 4 图槽分镜编排"} > 进入分镜编排