From f3f4c5653524ec0d896fd3a5434fd3a1d370b484 Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 06:17:00 +0800 Subject: [PATCH] auto-save 2026-05-14 06:16 (~5) --- .memory/worklog.json | 27 ++++---- api/main.py | 7 +- docs/source-analysis.html | 16 +++-- web/components/lightbox.tsx | 120 +++++++++++++++++++++------------ web/components/nodes/index.tsx | 6 +- 5 files changed, 108 insertions(+), 68 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index eb806a4..8f126e4 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,19 +1,5 @@ { "entries": [ - { - "files_changed": 3, - "hash": "4138bea", - "message": "auto-save 2026-05-12 17:00 (~3)", - "ts": "2026-05-12T17:01:09+08:00", - "type": "commit" - }, - { - "files_changed": 4, - "hash": "94afd6d", - "message": "auto-save 2026-05-12 17:06 (+1, ~3)", - "ts": "2026-05-12T17:06:43+08:00", - "type": "commit" - }, { "files_changed": 3, "hash": "e1bc89a", @@ -3353,6 +3339,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 4 项未提交变更 · 最近提交:auto-save 2026-05-14 06:05 (~1)", "files_changed": 4 + }, + { + "ts": "2026-05-14T06:11:29+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 06:11 (~6)", + "hash": "871ced6", + "files_changed": 6 + }, + { + "ts": "2026-05-13T22:13:14Z", + "type": "session-heartbeat", + "message": "Claude 会话活跃 · 最近命令:claude · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 06:11 (~6)", + "files_changed": 2 } ] } diff --git a/api/main.py b/api/main.py index c95d2bd..6fcd747 100644 --- a/api/main.py +++ b/api/main.py @@ -2077,15 +2077,18 @@ def generate_scene_asset(job_id: str, idx: int, req: GenerateSceneAssetReq) -> J confirmed_subjects = [ (e.name_en or e.name_zh).strip() - for e in (frame.elements or []) + for ref_frame in job.frames + for e in (ref_frame.elements or []) if (e.subject_assets or []) ] if not confirmed_subjects: confirmed_subjects = [ (e.name_en or e.name_zh).strip() - for e in (frame.elements or []) + for ref_frame in job.frames + for e in (ref_frame.elements or []) if (e.name_en or e.name_zh).strip() ][:3] + confirmed_subjects = list(dict.fromkeys([x for x in confirmed_subjects if x]))[:3] subject_clause = ( "Confirmed foreground subject(s) to remove: " + ", ".join(confirmed_subjects) + ". " if confirmed_subjects diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 5f2d86c..0f01d1b 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -682,6 +682,8 @@ api/main.py
SceneAsset {
   id, label, url,
   width, height, quality, size,
+  scene_mode: remove_subject | similar | style,
+  scene_style,
   quality_report
 }
 
@@ -859,7 +861,7 @@ SubjectAsset {
               Layout
             
             
-

问题:“原图/清洗、场景图、主体资产、审核”都应遵循同一结构:左侧负责看图和框选,右侧负责操作、状态和结果;旧布局把部分操作塞在左侧下方,导致左侧满、右侧空。

+

问题:“原图/清洗、主体资产、场景图、审核”都应遵循同一结构:左侧负责看图和框选,右侧负责操作、状态和结果;旧布局把部分操作塞在左侧下方,导致左侧满、右侧空。

改动:FrameLightbox 统一为左侧主图、右侧操作栏。清洗按钮、批量清洗、清洗结果预览、场景图生成/复制、主体识别/主体资产包和审核状态都在右侧;切换到非清洗页时会退出框选模式,避免画框状态残留。

影响:web/components/lightbox.tsxdocs/source-analysis.html

@@ -896,7 +898,7 @@ SubjectAsset {

问题:面板标题仍叫“关键帧详情 · 元素提取”,里面还露出普通 cutout 抠图、AI 提取、元素清单等旧流程入口;“选用此帧 / 加入目标帧”也无法说明实际价值,因为抽帧阶段已经筛过图,进入画面工作台的关键帧默认就应该参与素材准备。

-

改动:KeyframePanelNode 标题改为“关键帧素材准备”;FrameLightbox 的主体页改成“主体识别 / 主体清单 / 主体资产包”,移除普通抠图列表、AI 提取按钮、详情内的目标帧开关以及手动/框选加主体入口,改为只显示“已在素材准备流程”的状态说明;VisualLabNode 上方缩略图也不再展示普通抠图分组,只展示关键帧、场景图、主体包和视频任务。

+

改动:KeyframePanelNode 标题改为“关键帧素材准备”;FrameLightbox 的主体页改成“主体识别 / 主体清单 / 主体资产包”,移除普通抠图列表、AI 提取按钮、详情内的目标帧开关以及手动/框选加主体入口,改为只显示“已在素材准备流程”的状态说明;VisualLabNode 上方缩略图也不再展示普通抠图分组,只展示关键帧、主体包、场景图和视频任务。

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

@@ -907,20 +909,20 @@ SubjectAsset { UX
-

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

-

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

+

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

+

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

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

-

2026-05-14 · 画面工作台增加场景图和主体资产包

+

2026-05-14 · 画面工作台增加主体资产包和场景图

Visual Lab Assets
-

问题:抽帧阶段已经筛过图,画面工作台第一步应把已选关键帧转成可用于生视频的干净素材:每帧一张场景图,同一主体一套多视角/动作/表情图,而不是继续手动逐张抠普通 cutout。

-

改动:KeyFrame 新增 scene_assetsquality_reportKeyElement 新增 subject_kindsubject_assets。后端新增 generateSceneAssetgenerateSubjectAssets,主体资产支持白/黑背景、原尺寸/固定尺寸、物体视角、人物/生物动作与喜怒哀乐等表情;当已选关键帧共同指向一个主体时,前端会把这些帧作为 source_frame_indices 传入,后端拼接参考板。

+

问题:抽帧阶段已经筛过图,画面工作台第一步应把已选关键帧转成可用于生视频的干净素材:同一主体一套多视角/动作/表情图,再基于主体生成每帧去主体场景图,而不是继续手动逐张抠普通 cutout。

+

改动:KeyFrame 新增 scene_assetsquality_reportKeyElement 新增 subject_kindsubject_assets。后端新增 generateSubjectAssetsgenerateSceneAsset,主体资产支持白/黑背景、原尺寸/固定尺寸、物体视角、人物/生物动作与喜怒哀乐等表情;当已选关键帧共同指向一个主体时,前端会把这些帧作为 source_frame_indices 传入,后端拼接参考板。

影响:api/main.pyweb/lib/api.tsweb/components/lightbox.tsxweb/components/nodes/index.tsxdocs/source-analysis.html。生成出的素材保存在 jobs/<jobId>/assets,可作为 asset 类型复制到后续分镜槽位。

diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 961d44b..087944a 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -86,6 +86,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const [subjectViews, setSubjectViews] = useState>({}) const [activeTab, setActiveTab] = useState("clean") const [editingElement, setEditingElement] = useState<{ + frameIndex: number id: string name_zh: string name_en: string @@ -138,8 +139,28 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const cleanedFrameCount = frames.filter((frame) => frame.cleaned_applied || frame.cleaned_url).length const pendingCleanFrames = frames.filter((frame) => !frame.cleaned_applied && !frame.cleaned_url) const selectedFrameIndices = Array.from(selected).sort((a, b) => a - b) - const sharedSubjectFrameIndices = selectedFrameIndices.length > 1 ? selectedFrameIndices : [f.index] - const subjectAssetCount = elements.reduce((sum, item) => sum + (item.subject_assets?.length ?? 0), 0) + const subjectReferenceFrameIndices = (selectedFrameIndices.length > 0 ? selectedFrameIndices : frames.map((frame) => frame.index)) + .filter((idx, pos, arr) => arr.indexOf(idx) === pos) + const subjectReferenceLabel = selectedFrameIndices.length > 0 + ? `${subjectReferenceFrameIndices.length} 已选帧参考` + : `${subjectReferenceFrameIndices.length} 全部帧参考` + const subjectElementRefs = frames.flatMap((frame) => + (frame.elements ?? []).map((element) => ({ + frameIndex: frame.index, + frameLabel: `分镜 ${frame.index + 1}`, + element, + })), + ) + const activeSubjectRefs = elements.map((element) => ({ + frameIndex: f.index, + frameLabel: `分镜 ${f.index + 1}`, + element, + })) + const subjectDisplayRefs = activeSubjectRefs.length > 0 + ? activeSubjectRefs + : subjectElementRefs.slice(0, 1) + const hasUnifiedSubject = subjectElementRefs.length > 0 + const subjectAssetCount = subjectElementRefs.reduce((sum, item) => sum + (item.element.subject_assets?.length ?? 0), 0) const hasSubjectAssets = subjectAssetCount > 0 const qualityWarnings = [ ...(f.quality_report?.warnings ?? []), @@ -231,21 +252,21 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o } } - const handleGenerateSubjectPackage = async (elementId: string) => { + const handleGenerateSubjectPackage = async (elementId: string, frameIdx = f.index) => { const kind = subjectKinds[elementId] ?? "object" const defaultViews = (kind === "living" ? LIVING_VIEW_OPTIONS : OBJECT_VIEW_OPTIONS).map(([value]) => value) const views = subjectViews[elementId]?.length ? subjectViews[elementId] : defaultViews setSubjectGenerating(elementId) try { - const updated = await generateSubjectAssets(jobId, f.index, elementId, { + const updated = await generateSubjectAssets(jobId, frameIdx, elementId, { subject_kind: kind, background: subjectBackgrounds[elementId] ?? "white", size: assetSize, - source_frame_indices: sharedSubjectFrameIndices, + source_frame_indices: subjectReferenceFrameIndices, views, }) onJobUpdate?.(updated) - toast.success(`主体资产包已生成 · ${views.length} 张`) + toast.success(`统一主体资产包已生成 · ${views.length} 张 · ${subjectReferenceFrameIndices.length} 帧参考`) } catch (e) { toast.error("主体资产包生成失败:" + (e instanceof Error ? e.message : String(e))) } finally { @@ -325,6 +346,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const handleAddElement = async (name_zh: string, name_en?: string, position?: string, source: "auto" | "manual" = "manual") => { const zh = name_zh.trim() if (!zh) return + if (hasUnifiedSubject) { + toast.message("当前流程只保留一个主体;如需更换,请先删除现有统一主体") + return + } try { const updated = await addElement(jobId, f.index, { name_zh: zh, name_en, position, source }) onJobUpdate?.(updated) @@ -333,9 +358,9 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o } } - const handleDeleteElement = async (id: string) => { + const handleDeleteElement = async (id: string, frameIdx = f.index) => { try { - const updated = await deleteElement(jobId, f.index, id) + const updated = await deleteElement(jobId, frameIdx, id) onJobUpdate?.(updated) if (editingElement?.id === id) setEditingElement(null) } catch (e) { @@ -346,7 +371,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const handleUpdateElement = async () => { if (!editingElement || !editingElement.name_zh.trim()) return try { - const updated = await updateElement(jobId, f.index, editingElement.id, { + const updated = await updateElement(jobId, editingElement.frameIndex, editingElement.id, { name_zh: editingElement.name_zh, name_en: editingElement.name_en, position: editingElement.position, @@ -447,7 +472,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o ))}
- {hasSubjectAssets ? `${subjectAssetCount} 主体资产` : "主体待生成"} + {hasSubjectAssets ? `统一主体 ${subjectAssetCount} 张` : hasUnifiedSubject ? "统一主体待生成" : "统一主体待选择"} · {latestSceneAsset ? "场景已生成" : "场景待生成"}
@@ -677,7 +702,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
- 先根据主体资产确认要移除的主体,再补全空场景;之后可生成相似新场景或同构换风格场景。 + 先用多张关键帧生成统一主体资产,再按当前关键帧去除主体并补全空场景;之后可生成相似新场景或同构换风格场景。
0 ? "border-violet-300/35 bg-violet-500/12 text-violet-100" : "border-white/10 bg-black/25 text-white/55"}`}>
主体
-
{elements.length} 个
+
{hasUnifiedSubject ? "1 个" : "未选"}
0 ? "border-violet-300/35 bg-violet-500/12 text-violet-100" : "border-white/10 bg-black/25 text-white/55"}`}> -
资产
+
主体包
{subjectAssetCount} 张
@@ -805,7 +830,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
- 主体识别 + 统一主体识别 {desc && 已识别}
) : ( @@ -847,20 +872,23 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o {desc.objects && desc.objects.length > 0 && (
- 候选主体 · 点击加入「主体清单」 + 候选主体 · 选择唯一主体
{desc.objects.map((o, i) => { const alreadyIn = elements.some((e) => e.name_zh === o.name) + const locked = hasUnifiedSubject && !alreadyIn return (
-
-
-
主体资产包
- - {sharedSubjectFrameIndices.length > 1 ? `${sharedSubjectFrameIndices.length} 帧参考` : "当前帧参考"} - +
+
+
统一主体资产包
+ + {subjectReferenceLabel} +
-
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 1764da4..a0e6db0 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -1561,13 +1561,13 @@ export function VisualLabNode({ data, selected }: any) { onClick={(e) => { e.stopPropagation(); openFirstFrame() }} disabled={!job || frames.length === 0} className="min-h-14 rounded-md border border-white/10 px-2 py-2 text-left transition hover:border-violet-300/50 hover:bg-violet-400/10 disabled:opacity-35" - title="生成主体多视角 / 动作 / 表情资产包" + title="用多张关键帧生成统一主体的多视角 / 动作 / 表情资产包" >
{subjectAssetCount}
-
主体资产
+
统一主体