From edb54e141ab5925334847d838d584e2f3741bf3e Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 07:01:12 +0800 Subject: [PATCH] auto-save 2026-05-14 07:01 (+1, ~4) --- .memory/worklog.json | 27 ++-- docs/source-analysis.html | 41 ++++- web/components/lightbox.tsx | 43 +++++- web/components/product-library-picker.tsx | 179 ++++++++++++++++++++++ web/components/storyboard-workbench.tsx | 19 ++- 5 files changed, 286 insertions(+), 23 deletions(-) create mode 100644 web/components/product-library-picker.tsx diff --git a/.memory/worklog.json b/.memory/worklog.json index a3280c9..b801ecc 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,19 +1,5 @@ { "entries": [ - { - "files_changed": 3, - "hash": "67bbdae", - "message": "auto-save 2026-05-12 19:08 (~3)", - "ts": "2026-05-12T19:09:08+08:00", - "type": "commit" - }, - { - "files_changed": 3, - "hash": "30a4c46", - "message": "auto-save 2026-05-12 19:14 (~3)", - "ts": "2026-05-12T19:14:42+08:00", - "type": "commit" - }, { "files_changed": 3, "hash": "5a86328", @@ -3344,6 +3330,19 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 06:49 (~1)", "files_changed": 1 + }, + { + "ts": "2026-05-14T06:55:41+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 06:55 (+1, ~3)", + "hash": "aff05b8", + "files_changed": 45 + }, + { + "ts": "2026-05-13T22:58:51Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 06:55 (+1, ~3)", + "files_changed": 3 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 386c928..3052190 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -556,7 +556,7 @@
3

清洗水印

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

4

主体识别

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

5

素材准备

清洗关键帧,把多张关键帧作为同一主体的参考,先重绘六张标准站立主体资产图,再按关键帧生成多个去主体、相似或换风格场景图。

-
6

分镜改造

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

+
6

分镜改造

把参考主体、场景、动作和 SKG 产品放入分镜结构;SKG 产品可从内置白底图库直接加入产品参考组。

7

生成视频

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

8

合成成品

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

@@ -571,7 +571,8 @@ 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/product-library-picker.tsxSKG 内置白底产品图库选择器:搜索、品类筛选、预览尺寸,并把库内图片复制为当前 job 的 assetweb/components/storyboard-bar.tsx顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。 web/components/storyboard-workbench.tsx顶部分镜编排条下方的明细区:4 图槽、改造目标、时长、自动保存。 web/lib/api.ts前端类型和 API client,是前后端数据契约镜像。 @@ -583,6 +584,7 @@ + @@ -623,13 +625,13 @@ api/main.py
你看到的区域关键帧素材审核面板
-
主要源码FrameLightbox;按“原图/清洗、主体资产、场景图、审核”四个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,场景图页左侧显示全部关键帧并可勾选场景参考;右侧承载当前页操作、状态和结果。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立主体图;场景图依赖主体资产,右侧通过地点、生成方式、风格和参考要素拼出可编辑 prompt,再按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 cleanupFrameaddElementgenerateSubjectAssetsgenerateSceneAsset
-
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图和质量风险应该如何审核”。
+
主要源码FrameLightbox;按“原图/清洗、主体资产、场景图、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,场景图页左侧显示全部关键帧并可勾选场景参考,产品融合页左侧接入内置 SKG 白底图库;右侧承载当前页操作、状态和结果。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立主体图;场景图依赖主体资产,右侧通过地点、生成方式、风格和参考要素拼出可编辑 prompt,再按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 cleanupFrameaddElementgenerateSubjectAssetsgenerateSceneAssetlistProductLibrarycopyProductLibraryAsset
+
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图、SKG 产品融合参考和质量风险应该如何审核”。
你看到的区域顶部分镜头编排下拉面板
-
主要源码StoryboardWorkbench;保存到 frame.storyboard;接口 PUT /storyboard
-
适合怎么描述“每个分镜需要哪些图片槽、哪些改造说明,如何为视频生成做准备”。
+
主要源码StoryboardWorkbench;保存到 frame.storyboard;接口 PUT /storyboard。SKG 产品参考区同时支持上传、剪贴板和内置白底产品库。
+
适合怎么描述“每个分镜需要哪些图片槽、哪些改造说明,哪些 SKG 产品图要作为视频生成参考”。
@@ -692,6 +694,18 @@ SubjectAsset { background: white | black, width, height, size, source_frame_indices[] +} + +
+

ProductLibraryItem

+

内置 SKG 白底图库条目。实际图片保存在 api/product_library/skg-products/images,被选中时会复制到 jobs/<jobId>/assets,再以普通 ImageRef(kind="asset") 进入产品参考组。

+
ProductLibraryItem {
+  id, handle, title, product_type,
+  image_index, filename, url,
+  width, height,
+  white_score,
+  source_path,
+  tags[]
 }
@@ -732,6 +746,8 @@ SubjectAsset {
+ + @@ -841,6 +857,19 @@ SubjectAsset {

变更记录

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

+
+
+

2026-05-14 · 增加产品融合和 SKG 内置白底图库

+ FrameLightbox + 产品融合 +
+
+

问题:生成视频需要稳定的 SKG 产品真源,不能每次都依赖临时上传或从参考视频里找产品图;桌面已有整理过的 SKG 产品图,应作为内置数据库使用。

+

改动:从桌面 skg_product_downloads/all_products 的 gallery 中筛出 41 张白底产品图,生成 api/product_library/skg-products/manifest.json 和压缩预览图。FrameLightbox 新增“产品融合”页签,StoryboardWorkbench 的 SKG 产品参考区也接入同一个 ProductLibraryPicker,支持搜索、品类筛选、尺寸预览和一键加入。

+

后端:新增 GET /product-library/skgGET /product-library/skg/images/{filename}POST /jobs/{job_id}/assets/product-library。选中库内产品图时,后端会复制成当前 job 的 asset,后续仍通过既有 product_images 进入生视频接口。

+

影响:api/main.pyapi/product_library/skg-productsweb/lib/api.tsweb/components/product-library-picker.tsxweb/components/lightbox.tsxweb/components/storyboard-workbench.tsxdocs/source-analysis.html

+
+

2026-05-14 · 场景图改为全图参考和关键词 Prompt

diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index 306a73b..17833e7 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -8,6 +8,7 @@ import { generateSceneAsset, generateSubjectAssets, type AssetBackground, type AssetSize, type KeyFrame, type Job, type ImageRef, type SceneMode, type SceneStyle, type SubjectKind, } from "@/lib/api" +import { ProductLibraryPicker } from "@/components/product-library-picker" import { toast } from "sonner" interface Props { @@ -63,12 +64,13 @@ const LIVING_VIEW_GROUPS = [ { title: "动作补充", hint: "需要动作镜头时再勾,仍保持同一人物身份", options: LIVING_ACTION_OPTIONS }, ] -type LightboxTab = "clean" | "scene" | "subject" | "review" +type LightboxTab = "clean" | "scene" | "subject" | "product" | "review" const LIGHTBOX_TABS: Array<{ key: LightboxTab; label: string }> = [ { key: "clean", label: "原图/清洗" }, { key: "subject", label: "主体资产" }, { key: "scene", label: "场景图" }, + { key: "product", label: "产品融合" }, { key: "review", label: "审核" }, ] @@ -219,6 +221,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o ] const isSubjectTab = activeTab === "subject" const isSceneTab = activeTab === "scene" + const isProductTab = activeTab === "product" const isCleanTab = activeTab === "clean" const sceneReferenceFrameIndices = (selectedFrameIndices.length > 0 ? selectedFrameIndices : [f.index]) .filter((idx, pos, arr) => arr.indexOf(idx) === pos) @@ -573,6 +576,8 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o ? { flex: "1 1 360px", minWidth: 220, maxWidth: 460, minHeight: 0 } : isSceneTab ? { flex: "1 1 430px", minWidth: 280, maxWidth: 560, minHeight: 0 } + : isProductTab + ? { flex: "1 1 600px", minWidth: 360, maxWidth: 760, minHeight: 0 } : isCleanTab ? { flex: "1 1 500px", minWidth: 300, maxWidth: 600, minHeight: 0 } : { flex: "1 1 560px", minWidth: 300, maxWidth: 680, minHeight: 0 }} @@ -699,6 +704,14 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o 左侧显示全部关键帧;点图片设为生成目标,点“选”加入场景参考。未选择时默认只参考当前目标帧。
+ ) : isProductTab ? ( + onCopyImage?.(ref)} + /> ) : (
@@ -1063,6 +1076,32 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o )} )} + {activeTab === "product" && ( +
+
产品融合目标
+
+ {`frame +
+ 分镜 {f.index + 1} + {f.timestamp.toFixed(2)}s +
+
+
+
+
图库来源
+
桌面 SKG 产品图 · gallery 白底筛选
+
+
+
使用方式
+
复制产品图后,在画面工作台加入 SKG 产品参考组。
+
+
+
生成约束
+
白底图保留外观、颜色、结构;有人物的产品示范图也可作为佩戴参考。
+
+
+
+ )} {activeTab === "review" && (
素材审核
diff --git a/web/components/product-library-picker.tsx b/web/components/product-library-picker.tsx new file mode 100644 index 0000000..6286899 --- /dev/null +++ b/web/components/product-library-picker.tsx @@ -0,0 +1,179 @@ +"use client" + +import { useEffect, useMemo, useState } from "react" +import { Copy, Loader2, Plus, Search } from "lucide-react" +import { + apiAssetUrl, + copyProductLibraryAsset, + listProductLibrary, + type ImageRef, + type ProductLibraryItem, +} from "@/lib/api" +import { toast } from "sonner" + +interface ProductLibraryPickerProps { + jobId: string + onPick: (ref: ImageRef, item: ProductLibraryItem) => void + disabled?: boolean + buttonLabel?: string + title?: string + compact?: boolean + maxItems?: number + className?: string +} + +export function ProductLibraryPicker({ + jobId, + onPick, + disabled = false, + buttonLabel = "加入", + title = "内置白底产品库", + compact = false, + maxItems, + className = "", +}: ProductLibraryPickerProps) { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(false) + const [query, setQuery] = useState("") + const [productType, setProductType] = useState("all") + const [addingId, setAddingId] = useState(null) + + useEffect(() => { + let alive = true + setLoading(true) + listProductLibrary() + .then((next) => { + if (alive) setItems(next) + }) + .catch((e) => { + if (alive) toast.error("产品库读取失败:" + (e instanceof Error ? e.message : String(e))) + }) + .finally(() => { + if (alive) setLoading(false) + }) + return () => { + alive = false + } + }, []) + + const productTypes = useMemo(() => { + return Array.from(new Set(items.map((item) => item.product_type).filter(Boolean))).sort() + }, [items]) + + const filteredItems = useMemo(() => { + const q = query.trim().toLowerCase() + const next = items.filter((item) => { + if (productType !== "all" && item.product_type !== productType) return false + if (!q) return true + const haystack = [ + item.title, + item.handle, + item.product_type, + item.source_path, + String(item.image_index), + ].join(" ").toLowerCase() + return haystack.includes(q) + }) + return typeof maxItems === "number" ? next.slice(0, maxItems) : next + }, [items, maxItems, productType, query]) + + const handlePick = async (item: ProductLibraryItem) => { + if (disabled || addingId) return + setAddingId(item.id) + try { + const ref = await copyProductLibraryAsset(jobId, item.id) + onPick(ref, item) + toast.success(`已${buttonLabel}:${item.title}`) + } catch (e) { + toast.error("产品图加入失败:" + (e instanceof Error ? e.message : String(e))) + } finally { + setAddingId(null) + } + } + + return ( +
+
+
+ {title} + {filteredItems.length}/{items.length} +
+ {loading && } +
+ +
+ + +
+ + {loading ? ( +
+ + 读取产品库 +
+ ) : filteredItems.length === 0 ? ( +
+ 没有匹配的白底产品图 +
+ ) : ( +
+ {filteredItems.map((item) => { + const busy = addingId === item.id + return ( +
+
+ {item.title} +
+ {item.width}×{item.height} +
+
+
+
+ {item.title} +
+
+ + #{item.image_index} · {item.product_type || "SKG"} + + +
+
+
+ ) + })} +
+ )} +
+ ) +} diff --git a/web/components/storyboard-workbench.tsx b/web/components/storyboard-workbench.tsx index d22f53f..8c78a41 100644 --- a/web/components/storyboard-workbench.tsx +++ b/web/components/storyboard-workbench.tsx @@ -5,6 +5,7 @@ import { type Job, type StoryboardScene, type ImageRef, updateStoryboard, resolveImageRefUrl, uploadStoryboardAsset, } from "@/lib/api" +import { ProductLibraryPicker } from "@/components/product-library-picker" import { toast } from "sonner" interface Props { @@ -145,6 +146,13 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU const next = refs.slice(0, 6) queueSave({ ...form, product_image: next[0] ?? null, product_images: next }) } + const addProductRef = (ref: ImageRef) => { + if (productRefs.length >= 6) { + toast.error("最多添加 6 张产品参考") + return + } + setProductRefs([...productRefs, ref]) + } const addProductFiles = async (files: FileList | File[]) => { if (!job) return const room = 6 - productRefs.length @@ -299,7 +307,7 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU toast.error("最多添加 6 张产品参考") return } - setProductRefs([...productRefs, clipboard]) + addProductRef(clipboard) toast.success("已添加产品参考") }} disabled={!clipboard || productRefs.length >= 6} @@ -374,6 +382,15 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU
+ = 6} + onPick={(ref) => addProductRef(ref)} + /> + {/* 改造 brief:明确“借鉴参考 → 变成 SKG 产品视频”,避免直接复刻 */}
api/main.pyFastAPI 单文件后端:状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、文件返回。
api/product_library/skg-products内置 SKG 白底产品图库:manifest.json 记录从桌面产品图筛出的 gallery 白底图,images/ 存 41 张压缩后的参考图。
jobs/<jobId>/state.json运行时状态文件,不在源码列表里,但刷新恢复依赖它。
jobs/<jobId>/frames关键帧 jpg。注意 frame.index 是稳定 ID,不等于数组下标。
jobs/<jobId>/cleaned清洗后待应用图片。
元素提取POST /elements/{element_id}/cutoutcutoutElement调用图像模型生成独立白底素材图,每次累积一张 cutout。
主体资产包POST /elements/{element_id}/subject-assetsgenerateSubjectAssets根据参考帧重新绘制一个统一主体资产包;前端默认把全部关键帧作为 source_frame_indices,如果用户手动选择了关键帧则只传已选帧,后端拼参考板。人物默认输出六张身份标准图,另有表情补充和动作补充分组可选;纯白/黑背景,不含其他元素,并裁去空白让主体占满画面。
场景资产POST /frames/{idx}/scene-assetgenerateSceneAsset在统一主体资产之后,按当前关键帧生成去主体背景板;请求包含 scene_modescene_stylepromptsource_frame_indices,可用左侧选择的参考帧 + 右侧关键词生成原场景补背景、相似新场景或同构换风格,保留历史版本用于人工审核。
产品图库GET /product-library/skglistProductLibrary读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。
产品图入库到 jobPOST /jobs/{id}/assets/product-librarycopyProductLibraryAsset把一个内置产品图库条目复制为当前 job 的普通 asset,返回 ImageRef(kind="asset"),用于画面工作台产品融合和分镜产品参考组。
分镜保存PUT /frames/{idx}/storyboardupdateStoryboard保存 4 图槽、时长和改造说明。
生图POST /frames/{idx}/generategenerateImage基于关键帧或已选生成图做 image-to-image,目前可用。