Update image and video workflow nodes

This commit is contained in:
o1key
2026-05-28 16:44:30 +08:00
parent 3f0f4099fb
commit 5d9aff9ca7
21 changed files with 3507 additions and 381 deletions
+46 -40
View File
@@ -20,11 +20,12 @@ _seeder_filter = lambda record: not any(
)
logging.getLogger().addFilter(_seeder_filter)
from .nodes import NanoBananaPro, BatchNanoBananaPro, GoogleGemini, LoadFile, ImageStitchPro, BatchCleanMetadata, VideoPreview, GoogleVeo, FluxImageEdit, UniversalLLMChat, KlingVideo, KlingFirstLastFrame, KlingMotionControlTest, AspectRatioPreset, BatchImagesO1key, Seedance, SeedanceMultiModal, StreamPreview, DoubaoImage, O1keyGPTImage, O1keyGrokImage, KVideoFirstLast, KVideoImage2Video
from .nodes import NanoBananaPro, BatchNanoBananaPro, GoogleGemini, LoadFile, ImageStitchPro, BatchCleanMetadata, VideoPreview, GoogleVeo, FluxImageEdit, UniversalLLMChat, KlingVideo, KlingFirstLastFrame, KlingMotionControlTest, AspectRatioPreset, BatchImagesO1key, Seedance, SeedanceMultiModal, StreamPreview, DoubaoImage, O1keyGPTImage, O1keyGPTImageBatch, O1keyGrokImage, KVideoFirstLast, KVideoImage2Video
from .nodes import K3Video, K3VideoFirstLast, K3MotionControl, K3MotionVideoCheck, NanoBananaV2, NanoBananaV2Batch, SaveImageFormat
from .nodes import O1keySavePSD
from .nodes import O1keyRemoveBackground
from .nodes import O1keyColorRemoveBG
from .nodes import O1keyGridSplitter
# 报错弹框友好文案(不修改原节点代码,仅在外层统一处理)
_MSG_TIMEOUT = "API 请求超时,请稍后重试或检查网络。"
@@ -85,6 +86,7 @@ NODE_CLASS_MAPPINGS = {
"StreamPreview": StreamPreview,
"DoubaoImage": DoubaoImage,
"O1keyGPTImage": O1keyGPTImage,
"O1keyGPTImageBatch": O1keyGPTImageBatch,
"O1keyGrokImage": O1keyGrokImage,
"KVideoFirstLast": KVideoFirstLast,
"KVideoImage2Video": KVideoImage2Video,
@@ -98,6 +100,7 @@ NODE_CLASS_MAPPINGS = {
"O1keySavePSD": O1keySavePSD,
"O1keyRemoveBackground": O1keyRemoveBackground,
"O1keyColorRemoveBG": O1keyColorRemoveBG,
"O1keyGridSplitter": O1keyGridSplitter,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -123,6 +126,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"StreamPreview": "流式文本预览",
"DoubaoImage": "豆包生图",
"O1keyGPTImage": "o1key GPT Image",
"O1keyGPTImageBatch": "o1key GPT Image(批量)",
"O1keyGrokImage": "Grok Image",
"KVideoFirstLast": "K26 图生视频(首尾帧)",
"KVideoImage2Video": "K26 图生视频",
@@ -136,6 +140,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"O1keySavePSD": "保存 PSD(分层)",
"O1keyRemoveBackground": "去背景(rembg",
"O1keyColorRemoveBG": "颜色去背景",
"O1keyGridSplitter": "合并图智能切割",
}
WEB_DIRECTORY = "./web"
@@ -149,6 +154,35 @@ try:
import folder_paths
from .utils.config import CONFIG_FILE, load_config, NETWORK_ROUTES
def _get_o1key_server_port():
try:
import comfy.cli_args as _cli_args
args = getattr(_cli_args, "args", None)
port = getattr(args, "port", None) if args else None
port = port or getattr(_cli_args, "server_port", None) or getattr(_cli_args, "port", None)
if port is not None:
return str(int(port))
except Exception:
pass
try:
import sys as _sys
for idx, arg in enumerate(_sys.argv):
if arg in ("--port", "--listen-port") and idx + 1 < len(_sys.argv):
return str(int(_sys.argv[idx + 1]))
for prefix in ("--port=", "--listen-port="):
if arg.startswith(prefix):
return str(int(arg.split("=", 1)[1]))
except Exception:
pass
return "8188"
def _get_o1key_history_meta_file(output_dir):
import os as _os_history
return _os_history.path.join(
output_dir,
f".o1key_history_{_get_o1key_server_port()}.json",
)
@PromptServer.instance.routes.get("/o1key/input_dir")
async def get_input_dir(request):
import os
@@ -220,11 +254,11 @@ try:
@PromptServer.instance.routes.get("/o1key/output_history")
async def get_output_history(request):
"""读取 output 目录文件,按执行分组返回 /api/jobs 兼容格式"""
import os, uuid, json as _json
import os, json as _json
limit = int(request.query.get("limit", "200"))
offset = int(request.query.get("offset", "0"))
output_dir = os.path.abspath(folder_paths.get_output_directory())
meta_file = os.path.join(output_dir, ".o1key_history.json")
meta_file = _get_o1key_history_meta_file(output_dir)
meta = {}
if os.path.isfile(meta_file):
try:
@@ -235,7 +269,7 @@ try:
supported_ext = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.mp4', '.webm'}
# 收集所有文件并按 workflow_id 分组
all_files = []
for fname in os.listdir(output_dir):
for fname in meta.keys():
ext = os.path.splitext(fname)[1].lower()
if ext not in supported_ext:
continue
@@ -247,14 +281,11 @@ try:
all_files.append({"name": fname, "mtime": mtime, "media": media})
# 按 workflow_id 分组(同一次执行合并为一个 job)
groups = {}
ungrouped = []
for f in all_files:
m = meta.get(f["name"], {})
wid = m.get("workflow_id")
if wid:
groups.setdefault(wid, []).append((f, m))
else:
ungrouped.append((f, {}))
# 构建 job 列表
jobs = []
for wid, items in groups.items():
@@ -280,27 +311,6 @@ try:
"execution_error": None,
"workflow_id": wid,
})
# 无元数据的文件各自作为独立 job
for f, m in ungrouped:
mtime_ms = int(f["mtime"] * 1000)
job_id = str(uuid.uuid5(uuid.NAMESPACE_URL, f["name"]))
jobs.append({
"id": job_id,
"status": "completed",
"create_time": mtime_ms,
"execution_start_time": mtime_ms,
"execution_end_time": mtime_ms,
"preview_output": {
"filename": f["name"],
"subfolder": "",
"type": "output",
"nodeId": "0",
"mediaType": f["media"],
},
"outputs_count": 1,
"execution_error": None,
"workflow_id": None,
})
# 按时间倒序排列,分页
jobs.sort(key=lambda x: x["create_time"], reverse=True)
total = len(jobs)
@@ -349,11 +359,11 @@ try:
@PromptServer.instance.routes.get("/o1key/job_detail/{job_id}")
async def get_job_detail(request):
"""根据 job_id (workflow_id 或 uuid5) 返回含工作流的 job 详情"""
import os, uuid, struct, json as _json
"""根据 job_id 返回当前端口持久化历史中的 job 详情"""
import os, struct, json as _json
job_id = request.match_info["job_id"]
output_dir = os.path.abspath(folder_paths.get_output_directory())
meta_file = os.path.join(output_dir, ".o1key_history.json")
meta_file = _get_o1key_history_meta_file(output_dir)
meta = {}
if os.path.isfile(meta_file):
try:
@@ -362,17 +372,15 @@ try:
except Exception:
pass
supported_ext = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.mp4', '.webm'}
# 查找属于该 job 的所有文件(按 workflow_id 或 uuid5 匹配)
# 只在当前端口的持久化记录中查找该 job 的文件
matched_files = []
for fname in os.listdir(output_dir):
for fname in meta.keys():
ext = os.path.splitext(fname)[1].lower()
if ext not in supported_ext:
continue
m = meta.get(fname, {})
if m.get("workflow_id") == job_id:
matched_files.append(fname)
elif str(uuid.uuid5(uuid.NAMESPACE_URL, fname)) == job_id:
matched_files.append(fname)
if not matched_files:
return web.json_response({"error": "not found"}, status=404)
# 用最新文件作为代表
@@ -444,13 +452,13 @@ try:
@PromptServer.instance.routes.post("/o1key/delete_history")
async def delete_history_item(request):
"""删除持久化历史记录及对应的输出文件"""
import os, uuid, json as _json
import os, json as _json
body = await request.json()
job_ids = body.get("delete", [])
if not job_ids:
return web.json_response({"success": False, "error": "missing ids"}, status=400)
output_dir = os.path.abspath(folder_paths.get_output_directory())
meta_file = os.path.join(output_dir, ".o1key_history.json")
meta_file = _get_o1key_history_meta_file(output_dir)
meta = {}
if os.path.isfile(meta_file):
try:
@@ -464,8 +472,6 @@ try:
for fname, m in list(meta.items()):
if m.get("workflow_id") == job_id:
files_to_remove.append(fname)
elif str(uuid.uuid5(uuid.NAMESPACE_URL, fname)) == job_id:
files_to_remove.append(fname)
for fname in files_to_remove:
meta.pop(fname, None)
fpath = os.path.join(output_dir, fname)
@@ -574,7 +580,7 @@ try:
end_time = _time.time()
start_time = tracker["start"]
output_dir = _os.path.abspath(folder_paths.get_output_directory())
meta_file = _os.path.join(output_dir, ".o1key_history.json")
meta_file = _get_o1key_history_meta_file(output_dir)
meta = {}
if _os.path.isfile(meta_file):
try: