From 3733151ae42de0964187ff87d6291c9f1208135d Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 14 May 2026 12:37:30 +0800 Subject: [PATCH] auto-save 2026-05-14 12:37 (~6) --- .memory/worklog.json | 27 ++++---- docs/source-analysis.html | 20 ++++-- web/app/page.tsx | 3 +- web/components/dashboard.tsx | 2 + web/components/lightbox.tsx | 119 +-------------------------------- web/components/nodes/index.tsx | 2 + 6 files changed, 36 insertions(+), 137 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 8e7c09c..50d2070 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,19 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "714db7d", - "message": "auto-save 2026-05-13 05:46 (~1)", - "ts": "2026-05-13T05:46:19+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "91d2d64", - "message": "auto-save 2026-05-13 05:52 (~1)", - "ts": "2026-05-13T05:52:14+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "4a9264a", @@ -3289,6 +3275,19 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 2 项未提交变更 · 最近提交:auto-save 2026-05-14 12:26 (~4)", "files_changed": 2 + }, + { + "ts": "2026-05-14T12:32:00+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 12:31 (~2)", + "hash": "01ab67e", + "files_changed": 2 + }, + { + "ts": "2026-05-14T04:36:11Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 6 项未提交变更 · 最近提交:auto-save 2026-05-14 12:31 (~2)", + "files_changed": 6 } ] } diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 8e55b81..8ea2a55 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -629,7 +629,7 @@ api/main.py
你看到的区域关键帧素材审核面板
-
主要源码FrameLightbox;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 applyCleanedFrame,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行直接显示首帧、尾帧、同一产品 4 个角度图、描述词、秒数和单条生成按钮,便于一次看完 6 条视频。产品融合槽位的“粘贴”优先使用应用内 clipboard,也支持选中槽位后 Cmd+V 粘贴系统图片。右侧保留 GPT Image 2 / Seedance 固定模型、当前镜头状态、桌面四角度一键填充、20 条产品使用描述模板、批量排队和产品图库选用;产品图库选中后会填入当前镜头下一个产品角度槽。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 prompt 做文字生图,生成结果写入 scene_assets 但以 asset_role=first_frame/last_frame 标记,并自动传入当前产品融合镜头。相关接口包括 cleanupFrameapplyCleanedFrameaddElementgenerateSubjectAssetsgenerateSceneAssetlistProductLibrarycopyProductLibraryAssetgenerateProductFusionDescriptions
+
主要源码FrameLightbox;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 applyCleanedFrame,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行只显示首帧、尾帧、已预填动作描述、秒数、生成按钮和对应视频结果;四张桌面 SKG 产品图作为固定产品参考,生成时通过 copyProductLibraryAsset 自动写入镜头,不再暴露产品角度槽、产品融合辅助栏或产品图库选择器。产品融合槽位的“粘贴”优先使用应用内 clipboard,也支持选中槽位后 Cmd+V 粘贴系统图片。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 prompt 做文字生图,生成结果写入 scene_assets 但以 asset_role=first_frame/last_frame 标记,并自动传入当前产品融合镜头。相关接口包括 cleanupFrameapplyCleanedFrameaddElementgenerateSubjectAssetsgenerateSceneAssetcopyProductLibraryAsset
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图、产品融合镜头组和质量风险应该如何审核”。
@@ -748,7 +748,7 @@ SubjectAsset {

ProductFusionShot

-

产品融合镜头组的单行数据。每个关键帧最多 6 行,首帧、尾帧、四张同一产品不同角度图、动作描述和秒数一一对应;生成时直接把首尾帧和产品角度图作为 Seedance 垫图提交。

+

产品融合镜头组的单行数据。每个关键帧最多 6 行,用户只补首帧、尾帧和必要时微调动作描述、秒数;四张桌面 SKG 产品角度图固定隐藏填充,生成时直接把首尾帧和固定产品图作为 Seedance 垫图提交。

ProductFusionShot {
   id,
   first_image,
@@ -806,7 +806,7 @@ SubjectAsset {
             产品图库GET /product-library/skglistProductLibrary读取内置 SKG 白底图库 manifest,返回产品标题、品类、尺寸、白底评分和预览图 URL。
             产品图入库到 jobPOST /jobs/{id}/assets/product-librarycopyProductLibraryAsset把一个内置产品图库条目复制为当前 job 的普通 asset,返回 ImageRef(kind="asset"),用于画面工作台产品融合和分镜产品参考组。
             产品融合引导图POST /jobs/{id}/product-fusion/guidecreateProductFusionGuide旧流程兼容接口:读取产品图和白底人物图,按 product_region 合成位置引导图。当前首尾帧流程不再主动调用它。
-            产品融合描述词POST /jobs/{id}/product-fusion/descriptionsgenerateProductFusionDescriptions生成 20 条产品融合动作描述库,前端每次按 6 条轮换套用到 6 行镜头;输入重点变为首帧、尾帧和四张产品角度图,有 LLM 配置时用 REWRITE_MODEL 生成 JSON,无配置或失败时回退到本地 20 条精准模板。
+            产品融合描述词POST /jobs/{id}/product-fusion/descriptionsgenerateProductFusionDescriptions兼容接口:可生成 20 条产品融合动作描述库。当前前端默认直接用本地 20 条精准模板预填 6 行镜头,不再显示单独的 AI 草拟入口。
             分镜保存PUT /frames/{idx}/storyboardupdateStoryboard保存 4 图槽、时长和改造说明。
             生图POST /frames/{idx}/generategenerateImage基于关键帧或已选生成图做 image-to-image,目前可用。
           
@@ -925,7 +925,7 @@ SubjectAsset {
             
             

问题:产品融合视频的动作描述不能泛泛写“人物使用产品”,需要稳定表达透明骨架人在具体场景中佩戴 SKG 产品,并呈现舒适享受状态。

-

改动:前端内置 20 条产品使用描述模板,覆盖卧室、客厅、办公、浴室、阳台、影棚、阅读角等场景;“AI 草拟 6 条”每次从 20 条中按 6 条轮换套用,便于多次生成不同镜头组。

+

改动:前端内置 20 条产品使用描述模板,覆盖卧室、客厅、办公、浴室、阳台、影棚、阅读角等场景;当前产品融合页默认把前 6 条预填到 6 行镜头,用户只在需要时直接改每行描述。

后端:generateProductFusionDescriptions 的兜底模板同步扩为 20 条,LLM 提示也改为生成 20 条 35-70 字描述,要求包含场景、佩戴/展示动作和舒适表情,同时排除医疗治疗承诺。

影响:api/main.pyweb/components/lightbox.tsxdocs/source-analysis.html

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

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

+
+
+

2026-05-14 · 产品融合收敛为首尾帧 + 固定四产品图

+ FrameLightbox + 产品融合 +
+
+

问题:产品融合页继续显示产品角度槽、辅助栏和产品图库会把操作变复杂;当前工作流只需要用户手动补人物首尾帧,产品图固定来自桌面 4 张 SKG 图。

+

改动:“产品融合”页每行只保留首帧、尾帧、已预填描述词、秒数、生成按钮和行末视频结果。生成单条或批量视频前,前端自动把内置的 4 张桌面 SKG 产品图复制为当前 job asset 并写入 product_images[4];视频 prompt 增加 产品融合镜头ID 标记,用来把生成结果显示回对应行。

+

影响:web/components/lightbox.tsxweb/app/page.tsxweb/components/nodes/index.tsxweb/components/dashboard.tsxdocs/source-analysis.html

+
+

2026-05-14 · 产品融合镜头组改为纵向 6 行工作表

diff --git a/web/app/page.tsx b/web/app/page.tsx index b6b2152..f67e4d7 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -522,13 +522,14 @@ export default function Home() { if (!frame) return const productRefs = (shot.product_images ?? []).filter(Boolean).slice(0, 4) as ImageRef[] if (!shot.first_image || !shot.last_image || productRefs.length < 4 || !shot.action_text?.trim()) { - toast.error("产品融合镜头缺少首帧、尾帧、四张产品角度图或描述词") + toast.error("产品融合镜头缺少首帧、尾帧、固定产品图或描述词") return } const duration = shot.duration && shot.duration > 0 ? shot.duration : 5 const labelOf = (ref?: ImageRef | null, fallback = "未提供") => ref?.label || fallback try { const prompt = [ + `产品融合镜头ID:${shot.id || `shot-${frameIdx + 1}`}`, `竖屏 9:16,${duration.toFixed(1)} 秒,Seedance 图生视频。`, "图片模型固定为 GPT Image 2:首帧和尾帧已经由文字生图生成,用来锁定透明骨架人角色、场景构图和动作起止状态。", "视频模型固定为 Seedance:使用首帧作为起始画面、尾帧作为结束画面,并用四张同一 SKG 产品不同角度白底图作为垫图/产品身份参考。", diff --git a/web/components/dashboard.tsx b/web/components/dashboard.tsx index 4b324b7..278e124 100644 --- a/web/components/dashboard.tsx +++ b/web/components/dashboard.tsx @@ -320,6 +320,7 @@ export const Dashboard = forwardRef(function Dashboard({ embedded jobId={data.job.id} frames={data.job.frames} + generatedVideos={data.job.generated_videos ?? []} activeIndex={data.expandedFrame} selected={data.selectedFrames} onClose={data.onCloseExpandedFrame} @@ -333,6 +334,7 @@ export const Dashboard = forwardRef(function Dashboard({ clipboard={data.clipboard} onCopyImage={data.onCopyImage} onGenerateProductFusionVideo={data.onGenerateProductFusionVideo} + onDeleteVideo={data.onDeleteVideo} /> ) : ( renderSection(t.key) diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index d8b007d..97e7547 100644 --- a/web/components/lightbox.tsx +++ b/web/components/lightbox.tsx @@ -5,7 +5,7 @@ import { X, ChevronLeft, ChevronRight, Check, Sparkles, Wand2, Loader2, Eye, Ref import { frameUrl, cleanedFrameUrl, apiAssetUrl, describeFrame, cleanupFrame, applyCleanedFrame, discardCleanedFrame, addElement, updateElement, deleteElement, - generateSceneAsset, generateSubjectAssets, generateProductFusionDescriptions, resolveImageRefUrl, uploadStoryboardAsset, updateStoryboard, copyProductLibraryAsset, + generateSceneAsset, generateSubjectAssets, resolveImageRefUrl, uploadStoryboardAsset, updateStoryboard, copyProductLibraryAsset, type AssetBackground, type AssetSize, type KeyFrame, type Job, type ImageRef, type ProductFusionShot, type SceneAssetRole, type SceneStyle, type SubjectKind, } from "@/lib/api" import { TRANSPARENT_HUMAN_FRAME_STANDARD, TRANSPARENT_HUMAN_UI_SUMMARY } from "@/lib/workflow-target" @@ -200,8 +200,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex const [fusionUploadTarget, setFusionUploadTarget] = useState(null) const [fusionGenerating, setFusionGenerating] = useState(null) const [fusionSaving, setFusionSaving] = useState(false) - const [fusionFillingProducts, setFusionFillingProducts] = useState<"current" | "all" | null>(null) - const [fusionDraftPage, setFusionDraftPage] = useState(0) const [editingElement, setEditingElement] = useState<{ frameIndex: number id: string @@ -338,7 +336,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex sceneExtraKeywords.trim() ? `额外关键词:${sceneExtraKeywords.trim()}。` : "", "要求:单一透明骨架人清晰可见,人物占画面主体,首尾帧可连续生成视频;无文字、水印、平台 UI、恐怖解剖感。", ].filter(Boolean).join("\n") - const currentFusionShot = fusionShots[activeFusionShot] ?? fusionShots[0] const fusionReadyCount = fusionShots.filter((shot) => shot.first_image && shot.last_image && shot.action_text?.trim() ).length @@ -397,31 +394,7 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex requestAnimationFrame(() => fusionFileInputRef.current?.click()) } - const draftFusionDescriptions = async () => { - const actions = PRODUCT_FUSION_DESCRIPTION_PRESETS - let descriptions = actions - try { - const result = await generateProductFusionDescriptions(jobId, fusionShots) - descriptions = result.descriptions.length >= PRODUCT_FUSION_DESCRIPTION_PRESETS.length ? result.descriptions : actions - } catch (e) { - toast.error("AI 描述生成失败,已使用本地草稿") - } - const start = (fusionDraftPage * FUSION_SHOT_COUNT) % descriptions.length - const selectedDescriptions = Array.from({ length: FUSION_SHOT_COUNT }, (_, i) => ( - descriptions[(start + i) % descriptions.length] || actions[i] - )) - const next = fusionShots.map((shot, i) => ({ - ...shot, - action_text: selectedDescriptions[i] || shot.action_text || actions[i], - })) - setFusionShots(next) - setFusionDraftPage((prev) => prev + 1) - void persistFusionShots(next) - toast.success(`已套用 6 条动作描述 · 模板 ${start + 1}-${Math.min(start + FUSION_SHOT_COUNT, descriptions.length)}`) - } - const ensureFixedProductAngles = async (indexes: number[]) => { - setFusionFillingProducts("all") try { const reusableRefs = fusionShots.find((shot) => (shot.product_images ?? []).filter(Boolean).length >= PRODUCT_ANGLE_COUNT) ?.product_images?.slice(0, PRODUCT_ANGLE_COUNT) @@ -439,8 +412,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex } catch (e) { toast.error("桌面产品角度填充失败:" + (e instanceof Error ? e.message : String(e))) return null - } finally { - setFusionFillingProducts(null) } } @@ -1628,94 +1599,6 @@ export function FrameLightbox({ jobId, frames, generatedVideos = [], activeIndex )} - {activeTab === "product" && ( - <> -
-
-
产品融合辅助
- {fusionSaving ? "保存中" : "自动保存"} -
-
-
-
图片模型
-
GPT Image 2
-
-
-
视频模型
-
Seedance
-
-
-
-
- 当前镜头 {activeFusionShot + 1} - {currentFusionShot?.duration ?? 5}s -
-
- 首帧 - 尾帧 - = PRODUCT_ANGLE_COUNT ? "text-emerald-200/80" : "text-white/35"}>产品角度 {currentFusionProductCount}/{PRODUCT_ANGLE_COUNT} - 描述词 -
-
- {currentFusionShot?.action_text?.trim() || "描述词未填写"} -
-
-
- - -
-
- - -
-
- { - const nextEmpty = PRODUCT_ANGLE_LABELS.findIndex((_, idx) => !currentFusionProducts[idx]) - assignFusionImage({ - shotIndex: activeFusionShot, - slot: "product_images", - productIndex: nextEmpty >= 0 ? nextEmpty : 0, - }, ref) - }} - /> - - )} {activeTab === "review" && (
素材审核
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 8222a0e..7cbd56e 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -1955,6 +1955,7 @@ export function KeyframePanelNode({ data }: any) { embedded jobId={d.job.id} frames={d.job.frames} + generatedVideos={d.job.generated_videos ?? []} activeIndex={d.expandedFrame} selected={d.selectedFrames} onClose={d.onCloseExpandedFrame} @@ -1964,6 +1965,7 @@ export function KeyframePanelNode({ data }: any) { clipboard={d.clipboard} onCopyImage={d.onCopyImage} onGenerateProductFusionVideo={d.onGenerateProductFusionVideo} + onDeleteVideo={d.onDeleteVideo} />