升级角色动作外部接口与辅助工具
外部接口返回正式 resource 与 asset,并以数组顺序表达动作帧。 helper 只调用一次动作生成接口并直接消费正式结果,不再二次创建资源或兼容旧运行字段。 同步 OpenAPI、接口选择说明与 helper 自测。
This commit is contained in:
@@ -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": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user