From be63c54a84c97eaf52523a586fd48fd5de56c70a 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 10:33:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E8=A7=92=E8=89=B2=E5=8A=A8?= =?UTF-8?q?=E4=BD=9C=E5=A4=96=E9=83=A8=E6=8E=A5=E5=8F=A3=E4=B8=8E=E8=BE=85?= =?UTF-8?q?=E5=8A=A9=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 外部接口返回正式 resource 与 asset,并以数组顺序表达动作帧。 helper 只调用一次动作生成接口并直接消费正式结果,不再二次创建资源或兼容旧运行字段。 同步 OpenAPI、接口选择说明与 helper 自测。 --- .../genarrative-external-editor-api/SKILL.md | 5 +- .../references/api-selection.md | 4 +- .../scripts/genarrative_external_api.py | 65 +++++------ .../genarrative-external-v1.openapi.json | 106 +++++++++++++++--- 4 files changed, 130 insertions(+), 50 deletions(-) diff --git a/.codex/skills/genarrative-external-editor-api/SKILL.md b/.codex/skills/genarrative-external-editor-api/SKILL.md index 07a5f736f..00946e5ff 100644 --- a/.codex/skills/genarrative-external-editor-api/SKILL.md +++ b/.codex/skills/genarrative-external-editor-api/SKILL.md @@ -25,7 +25,7 @@ Prefer the bundled Python helper for runnable examples: `scripts/genarrative_ext - canvas name when no current canvas session exists; existing `projectId`, folder/resource IDs only when resuming a known project - media type, prompt, references, dimensions, model, ratio, duration, and resolution - whether referenced media is already uploaded as `objectKey` or still local -5. Every external generation must write to both the canvas and the asset library. Include `projectId`, `assetFolderId`, a display label, and `canvasCompletion` whenever the target endpoint supports them. For character animation, use the helper's two-step fallback: generate with `projectId` + `canvasCompletion`, then create a library asset from the first returned frame in the session folder. +5. Every external generation must write to both the canvas and the asset library. Include `projectId`, `assetFolderId`, a display label, and `canvasCompletion` whenever the target endpoint supports them. Character animation writes both the preview video and final transparent image sequence in the generation request and returns the final `resource` / `asset`; use their IDs directly and do not create a duplicate first-frame resource or asset. 6. If the user lacks an API Key, guide setup before request design. 7. Read `references/api-selection.md` before finalizing any request. Use the core table below for fast routing, then verify details in the reference. 8. Use `scripts/genarrative_external_api.py` when the user wants runnable Python, reference image upload, canvas/folder session setup, art-spec carrying, or a chain that should execute with fewer hand-written curl steps. @@ -326,8 +326,7 @@ Use an existing canvas layer ID when the image came from a project layer. If it The helper uses a 420 second timeout for generation calls, including character animation and video. Direct HTTP clients should not use a 70 second request timeout for animation. -Character animation currently returns canvas completion data but not a direct `asset` payload. To keep the "canvas + asset library" invariant, call `client.animate_character(..., canvasSession=session, canvasTitle="...")`; the helper creates a library asset from the first returned frame in the session folder after the animation call succeeds. - +Character animation returns both canvas completion data and the canonical final `resource` / `asset` payloads. Call `client.animate_character(..., canvasSession=session, canvasTitle="...")`; the helper sends the project, folder, label, and canvas completion context directly to the generation endpoint. The generation response includes `frames`, `previewVideoPath`, `frameCount`, `fps`, and `durationSeconds`; `frames` and the final resource / asset `imageSequenceFrames` both use array position as the only playback order and never carry `frameIndex`. The preview is persisted separately with `assetKind: "video"`. The final transparent action has `assetKind: "character-animation"` plus `imageSequenceFrames` and `imageSequenceDurationMs`. When project context is present, the canvas layer must use the returned final `resource.resourceId`, whose `sourceResourceId` records the preview-resource lineage. Derive rendering behavior from `assetKind`; For video generation, always include `mode: "std"`. When using image/video/audio references, default to `model: "seedance2.0-fast"` unless the user asks for another listed model, because reference media support is limited to the Seedance 2.0 family. For image edit/redraw that should replace an existing canvas layer, pass `projectId` and `targetLayerId`. If the user instead gives an explicit `canvasCompletion`, let that placement win. diff --git a/.codex/skills/genarrative-external-editor-api/references/api-selection.md b/.codex/skills/genarrative-external-editor-api/references/api-selection.md index c3f18feb8..4f6df7462 100644 --- a/.codex/skills/genarrative-external-editor-api/references/api-selection.md +++ b/.codex/skills/genarrative-external-editor-api/references/api-selection.md @@ -79,7 +79,7 @@ Ask a follow-up only when two routes could both be correct and produce different | Edit/redraw image | `POST /api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` | | Generate icon spritesheet | `POST /api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Extract assets from UI design | `POST /api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` | -| Generate character animation | `POST /api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `canvasCompletion`; then create a library asset from the first returned frame | +| Generate character animation | `POST /api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion`; response includes transient generation timing plus the final `assetKind="character-animation"` resource / asset with `imageSequenceFrames` and `imageSequenceDurationMs` | | Generate video | `POST /api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` | | Generate sound effect | `POST /api/external/v1/editor/audios/sound-effects/generations` | `prompt`, `duration` | `model`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | | Generate background music | `POST /api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` | @@ -113,7 +113,7 @@ Icon spritesheet generation with pixel-art snapping: } ``` -All generation requests should be placed into both the current canvas and its same-name asset-library folder. For endpoints that support `assetLabel`, pass it. For UI extraction, use `spritesheetLabel`. For icon spritesheet, the folder is enough. For character animation, the endpoint does not return `asset`; after success call `POST /api/external/v1/editor/assets` using the first returned frame as `imageSrc`, the session `assetFolderId`, and `assetKind: "character-animation"`. +All generation requests should be placed into both the current canvas and its same-name asset-library folder. For endpoints that support `assetLabel`, pass it. For UI extraction, use `spritesheetLabel`. For icon spritesheet, the folder is enough. For character animation, pass `assetFolderId` and `assetLabel` to the generation endpoint and use the returned final `resource` / `asset`; never create another resource or asset from its first frame. ## HTTP 2xx Warning Handling diff --git a/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py b/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py index a099fd8c9..67c4eb054 100644 --- a/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py +++ b/.codex/skills/genarrative-external-editor-api/scripts/genarrative_external_api.py @@ -502,16 +502,14 @@ class GenarrativeExternalClient: source_layer_id: str, **fields: Any, ) -> Any: - session, asset_label = self._apply_canvas_session_fields( + self._apply_canvas_session_fields( fields, fields.get("canvasTitle", "角色动画"), source_width, source_height, - asset_label_field=None, + asset_label_field="assetLabel", ) prompt_text = self._apply_art_spec(fields, prompt_text) - fields.pop("assetFolderId", None) - fields.pop("assetLabel", None) body = { "sourceLayerId": source_layer_id, "sourceImageSrc": source_image_src, @@ -525,35 +523,12 @@ class GenarrativeExternalClient: **fields, "model": "seedance2.0-fast", } - result = self.request_json( + return self.request_json( "POST", "/api/external/v1/editor/character-animations/generations", body, timeout=GENERATION_REQUEST_TIMEOUT_SECONDS, ) - if isinstance(session, dict) and isinstance(result, dict) and not result.get("asset"): - frames = result.get("frames") - first_frame = frames[0] if isinstance(frames, list) and frames else None - folder_id = normalize_optional_text(session.get("assetFolderId")) - if isinstance(first_frame, dict) and folder_id: - asset = self.create_asset( - folder_id, - asset_label, - first_frame["imageSrc"], - int(first_frame["width"]), - int(first_frame["height"]), - prompt=result.get("prompt"), - model=result.get("model"), - provider="ark", - taskId=result.get("taskId"), - assetKind="character-animation", - generationInputs={ - "frames": frames, - "previewVideoPath": result.get("previewVideoPath"), - }, - ) - result["asset"] = unwrap_envelope(asset).get("asset") - return result def generate_video(self, prompt: str, **fields: Any) -> Any: fields.pop("mode", None) @@ -627,14 +602,31 @@ def _self_test() -> None: timeout: int = DEFAULT_REQUEST_TIMEOUT_SECONDS, ) -> Any: calls.append({"method": method, "path": path, "body": body, "timeout": timeout}) - if path == "/api/external/v1/editor/assets": - return {"asset": {"assetId": "editor-asset-demo"}} return { "taskId": "task-demo", "model": "seedance2.0-fast", "prompt": "角色呼吸", "previewVideoPath": "/generated/preview.mp4", - "frames": [{"frameIndex": 1, "imageSrc": "/generated/frame01.png", "width": 512, "height": 768}], + "frames": [{"imageSrc": "/generated/frame01.png", "width": 512, "height": 768}], + "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, + }, } client.request_json = fake_request_json # type: ignore[method-assign] @@ -649,9 +641,18 @@ def _self_test() -> None: ) assert calls[0]["timeout"] == GENERATION_REQUEST_TIMEOUT_SECONDS assert calls[0]["body"]["projectId"] == "proj-demo" + assert calls[0]["body"]["assetFolderId"] == "editor-asset-folder-demo" + assert calls[0]["body"]["assetLabel"] == "角色呼吸动画" assert calls[0]["body"]["canvasCompletion"]["title"] == "角色呼吸动画" - assert calls[1]["path"] == "/api/external/v1/editor/assets" + assert len(calls) == 1 + assert "frameIndex" not in result["frames"][0] + assert result["resource"]["resourceId"] == "editor-resource-demo" + assert result["resource"]["sourceResourceId"] == "editor-resource-preview-demo" + assert "frameIndex" not in result["resource"]["imageSequenceFrames"][0] assert result["asset"]["assetId"] == "editor-asset-demo" + assert result["asset"]["assetKind"] == "character-animation" + assert len(result["asset"]["imageSequenceFrames"]) == 2 + assert result["asset"]["imageSequenceDurationMs"] == 4000 calls.clear() client.generate_icon_spritesheet( "editor-resource-spec", diff --git a/docs/openapi/genarrative-external-v1.openapi.json b/docs/openapi/genarrative-external-v1.openapi.json index ff13717ab..c4e2856b5 100644 --- a/docs/openapi/genarrative-external-v1.openapi.json +++ b/docs/openapi/genarrative-external-v1.openapi.json @@ -1896,7 +1896,19 @@ "type": [ "string", "null" - ] + ], + "description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。" + }, + "imageSequenceFrames": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EditorImageSequenceFrame" + } + }, + "imageSequenceDurationMs": { + "type": "integer", + "minimum": 1, + "description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。" }, "generationInputs": { "$ref": "#/components/schemas/JsonValue" @@ -1979,7 +1991,19 @@ "type": [ "string", "null" - ] + ], + "description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。" + }, + "imageSequenceFrames": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EditorImageSequenceFrame" + } + }, + "imageSequenceDurationMs": { + "type": "integer", + "minimum": 1, + "description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。" }, "generationInputs": { "$ref": "#/components/schemas/JsonValue" @@ -2315,7 +2339,19 @@ "type": [ "string", "null" - ] + ], + "description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。" + }, + "imageSequenceFrames": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EditorImageSequenceFrame" + } + }, + "imageSequenceDurationMs": { + "type": "integer", + "minimum": 1, + "description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。" }, "generationInputs": { "$ref": "#/components/schemas/JsonValue" @@ -2458,6 +2494,18 @@ "sourceType": { "type": "string" }, + "imageSequenceFrames": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EditorImageSequenceFrame" + }, + "description": "assetKind=character-animation 时的完整可播放帧集合。" + }, + "imageSequenceDurationMs": { + "type": "integer", + "minimum": 1, + "description": "assetKind=character-animation 时完整图片序列播放一次的毫秒时长;与音频、视频生成参数 durationSeconds 无关。" + }, "prompt": { "type": [ "string", @@ -2492,7 +2540,8 @@ "type": [ "string", "null" - ] + ], + "description": "权威媒体类别。character-animation 渲染为序列帧,video 渲染为视频,audio/sound-effect/background-music 渲染为音频,其余渲染为图片。" }, "generationInputs": { "$ref": "#/components/schemas/JsonValue" @@ -3444,7 +3493,7 @@ "string", "null" ], - "description": "角色动作绿幕预览视频写入账号素材库的文件夹;省略时写入默认项目素材文件夹。" + "description": "角色动作预览视频和最终透明序列帧素材写入账号素材库的文件夹;省略时写入默认项目素材文件夹。" }, "assetLabel": { "type": [ @@ -3457,22 +3506,29 @@ }, "additionalProperties": false }, - "EditorCharacterAnimationFrame": { + "EditorImageSequenceFrame": { "type": "object", "required": [ - "frameIndex", "imageSrc", "width", "height" ], "properties": { - "frameIndex": { - "type": "integer", - "minimum": 0 - }, "imageSrc": { "type": "string" }, + "objectKey": { + "type": [ + "string", + "null" + ] + }, + "assetObjectId": { + "type": [ + "string", + "null" + ] + }, "width": { "type": "integer", "minimum": 1 @@ -3481,7 +3537,9 @@ "type": "integer", "minimum": 1 } - } + }, + "additionalProperties": false, + "description": "图片序列帧。数组位置是唯一播放顺序,不携带额外序号字段。" }, "EditorCharacterAnimationGenerationResponse": { "type": "object", @@ -3518,7 +3576,7 @@ "frames": { "type": "array", "items": { - "$ref": "#/components/schemas/EditorCharacterAnimationFrame" + "$ref": "#/components/schemas/EditorImageSequenceFrame" } }, "frameCount": { @@ -3556,6 +3614,28 @@ ], "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": [ {