fix: 视频节点改用临时文件,避免与下游保存节点重复落盘

- K3_video.py / K3_video_firstlast.py / K3_motion_control.py
- K_video.py
- kling_video.py (KlingVideo / KlingFirstLastFrame / KlingMotionControlTest)
- seedance_video.py (Seedance / SeedanceMultiModal)

所有视频节点下载视频时改写到 tempfile.mkstemp() 临时路径,
最终落盘完全交给下游保存节点处理,不再重复写入 output/video/。
同步清理已无用的 _get_video_dir / _get_next_counter / import re 等辅助代码。
This commit is contained in:
Jony
2026-04-26 22:18:22 +08:00
parent 8299594646
commit c646b0d1d7
6 changed files with 28 additions and 190 deletions
+4 -27
View File
@@ -8,8 +8,8 @@ import asyncio
import io import io
import json import json
import os import os
import re
import struct import struct
import tempfile
import aiohttp import aiohttp
@@ -36,27 +36,6 @@ _POLL_MAX = 15
# ── 工具函数 ─────────────────────────────────────────────────────────────────── # ── 工具函数 ───────────────────────────────────────────────────────────────────
def _get_video_dir() -> str:
if _FOLDER_PATHS_OK:
base = folder_paths.get_output_directory()
else:
plugin = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base = os.path.join(os.path.dirname(os.path.dirname(plugin)), "output")
d = os.path.join(base, "video")
os.makedirs(d, exist_ok=True)
return d
def _next_counter(directory: str, prefix: str) -> int:
pattern = re.compile(rf"^{re.escape(prefix)}_(\d+)")
max_n = 0
if os.path.exists(directory):
for f in os.listdir(directory):
m = pattern.match(f)
if m:
max_n = max(max_n, int(m.group(1)))
return max_n + 1
# ── 视频时长检测(纯标准库,跨平台) ────────────────────────────────────────── # ── 视频时长检测(纯标准库,跨平台) ──────────────────────────────────────────
@@ -219,10 +198,8 @@ class K3MotionControl:
if prompt: if prompt:
body["prompt"] = prompt body["prompt"] = prompt
# ── 保存路径 ────────────────────────────────────────────────── # ── 保存路径(临时文件,避免与下游保存节点重复落盘)──────────────────
video_dir = _get_video_dir() tmp_fd, save_path = tempfile.mkstemp(suffix=".mp4", prefix="k3_motion_")
counter = _next_counter(video_dir, "k3_motion")
save_path = os.path.join(video_dir, f"k3_motion_{counter:05d}.mp4")
connector = aiohttp.TCPConnector(ssl=False, force_close=True) connector = aiohttp.TCPConnector(ssl=False, force_close=True)
async with aiohttp.ClientSession(connector=connector) as session: async with aiohttp.ClientSession(connector=connector) as session:
@@ -311,7 +288,7 @@ class K3MotionControl:
async with session.get(video_result_url, allow_redirects=True) as resp: async with session.get(video_result_url, allow_redirects=True) as resp:
if resp.status != 200: if resp.status != 200:
raise RuntimeError(f"视频下载失败 ({resp.status})") raise RuntimeError(f"视频下载失败 ({resp.status})")
os.makedirs(os.path.dirname(save_path), exist_ok=True) os.close(tmp_fd)
with open(save_path, "wb") as f: with open(save_path, "wb") as f:
async for chunk in resp.content.iter_chunked(8192): async for chunk in resp.content.iter_chunked(8192):
f.write(chunk) f.write(chunk)
+4 -28
View File
@@ -7,7 +7,7 @@ K3 图生视频 自研节点(图生视频 / 多镜头)
import asyncio import asyncio
import json import json
import os import os
import re import tempfile
import aiohttp import aiohttp
@@ -47,28 +47,6 @@ _POLL_MAX = 15
# ── 工具函数 ─────────────────────────────────────────────────────────────────── # ── 工具函数 ───────────────────────────────────────────────────────────────────
def _get_video_dir() -> str:
if _FOLDER_PATHS_OK:
base = folder_paths.get_output_directory()
else:
plugin = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base = os.path.join(os.path.dirname(os.path.dirname(plugin)), "output")
d = os.path.join(base, "video")
os.makedirs(d, exist_ok=True)
return d
def _next_counter(directory: str, prefix: str) -> int:
pattern = re.compile(rf"^{re.escape(prefix)}_(\d+)")
max_n = 0
if os.path.exists(directory):
for f in os.listdir(directory):
m = pattern.match(f)
if m:
max_n = max(max_n, int(m.group(1)))
return max_n + 1
def _image_to_base64(tensor) -> str: def _image_to_base64(tensor) -> str:
pil = tensor_to_pil(tensor) pil = tensor_to_pil(tensor)
return encode_image_to_base64(pil[0], format="PNG") return encode_image_to_base64(pil[0], format="PNG")
@@ -259,10 +237,8 @@ class K3Video:
def _progress(pct: int): def _progress(pct: int):
if pbar: pbar.update_absolute(5 + int(pct * 0.94), 100) if pbar: pbar.update_absolute(5 + int(pct * 0.94), 100)
# ── 保存路径 ────────────────────────────────────────────────── # ── 保存路径(临时文件,避免与下游保存节点重复落盘)──────────────────
video_dir = _get_video_dir() tmp_fd, save_path = tempfile.mkstemp(suffix=".mp4", prefix="k3_")
counter = _next_counter(video_dir, "k3")
save_path = os.path.join(video_dir, f"k3_{counter:05d}.mp4")
connector = aiohttp.TCPConnector(ssl=False, force_close=True) connector = aiohttp.TCPConnector(ssl=False, force_close=True)
async with aiohttp.ClientSession(connector=connector) as session: async with aiohttp.ClientSession(connector=connector) as session:
@@ -345,7 +321,7 @@ class K3Video:
async with session.get(video_url, allow_redirects=True) as resp: async with session.get(video_url, allow_redirects=True) as resp:
if resp.status != 200: if resp.status != 200:
raise RuntimeError(f"视频下载失败 ({resp.status})") raise RuntimeError(f"视频下载失败 ({resp.status})")
os.makedirs(os.path.dirname(save_path), exist_ok=True) os.close(tmp_fd)
with open(save_path, "wb") as f: with open(save_path, "wb") as f:
async for chunk in resp.content.iter_chunked(8192): async for chunk in resp.content.iter_chunked(8192):
f.write(chunk) f.write(chunk)
+4 -28
View File
@@ -6,7 +6,7 @@
import asyncio import asyncio
import json import json
import os import os
import re import tempfile
import aiohttp import aiohttp
@@ -36,28 +36,6 @@ _POLL_MAX = 15
# ── 工具函数 ─────────────────────────────────────────────────────────────────── # ── 工具函数 ───────────────────────────────────────────────────────────────────
def _get_video_dir() -> str:
if _FOLDER_PATHS_OK:
base = folder_paths.get_output_directory()
else:
plugin = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base = os.path.join(os.path.dirname(os.path.dirname(plugin)), "output")
d = os.path.join(base, "video")
os.makedirs(d, exist_ok=True)
return d
def _next_counter(directory: str, prefix: str) -> int:
pattern = re.compile(rf"^{re.escape(prefix)}_(\d+)")
max_n = 0
if os.path.exists(directory):
for f in os.listdir(directory):
m = pattern.match(f)
if m:
max_n = max(max_n, int(m.group(1)))
return max_n + 1
def _prepare_image_base64(tensor) -> str: def _prepare_image_base64(tensor) -> str:
"""转换并校验图片,不符合约束时自动等比缩放后返回 base64。""" """转换并校验图片,不符合约束时自动等比缩放后返回 base64。"""
import io import io
@@ -198,10 +176,8 @@ class K3VideoFirstLast:
def _progress(pct: int): def _progress(pct: int):
if pbar: pbar.update_absolute(5 + int(pct * 0.94), 100) if pbar: pbar.update_absolute(5 + int(pct * 0.94), 100)
# ── 保存路径 ────────────────────────────────────────────────── # ── 保存路径(临时文件,避免与下游保存节点重复落盘)──────────────────
video_dir = _get_video_dir() tmp_fd, save_path = tempfile.mkstemp(suffix=".mp4", prefix="k3fl_")
counter = _next_counter(video_dir, "k3fl")
save_path = os.path.join(video_dir, f"k3fl_{counter:05d}.mp4")
connector = aiohttp.TCPConnector(ssl=False, force_close=True) connector = aiohttp.TCPConnector(ssl=False, force_close=True)
async with aiohttp.ClientSession(connector=connector) as session: async with aiohttp.ClientSession(connector=connector) as session:
@@ -284,7 +260,7 @@ class K3VideoFirstLast:
async with session.get(video_url, allow_redirects=True) as resp: async with session.get(video_url, allow_redirects=True) as resp:
if resp.status != 200: if resp.status != 200:
raise RuntimeError(f"视频下载失败 ({resp.status})") raise RuntimeError(f"视频下载失败 ({resp.status})")
os.makedirs(os.path.dirname(save_path), exist_ok=True) os.close(tmp_fd)
with open(save_path, "wb") as f: with open(save_path, "wb") as f:
async for chunk in resp.content.iter_chunked(8192): async for chunk in resp.content.iter_chunked(8192):
f.write(chunk) f.write(chunk)
+4 -28
View File
@@ -5,7 +5,7 @@ K26 图生视频节点
import asyncio import asyncio
import json import json
import os import os
import re import tempfile
import aiohttp import aiohttp
@@ -30,28 +30,6 @@ _POLL_INIT = 3
_POLL_MAX = 15 _POLL_MAX = 15
def _get_video_dir() -> str:
if _FOLDER_PATHS_OK:
base = folder_paths.get_output_directory()
else:
plugin = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base = os.path.join(os.path.dirname(os.path.dirname(plugin)), "output")
d = os.path.join(base, "video")
os.makedirs(d, exist_ok=True)
return d
def _next_counter(directory: str, prefix: str) -> int:
pattern = re.compile(rf"^{re.escape(prefix)}_(\d+)")
max_n = 0
if os.path.exists(directory):
for f in os.listdir(directory):
m = pattern.match(f)
if m:
max_n = max(max_n, int(m.group(1)))
return max_n + 1
def _image_to_base64(tensor) -> str: def _image_to_base64(tensor) -> str:
pil = tensor_to_pil(tensor) pil = tensor_to_pil(tensor)
return encode_image_to_base64(pil[0], format="PNG") return encode_image_to_base64(pil[0], format="PNG")
@@ -129,10 +107,8 @@ class KVideo:
def _progress(pct: int): def _progress(pct: int):
if pbar: pbar.update_absolute(5 + int(pct * 0.94), 100) if pbar: pbar.update_absolute(5 + int(pct * 0.94), 100)
# ── 保存路径 ────────────────────────────────────────────────── # ── 保存路径(临时文件,避免与下游保存节点重复落盘)──────────────────
video_dir = _get_video_dir() tmp_fd, save_path = tempfile.mkstemp(suffix=".mp4", prefix="k26_")
counter = _next_counter(video_dir, "k26")
save_path = os.path.join(video_dir, f"k26_{counter:05d}.mp4")
connector = aiohttp.TCPConnector(ssl=False, force_close=True) connector = aiohttp.TCPConnector(ssl=False, force_close=True)
async with aiohttp.ClientSession(connector=connector) as session: async with aiohttp.ClientSession(connector=connector) as session:
@@ -216,7 +192,7 @@ class KVideo:
async with session.get(video_url, allow_redirects=True) as resp: async with session.get(video_url, allow_redirects=True) as resp:
if resp.status != 200: if resp.status != 200:
raise RuntimeError(f"视频下载失败 ({resp.status})") raise RuntimeError(f"视频下载失败 ({resp.status})")
os.makedirs(os.path.dirname(save_path), exist_ok=True) os.close(tmp_fd)
with open(save_path, "wb") as f: with open(save_path, "wb") as f:
async for chunk in resp.content.iter_chunked(8192): async for chunk in resp.content.iter_chunked(8192):
f.write(chunk) f.write(chunk)
+7 -42
View File
@@ -3,7 +3,7 @@ Kling 3.0 Video Nodes
""" """
import os import os
import re import tempfile
from ..clients.kling_client import KlingClient from ..clients.kling_client import KlingClient
from ..clients.gemini_client import GeminiAPIClient from ..clients.gemini_client import GeminiAPIClient
@@ -11,35 +11,6 @@ from ..utils.image_utils import tensor_to_pil, encode_image_to_base64
from comfy_api.latest import InputImpl from comfy_api.latest import InputImpl
try:
import folder_paths
FOLDER_PATHS_AVAILABLE = True
except ImportError:
FOLDER_PATHS_AVAILABLE = False
def _get_video_output_dir() -> str:
if FOLDER_PATHS_AVAILABLE:
base = folder_paths.get_output_directory()
else:
plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base = os.path.join(os.path.dirname(os.path.dirname(plugin_dir)), "output")
video_dir = os.path.join(base, "video")
os.makedirs(video_dir, exist_ok=True)
return video_dir
def _get_next_counter(directory: str, prefix: str) -> int:
if not os.path.exists(directory):
return 1
pattern = re.compile(rf"^{re.escape(prefix)}_(\d+)")
max_counter = 0
for f in os.listdir(directory):
m = pattern.match(f)
if m:
max_counter = max(max_counter, int(m.group(1)))
return max_counter + 1
def _tensor_to_base64(tensor) -> str: def _tensor_to_base64(tensor) -> str:
"""ComfyUI IMAGE tensor → base64 PNG 字符串""" """ComfyUI IMAGE tensor → base64 PNG 字符串"""
@@ -276,10 +247,8 @@ class KlingVideo:
body["metadata"] = {"aspect_ratio": aspect_ratio} body["metadata"] = {"aspect_ratio": aspect_ratio}
endpoint_type = "text2video" endpoint_type = "text2video"
# ── 保存路径 ────────────────────────────────────────────────── # ── 保存路径(临时文件,避免与下游保存节点重复落盘)──────────────────
video_dir = _get_video_output_dir() _, save_path = tempfile.mkstemp(suffix=".mp4", prefix="kling_")
counter = _get_next_counter(video_dir, "kling")
save_path = os.path.join(video_dir, f"kling_{counter:05d}.mp4")
client = KlingClient() client = KlingClient()
@@ -418,10 +387,8 @@ class KlingFirstLastFrame:
else: else:
body["prompt"] = prompt body["prompt"] = prompt
# 保存路径 # 保存路径(临时文件,避免与下游保存节点重复落盘)
video_dir = _get_video_output_dir() _, save_path = tempfile.mkstemp(suffix=".mp4", prefix="kling_")
counter = _get_next_counter(video_dir, "kling")
save_path = os.path.join(video_dir, f"kling_{counter:05d}.mp4")
client = KlingClient() client = KlingClient()
@@ -589,10 +556,8 @@ class KlingMotionControlTest:
"keep_original_sound": "yes" if keep_original_sound == "打开" else "no", "keep_original_sound": "yes" if keep_original_sound == "打开" else "no",
} }
# ── 保存路径 ────────────────────────────────────────────────── # ── 保存路径(临时文件,避免与下游保存节点重复落盘)──────────────────
video_dir = _get_video_output_dir() _, save_path = tempfile.mkstemp(suffix=".mp4", prefix="kling_motion_")
counter = _get_next_counter(video_dir, "kling_motion")
save_path = os.path.join(video_dir, f"kling_motion_{counter:05d}.mp4")
client = KlingClient() client = KlingClient()
+5 -37
View File
@@ -6,7 +6,7 @@ Seedance 视频生成节点
import io import io
import os import os
import re import tempfile
import aiohttp import aiohttp
import torch import torch
@@ -18,12 +18,6 @@ from ..utils.r2_uploader import upload_video, upload_audio
from comfy_api.latest import InputImpl from comfy_api.latest import InputImpl
try:
import folder_paths
FOLDER_PATHS_AVAILABLE = True
except ImportError:
FOLDER_PATHS_AVAILABLE = False
# ── 模型列表 ────────────────────────────────────────────────────────────────── # ── 模型列表 ──────────────────────────────────────────────────────────────────
@@ -43,29 +37,6 @@ def _supports_camera_fixed(model: str) -> bool:
# ── 工具函数 ────────────────────────────────────────────────────────────────── # ── 工具函数 ──────────────────────────────────────────────────────────────────
def _get_video_output_dir() -> str:
if FOLDER_PATHS_AVAILABLE:
base = folder_paths.get_output_directory()
else:
plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base = os.path.join(os.path.dirname(os.path.dirname(plugin_dir)), "output")
video_dir = os.path.join(base, "video")
os.makedirs(video_dir, exist_ok=True)
return video_dir
def _get_next_counter(directory: str, prefix: str) -> int:
if not os.path.exists(directory):
return 1
pattern = re.compile(rf"^{re.escape(prefix)}_(\d+)")
max_counter = 0
for f in os.listdir(directory):
m = pattern.match(f)
if m:
max_counter = max(max_counter, int(m.group(1)))
return max_counter + 1
def _tensor_to_base64_url(tensor) -> str: def _tensor_to_base64_url(tensor) -> str:
"""ComfyUI IMAGE tensor → data:image/png;base64,xxx""" """ComfyUI IMAGE tensor → data:image/png;base64,xxx"""
pil_images = tensor_to_pil(tensor) pil_images = tensor_to_pil(tensor)
@@ -267,9 +238,8 @@ class Seedance:
"metadata": metadata, "metadata": metadata,
} }
video_dir = _get_video_output_dir() # 保存路径(临时文件,避免与下游保存节点重复落盘)
counter = _get_next_counter(video_dir, file_prefix) _, save_path = tempfile.mkstemp(suffix=".mp4", prefix=f"{file_prefix}_")
save_path = os.path.join(video_dir, f"{file_prefix}_{counter:05d}.mp4")
client = SeedanceClient() client = SeedanceClient()
pbar = _make_pbar() pbar = _make_pbar()
@@ -440,10 +410,8 @@ class SeedanceMultiModal:
if first_image_url: if first_image_url:
body["image"] = first_image_url body["image"] = first_image_url
# ── 保存路径 ────────────────────────────────────────────────────── # ── 保存路径(临时文件,避免与下游保存节点重复落盘)──────────────────
video_dir = _get_video_output_dir() _, save_path = tempfile.mkstemp(suffix=".mp4", prefix="seedance_mm_")
counter = _get_next_counter(video_dir, "seedance_mm")
save_path = os.path.join(video_dir, f"seedance_mm_{counter:05d}.mp4")
client = SeedanceClient() client = SeedanceClient()
pbar = _make_pbar() pbar = _make_pbar()