From 7b1adbd5a28ab68f73d8f7b444bc731bacc18672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 4 Aug 2026 11:41:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E5=8A=A8=E4=BD=9C=E8=A1=80=E7=BC=98=E4=B8=8E?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 无项目生成返回正式账号素材时沿用素材记录的直接来源资源。 序列帧导出失败提示只按帧源是否存在分类,不把可选时长误报为缺帧。 补充账号素材独立返回时的动作血缘回归测试和角色动作方案说明。 --- ...辑器】画板角色形象生成入口设计-2026-06-15.md | 2 +- .../ImageCanvasGenerationLayerModel.test.ts | 55 +++++++++++++++++++ .../ImageCanvasGenerationLayerModel.ts | 5 +- .../useImageCanvasAssetExportWorkflow.ts | 6 +- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md index 648bfdf95..d13a2a2bb 100644 --- a/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md +++ b/docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md @@ -183,7 +183,7 @@ - BgFilter、阿里云或本地键色返回透明结果后,父流程继续通过现有最终帧 finalizer 转为 RGBA8,按宽高比居中放入最终目标尺寸,并使用 `RGBA(0,0,0,0)` 补边;最终帧 OSS 与业务写回仍由父流程完成。上述样例最终输出必须为 `323×480 RGBA8 PNG`,顶部和底部各 `23px` 透明 padding,内容区域完整保留抠图结果。 - 全部 `32 / 40 / 48` 帧以覆盖本次所有帧的无序在途集合向内部 worker 提交,允许乱序完成并最终按后端内部 `frame_index` 排序;该索引只服务并发收口、OSS 命名、日志和错误定位,不写入正式 asset / resource 帧对象。BgFilter provider 的实际在途请求受唯一 worker 的 `N / Q` 限制。任一帧最终失败时先排空全部已启动 Future,再让整项任务失败退款,不发布缺帧动画。 - 抽帧结果写入 OSS,并返回帧路径、帧尺寸、帧数、fps、预览视频路径、模型、价格和实际 prompt。 -- 画板前端回填角色动作结果时,以正式 resource / asset 的 `assetKind: "character-animation"` 选择序列帧播放器并以首帧作为图层主图;带项目上下文的生成必须返回正式 project resource,图层直接使用其 `resourceId`。无项目放置只允许绑定响应中的正式账号素材,不从生成结果 `frames` 构造自包含 `local-resource-character-animation-*`。预览视频作为独立 `assetKind: "video"` 资源保存,但不复制进最终动作 layout。 +- 画板前端回填角色动作结果时,以正式 resource / asset 的 `assetKind: "character-animation"` 选择序列帧播放器并以首帧作为图层主图;带项目上下文的生成必须返回正式 project resource,图层直接使用其 `resourceId`。无项目放置只允许绑定响应中的正式账号素材,不从生成结果 `frames` 构造自包含 `local-resource-character-animation-*`;此时图层的 `sourceResourceId` 继续使用正式账号素材记录的直接来源,不回退成请求中的原角色资源。预览视频作为独立 `assetKind: "video"` 资源保存,但不复制进最终动作 layout。 - 角色动作图层在画布中循环展示透明 PNG 帧;刷新恢复只从 project resource 的 `imageSequenceFrames/imageSequenceDurationMs` 读取正式结果。动作 layer layout 只保存 `layerId/resourceId` 和几何、层级、可见性等放置信息,不保存 `mediaType`、动作帧、时长、预览、生成输入或资源元数据副本;资源缺字段时按损坏数据失败关闭。 - 点击角色动作图层下载时,必须打包下载序列帧 ZIP;画布素材 ZIP 导出时,角色动作写入 `sequences/<编号-标题>/frames/`,而不是导出预览视频。 - 角色动作生成 BFF 在同一请求内保存两类账号素材:上游预览 MP4 使用 `assetKind=video` 并承载生成成本,最终透明帧集使用 `assetKind=character-animation` 且派生成本为 0;响应中的 `resource` / `asset` 固定指向最终透明帧集对应的项目资源与账号素材。调用方必须直接使用它们,不得再以第一帧调用资源或素材创建接口。 diff --git a/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts b/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts index 790d4a886..8e04fcf78 100644 --- a/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts @@ -733,4 +733,59 @@ describe('ImageCanvasGenerationLayerModel', () => { imageSequenceDurationMs: 4_000, }); }); + + it('keeps persisted animation lineage when generation returns only an asset', () => { + const frames = [ + { + imageSrc: '/generated-character-drafts/editor/frame1.png', + width: 512, + height: 768, + }, + { + imageSrc: '/generated-character-drafts/editor/frame2.png', + width: 512, + height: 768, + }, + ]; + const layer = createCharacterAnimationResultLayer({ + generated: { + taskId: 'character-animation-task-asset-only', + model: 'seedance2.0-fast', + prompt: '跳跃动作', + previewVideoPath: '/generated-character-drafts/editor/preview.mp4', + frames, + frameCount: 2, + durationSeconds: 4, + fps: 8, + priceMudPoints: 40, + asset: { + assetId: 'asset-character-animation-final', + folderId: 'user-1:asset-folder:project', + label: '角色跳跃', + imageSrc: frames[0]!.imageSrc, + width: 512, + height: 768, + sourceType: 'generated', + sourceResourceId: 'resource-character-animation-preview', + assetKind: 'character-animation', + imageSequenceFrames: frames, + imageSequenceDurationMs: 4_000, + }, + }, + generatedIndex: 17, + title: '角色跳跃', + canvasSize: { width: 900, height: 640 }, + viewport: { x: 0, y: 0, scale: 1 }, + generationInputs: createGenerationInputs(), + sourceLayer: createSourceLayer({ + resourceId: 'resource-original-character', + }), + }); + + expect(layer).toMatchObject({ + resourceId: 'local-resource-asset-character-animation-final', + sourceResourceId: 'resource-character-animation-preview', + sourceAssetId: 'asset-character-animation-final', + }); + }); }); diff --git a/src/components/image-editor/ImageCanvasGenerationLayerModel.ts b/src/components/image-editor/ImageCanvasGenerationLayerModel.ts index 8d45623a7..6672aebf9 100644 --- a/src/components/image-editor/ImageCanvasGenerationLayerModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationLayerModel.ts @@ -593,7 +593,10 @@ export function createCharacterAnimationResultLayer({ objectKey: persisted?.objectKey, assetObjectId: persisted?.assetObjectId, sourceAssetId: asset?.assetId, - sourceResourceId: resource?.sourceResourceId ?? sourceLayer?.resourceId, + sourceResourceId: + resource?.sourceResourceId ?? + asset?.sourceResourceId ?? + sourceLayer?.resourceId, groupId: sourceLayer?.groupId, generationInputs: persisted?.generationInputs ?? generationInputs, generatedAssetSnapshot: asset, diff --git a/src/components/image-editor/useImageCanvasAssetExportWorkflow.ts b/src/components/image-editor/useImageCanvasAssetExportWorkflow.ts index 3441e37d2..7166b6b52 100644 --- a/src/components/image-editor/useImageCanvasAssetExportWorkflow.ts +++ b/src/components/image-editor/useImageCanvasAssetExportWorkflow.ts @@ -302,12 +302,10 @@ export function useImageCanvasAssetExportWorkflow({ } }) .catch(() => { - const hasUsableFrameSource = - (layer.imageSequenceFrames?.length ?? 0) > 0 && - (layer.imageSequenceDurationMs ?? 0) > 0; + const hasFrameSource = (layer.imageSequenceFrames?.length ?? 0) > 0; setAssetExportStatus({ tone: 'error', - message: hasUsableFrameSource + message: hasFrameSource ? '序列帧导出失败' : `角色动作素材“${layer.title}”不完整:缺少可用序列帧。`, });