diff --git a/.memory/worklog.json b/.memory/worklog.json index 66b8d91..1e4dbda 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,26 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "fed62f1", - "message": "auto-save 2026-05-13 04:41 (~1)", - "ts": "2026-05-13T04:41:28+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "3e34b63", - "message": "auto-save 2026-05-13 04:47 (~1)", - "ts": "2026-05-13T04:47:22+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "hash": "efc3214", - "message": "auto-save 2026-05-13 04:53 (~1)", - "ts": "2026-05-13T04:53:15+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "5396b55", @@ -3295,6 +3274,25 @@ "message": "auto-save 2026-05-14 11:58 (~4)", "hash": "f0c6c5b", "files_changed": 4 + }, + { + "ts": "2026-05-14T12:04:20+08:00", + "type": "commit", + "message": "auto-save 2026-05-14 12:04 (~4)", + "hash": "9f3e28d", + "files_changed": 4 + }, + { + "ts": "2026-05-14T04:06:10Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 12:04 (~4)", + "files_changed": 1 + }, + { + "ts": "2026-05-14T04:08:39Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 5 项未提交变更 · 最近提交:auto-save 2026-05-14 12:04 (~4)", + "files_changed": 5 } ] } diff --git a/api/main.py b/api/main.py index 43d9e05..8576a82 100644 --- a/api/main.py +++ b/api/main.py @@ -3722,15 +3722,15 @@ def generate_product_fusion_descriptions(job_id: str, req: ProductFusionDescript last = (shot.last_image or {}).get("label") or "尾帧未填" products = [ (ref or {}).get("label") or f"产品角度{idx + 1}未填" - for idx, ref in enumerate((shot.product_images or [])[:3]) + for idx, ref in enumerate((shot.product_images or [])[:4]) ] - while len(products) < 3: + while len(products) < 4: products.append(f"产品角度{len(products) + 1}未填") - shot_lines.append(f"{i}. 首帧={first};尾帧={last};产品角度={products[0]} / {products[1]} / {products[2]};已有描述={shot.action_text or '空'}") + shot_lines.append(f"{i}. 首帧={first};尾帧={last};产品角度={products[0]} / {products[1]} / {products[2]} / {products[3]};已有描述={shot.action_text or '空'}") prompt = ( "你是 SKG 产品短视频分镜导演。请为 6 条产品融合镜头各写一条中文动作描述," "每条 20-45 字,必须说明透明骨架人在做什么、产品如何佩戴/展示、动作如何从首帧自然过渡到尾帧。" - "产品是 SKG 白色 U 形颈部/肩颈按摩仪,三张产品角度图是同一产品的身份真源;不要写医疗治疗承诺,不要出现竞品。" + "产品是 SKG 白色 U 形颈部/肩颈按摩仪,四张产品角度图是同一产品的身份真源;不要写医疗治疗承诺,不要出现竞品。" "输出 JSON:{\"descriptions\":[\"...\", \"...\"]}。\n\n" + "\n".join(shot_lines) ) diff --git a/api/product_library/skg-products/images/skg-desktop-product-angle-01.jpg b/api/product_library/skg-products/images/skg-desktop-product-angle-01.jpg new file mode 100644 index 0000000..fea125c Binary files /dev/null and b/api/product_library/skg-products/images/skg-desktop-product-angle-01.jpg differ diff --git a/api/product_library/skg-products/images/skg-desktop-product-angle-02.jpg b/api/product_library/skg-products/images/skg-desktop-product-angle-02.jpg new file mode 100644 index 0000000..74b1f31 Binary files /dev/null and b/api/product_library/skg-products/images/skg-desktop-product-angle-02.jpg differ diff --git a/api/product_library/skg-products/images/skg-desktop-product-angle-03.jpg b/api/product_library/skg-products/images/skg-desktop-product-angle-03.jpg new file mode 100644 index 0000000..10fd2d8 Binary files /dev/null and b/api/product_library/skg-products/images/skg-desktop-product-angle-03.jpg differ diff --git a/api/product_library/skg-products/images/skg-desktop-product-angle-04.jpg b/api/product_library/skg-products/images/skg-desktop-product-angle-04.jpg new file mode 100644 index 0000000..dedc1fd Binary files /dev/null and b/api/product_library/skg-products/images/skg-desktop-product-angle-04.jpg differ diff --git a/api/product_library/skg-products/manifest.json b/api/product_library/skg-products/manifest.json index dd1fd8d..f5fbbd7 100644 --- a/api/product_library/skg-products/manifest.json +++ b/api/product_library/skg-products/manifest.json @@ -1,8 +1,96 @@ { "source": "/Users/kangwan/Desktop/skg/skg_product_downloads", "filter": "all_products/gallery only; border white_score >= 0.78 or white_score >= 0.62 and near_white_score >= 0.90", - "count": 41, + "count": 45, "items": [ + { + "id": "desktop-skg-product-angle-01", + "handle": "desktop-skg-product-four-angle", + "title": "Desktop SKG Product Four-Angle Set", + "product_type": "Neck Massager", + "image_type": "desktop-angle", + "image_index": 1, + "filename": "images/skg-desktop-product-angle-01.jpg", + "url": "/product-library/skg/images/skg-desktop-product-angle-01.jpg", + "width": 1300, + "height": 1300, + "source_path": "/Users/kangwan/Desktop/skg产品1.jpg", + "white_score": 1.0, + "near_white_score": 1.0, + "has_people": false, + "tags": [ + "white-bg", + "desktop-angle", + "four-angle-set", + "Neck Massager" + ] + }, + { + "id": "desktop-skg-product-angle-02", + "handle": "desktop-skg-product-four-angle", + "title": "Desktop SKG Product Four-Angle Set", + "product_type": "Neck Massager", + "image_type": "desktop-angle", + "image_index": 2, + "filename": "images/skg-desktop-product-angle-02.jpg", + "url": "/product-library/skg/images/skg-desktop-product-angle-02.jpg", + "width": 1300, + "height": 1300, + "source_path": "/Users/kangwan/Desktop/skg产品2.jpg", + "white_score": 1.0, + "near_white_score": 1.0, + "has_people": false, + "tags": [ + "white-bg", + "desktop-angle", + "four-angle-set", + "Neck Massager" + ] + }, + { + "id": "desktop-skg-product-angle-03", + "handle": "desktop-skg-product-four-angle", + "title": "Desktop SKG Product Four-Angle Set", + "product_type": "Neck Massager", + "image_type": "desktop-angle", + "image_index": 3, + "filename": "images/skg-desktop-product-angle-03.jpg", + "url": "/product-library/skg/images/skg-desktop-product-angle-03.jpg", + "width": 1300, + "height": 1300, + "source_path": "/Users/kangwan/Desktop/skg产品3.jpg", + "white_score": 1.0, + "near_white_score": 1.0, + "has_people": false, + "tags": [ + "white-bg", + "desktop-angle", + "four-angle-set", + "Neck Massager" + ] + }, + { + "id": "desktop-skg-product-angle-04", + "handle": "desktop-skg-product-four-angle", + "title": "Desktop SKG Product Four-Angle Set", + "product_type": "Neck Massager", + "image_type": "desktop-angle", + "image_index": 4, + "filename": "images/skg-desktop-product-angle-04.jpg", + "url": "/product-library/skg/images/skg-desktop-product-angle-04.jpg", + "width": 1300, + "height": 1300, + "source_path": "/Users/kangwan/Desktop/skg产品4.jpg", + "white_score": 1.0, + "near_white_score": 1.0, + "has_people": false, + "tags": [ + "white-bg", + "desktop-angle", + "four-angle-set", + "Neck Massager" + ] + }, { "id": "g7-pro-fold-neck-massager-01", "handle": "g7-pro-fold-neck-massager", @@ -863,4 +951,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 70ce738..dc1c283 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -629,7 +629,7 @@ api/main.py
你看到的区域关键帧素材审核面板
-
主要源码FrameLightbox;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 applyCleanedFrame,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行直接显示首帧、尾帧、同一产品 3 个角度图、描述词、秒数和单条生成按钮,便于一次看完 6 条视频。产品融合槽位的“粘贴”优先使用应用内 clipboard,也支持选中槽位后 Cmd+V 粘贴系统图片。右侧保留 GPT Image 2 / Seedance 固定模型、当前镜头状态、AI 描述草稿、批量排队和产品图库选用;产品图库选中后会填入当前镜头下一个产品角度槽。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 prompt 做文字生图,生成结果写入 scene_assets 但以 asset_role=first_frame/last_frame 标记,并自动传入当前产品融合镜头。相关接口包括 cleanupFrameapplyCleanedFrameaddElementgenerateSubjectAssetsgenerateSceneAssetlistProductLibrarycopyProductLibraryAssetgenerateProductFusionDescriptions
+
主要源码FrameLightbox;按“原图/清洗、主体资产、首尾帧、产品融合、审核”五个页签组织;左侧只放主图/框选画布,但主体资产页左侧改为全部已清洗/已选参考帧网格,首尾帧页左侧显示全部关键帧并可勾选人物/机位参考。主体识别页会显示透明骨架人目标和 Vision 验收分数。清洗页右侧支持一键清洗未处理帧、单张替换清洗版和一键替换全部待应用清洗版;批量替换顺序调用 applyCleanedFrame,不新增后端接口。产品融合页左侧是纵向 6 行镜头工作表:每行直接显示首帧、尾帧、同一产品 4 个角度图、描述词、秒数和单条生成按钮,便于一次看完 6 条视频。产品融合槽位的“粘贴”优先使用应用内 clipboard,也支持选中槽位后 Cmd+V 粘贴系统图片。右侧保留 GPT Image 2 / Seedance 固定模型、当前镜头状态、AI 描述草稿、批量排队和产品图库选用;产品图库选中后会填入当前镜头下一个产品角度槽。主体资产页只确认一个统一主体,后端按参考重绘六张纯背景、占满画面的标准站立透明骨架人资产图;首尾帧页通过地点、风格、参考要素和可编辑 prompt 做文字生图,生成结果写入 scene_assets 但以 asset_role=first_frame/last_frame 标记,并自动传入当前产品融合镜头。相关接口包括 cleanupFrameapplyCleanedFrameaddElementgenerateSubjectAssetsgenerateSceneAssetlistProductLibrarycopyProductLibraryAssetgenerateProductFusionDescriptions
适合怎么描述“这一组关键帧如何共同生成一个统一主体包;某张关键帧的水印、去主体场景图、产品融合镜头组和质量风险应该如何审核”。
@@ -748,12 +748,12 @@ SubjectAsset {

ProductFusionShot

-

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

+

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

ProductFusionShot {
   id,
   first_image,
   last_image,
-  product_images[3],
+  product_images[4],
   action_text,
   duration,
   image_model: gpt-image-2,
@@ -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为 6 行产品融合镜头生成动作描述草稿;输入重点变为首帧、尾帧和三张产品角度图,有 LLM 配置时用 REWRITE_MODEL 生成 JSON,无配置或失败时回退到本地镜头模板。
+            产品融合描述词POST /jobs/{id}/product-fusion/descriptionsgenerateProductFusionDescriptions为 6 行产品融合镜头生成动作描述草稿;输入重点变为首帧、尾帧和四张产品角度图,有 LLM 配置时用 REWRITE_MODEL 生成 JSON,无配置或失败时回退到本地镜头模板。
             分镜保存PUT /frames/{idx}/storyboardupdateStoryboard保存 4 图槽、时长和改造说明。
             生图POST /frames/{idx}/generategenerateImage基于关键帧或已选生成图做 image-to-image,目前可用。
           
@@ -925,8 +925,8 @@ SubjectAsset {
             
             

问题:原产品融合依赖白底人物、手动画区域、场景图和融合引导图,但当前透明骨架人二创流程更需要文字生成首尾帧,再把产品真源作为垫图传给视频模型。

-

改动:“场景图”页签改名为“首尾帧”,右侧用地点、风格、参考要素和 prompt 生成首帧/尾帧,生成后自动填入当前产品融合镜头。产品融合 6 行工作表改为首帧、尾帧、三张同一产品不同角度图、描述词、秒数和生成按钮。

-

后端:generateSceneAsset 新增 asset_rolefirst_frame/last_frame 走文字生图并标记资产角色;ProductFusionShot 新增 first_imagelast_imageproduct_images,视频提交直接把首尾帧和三张产品图交给 Seedance。

+

改动:“场景图”页签改名为“首尾帧”,右侧用地点、风格、参考要素和 prompt 生成首帧/尾帧,生成后自动填入当前产品融合镜头。产品融合 6 行工作表改为首帧、尾帧、四张同一产品不同角度图、描述词、秒数和生成按钮。

+

后端:generateSceneAsset 新增 asset_rolefirst_frame/last_frame 走文字生图并标记资产角色;ProductFusionShot 新增 first_imagelast_imageproduct_images,视频提交直接把首尾帧和四张产品图交给 Seedance。

影响:api/main.pyweb/lib/api.tsweb/app/page.tsxweb/components/lightbox.tsxdocs/source-analysis.html

diff --git a/web/app/page.tsx b/web/app/page.tsx index b328f48..b6b2152 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -520,9 +520,9 @@ export default function Home() { if (!job) return const frame = job.frames.find((f) => f.index === frameIdx) if (!frame) return - const productRefs = (shot.product_images ?? []).filter(Boolean).slice(0, 3) as ImageRef[] - if (!shot.first_image || !shot.last_image || productRefs.length < 3 || !shot.action_text?.trim()) { - toast.error("产品融合镜头缺少首帧、尾帧、三张产品角度图或描述词") + 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("产品融合镜头缺少首帧、尾帧、四张产品角度图或描述词") return } const duration = shot.duration && shot.duration > 0 ? shot.duration : 5 @@ -531,17 +531,18 @@ export default function Home() { const prompt = [ `竖屏 9:16,${duration.toFixed(1)} 秒,Seedance 图生视频。`, "图片模型固定为 GPT Image 2:首帧和尾帧已经由文字生图生成,用来锁定透明骨架人角色、场景构图和动作起止状态。", - "视频模型固定为 Seedance:使用首帧作为起始画面、尾帧作为结束画面,并用三张同一 SKG 产品不同角度白底图作为垫图/产品身份参考。", + "视频模型固定为 Seedance:使用首帧作为起始画面、尾帧作为结束画面,并用四张同一 SKG 产品不同角度白底图作为垫图/产品身份参考。", `首帧:${labelOf(shot.first_image, "透明骨架人首帧")}。起始人物形象、姿态、构图和场景氛围以这张图为准。`, `尾帧:${labelOf(shot.last_image, "透明骨架人尾帧")}。结束人物状态、画面落点和场景延续以这张图为准。`, `产品角度图 1:${labelOf(productRefs[0], "SKG 产品正面/主视角")}。`, `产品角度图 2:${labelOf(productRefs[1], "SKG 产品侧面/斜侧视角")}。`, `产品角度图 3:${labelOf(productRefs[2], "SKG 产品背面/细节视角")}。`, + `产品角度图 4:${labelOf(productRefs[3], "SKG 产品补充/底部或佩戴视角")}。`, `动作描述:${shot.action_text.trim()}`, TRANSPARENT_HUMAN_VIDEO_PROMPT, "融合要求:产品必须自然出现在透明骨架人动作中,尺寸可信,透视一致,贴合身体/手部/使用区域,不能悬浮、漂移、融化、扭曲或变成其他物体。", "首尾连续性:镜头从首帧自然运动到尾帧,中间不要跳切,不换角色,不换产品,不突然改变场景。", - "产品一致性:严格保持 SKG 产品外观、颜色、材质、U 形结构、按摩触点、按键和比例;三张产品角度图是产品身份真源。", + "产品一致性:严格保持 SKG 产品外观、颜色、材质、U 形结构、按摩触点、按键和比例;四张产品角度图是产品身份真源。", "场景要求:背景、空间、光线和阴影要自然统一,不要出现水印、平台 UI、字幕或竞品包装。", "商业质感:真实拍摄感、干净高级、产品清楚可辨、人物动作自然、镜头稳定。", "禁止:文字、水印、随机品牌、非 SKG 产品、医学治疗承诺、夸张病症、恐怖元素、产品位置漂移、透明衣服但非透明身体。", diff --git a/web/components/lightbox.tsx b/web/components/lightbox.tsx index f8f61b0..9c3862f 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, + generateSceneAsset, generateSubjectAssets, generateProductFusionDescriptions, 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 { ProductLibraryPicker } from "@/components/product-library-picker" @@ -106,13 +106,20 @@ const SCENE_REFERENCE_OPTIONS = [ const FUSION_SHOT_COUNT = 6 const FUSION_DURATIONS = [4, 5, 6, 8, 10, 12, 15] +const PRODUCT_ANGLE_COUNT = 4 +const DESKTOP_PRODUCT_ANGLE_IDS = [ + "desktop-skg-product-angle-01", + "desktop-skg-product-angle-02", + "desktop-skg-product-angle-03", + "desktop-skg-product-angle-04", +] type FusionUploadTarget = { shotIndex: number slot: "first_image" | "last_image" | "product_images" productIndex?: number } type FusionFrameRole = "first_image" | "last_image" -const PRODUCT_ANGLE_LABELS = ["产品角度 1", "产品角度 2", "产品角度 3"] +const PRODUCT_ANGLE_LABELS = ["产品角度 1", "产品角度 2", "产品角度 3", "产品角度 4"] const createFusionShots = (): ProductFusionShot[] => Array.from({ length: FUSION_SHOT_COUNT }, (_, i) => ({ @@ -137,7 +144,7 @@ const normalizeFusionShots = (shots?: ProductFusionShot[] | null): ProductFusion return base.map((item, i) => { const shot = shots[i] ?? {} const productImages = shot.product_images?.length - ? shot.product_images.slice(0, 3) + ? shot.product_images.slice(0, PRODUCT_ANGLE_COUNT) : shot.product_image ? [shot.product_image] : [] @@ -170,6 +177,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const [fusionUploadTarget, setFusionUploadTarget] = useState(null) const [fusionGenerating, setFusionGenerating] = useState(null) const [fusionSaving, setFusionSaving] = useState(false) + const [fusionFillingProducts, setFusionFillingProducts] = useState<"current" | "all" | null>(null) const [editingElement, setEditingElement] = useState<{ frameIndex: number id: string @@ -312,7 +320,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const currentFusionFirstUrl = currentFusionShot?.first_image ? resolveImageRefUrl(jobId, currentFusionShot.first_image) : "" const currentFusionLastUrl = currentFusionShot?.last_image ? resolveImageRefUrl(jobId, currentFusionShot.last_image) : "" const fusionReadyCount = fusionShots.filter((shot) => - shot.first_image && shot.last_image && (shot.product_images ?? []).filter(Boolean).length >= 3 && shot.action_text?.trim() + shot.first_image && shot.last_image && (shot.product_images ?? []).filter(Boolean).length >= PRODUCT_ANGLE_COUNT && shot.action_text?.trim() ).length const persistFusionShots = async (nextShots: ProductFusionShot[]) => { @@ -341,9 +349,9 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const current = fusionShots[index] if (!current) return if (target.slot === "product_images") { - const productImages = [...(current.product_images ?? [])].slice(0, 3) - const inferredIndex = [0, 1, 2].find((idx) => !productImages[idx]) ?? 0 - const safeIndex = Math.max(0, Math.min(2, target.productIndex ?? inferredIndex)) + const productImages = [...(current.product_images ?? [])].slice(0, PRODUCT_ANGLE_COUNT) + const inferredIndex = PRODUCT_ANGLE_LABELS.findIndex((_, idx) => !productImages[idx]) + const safeIndex = Math.max(0, Math.min(PRODUCT_ANGLE_COUNT - 1, target.productIndex ?? (inferredIndex >= 0 ? inferredIndex : 0))) productImages[safeIndex] = ref updateFusionShot(index, { product_images: productImages, product_image: productImages[0] ?? null, guide_image: null }, true) return @@ -402,10 +410,29 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o toast.success("已生成 6 条动作描述草稿,可继续手工修改") } + const fillDesktopProductAngles = async (scope: "current" | "all") => { + setFusionFillingProducts(scope) + try { + const refs = await Promise.all(DESKTOP_PRODUCT_ANGLE_IDS.map((id) => copyProductLibraryAsset(jobId, id))) + const next = fusionShots.map((shot, index) => ( + scope === "all" || index === activeFusionShot + ? { ...shot, product_images: refs, product_image: refs[0] ?? null, guide_image: null } + : shot + )) + setFusionShots(next) + void persistFusionShots(next) + toast.success(scope === "all" ? "已把桌面 4 个产品角度填入 6 个镜头" : `已填入镜头 ${activeFusionShot + 1} 的 4 个产品角度`) + } catch (e) { + toast.error("桌面产品角度填充失败:" + (e instanceof Error ? e.message : String(e))) + } finally { + setFusionFillingProducts(null) + } + } + const runFusionVideo = async (index: number) => { const shot = fusionShots[index] - if (!shot?.first_image || !shot.last_image || (shot.product_images ?? []).filter(Boolean).length < 3 || !shot.action_text?.trim()) { - toast.error(`镜头 ${index + 1} 还缺首帧、尾帧、三张产品角度图或描述词`) + if (!shot?.first_image || !shot.last_image || (shot.product_images ?? []).filter(Boolean).length < PRODUCT_ANGLE_COUNT || !shot.action_text?.trim()) { + toast.error(`镜头 ${index + 1} 还缺首帧、尾帧、四张产品角度图或描述词`) return } setFusionGenerating(index) @@ -419,7 +446,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const runAllFusionVideos = async () => { const indexes = fusionShots .map((shot, i) => ({ shot, i })) - .filter(({ shot }) => shot.first_image && shot.last_image && (shot.product_images ?? []).filter(Boolean).length >= 3 && shot.action_text?.trim()) + .filter(({ shot }) => shot.first_image && shot.last_image && (shot.product_images ?? []).filter(Boolean).length >= PRODUCT_ANGLE_COUNT && shot.action_text?.trim()) .map(({ i }) => i) if (indexes.length === 0) { toast.error("还没有完整的融合镜头") @@ -973,7 +1000,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
- 6 条视频镜头从上到下排列;每条使用文字描述 + 首帧 + 尾帧 + 同一产品 3 个角度图,作为 Seedance 垫图生成视频。 + 6 条视频镜头从上到下排列;每条使用文字描述 + 首帧 + 尾帧 + 同一产品 4 个角度图,作为 Seedance 垫图生成视频。
{fusionShots.map((shot, i) => { @@ -981,10 +1008,10 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o const productImages = shot.product_images ?? [] const firstUrl = shot.first_image ? resolveImageRefUrl(jobId, shot.first_image) : "" const lastUrl = shot.last_image ? resolveImageRefUrl(jobId, shot.last_image) : "" - const productUrls = [0, 1, 2].map((productIndex) => + const productUrls = PRODUCT_ANGLE_LABELS.map((_, productIndex) => productImages[productIndex] ? resolveImageRefUrl(jobId, productImages[productIndex]) : "" ) - const ready = !!(shot.first_image && shot.last_image && productImages.filter(Boolean).length >= 3 && shot.action_text?.trim()) + const ready = !!(shot.first_image && shot.last_image && productImages.filter(Boolean).length >= PRODUCT_ANGLE_COUNT && shot.action_text?.trim()) const busy = fusionGenerating === i || fusionGenerating === "all" const pasteIntoSlot = (target: FusionUploadTarget, label: string) => { setActiveFusionShot(i) @@ -1043,12 +1070,12 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o
) const productAngleSlots = ( -
- {[0, 1, 2].map((productIndex) => ( +
+ {PRODUCT_ANGLE_LABELS.map((label, productIndex) => (
{imageSlot( { shotIndex: i, slot: "product_images", productIndex }, - PRODUCT_ANGLE_LABELS[productIndex], + label, productUrls[productIndex], productImages[productIndex], true, @@ -1066,7 +1093,7 @@ export function FrameLightbox({ jobId, frames, activeIndex, selected, onClose, o : "border-white/10 bg-black/20 hover:border-amber-300/35" }`} > -
+
+
+ + +