修正动画轮询精简结果的文档与测试
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

删除未被接口引用的旧动画生成响应定义
明确顶层帧与秒级时长及正式序列记录读取路径
修正 Python 自测并补充动画精简结果回归
同步外部生成方案与共享排障记忆
关联 #495 第二项
This commit is contained in:
2026-10-07 21:36:55 +08:00
parent 6df0bfc8f3
commit a948722d1a
6 changed files with 67 additions and 122 deletions
@@ -134,7 +134,7 @@ A minimal `canvasCompletion` is:
Background removal preserves the source image dimensions. For normal canvas placement, the Python helper therefore requires the real `source_width` and `source_height` whenever `canvasSession` is used without an explicit `canvasWidth` plus `canvasHeight`; it never substitutes a square default. Passing `targetLayerId` instead selects in-place replacement, so the helper keeps the session's project/library fields without injecting `canvasCompletion` and rejects callers that explicitly combine both placement modes. The request `assetKind` is optional, static-image only, and must equal the authoritative source type when one exists. An in-place target must resolve to the same authoritative source object; a raw object key is bound to that target resource instead of relying on project-list order.
Character animation accepts `assetFolderId` and `assetLabel` and persists the generated sequence. Consume the returned animation artifacts and persisted identities; do not synthesize a duplicate animation asset from the first frame. Use complete project/library records when complete persisted state is needed.
Character animation accepts `assetFolderId` and `assetLabel` and persists the generated sequence. In a completed polling response, use `result.frames` for the ordered frame artifacts and `result.durationSeconds` for the duration in seconds. The nested `result.resource` and `result.asset` are compact references, not complete records; they do not include `imageSequenceFrames` or `imageSequenceDurationMs`. Read the complete project (`GET /api/external/v1/editor/projects/{projectId}`) or asset library (`GET /api/external/v1/editor/assets/library`) and match `resourceId` or `assetId` to obtain those formal sequence fields, whose duration is in milliseconds. MCP equivalents are `find_assets/get_project_resources` and `find_assets/list_library`. Do not synthesize a duplicate animation asset from the first frame.
For the lower-level asset/resource creation endpoints, `generationInputs` follows the metadata and media-runtime boundaries described in [Generation Inputs Metadata](#generation-inputs-metadata).
@@ -795,25 +795,20 @@ def _self_test() -> None:
"model": "seedance2.0-fast",
"prompt": "角色呼吸",
"previewVideoPath": "/generated/preview.mp4",
"frames": [{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768}],
"frames": [
{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768},
{"imageSrc": "/generated/frame02.png", "width": 512, "height": 768},
],
"frameCount": 2,
"durationSeconds": 4,
"resource": {
"resourceId": "editor-resource-demo",
"assetKind": "character-animation",
"sourceResourceId": "editor-resource-preview-demo",
"imageSequenceFrames": [
{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768},
{"imageSrc": "/generated/frame02.png", "width": 512, "height": 768},
],
"imageSequenceDurationMs": 4000,
},
"asset": {
"assetId": "editor-asset-demo",
"assetKind": "character-animation",
"imageSequenceFrames": [
{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768},
{"imageSrc": "/generated/frame02.png", "width": 512, "height": 768},
],
"imageSequenceDurationMs": 4000,
},
}
if method == "POST":
@@ -841,8 +836,13 @@ def _self_test() -> None:
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
assert result["asset"]["assetId"] == "editor-asset-demo"
assert result["asset"]["assetKind"] == "character-animation"
assert len(result["asset"]["imageSequenceFrames"]) == 2
assert result["asset"]["imageSequenceDurationMs"] == 4000
assert result["resource"]["resourceId"] == "editor-resource-demo"
assert result["frames"] == [
{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768},
{"imageSrc": "/generated/frame02.png", "width": 512, "height": 768},
]
assert result["frameCount"] == 2
assert result["durationSeconds"] == 4
calls.clear()
background_result = client.remove_background(
"uploads/source.png",
@@ -4114,113 +4114,6 @@
"additionalProperties": false,
"description": "图片序列帧。数组位置是唯一播放顺序,不携带额外序号字段;每帧必须同时携带 objectKey 与 assetObjectId,imageSrc 按 objectKey 规范化为持久站内路径。"
},
"EditorCharacterAnimationGenerationResponse": {
"type": "object",
"required": [
"ok",
"taskId",
"model",
"prompt",
"previewVideoPath",
"frames",
"frameCount",
"durationSeconds",
"frameWidth",
"frameHeight",
"fps",
"priceMudPoints"
],
"properties": {
"ok": {
"type": "boolean"
},
"taskId": {
"type": "string"
},
"model": {
"type": "string"
},
"prompt": {
"type": "string"
},
"previewVideoPath": {
"type": "string"
},
"frames": {
"type": "array",
"items": {
"$ref": "#/components/schemas/EditorImageSequenceFrame"
}
},
"frameCount": {
"type": "integer",
"minimum": 1
},
"durationSeconds": {
"type": "integer",
"minimum": 1
},
"frameWidth": {
"type": "integer",
"minimum": 1
},
"frameHeight": {
"type": "integer",
"minimum": 1
},
"fps": {
"type": "integer",
"minimum": 1
},
"priceMudPoints": {
"type": "integer",
"minimum": 0
},
"project": {
"anyOf": [
{
"$ref": "#/components/schemas/EditorProject"
},
{
"type": "null"
}
],
"description": "当请求携带 canvasCompletion 且服务端成功写入画布布局时返回最新项目快照。"
},
"resource": {
"anyOf": [
{
"$ref": "#/components/schemas/EditorProjectResource"
},
{
"type": "null"
}
],
"description": "最终透明角色动作对应的项目资源。携带 projectId 时,画布图层必须直接引用其 resourceId,不得再次创建重复资源。"
},
"asset": {
"anyOf": [
{
"$ref": "#/components/schemas/EditorAsset"
},
{
"type": "null"
}
],
"description": "最终透明角色动作素材。assetKind 为 character-animation,并直接包含序列帧字段。"
},
"queueState": {
"anyOf": [
{
"$ref": "#/components/schemas/ExternalGenerationJobStatusRecord"
},
{
"type": "null"
}
]
}
}
},
"EditorVideoGenerationRequest": {
"type": "object",
"required": [
@@ -4776,7 +4669,7 @@
}
},
"additionalProperties": true,
"description": "其它生成类型或历史结果的兼容 compact result。背景音乐(audioKind=background-music)与不带 audioKind 的图片、视频、图标序列帧、角色动作和 UI 拆解结果都落在这里。pixelArt 图片的 width/height、图标结果的 spritesheetWidth/spritesheetHeight 及关联 resource/asset 尺寸均表示最终逻辑分辨率 PNG 的实际值,不保证等于请求档位、provider 回图或画布 placeholder。该 fallback 明确排除 audioKind=sound-effect,避免吞掉 SFX 专用分支。"
"description": "其它生成类型或历史结果的兼容 compact result。背景音乐(audioKind=background-music)与不带 audioKind 的图片、视频、图标序列帧、角色动作和 UI 拆解结果都落在这里。角色动作使用顶层 frames(有序帧产物)与 durationSeconds(秒级时长);内嵌 resource/asset 是精简引用,不包含 imageSequenceFrames/imageSequenceDurationMs。需要正式序列记录时,按返回的 resourceId/assetId 查询 GET /api/external/v1/editor/projects/{projectId} 或 GET /api/external/v1/editor/assets/library,正式 imageSequenceDurationMs 单位为毫秒。pixelArt 图片的 width/height、图标结果的 spritesheetWidth/spritesheetHeight 及关联 resource/asset 尺寸均表示最终逻辑分辨率 PNG 的实际值,不保证等于请求档位、provider 回图或画布 placeholder。该 fallback 明确排除 audioKind=sound-effect,避免吞掉 SFX 专用分支。"
},
"ExternalEditorGenerationCompletedResult": {
"oneOf": [
@@ -2,6 +2,11 @@
这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。
## External 动画轮询的精简引用不等于完整序列记录
- 动画 completed 结果通过顶层 `frames` 和 `durationSeconds` 提供有序帧与秒级时长;内嵌 `resource/asset` 只提供精简引用。正式 `imageSequenceFrames/imageSequenceDurationMs` 从完整项目或素材库按返回的 ID 读取,后者时长单位为毫秒。内嵌引用没有这两个字段不能据此判定持久化丢帧,也不能用首帧重复创建动画素材。
- 排查时分别核对真实精简函数、正式记录和 [External v1 OpenAPI](../../openapi/genarrative-external-v1.openapi.json);Python helper 的模拟响应必须遵循同一结构,不能用虚构的嵌套完整记录证明轮询契约成立。
## 2026-10-05 generationInputs 的保存与复用不等于自动生效或完整透传
- **现象 / 根因**:调用方把构图等要求只写入 `generationInputs.artSpec`,但实际生图输入没有这些要求;把 `JsonValue` 和“可复用规范”误读为服务端会自动组装提示词、完整保留全部元数据或自动用于下一次生成。
@@ -41,6 +41,8 @@ VectorEngine `gpt-image-2`、音频、LLM 等外部生成不能由面向外部
External API job 复用同一个 `result_payload_json` 列,但只额外保存 `result` compact 引用:允许 objectKey、resource/asset ID、assetObjectId、尺寸、媒体类型、taskId 和告警;禁止完整 project/canvas、大布局、Data URL、Blob URL、临时 signed URL、provider 原始响应和 lease/fencing 控制字段。普通站内 job 继续保持原 payload 语义,不能为了 External 查询把所有队列结果扩成第二套资产 read model。
角色动画的 completed compact result 通过顶层 `frames` 保留有序帧产物,通过 `durationSeconds` 保留秒级时长;内嵌 `resource/asset` 仍是精简引用,不包含 `imageSequenceFrames/imageSequenceDurationMs`。调用方需要正式序列记录时,按返回的 `resourceId/assetId` 从完整项目或素材库读取,正式时长单位为毫秒;不得把精简引用当完整记录,或根据首帧重复创建动画素材。OpenAPI、Skill 与 helper 自测共同遵循此读路径。
不带 `summary / summaries` 的旧 `get / list / acknowledge_external_generation_job*` procedure 只保留给受控内部兼容,不是 BFF 正式读取入口。
幂等读取回归使用 `npm run check:editor-idempotency-procedures`:隔离 standalone 验证 pending、running、completed 的原始请求身份、同键重放唯一任务及跨 owner / 不存在任务拒绝,不调用生成 Provider。脚本仅在临时构建目录注入公开测试 bootstrap hash;若设置 `GENARRATIVE_EDITOR_IDEMPOTENCY_SMOKE_WASM` 复用预编译模块,该模块也必须使用脚本中的测试 hash 构建,且不得用于部署。
@@ -19846,6 +19846,51 @@ mod tests {
}
}
#[test]
fn compact_external_character_animation_result_keeps_frames_duration_and_references() {
let frames = json!([
{
"imageSrc": "/generated/animation/frame01.png",
"objectKey": "generated/animation/frame01.png",
"assetObjectId": "asset-object-frame01",
"width": 192,
"height": 256
},
{
"imageSrc": "/generated/animation/frame02.png",
"objectKey": "generated/animation/frame02.png",
"assetObjectId": "asset-object-frame02",
"width": 256,
"height": 192
}
]);
let result = compact_external_api_generation_result(json!({
"ok": true,
"durationSeconds": 4,
"frameCount": 2,
"frames": frames,
"resource": {
"resourceId": "resource-animation",
"objectKey": "generated/animation/frame01.png",
"assetObjectId": "asset-object-frame01"
},
"asset": {
"assetId": "asset-animation",
"objectKey": "generated/animation/frame01.png",
"assetObjectId": "asset-object-frame01"
},
"provider": "internal-provider"
}));
assert_eq!(result["frames"], frames);
assert_eq!(result["durationSeconds"], 4);
assert_eq!(result["frameCount"], 2);
assert_eq!(result["resource"]["resourceId"], "resource-animation");
assert_eq!(result["asset"]["assetId"], "asset-animation");
assert_eq!(result["resource"]["assetObjectId"], "asset-object-frame01");
assert_eq!(result["asset"]["assetObjectId"], "asset-object-frame01");
}
#[test]
fn game_creator_completed_jobs_keep_stable_results_for_all_current_media_kinds() {
let fixtures = [