diff --git a/.memory/worklog.json b/.memory/worklog.json
index 0fef4c2..844d322 100644
--- a/.memory/worklog.json
+++ b/.memory/worklog.json
@@ -1,26 +1,5 @@
{
"entries": [
- {
- "files_changed": 3,
- "hash": "ca0d6f1",
- "message": "auto-save 2026-05-12 20:04 (~3)",
- "ts": "2026-05-12T20:04:48+08:00",
- "type": "commit"
- },
- {
- "files_changed": 3,
- "hash": "138d68d",
- "message": "auto-save 2026-05-12 20:10 (~3)",
- "ts": "2026-05-12T20:10:22+08:00",
- "type": "commit"
- },
- {
- "files_changed": 1,
- "hash": "ae2b4bc",
- "message": "auto-save 2026-05-12 21:21 (~1)",
- "ts": "2026-05-12T21:21:43+08:00",
- "type": "commit"
- },
{
"files_changed": 1,
"hash": "d2d232a",
@@ -3339,6 +3318,25 @@
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 07:17 (~1)",
"files_changed": 1
+ },
+ {
+ "ts": "2026-05-14T07:23:13+08:00",
+ "type": "commit",
+ "message": "auto-save 2026-05-14 07:23 (~4)",
+ "hash": "a6773a8",
+ "files_changed": 4
+ },
+ {
+ "ts": "2026-05-13T23:23:14Z",
+ "type": "session-heartbeat",
+ "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 07:23 (~4)",
+ "files_changed": 1
+ },
+ {
+ "ts": "2026-05-13T23:28:52Z",
+ "type": "session-heartbeat",
+ "message": "Codex 会话活跃 · 最近命令:codex · 6 项未提交变更 · 最近提交:auto-save 2026-05-14 07:23 (~4)",
+ "files_changed": 6
}
]
}
diff --git a/docs/source-analysis.html b/docs/source-analysis.html
index 3052190..f7d3d27 100644
--- a/docs/source-analysis.html
+++ b/docs/source-analysis.html
@@ -556,8 +556,8 @@
3
清洗水印
对关键帧做全图或区域清洗,必要时应用为当前参考图。
4
主体识别
识别场景和主体候选,只是候选,不应锁死。
5
素材准备
清洗关键帧,把多张关键帧作为同一主体的参考,先重绘六张标准站立主体资产图,再按关键帧生成多个去主体、相似或换风格场景图。
- 6
分镜改造
把参考主体、场景、动作和 SKG 产品放入分镜结构;SKG 产品可从内置白底图库直接加入产品参考组。
- 7
生成视频
用分镜 4 图槽、改造目标和时长调用 Seedance / Kling / Veo 3 生视频 API,结果回写到画面工作台节点。
+ 6
分镜改造
把参考主体、场景、动作和 SKG 产品放入分镜结构;产品融合使用 6 行镜头组,每行绑定产品图、白底人物图、产品区域、场景图、描述词和秒数。
+ 7
生成视频
普通分镜可调用 Seedance / Kling / Veo 3;产品融合固定用 GPT Image 2 生成位置引导图,再用 Seedance 按秒数生成视频,结果回写到画面工作台节点。
8
合成成品
片段、字幕、配音、转场合成最终 mp4。当前未实现。
@@ -571,7 +571,7 @@
web/app/page.tsx | 产品工作台主状态:jobs、activeJobId、selectedFrames、clipboard、ReactFlow 节点和边;负责打开/找回画布工作面板。 |
web/components/nodes/index.tsx | DAG 节点定义:Input、VisualLab、Audio、Compose,以及画布工作面板 KeyframePanel / VideoFramePanel;旧 Keyframe/Storyboard/VideoGen 组件保留但不再挂主画布。 |
- web/components/lightbox.tsx | 关键帧素材准备面板:清洗、统一主体候选、参考帧网格、六张主体重绘图、每帧去主体场景图、产品融合和审核。 |
+ web/components/lightbox.tsx | 关键帧素材准备面板:清洗、统一主体候选、参考帧网格、六张主体重绘图、每帧去主体场景图、6 行产品融合镜头组和审核。 |
web/components/product-library-picker.tsx | SKG 内置白底产品图库选择器:搜索、品类筛选、预览尺寸,并把库内图片复制为当前 job 的 asset。 |
web/components/storyboard-bar.tsx | 顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。 |
web/components/storyboard-workbench.tsx | 顶部分镜编排条下方的明细区:4 图槽、改造目标、时长、自动保存。 |
@@ -625,8 +625,8 @@ api/main.py
你看到的区域关键帧素材审核面板
-
主要源码FrameLightbox;按“原图/清洗、主体资产、场景图、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,场景图页左侧显示全部关键帧并可勾选场景参考,产品融合页左侧接入内置 SKG 白底图库;右侧承载当前页操作、状态和结果。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立主体图;场景图依赖主体资产,右侧通过地点、生成方式、风格和参考要素拼出可编辑 prompt,再按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 cleanupFrame、addElement、generateSubjectAssets、generateSceneAsset、listProductLibrary 和 copyProductLibraryAsset。
-
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图、SKG 产品融合参考和质量风险应该如何审核”。
+
主要源码FrameLightbox;按“原图/清洗、主体资产、场景图、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,场景图页左侧显示全部关键帧并可勾选场景参考,产品融合页左侧改为 6 行镜头表:产品图、白底人物图、人物图上的产品区域、场景图和描述词一一对应;右侧承载当前镜头秒数、GPT Image 2 / Seedance 固定模型、AI 描述草稿、单条生成和批量排队。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立主体图;场景图依赖主体资产,右侧通过地点、生成方式、风格和参考要素拼出可编辑 prompt,再按当前关键帧生成去主体原场景、相似新场景或同构换风格。相关接口包括 cleanupFrame、addElement、generateSubjectAssets、generateSceneAsset、listProductLibrary、copyProductLibraryAsset 和 createProductFusionGuide。
+
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图、产品融合镜头组和质量风险应该如何审核”。
你看到的区域顶部分镜头编排下拉面板
@@ -706,6 +706,22 @@ SubjectAsset {
white_score,
source_path,
tags[]
+}
+
+
+
ProductFusionShot
+
产品融合镜头组的单行数据。每个关键帧最多 6 行,产品图、人物图、产品区域、场景图、动作描述和秒数一一对应;生成时先创建融合引导图,再提交 Seedance。
+
ProductFusionShot {
+ id,
+ product_image,
+ person_image,
+ product_region: { x, y, w, h },
+ scene_image,
+ action_text,
+ duration,
+ image_model: gpt-image-2,
+ video_model: seedance,
+ guide_image
}
@@ -748,6 +764,7 @@ SubjectAsset {
| 场景资产 | POST /frames/{idx}/scene-asset | generateSceneAsset | 在统一主体资产之后,按当前关键帧生成去主体背景板;请求包含 scene_mode、scene_style、prompt 和 source_frame_indices,可用左侧选择的参考帧 + 右侧关键词生成原场景补背景、相似新场景或同构换风格,保留历史版本用于人工审核。 |
| 产品图库 | GET /product-library/skg | listProductLibrary | 读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。 |
| 产品图入库到 job | POST /jobs/{id}/assets/product-library | copyProductLibraryAsset | 把一个内置产品图库条目复制为当前 job 的普通 asset,返回 ImageRef(kind="asset"),用于画面工作台产品融合和分镜产品参考组。 |
+ | 产品融合引导图 | POST /jobs/{id}/product-fusion/guide | createProductFusionGuide | 读取产品图和白底人物图,按用户在人物图上画出的 product_region 合成一张位置引导图;前端固定显示图片模型为 GPT Image 2,返回普通 asset 作为 Seedance 首帧。 |
| 分镜保存 | PUT /frames/{idx}/storyboard | updateStoryboard | 保存 4 图槽、时长和改造说明。 |
| 生图 | POST /frames/{idx}/generate | generateImage | 基于关键帧或已选生成图做 image-to-image,目前可用。 |
@@ -857,6 +874,19 @@ SubjectAsset {
变更记录
这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。
+
+
+ 2026-05-14 · 产品融合改为 6 行区域约束镜头组
+ FrameLightbox
+ Seedance
+
+
+
问题:只把产品图作为参考图无法解决尺寸和位置融合,模型不知道产品应该放在人物或场景里的哪个区域。
+
改动:FrameLightbox 的“产品融合”页改为 6 行镜头组。每行绑定产品图、白底人物图、手动画出的产品区域、场景图、描述词和视频秒数;图片槽支持上传和粘贴,产品图也可从内置 SKG 白底图库选用。右侧固定显示图片模型 GPT Image 2 和视频模型 Seedance,支持 AI 草拟 6 条动作描述、单条生成和批量排队。
+
后端:新增 POST /jobs/{job_id}/product-fusion/guide。它把产品图按 product_region 合成到白底人物图上,生成普通 asset 引导图;前端再把该引导图作为 Seedance 首帧,并把产品图、人物图、场景图作为参考图提交。
+
影响:api/main.py、web/lib/api.ts、web/app/page.tsx、web/components/lightbox.tsx、web/components/nodes/index.tsx、web/components/dashboard.tsx、docs/source-analysis.html。
+
+
2026-05-14 · 增加产品融合和 SKG 内置白底图库
diff --git a/web/app/page.tsx b/web/app/page.tsx
index e524400..76f6910 100644
--- a/web/app/page.tsx
+++ b/web/app/page.tsx
@@ -18,8 +18,8 @@ import {
import { ThemeToggle } from "@/components/theme-toggle"
import {
addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteJob, deleteFrame, deleteGeneratedImage,
- deleteGeneratedVideo, deleteCutout, generateStoryboardVideo,
- type Job, type ImageRef, type StoryboardScene, type FrameExtractMode, type FrameExtractQuality, type FrameExtractTarget,
+ deleteGeneratedVideo, deleteCutout, generateStoryboardVideo, createProductFusionGuide,
+ type Job, type ImageRef, type ProductFusionShot, type StoryboardScene, type FrameExtractMode, type FrameExtractQuality, type FrameExtractTarget,
} from "@/lib/api"
const NODE_TYPES = {
@@ -443,6 +443,60 @@ export default function Home() {
}
}, [job, selectedFrames, setJob])
+ const handleGenerateProductFusionVideo = useCallback(async (frameIdx: number, shot: ProductFusionShot) => {
+ if (!job) return
+ const frame = job.frames.find((f) => f.index === frameIdx)
+ if (!frame) return
+ if (!shot.product_image || !shot.person_image || !shot.scene_image || !shot.product_region || !shot.action_text?.trim()) {
+ toast.error("产品融合镜头缺少产品图、人物图、区域、场景图或描述词")
+ return
+ }
+ const duration = shot.duration && shot.duration > 0 ? shot.duration : 5
+ const labelOf = (ref?: ImageRef | null, fallback = "未提供") => ref?.label || fallback
+ try {
+ toast.info(`生成融合引导图 · GPT Image 2 位置约束 · 镜头 ${shot.id || ""}`)
+ const guideRef = await createProductFusionGuide(job.id, {
+ ...shot,
+ image_model: "gpt-image-2",
+ video_model: "seedance",
+ })
+ const region = shot.product_region
+ const prompt = [
+ `竖屏 9:16,${duration.toFixed(1)} 秒,Seedance 产品融合视频。`,
+ "图片模型固定为 GPT Image 2:已根据白底人物图和手动画框生成产品融合引导图;引导图是产品尺寸、位置、贴合关系和起始构图的最高优先级参考。",
+ "视频模型固定为 Seedance:生成单镜头连续视频,不跳切,不换主体,不改变产品身份。",
+ `产品区域:x=${region.x.toFixed(3)}, y=${region.y.toFixed(3)}, w=${region.w.toFixed(3)}, h=${region.h.toFixed(3)}。产品只能在这个框对应的人物/身体/手部区域内融合,不能漂移到其他位置,也不能明显超出框架。`,
+ `产品图:${labelOf(shot.product_image, "SKG 白底产品图")}。严格保持 SKG 产品外观、颜色、材质、U 形结构、按摩触点、按键和比例。`,
+ `白底人物图:${labelOf(shot.person_image, "人物姿态参考")}。人物姿态、手部接触点和产品佩戴关系以这张图为准。`,
+ `场景图:${labelOf(shot.scene_image, "场景参考")}。背景、空间、光线和气氛以这张图为准,但不要改变产品框内位置。`,
+ `动作描述:${shot.action_text.trim()}`,
+ "融合要求:产品必须按引导图位置自然贴合人物或手部,尺寸可信,透视一致,边缘清晰,不能悬浮、穿帮、融化、扭曲或变成其他物体。",
+ "场景要求:把白底人物姿态自然放入场景图的环境中,光线方向和阴影要统一,背景不要出现水印、平台 UI、字幕或竞品包装。",
+ "商业质感:真实拍摄感、干净高级、产品清楚可辨、人物动作自然、镜头稳定。",
+ "禁止:文字、水印、随机品牌、非 SKG 产品、医学治疗承诺、夸张病症、恐怖元素、产品位置漂移、产品超过指定融合区域。",
+ ].join("\n")
+ const updated = await generateStoryboardVideo(job.id, frameIdx, {
+ prompt,
+ duration,
+ first_image: guideRef,
+ last_image: null,
+ product_images: [shot.product_image, shot.person_image, shot.scene_image].filter(Boolean) as ImageRef[],
+ subject_image: shot.person_image,
+ scene_image: shot.scene_image,
+ product_image: shot.product_image,
+ action_image: guideRef,
+ source_ref: null,
+ model: "seedance",
+ size: "720x1280",
+ })
+ setJob(updated)
+ void navigator.clipboard?.writeText(prompt).catch(() => {})
+ toast.success("产品融合视频已进入 Video Gen 队列")
+ } catch (e) {
+ toast.error("产品融合生成失败:" + (e instanceof Error ? e.message : String(e)))
+ }
+ }, [job, setJob])
+
// 启动恢复:URL ?job=xxx,yyy 优先;否则从后端拉全部历史(按 mtime 倒序,最新放末尾)
useEffect(() => {
const params = new URLSearchParams(window.location.search)
@@ -597,9 +651,10 @@ export default function Home() {
setWorkbenchOpen(true)
},
onCopyImage: handleCopyImage,
+ onGenerateProductFusionVideo: handleGenerateProductFusionVideo,
pinnedNodes,
onToggleNodePin: handleToggleNodePin,
- }), [job, jobs, activeJobId, submitting, analyzing, frameTargets, frameCounts, frameQualities, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, framePanelDock, videoPanelJobId, videoPanelScale, videoPanelDock, handleSubmit, handleUpload, handleAnalyze, handleAnalyzeJob, handleFrameTargetChange, handleFrameCountChange, handleFrameQualityChange, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleAddManualFrameForJob, handleOpenVideoPanel, handleVideoPanelScaleChange, handleSwitchJob, setJob, handleDeleteJob, handleDeleteFrame, handleDeleteFrameForJob, handleDeleteGenerated, handleDeleteVideo, handleDeleteCutout, handleCopyImage, pinnedNodes, handleToggleNodePin])
+ }), [job, jobs, activeJobId, submitting, analyzing, frameTargets, frameCounts, frameQualities, selectedFrames, expandedFrame, framePanelScale, framePanelPinned, framePanelDock, videoPanelJobId, videoPanelScale, videoPanelDock, handleSubmit, handleUpload, handleAnalyze, handleAnalyzeJob, handleFrameTargetChange, handleFrameCountChange, handleFrameQualityChange, handleToggleFrame, handleOpenFramePanel, handleFramePanelScaleChange, handleAddManualFrame, handleAddManualFrameForJob, handleOpenVideoPanel, handleVideoPanelScaleChange, handleSwitchJob, setJob, handleDeleteJob, handleDeleteFrame, handleDeleteFrameForJob, handleDeleteGenerated, handleDeleteVideo, handleDeleteCutout, handleCopyImage, handleGenerateProductFusionVideo, pinnedNodes, handleToggleNodePin])
// 用 useNodesState 让 ReactFlow 自己管位置(避免轮询时重置 drag)
const savedSizes = useMemo(() => loadNodeSizes(), [])
diff --git a/web/components/dashboard.tsx b/web/components/dashboard.tsx
index 4b74e92..279a76a 100644
--- a/web/components/dashboard.tsx
+++ b/web/components/dashboard.tsx
@@ -328,6 +328,7 @@ export const Dashboard = forwardRef(function Dashboard({
setExpanded(new Set([key]))
}}
onCopyImage={data.onCopyImage}
+ onGenerateProductFusionVideo={data.onGenerateProductFusionVideo}
/>
) : (
renderSection(t.key)
diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx
index 6640979..4650dbe 100644
--- a/web/components/lightbox.tsx
+++ b/web/components/lightbox.tsx
@@ -971,13 +971,25 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
{label}
-
+
+
+
+
))}
@@ -1380,30 +1392,82 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
)}
{activeTab === "product" && (
-
- 产品融合目标
-
-

-
-
分镜 {f.index + 1}
-
{f.timestamp.toFixed(2)}s
+ <>
+
+
+
镜头 {activeFusionShot + 1} 设置
+
{fusionSaving ? "保存中" : "自动保存"}
-
-
-
-
图库来源
-
桌面 SKG 产品图 · gallery 白底筛选
+
-
-
使用方式
-
复制产品图后,在画面工作台加入 SKG 产品参考组。
+
+
+
+
+
-
-
生成约束
-
白底图保留外观、颜色、结构;有人物的产品示范图也可作为佩戴参考。
-
-
-
+
+
assignFusionImage("product_image", ref)}
+ />
+ >
)}
{activeTab === "review" && (
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx
index c9c1c93..e5d243d 100644
--- a/web/components/nodes/index.tsx
+++ b/web/components/nodes/index.tsx
@@ -17,7 +17,7 @@ import { toast } from "sonner"
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
import { HoverPreview } from "./hover-preview"
import {
- type Job, type ImageRef, type FrameExtractMode, type FrameExtractQuality, type FrameExtractTarget,
+ type Job, type ImageRef, type ProductFusionShot, type FrameExtractMode, type FrameExtractQuality, type FrameExtractTarget,
apiAssetUrl, effectiveFrameUrl, videoUrl, hasCutout, representativeCutoutUrl,
} from "@/lib/api"
import { FrameLightbox } from "@/components/lightbox"
@@ -73,6 +73,7 @@ export interface NodeData {
onOpenStoryboard?: (frameIdx: number) => void // 打开分镜头编排专属面板
onOpenWorkbench?: (frameIdx?: number) => void // 展开顶部分镜编排内嵌面板
onCopyImage?: (ref: ImageRef) => void // 复制图片到全局剪贴板(粘贴到分镜头编排插槽)
+ onGenerateProductFusionVideo?: (frameIdx: number, shot: ProductFusionShot) => Promise | void
pinnedNodes?: Set // 已钉住的节点 id 集合 — 钉住后位置 + 尺寸锁定
onToggleNodePin?: (id: string) => void
}
@@ -1957,6 +1958,7 @@ export function KeyframePanelNode({ data }: any) {
onToggleSelect={d.onToggleFrame}
onJobUpdate={d.onJobUpdate}
onCopyImage={d.onCopyImage}
+ onGenerateProductFusionVideo={d.onGenerateProductFusionVideo}
/>