升级角色动作外部接口与辅助工具

外部接口返回正式 resource 与 asset,并以数组顺序表达动作帧。
helper 只调用一次动作生成接口并直接消费正式结果,不再二次创建资源或兼容旧运行字段。
同步 OpenAPI、接口选择说明与 helper 自测。
This commit is contained in:
2026-08-04 10:33:19 +08:00
parent 00cff24cc1
commit be63c54a84
4 changed files with 130 additions and 50 deletions
@@ -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.
@@ -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
@@ -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",
@@ -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": [
{