Skip to content

Commit 6655738

Browse files
hubooyclaude
andcommitted
feat(jimeng_video): accept a local first-frame image for image_to_video
image_to_video required image_url — a publicly reachable URL — so the first frame had to be uploaded to public hosting before it could be animated. For unreleased client artwork that is an avoidable exposure. Add image_path, sent to the API as binary_data_base64, needing no public URL or object storage. A local file takes precedence over image_url when both are given, and its existence is checked before the API call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYThSL15CujvBwD1wuUmr9
1 parent 4eab34c commit 6655738

1 file changed

Lines changed: 35 additions & 7 deletions

File tree

tools/video/jimeng_video.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import base64
1718
import hashlib
1819
import hmac
1920
import json
@@ -101,6 +102,14 @@ class JimengVideo(BaseTool):
101102
"Must be publicly accessible."
102103
),
103104
},
105+
"image_path": {
106+
"type": "string",
107+
"description": (
108+
"Local first-frame image for image-to-video. Sent as "
109+
"binary_data_base64 so no public URL or object storage is "
110+
"needed. Takes precedence over image_url when both are set."
111+
),
112+
},
104113
"frames": {
105114
"type": "integer",
106115
"enum": [121, 241],
@@ -144,6 +153,7 @@ class JimengVideo(BaseTool):
144153
"prompt",
145154
"operation",
146155
"image_url",
156+
"image_path",
147157
"frames",
148158
"aspect_ratio",
149159
"seed",
@@ -191,11 +201,21 @@ def execute(self, inputs: dict[str, Any]) -> ToolResult:
191201
)
192202

193203
operation = inputs.get("operation", "text_to_video")
194-
if operation == "image_to_video" and not inputs.get("image_url"):
195-
return ToolResult(
196-
success=False,
197-
error="image_to_video requires image_url (public URL).",
198-
)
204+
if operation == "image_to_video":
205+
image_path = inputs.get("image_path")
206+
if not image_path and not inputs.get("image_url"):
207+
return ToolResult(
208+
success=False,
209+
error=(
210+
"image_to_video requires image_path (local file) or "
211+
"image_url (public URL)."
212+
),
213+
)
214+
if image_path and not Path(image_path).is_file():
215+
return ToolResult(
216+
success=False,
217+
error=f"image_path not found: {image_path}",
218+
)
199219

200220
start = time.time()
201221
try:
@@ -271,8 +291,16 @@ def _build_payload(inputs: dict[str, Any]) -> dict[str, Any]:
271291
"aspect_ratio": inputs.get("aspect_ratio", "16:9"),
272292
"seed": int(inputs.get("seed", -1)),
273293
}
274-
if operation == "image_to_video" and inputs.get("image_url"):
275-
payload["image_urls"] = [inputs["image_url"]]
294+
if operation == "image_to_video":
295+
# A local file wins over a URL: it needs no public hosting, so it is
296+
# the safer path for unreleased client artwork.
297+
image_path = inputs.get("image_path")
298+
if image_path:
299+
payload["binary_data_base64"] = [
300+
base64.b64encode(Path(image_path).read_bytes()).decode("ascii")
301+
]
302+
elif inputs.get("image_url"):
303+
payload["image_urls"] = [inputs["image_url"]]
276304
return payload
277305

278306
def _submit_task(self, payload: dict[str, Any], *, ak: str, sk: str) -> str:

0 commit comments

Comments
 (0)