feat: 接入动作控制节点并全面重构 Kling 节点
- 动作控制节点改走 new API 三段式流程(POST /v1/videos → 轮询 → 下载)
- KlingClient 新增 motion_control_async 方法,支持 id 轮询和内容下载
- 动作控制请求体字段更新:image→image_url,video→video_url,补充 model 字段
- 动作控制 model 名按平台规范动态拼接:kling-{版本}-motion-{mode}-{时长}s
- 动作控制节点新增时长参数(5/10/15s),完善参数顺序与命名
- 三个 Kling 节点显示名称统一重命名
- KlingVideo / KlingFirstLastFrame 补充 v2-6 模型支持与约束校验
Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.5
parent
2b64a45b8e
commit
9abd175316
+3
-3
@@ -93,9 +93,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"GoogleVeo": "Google Veo - ab",
|
"GoogleVeo": "Google Veo - ab",
|
||||||
"FluxImageEdit": "Flux2 图像编辑",
|
"FluxImageEdit": "Flux2 图像编辑",
|
||||||
"UniversalLLMChat": "全能LLM对话助手",
|
"UniversalLLMChat": "全能LLM对话助手",
|
||||||
"KlingVideo": "自研模型 3.0 视频",
|
"KlingVideo": "文/图生视频 自研模型",
|
||||||
"KlingFirstLastFrame": "自研模型 3.0 首尾帧到视频",
|
"KlingFirstLastFrame": "首尾帧生视频 自研模型",
|
||||||
"KlingMotionControlTest": "自研模型 动作控制(测试)",
|
"KlingMotionControlTest": "动作控制 自研模型",
|
||||||
"QuanNengShengTu": "全能生图",
|
"QuanNengShengTu": "全能生图",
|
||||||
"BatchQuanNengShengTu": "全能生图(批量)",
|
"BatchQuanNengShengTu": "全能生图(批量)",
|
||||||
"AspectRatioPreset": "图片宽高比预设",
|
"AspectRatioPreset": "图片宽高比预设",
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ class KlingClient:
|
|||||||
"motion_control": "/kling/v1/videos/motion-control",
|
"motion_control": "/kling/v1/videos/motion-control",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# new API 三段式端点(动作控制走这里)
|
||||||
|
NEW_API_CREATE = "/v1/videos"
|
||||||
|
NEW_API_STATUS = "/v1/videos/{video_id}"
|
||||||
|
NEW_API_CONTENT = "/v1/videos/{video_id}/content"
|
||||||
|
|
||||||
POLL_INITIAL_INTERVAL = 3
|
POLL_INITIAL_INTERVAL = 3
|
||||||
POLL_MAX_INTERVAL = 15
|
POLL_MAX_INTERVAL = 15
|
||||||
|
|
||||||
@@ -172,3 +177,114 @@ class KlingClient:
|
|||||||
if on_stage:
|
if on_stage:
|
||||||
on_stage("done")
|
on_stage("done")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
# ── 动作控制:走 new API 三段式流程 ──────────────────────────────
|
||||||
|
|
||||||
|
async def motion_control_async(
|
||||||
|
self,
|
||||||
|
body: Dict[str, Any],
|
||||||
|
save_path: str,
|
||||||
|
on_stage: Optional[Callable[[str], None]] = None,
|
||||||
|
on_progress: Optional[Callable[[int], None]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
动作控制专用入口:
|
||||||
|
POST /v1/videos → GET /v1/videos/{id} → GET /v1/videos/{id}/content
|
||||||
|
body 字段与 Kling 官方动作控制接口一致(image_url/video_url/prompt/...)。
|
||||||
|
"""
|
||||||
|
headers = {"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
interval = self.POLL_INITIAL_INTERVAL
|
||||||
|
|
||||||
|
connector = aiohttp.TCPConnector(force_close=True)
|
||||||
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
|
|
||||||
|
# 1. 提交
|
||||||
|
if on_stage:
|
||||||
|
on_stage("submitting")
|
||||||
|
create_url = f"{self.base_url}{self.NEW_API_CREATE}"
|
||||||
|
async with session.post(create_url, json=body, headers=headers) as resp:
|
||||||
|
text = await resp.text()
|
||||||
|
if resp.status != 200:
|
||||||
|
# 尝试提取友好错误信息
|
||||||
|
try:
|
||||||
|
err = json.loads(text)
|
||||||
|
msg = err.get("error", {}).get("message") or err.get("message") or text
|
||||||
|
except Exception:
|
||||||
|
msg = text
|
||||||
|
raise RuntimeError(f"动作控制提交失败 ({resp.status}): {msg}")
|
||||||
|
create_resp = json.loads(text)
|
||||||
|
|
||||||
|
video_id = create_resp.get("id")
|
||||||
|
if not video_id:
|
||||||
|
raise RuntimeError(f"API 未返回视频 ID,响应:{create_resp}")
|
||||||
|
if on_stage:
|
||||||
|
on_stage(f"submitted:{video_id}")
|
||||||
|
|
||||||
|
# 2. 轮询
|
||||||
|
status_url = f"{self.base_url}{self.NEW_API_STATUS.format(video_id=video_id)}"
|
||||||
|
while True:
|
||||||
|
async with session.get(status_url, headers=headers) as resp:
|
||||||
|
text = await resp.text()
|
||||||
|
if resp.status != 200:
|
||||||
|
try:
|
||||||
|
err = json.loads(text)
|
||||||
|
msg = err.get("error", {}).get("message") or err.get("message") or text
|
||||||
|
except Exception:
|
||||||
|
msg = text
|
||||||
|
raise RuntimeError(f"状态查询失败 ({resp.status}): {msg}")
|
||||||
|
status_resp = json.loads(text)
|
||||||
|
|
||||||
|
status = status_resp.get("status", "").lower()
|
||||||
|
progress_raw = status_resp.get("progress", 0)
|
||||||
|
try:
|
||||||
|
progress_pct = int(str(progress_raw).rstrip("%").strip())
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
progress_pct = 0
|
||||||
|
|
||||||
|
print(f"[动作控制] 生成中 {progress_pct}%")
|
||||||
|
if on_progress:
|
||||||
|
on_progress(progress_pct)
|
||||||
|
|
||||||
|
if status == "completed":
|
||||||
|
break
|
||||||
|
if status == "failed":
|
||||||
|
error_info = status_resp.get("error", {})
|
||||||
|
error_msg = (error_info.get("message", "未知错误")
|
||||||
|
if isinstance(error_info, dict) else str(error_info))
|
||||||
|
raise RuntimeError(f"动作控制生成失败:{error_msg}")
|
||||||
|
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
interval = min(interval * 1.5, self.POLL_MAX_INTERVAL)
|
||||||
|
|
||||||
|
# 3. 下载
|
||||||
|
if on_stage:
|
||||||
|
on_stage("downloading")
|
||||||
|
content_url = f"{self.base_url}{self.NEW_API_CONTENT.format(video_id=video_id)}"
|
||||||
|
async with session.get(content_url, headers=headers,
|
||||||
|
allow_redirects=True) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"视频下载失败 ({resp.status})")
|
||||||
|
content_type = resp.headers.get("Content-Type", "")
|
||||||
|
if "application/json" in content_type:
|
||||||
|
data = await resp.json()
|
||||||
|
download_url = data.get("url") or data.get("download_url")
|
||||||
|
if not download_url:
|
||||||
|
raise RuntimeError("视频下载失败:响应中未找到下载链接")
|
||||||
|
async with session.get(download_url) as dl_resp:
|
||||||
|
if dl_resp.status != 200:
|
||||||
|
raise RuntimeError(f"从下载链接获取视频失败 ({dl_resp.status})")
|
||||||
|
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||||
|
with open(save_path, "wb") as f:
|
||||||
|
async for chunk in dl_resp.content.iter_chunked(8192):
|
||||||
|
f.write(chunk)
|
||||||
|
else:
|
||||||
|
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||||
|
with open(save_path, "wb") as f:
|
||||||
|
async for chunk in resp.content.iter_chunked(8192):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
if on_stage:
|
||||||
|
on_stage("done")
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
|||||||
+67
-38
@@ -142,7 +142,7 @@ def _validate_image(tensor, label: str = "图片") -> None:
|
|||||||
|
|
||||||
|
|
||||||
class KlingVideo:
|
class KlingVideo:
|
||||||
"""Kling 3.0 视频生成节点(支持多镜头)"""
|
"""Kling 视频生成节点(支持多镜头)"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -150,6 +150,7 @@ class KlingVideo:
|
|||||||
"required": {
|
"required": {
|
||||||
"提示词": ("STRING", {"multiline": True, "default": ""}),
|
"提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
"反向提示词": ("STRING", {"multiline": True, "default": ""}),
|
"反向提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
|
"模型版本": (["v3", "v2-6"], {"default": "v3"}),
|
||||||
"时长": ([5, 10, 15],),
|
"时长": ([5, 10, 15],),
|
||||||
"分辨率": (["1080p", "720p"],),
|
"分辨率": (["1080p", "720p"],),
|
||||||
"宽高比": (["智能", "16:9", "9:16", "1:1"], {"default": "智能"}),
|
"宽高比": (["智能", "16:9", "9:16", "1:1"], {"default": "智能"}),
|
||||||
@@ -159,17 +160,17 @@ class KlingVideo:
|
|||||||
"optional": {
|
"optional": {
|
||||||
"起始帧": ("IMAGE",),
|
"起始帧": ("IMAGE",),
|
||||||
"镜头1_提示词": ("STRING", {"multiline": True, "default": ""}),
|
"镜头1_提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
"镜头1_时长": ("INT", {"default": 5, "min": 1, "max": 15, "step": 1}),
|
"镜头1_时长": ("STRING", {"default": "5"}),
|
||||||
"镜头2_提示词": ("STRING", {"multiline": True, "default": ""}),
|
"镜头2_提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
"镜头2_时长": ("INT", {"default": 5, "min": 1, "max": 15, "step": 1}),
|
"镜头2_时长": ("STRING", {"default": "5"}),
|
||||||
"镜头3_提示词": ("STRING", {"multiline": True, "default": ""}),
|
"镜头3_提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
"镜头3_时长": ("INT", {"default": 5, "min": 1, "max": 15, "step": 1}),
|
"镜头3_时长": ("STRING", {"default": "5"}),
|
||||||
"镜头4_提示词": ("STRING", {"multiline": True, "default": ""}),
|
"镜头4_提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
"镜头4_时长": ("INT", {"default": 5, "min": 1, "max": 15, "step": 1}),
|
"镜头4_时长": ("STRING", {"default": "5"}),
|
||||||
"镜头5_提示词": ("STRING", {"multiline": True, "default": ""}),
|
"镜头5_提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
"镜头5_时长": ("INT", {"default": 5, "min": 1, "max": 15, "step": 1}),
|
"镜头5_时长": ("STRING", {"default": "5"}),
|
||||||
"镜头6_提示词": ("STRING", {"multiline": True, "default": ""}),
|
"镜头6_提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
"镜头6_时长": ("INT", {"default": 5, "min": 1, "max": 15, "step": 1}),
|
"镜头6_时长": ("STRING", {"default": "5"}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +183,7 @@ class KlingVideo:
|
|||||||
"""生成视频(支持多镜头)"""
|
"""生成视频(支持多镜头)"""
|
||||||
prompt = kwargs["提示词"]
|
prompt = kwargs["提示词"]
|
||||||
negative_prompt = kwargs["反向提示词"]
|
negative_prompt = kwargs["反向提示词"]
|
||||||
|
model_ver = kwargs.get("模型版本", "v3")
|
||||||
duration = kwargs["时长"]
|
duration = kwargs["时长"]
|
||||||
resolution = kwargs["分辨率"]
|
resolution = kwargs["分辨率"]
|
||||||
aspect_ratio = kwargs["宽高比"]
|
aspect_ratio = kwargs["宽高比"]
|
||||||
@@ -192,12 +194,27 @@ class KlingVideo:
|
|||||||
mode = "pro" if resolution == "1080p" else "std"
|
mode = "pro" if resolution == "1080p" else "std"
|
||||||
voice = "voice" if generate_audio == "打开" else "novoice"
|
voice = "voice" if generate_audio == "打开" else "novoice"
|
||||||
|
|
||||||
|
# ── v2-6 模型约束校验 ──────────────────────────────────────────
|
||||||
|
if model_ver == "v2-6":
|
||||||
|
if duration == 15:
|
||||||
|
raise ValueError(
|
||||||
|
"v2-6 模型不支持 15s 时长,请选择 5s 或 10s。"
|
||||||
|
)
|
||||||
|
if mode == "std" and voice == "voice":
|
||||||
|
raise ValueError(
|
||||||
|
"v2-6 模型的标准画质(720p)不支持生成音频,请关闭生成音频或切换至 1080p。"
|
||||||
|
)
|
||||||
|
|
||||||
# ── 多镜头检测 ────────────────────────────────────────────────
|
# ── 多镜头检测 ────────────────────────────────────────────────
|
||||||
multi_prompt_list = []
|
multi_prompt_list = []
|
||||||
for i in range(1, 7):
|
for i in range(1, 7):
|
||||||
sb_prompt = kwargs.get(f"镜头{i}_提示词", "").strip()
|
sb_prompt = kwargs.get(f"镜头{i}_提示词", "").strip()
|
||||||
if sb_prompt:
|
if sb_prompt:
|
||||||
sb_duration = kwargs.get(f"镜头{i}_时长", 5)
|
raw_dur = kwargs.get(f"镜头{i}_时长", "5")
|
||||||
|
try:
|
||||||
|
sb_duration = int(str(raw_dur).strip()) if str(raw_dur).strip() else 5
|
||||||
|
except ValueError:
|
||||||
|
sb_duration = 5
|
||||||
multi_prompt_list.append({
|
multi_prompt_list.append({
|
||||||
"index": i,
|
"index": i,
|
||||||
"prompt": sb_prompt,
|
"prompt": sb_prompt,
|
||||||
@@ -219,7 +236,7 @@ class KlingVideo:
|
|||||||
|
|
||||||
# ── 构建模型名 & 请求体 ───────────────────────────────────────
|
# ── 构建模型名 & 请求体 ───────────────────────────────────────
|
||||||
import json, base64, copy
|
import json, base64, copy
|
||||||
model_name = f"kling-v3-{mode}-{duration}s-{voice}"
|
model_name = f"kling-{model_ver}-{mode}-{duration}s-{voice}"
|
||||||
|
|
||||||
body = {
|
body = {
|
||||||
"model": model_name,
|
"model": model_name,
|
||||||
@@ -312,7 +329,7 @@ class KlingVideo:
|
|||||||
|
|
||||||
|
|
||||||
class KlingFirstLastFrame:
|
class KlingFirstLastFrame:
|
||||||
"""Kling 3.0 首尾帧到视频节点"""
|
"""Kling 首尾帧到视频节点"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -321,10 +338,10 @@ class KlingFirstLastFrame:
|
|||||||
"首帧": ("IMAGE",),
|
"首帧": ("IMAGE",),
|
||||||
"尾帧": ("IMAGE",),
|
"尾帧": ("IMAGE",),
|
||||||
"提示词": ("STRING", {"multiline": True, "default": ""}),
|
"提示词": ("STRING", {"multiline": True, "default": ""}),
|
||||||
|
"模型": (["v3", "v2-6"], {"default": "v3"}),
|
||||||
|
"分辨率": (["1080p", "720p"],),
|
||||||
"时长": ([5, 10, 15],),
|
"时长": ([5, 10, 15],),
|
||||||
"生成音频": (["打开", "关闭"], {"default": "打开"}),
|
"生成音频": (["打开", "关闭"], {"default": "打开"}),
|
||||||
"模型": (["v3"],),
|
|
||||||
"分辨率": (["1080p", "720p"],),
|
|
||||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffff}),
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffff}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,7 +358,7 @@ class KlingFirstLastFrame:
|
|||||||
duration = kwargs["时长"]
|
duration = kwargs["时长"]
|
||||||
generate_audio = kwargs["生成音频"]
|
generate_audio = kwargs["生成音频"]
|
||||||
model_base = kwargs["模型"]
|
model_base = kwargs["模型"]
|
||||||
model_base = "kling-" + model_base # v3 → kling-v3(后端值还原)
|
model_base = "kling-" + model_base # v3/v2-6 → kling-v3/kling-v2-6(后端值还原)
|
||||||
resolution = kwargs["分辨率"]
|
resolution = kwargs["分辨率"]
|
||||||
seed = kwargs.get("seed", 0) # noqa: F841 — 触发 ComfyUI 缓存刷新
|
seed = kwargs.get("seed", 0) # noqa: F841 — 触发 ComfyUI 缓存刷新
|
||||||
|
|
||||||
@@ -351,9 +368,22 @@ class KlingFirstLastFrame:
|
|||||||
if duration not in (5, 10, 15):
|
if duration not in (5, 10, 15):
|
||||||
raise ValueError(f"时长仅支持 5、10、15 秒,当前值为 {duration},请重新选择。")
|
raise ValueError(f"时长仅支持 5、10、15 秒,当前值为 {duration},请重新选择。")
|
||||||
|
|
||||||
# 拼接模型名:kling-v3-{mode}-{dur}s-{voice}
|
# 拼接模型名:kling-{ver}-{mode}-{dur}s-{voice}
|
||||||
mode = "pro" if resolution == "1080p" else "std"
|
mode = "pro" if resolution == "1080p" else "std"
|
||||||
voice = "voice" if generate_audio == "打开" else "novoice"
|
voice = "voice" if generate_audio == "打开" else "novoice"
|
||||||
|
|
||||||
|
# ── v2-6 模型约束校验 ──────────────────────────────────────────
|
||||||
|
model_ver = kwargs["模型"] # "v3" or "v2-6"
|
||||||
|
if model_ver == "v2-6":
|
||||||
|
if duration == 15:
|
||||||
|
raise ValueError(
|
||||||
|
"v2-6 模型不支持 15s 时长,请选择 5s 或 10s。"
|
||||||
|
)
|
||||||
|
if mode == "std" and voice == "voice":
|
||||||
|
raise ValueError(
|
||||||
|
"v2-6 模型的标准画质(720p)不支持生成音频,请关闭生成音频或切换至 1080p。"
|
||||||
|
)
|
||||||
|
|
||||||
model_name = f"{model_base}-{mode}-{duration}s-{voice}"
|
model_name = f"{model_base}-{mode}-{duration}s-{voice}"
|
||||||
|
|
||||||
# 图片校验 & 转 base64
|
# 图片校验 & 转 base64
|
||||||
@@ -459,10 +489,11 @@ class KlingMotionControlTest:
|
|||||||
"参考视频": ("VIDEO",),
|
"参考视频": ("VIDEO",),
|
||||||
},
|
},
|
||||||
"optional": {
|
"optional": {
|
||||||
"保留原声": ("BOOLEAN", {"default": True}),
|
"模型": (["v3", "v2-6"], {"default": "v3"}),
|
||||||
|
"分辨率": (["1080p", "720p"],),
|
||||||
|
"时长": ([5, 10, 15], {"default": 5}),
|
||||||
"人物朝向": (["video", "image"],),
|
"人物朝向": (["video", "image"],),
|
||||||
"画质模式": (["专家", "标准"],),
|
"保留原声": (["打开", "关闭"], {"default": "打开"}),
|
||||||
"模型版本": (["v3"],),
|
|
||||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffff}),
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffff}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -473,18 +504,19 @@ class KlingMotionControlTest:
|
|||||||
CATEGORY = "comfyui_o1key/Kling"
|
CATEGORY = "comfyui_o1key/Kling"
|
||||||
|
|
||||||
async def generate(self, **kwargs):
|
async def generate(self, **kwargs):
|
||||||
"""动作控制(测试):VIDEO 类型参考视频 + 图片人物动作迁移"""
|
"""动作控制:VIDEO 类型参考视频 + 图片人物动作迁移(走 new API 三段式)"""
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
prompt = kwargs["提示词"]
|
prompt = kwargs["提示词"]
|
||||||
reference_image = kwargs["参考图片"]
|
reference_image = kwargs["参考图片"]
|
||||||
reference_video = kwargs["参考视频"]
|
reference_video = kwargs["参考视频"]
|
||||||
keep_original_sound = kwargs.get("保留原声", True)
|
keep_original_sound = kwargs.get("保留原声", "打开")
|
||||||
character_orientation = kwargs.get("人物朝向", "video")
|
character_orientation = kwargs.get("人物朝向", "video")
|
||||||
mode = kwargs.get("画质模式", "专家")
|
mode = kwargs.get("分辨率", "1080p")
|
||||||
mode = "pro" if mode == "专家" else "std" # 映射为 API 参数值
|
duration = kwargs.get("时长", 5)
|
||||||
model = kwargs.get("模型版本", "v3")
|
mode_api = "pro" if mode == "1080p" else "std" # 映射为 API 参数值
|
||||||
model = "kling-" + model # v3 → kling-v3(后端值还原)
|
model = kwargs.get("模型", "v3")
|
||||||
|
model_name = f"kling-{model}-motion-{mode_api}-{duration}s"
|
||||||
seed = kwargs.get("seed", 0) # noqa: F841 — 触发 ComfyUI 缓存刷新
|
seed = kwargs.get("seed", 0) # noqa: F841 — 触发 ComfyUI 缓存刷新
|
||||||
|
|
||||||
# ── 校验提示词 ────────────────────────────────────────────────
|
# ── 校验提示词 ────────────────────────────────────────────────
|
||||||
@@ -495,7 +527,6 @@ class KlingMotionControlTest:
|
|||||||
image_b64 = _tensor_to_base64(reference_image)
|
image_b64 = _tensor_to_base64(reference_image)
|
||||||
|
|
||||||
# ── 从 VIDEO 对象获取本地文件路径并读取 ───────────────────────
|
# ── 从 VIDEO 对象获取本地文件路径并读取 ───────────────────────
|
||||||
# ComfyUI VIDEO 对象有 .source_path 或通过 VideoFromFile 构造
|
|
||||||
video_path = None
|
video_path = None
|
||||||
if hasattr(reference_video, "source_path"):
|
if hasattr(reference_video, "source_path"):
|
||||||
video_path = reference_video.source_path
|
video_path = reference_video.source_path
|
||||||
@@ -537,7 +568,6 @@ class KlingMotionControlTest:
|
|||||||
f"参考视频时长须在 3~10 秒之间,当前为 {duration_sec:.1f}s。"
|
f"参考视频时长须在 3~10 秒之间,当前为 {duration_sec:.1f}s。"
|
||||||
)
|
)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# ffprobe 不可用时跳过时长校验,但打印提示
|
|
||||||
print("[动作控制] 警告:ffprobe 未找到,跳过视频时长校验。")
|
print("[动作控制] 警告:ffprobe 未找到,跳过视频时长校验。")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise
|
raise
|
||||||
@@ -548,21 +578,21 @@ class KlingMotionControlTest:
|
|||||||
with open(video_path, "rb") as f:
|
with open(video_path, "rb") as f:
|
||||||
video_b64 = base64.b64encode(f.read()).decode("utf-8")
|
video_b64 = base64.b64encode(f.read()).decode("utf-8")
|
||||||
|
|
||||||
# ── 构建请求体 ────────────────────────────────────────────────
|
# ── 构建请求体(new API 格式)─────────────────────────────────
|
||||||
body = {
|
body = {
|
||||||
|
"model": model_name,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
|
"image_url": image_b64,
|
||||||
|
"video_url": video_b64,
|
||||||
"character_orientation": character_orientation,
|
"character_orientation": character_orientation,
|
||||||
"mode": mode,
|
"mode": mode_api,
|
||||||
"model": model,
|
"keep_original_sound": "yes" if keep_original_sound == "打开" else "no",
|
||||||
"keep_original_sound": "yes" if keep_original_sound else "no",
|
|
||||||
"image": image_b64,
|
|
||||||
"video": video_b64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 保存路径 ──────────────────────────────────────────────────
|
# ── 保存路径 ──────────────────────────────────────────────────
|
||||||
video_dir = _get_video_output_dir()
|
video_dir = _get_video_output_dir()
|
||||||
counter = _get_next_counter(video_dir, "kling_motion_test")
|
counter = _get_next_counter(video_dir, "kling_motion")
|
||||||
save_path = os.path.join(video_dir, f"kling_motion_test_{counter:05d}.mp4")
|
save_path = os.path.join(video_dir, f"kling_motion_{counter:05d}.mp4")
|
||||||
|
|
||||||
client = KlingClient()
|
client = KlingClient()
|
||||||
|
|
||||||
@@ -592,8 +622,7 @@ class KlingMotionControlTest:
|
|||||||
if pbar: pbar.update_absolute(mapped, 100)
|
if pbar: pbar.update_absolute(mapped, 100)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result_path = await client.generate_async(
|
result_path = await client.motion_control_async(
|
||||||
endpoint_type="motion_control",
|
|
||||||
body=body,
|
body=body,
|
||||||
save_path=save_path,
|
save_path=save_path,
|
||||||
on_stage=on_stage,
|
on_stage=on_stage,
|
||||||
@@ -729,8 +758,8 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"KlingVideo": "自研模型 3.0 视频",
|
"KlingVideo": "文/图生视频 自研模型",
|
||||||
"KlingFirstLastFrame": "自研模型 3.0 首尾帧到视频",
|
"KlingFirstLastFrame": "首尾帧生视频 自研模型",
|
||||||
"KlingMotionControlTest": "自研模型 动作控制(测试)",
|
"KlingMotionControlTest": "动作控制 自研模型",
|
||||||
"AspectRatioPreset": "图片宽高比预设",
|
"AspectRatioPreset": "图片宽高比预设",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user