From 68846ca56d2df3e8dcf018d7f6e783252959425e Mon Sep 17 00:00:00 2001 From: Steve Tietze Date: Fri, 9 Jan 2026 11:26:07 +0700 Subject: [PATCH] kling motion fixes again --- kie_api/kling26motion_i2v.py | 27 +++------------- kie_api/upload.py | 63 ++++++++++++++++-------------------- kie_api/video.py | 55 +++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 57 deletions(-) diff --git a/kie_api/kling26motion_i2v.py b/kie_api/kling26motion_i2v.py index b5522ba..f9488c7 100644 --- a/kie_api/kling26motion_i2v.py +++ b/kie_api/kling26motion_i2v.py @@ -2,7 +2,6 @@ import json import time -from pathlib import Path from typing import Any import torch @@ -15,7 +14,7 @@ from .log import _log from .results import _extract_result_urls from .upload import _image_tensor_to_png_bytes, _truncate_url, _upload_image, _upload_video from .validation import _validate_prompt -from .video import _download_video, _video_bytes_to_comfy_video +from .video import _coerce_video_to_mp4_bytes, _download_video, _video_bytes_to_comfy_video CREATE_TASK_URL = "https://api.kie.ai/api/v1/jobs/createTask" @@ -111,29 +110,13 @@ def run_kling26motion_i2v_video( image_url = _upload_image(api_key, png_bytes) _log(log, f"Image upload success: {_truncate_url(image_url)}") - # Resolve the input video into bytes so it can be uploaded. - video_bytes: bytes - if isinstance(video, (bytes, bytearray)): - video_bytes = bytes(video) - elif isinstance(video, str): - try: - video_bytes = Path(video).read_bytes() - except OSError as exc: - raise RuntimeError(f"Failed to read video file: {exc}") from exc - elif isinstance(video, dict): - video_path = video.get("path") or video.get("filename") - if not video_path: - raise RuntimeError("video input dict must include a 'path' or 'filename'.") - try: - video_bytes = Path(video_path).read_bytes() - except OSError as exc: - raise RuntimeError(f"Failed to read video file: {exc}") from exc - else: - raise RuntimeError("video input must be bytes, a file path string, or a dict with a path.") + # Resolve the input video into MP4 bytes so it can be uploaded. + video_bytes, source_desc = _coerce_video_to_mp4_bytes(video) + _log(log, f"Motion video source: {source_desc}") # Upload the motion reference video using the shared upload helper. _log(log, "Uploading motion reference video for Kling 2.6 Motion I2V...") - video_url = _upload_video(api_key, video_bytes) + video_url = _upload_video(api_key, video_bytes, filename="motion.mp4") _log(log, f"Video upload success: {_truncate_url(video_url)}") # Normalize the resolution mode to the API's expected values. diff --git a/kie_api/upload.py b/kie_api/upload.py index c5591f4..064dc32 100644 --- a/kie_api/upload.py +++ b/kie_api/upload.py @@ -9,13 +9,12 @@ from .http import TransientKieError, requests UPLOAD_URL = "https://kieai.redpandaai.co/api/file-stream-upload" -UPLOAD_PATH = "images/user-uploads" +IMAGE_UPLOAD_PATH = "images/user-uploads" +VIDEO_UPLOAD_PATH = "videos/user-uploads" def _truncate_url(url: str, max_length: int = 80) -> str: - if len(url) <= max_length: - return url - return url[:max_length] + "..." + return url if len(url) <= max_length else url[:max_length] + "..." def _image_tensor_to_png_bytes(image: torch.Tensor) -> bytes: @@ -31,11 +30,11 @@ def _image_tensor_to_png_bytes(image: torch.Tensor) -> bytes: working = image.detach().cpu() working = working.contiguous() - height, width, _channels = working.shape + h, w, _ = working.shape data_bytes = bytes(working.view(-1).tolist()) try: - pil_image = Image.frombytes("RGB", (width, height), data_bytes) + pil_image = Image.frombytes("RGB", (w, h), data_bytes) except Exception as exc: raise RuntimeError("Failed to convert tensor to image.") from exc @@ -50,7 +49,7 @@ def _upload_image(api_key: str, png_bytes: bytes) -> str: UPLOAD_URL, headers={"Authorization": f"Bearer {api_key}"}, files={"file": ("image.png", png_bytes, "image/png")}, - data={"uploadPath": UPLOAD_PATH}, + data={"uploadPath": IMAGE_UPLOAD_PATH}, timeout=120, ) except requests.RequestException as exc: @@ -58,21 +57,15 @@ def _upload_image(api_key: str, png_bytes: bytes) -> str: if response.status_code == 429 or response.status_code >= 500: raise TransientKieError( - f"upload returned HTTP {response.status_code}: {response.text}", status_code=response.status_code + f"upload returned HTTP {response.status_code}: {response.text}", + status_code=response.status_code, ) - try: - payload_json: Any = response.json() - except json.JSONDecodeError as exc: - raise RuntimeError("Upload endpoint did not return valid JSON.") from exc + payload = response.json() + if not payload.get("success") or payload.get("code") != 200: + raise RuntimeError(f"Upload failed: {payload.get('msg')}") - success = payload_json.get("success") - code = payload_json.get("code") - if not success or code != 200: - raise RuntimeError(f"Upload failed (code={code}): {payload_json.get('msg')}") - - data = payload_json.get("data") or {} - url = data.get("downloadUrl") + url = (payload.get("data") or {}).get("downloadUrl") if not url: raise RuntimeError("Upload response missing downloadUrl.") @@ -80,36 +73,36 @@ def _upload_image(api_key: str, png_bytes: bytes) -> str: def _upload_video(api_key: str, video_bytes: bytes, filename: str = "video.mp4") -> str: - if not filename: - raise RuntimeError("filename is required for video uploads.") + if not isinstance(video_bytes, (bytes, bytearray)): + raise RuntimeError("video_bytes must be raw bytes.") + if len(video_bytes) == 0: + raise RuntimeError("video_bytes is empty.") + + if not filename.lower().endswith(".mp4"): + filename = f"{filename}.mp4" + try: response = requests.post( UPLOAD_URL, headers={"Authorization": f"Bearer {api_key}"}, files={"file": (filename, video_bytes, "video/mp4")}, - data={"uploadPath": UPLOAD_PATH, "fileName": filename}, - timeout=180, + data={"uploadPath": VIDEO_UPLOAD_PATH, "fileName": filename}, + timeout=300, ) except requests.RequestException as exc: raise RuntimeError(f"Failed to upload video: {exc}") from exc if response.status_code == 429 or response.status_code >= 500: raise TransientKieError( - f"upload returned HTTP {response.status_code}: {response.text}", status_code=response.status_code + f"upload returned HTTP {response.status_code}: {response.text}", + status_code=response.status_code, ) - try: - payload_json: Any = response.json() - except json.JSONDecodeError as exc: - raise RuntimeError("Upload endpoint did not return valid JSON.") from exc + payload = response.json() + if not payload.get("success") or payload.get("code") != 200: + raise RuntimeError(f"Upload failed: {payload.get('msg')}") - success = payload_json.get("success") - code = payload_json.get("code") - if not success or code != 200: - raise RuntimeError(f"Upload failed (code={code}): {payload_json.get('msg')}") - - data = payload_json.get("data") or {} - url = data.get("downloadUrl") + url = (payload.get("data") or {}).get("downloadUrl") if not url: raise RuntimeError("Upload response missing downloadUrl.") diff --git a/kie_api/video.py b/kie_api/video.py index eac3fda..8275ab4 100644 --- a/kie_api/video.py +++ b/kie_api/video.py @@ -25,6 +25,61 @@ def _download_video(url: str) -> bytes: return response.content +def _coerce_video_to_mp4_bytes(video) -> tuple[bytes, str]: + """Coerce ComfyUI VIDEO input into MP4 bytes for upload.""" + if isinstance(video, (bytes, bytearray)): + return bytes(video), "bytes" + + if isinstance(video, str): + try: + return Path(video).read_bytes(), f"path:{video}" + except OSError as exc: + raise RuntimeError(f"Failed to read video file: {exc}") from exc + + if isinstance(video, dict): + video_path = video.get("path") or video.get("filename") + if not video_path: + raise RuntimeError("video input dict must include a 'path' or 'filename'.") + try: + return Path(video_path).read_bytes(), f"dict_path:{video_path}" + except OSError as exc: + raise RuntimeError(f"Failed to read video file: {exc}") from exc + + temp_dir = None + for attr in ("get_temp_directory", "get_temp_folder", "get_output_directory"): + getter = getattr(folder_paths, attr, None) + if callable(getter): + temp_dir = Path(getter()) + break + if temp_dir is None: + temp_dir = Path.cwd() + + if hasattr(video, "save_to") and callable(getattr(video, "save_to")): + temp_path = temp_dir / f"kie_video_{int(time.time() * 1000)}.mp4" + try: + video.save_to(str(temp_path)) + return temp_path.read_bytes(), f"save_to:{temp_path}" + except Exception as exc: + raise RuntimeError(f"Failed to save Comfy VIDEO input: {exc}") from exc + + if hasattr(video, "save") and callable(getattr(video, "save")): + temp_path = temp_dir / f"kie_video_{int(time.time() * 1000)}.mp4" + try: + video.save(str(temp_path)) + return temp_path.read_bytes(), f"save:{temp_path}" + except Exception as exc: + raise RuntimeError(f"Failed to save Comfy VIDEO input: {exc}") from exc + + path_attr = getattr(video, "path", None) + if isinstance(path_attr, str): + try: + return Path(path_attr).read_bytes(), f"path_attr:{path_attr}" + except OSError as exc: + raise RuntimeError(f"Failed to read video file: {exc}") from exc + + raise RuntimeError("video input must be bytes, a file path string, a dict with a path, or a Comfy VIDEO object.") + + def _video_bytes_to_comfy_video(video_bytes: bytes): """ Convert MP4 bytes into a ComfyUI VIDEO object that the official SaveVideo node accepts.