kling motion fixes again
This commit is contained in:
@@ -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.
|
||||
|
||||
+28
-35
@@ -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.")
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user