diff --git a/__init__.py b/__init__.py index ca8130a..b906468 100644 --- a/__init__.py +++ b/__init__.py @@ -49,7 +49,6 @@ except Exception as e: _safe_merge("set_values_from_panel") -# giữ nguyên phần video_frame_guard (bọc try để không làm chết toàn bộ package) try: from .video_frame_guard import VHS_VideoFrameGuard, VHS_VideoPickMinFrames NODE_CLASS_MAPPINGS.update({ @@ -63,5 +62,9 @@ try: except Exception as e: print(f"[comfyui-sortlist] Skipped 'video_frame_guard' due to import error: {e}") -# QUAN TRỌNG: import value CUỐI để override node Set Value _safe_merge("value") +from .set_value_with_path import NODE_CLASS_MAPPINGS as _m, NODE_DISPLAY_NAME_MAPPINGS as _n +NODE_CLASS_MAPPINGS.update(_m) +NODE_DISPLAY_NAME_MAPPINGS.update(_n) + + diff --git a/set_value.js b/set_value.js index 1741b28..61f9afe 100644 --- a/set_value.js +++ b/set_value.js @@ -1,11 +1,4 @@ import { app } from "../../scripts/app.js"; - -/** - * UI-only extension for "Set Value" - * - KHÔNG rewrite prompt - * - KHÔNG patch queuePrompt/fetch - * - Không còn mask_text - */ app.registerExtension({ name: "vf9.SetValue.uiOnly", diff --git a/set_value_with_path.py b/set_value_with_path.py new file mode 100644 index 0000000..eaa05ed --- /dev/null +++ b/set_value_with_path.py @@ -0,0 +1,61 @@ +class SetValueWithPath: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + + "mask_prompt": ("STRING", { + "default": "", + "multiline": False, + "placeholder": "Tách mask", + }), + + "file_name": ("STRING", { + "default": "", + "multiline": False, + "placeholder": "Tên file", + }), + + "convert_path": ("STRING", { + "default": "", + "multiline": False, + "placeholder": "Đường dẫn video gốc", + }), + + "save_path": ("STRING", { + "default": "", + "multiline": False, + "placeholder": "Đường dẫn lưu video", + }), + + + "license_key": ("STRING", { + "default": "", + "multiline": False, + "placeholder": "License_key", + }), + } + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("mask_prompt", "file_name", "convert_path", "save_path", "license_key") + FUNCTION = "run" + CATEGORY = "comfyui-sortlist" + + def run(self, mask_prompt: str, file_name: str, convert_path: str, save_path: str, license_key: str): + return ( + mask_prompt or "", + file_name or "", + convert_path or "", + save_path or "", + license_key or "", + ) + + +NODE_CLASS_MAPPINGS = { + "Set Value With Path": SetValueWithPath, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Set Value With Path": "Set Value With Path", +} diff --git a/value.py b/value.py index db5228c..e964cbd 100644 --- a/value.py +++ b/value.py @@ -47,7 +47,7 @@ class VF9_SetValue: face=None, mask=None, ): - # batch inference + batch = 1 if isinstance(background, torch.Tensor) and background.ndim == 4: batch = int(background.shape[0]) @@ -62,7 +62,7 @@ class VF9_SetValue: def fb_mask(): return _white_mask(batch, 512, 512) - # check license (fail-closed) + try: res = check_license_shared(license_key) valid = (res.get("valid") is True) @@ -75,7 +75,6 @@ class VF9_SetValue: out_mask = mask if (enable_mask and mask is not None) else fb_mask() return (out_bg, out_face, out_mask) - # INVALID -> phá theo enable if enable_background: h, w = _rand_hw() out_bg = _black_image(batch, h, w) diff --git a/videos_nodes.py b/videos_nodes.py index e2120ff..0e9421b 100644 --- a/videos_nodes.py +++ b/videos_nodes.py @@ -1,191 +1,191 @@ -import os -import tempfile -import subprocess -import random -import requests -import hashlib -import socket -import time -import threading - -LICENSE_SERVER = "https://license.xgroup-service.com/api/validate" -LICENSE_HEARTBEAT = "https://license.xgroup-service.com/api/heartbeat" -LICENSE_TIMEOUT = 5 - -_LICENSE_CACHE = {} -_SESSION_CACHE = {} -_CACHE_TTL_SECONDS = 300 - -_HEARTBEAT_THREADS = {} -_HEARTBEAT_LOCK = threading.Lock() -_HEARTBEAT_INTERVAL_SECONDS = 20 - - -def _sha256_hex(s: str) -> str: - return hashlib.sha256(s.encode("utf-8")).hexdigest() - - -def get_user_id() -> str: - """User-based ID: - - Colab: dựa trên inode Google Drive mount (ổn định hơn reset runtime) - - Local: dựa trên home + hostname - """ - drive_path = "/content/drive/MyDrive" - try: - if os.path.exists(drive_path): - st = os.stat(drive_path) - raw = f"colab_drive:{st.st_dev}:{st.st_ino}" - return _sha256_hex(raw)[:16] - except Exception: - pass - - try: - home = os.path.expanduser("~") - raw = f"local:{home}:{socket.gethostname()}" - return _sha256_hex(raw)[:16] - except Exception: - return "local-user" - - -def get_or_create_session_id(license_key: str, user_id: str) -> str: - k = f"{license_key.strip()}:{user_id}" - if k in _SESSION_CACHE: - return _SESSION_CACHE[k] - sid = _sha256_hex(k + ":" + str(time.time()))[:16] - _SESSION_CACHE[k] = sid - return sid - - -def _ensure_heartbeat(cache_key: str, license_key: str, user_id: str, session_id: str): - """Start 1 daemon thread per cache_key to call /api/heartbeat every ~20s. - This keeps 'last_seen' updating smoothly in admin dashboard without forcing /api/validate. - """ - if not license_key or not user_id or not session_id: - return - - with _HEARTBEAT_LOCK: - if _HEARTBEAT_THREADS.get(cache_key): - return - _HEARTBEAT_THREADS[cache_key] = True - - def _loop(): - while True: - try: - requests.post( - LICENSE_HEARTBEAT, - json={"key": license_key, "user_id": user_id, "session_id": session_id}, - timeout=LICENSE_TIMEOUT - ) - except Exception: - pass - time.sleep(_HEARTBEAT_INTERVAL_SECONDS) - - t = threading.Thread(target=_loop, daemon=True) - t.start() - - -def check_license_shared(license_key: str) -> dict: - """Shared license check used by multiple nodes. - - Uses cached /api/validate (TTL=300s) - - If license is valid, starts heartbeat thread to keep session 'last_seen' fresh (10–30s) - """ - license_key = (license_key or "").strip() - if not license_key: - return {"valid": False} - - user_id = get_user_id() - cache_key = f"{license_key}:{user_id}" - session_id = get_or_create_session_id(license_key, user_id) - - now = time.time() - cached = _LICENSE_CACHE.get(cache_key) - if cached and (now - cached["ts"] <= _CACHE_TTL_SECONDS): - if cached.get("valid"): - _ensure_heartbeat(cache_key, license_key, user_id, session_id) - return {"valid": bool(cached.get("valid"))} - - try: - r = requests.post( - LICENSE_SERVER, - json={"key": license_key, "user_id": user_id, "session_id": session_id}, - timeout=LICENSE_TIMEOUT - ) - data = r.json() if r.content else {} - valid = bool(data.get("valid")) - except Exception: - valid = False - - _LICENSE_CACHE[cache_key] = {"valid": valid, "ts": now} - - if valid: - _ensure_heartbeat(cache_key, license_key, user_id, session_id) - - return {"valid": valid} - - -class VideoDirCombiner: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "video_list": ("STRING", {"multiline": True}), - "directory_path": ("STRING", {"default": ""}), - "output_filename": ("STRING", {"default": "output.mp4"}), - "license_key": ("STRING", {"default": ""}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("output_path",) - FUNCTION = "run" - CATEGORY = "video" - - def _merge_videos(self, video_paths, output_dir, output_name): - if not output_name.endswith(".mp4"): - output_name += ".mp4" - - output_dir = output_dir if output_dir else "." - os.makedirs(output_dir, exist_ok=True) - output_path = os.path.join(output_dir, output_name) - - with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: - for v in video_paths: - f.write(f"file '{v}'\n") - list_file = f.name - - subprocess.run( - [ - "ffmpeg", "-y", - "-f", "concat", - "-safe", "0", - "-i", list_file, - "-c", "copy", - output_path - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - - return (output_path,) - - def run(self, image, video_list, directory_path, output_filename, license_key): - videos = [v.strip() for v in video_list.split("\n") if v.strip()] - if not videos: - raise ValueError("video_list is empty") - - license_result = check_license_shared(license_key) - - if not license_result.get("valid"): - random.shuffle(videos) - - return self._merge_videos(videos, directory_path, output_filename) - - -NODE_CLASS_MAPPINGS = { - "video_dir_combiner": VideoDirCombiner -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "video_dir_combiner": "Video Dir Combiner" -} +import os +import tempfile +import subprocess +import random +import requests +import hashlib +import socket +import time +import threading + +LICENSE_SERVER = "https://license.xgroup-service.com/api/validate" +LICENSE_HEARTBEAT = "https://license.xgroup-service.com/api/heartbeat" +LICENSE_TIMEOUT = 5 + +_LICENSE_CACHE = {} +_SESSION_CACHE = {} +_CACHE_TTL_SECONDS = 300 + +_HEARTBEAT_THREADS = {} +_HEARTBEAT_LOCK = threading.Lock() +_HEARTBEAT_INTERVAL_SECONDS = 20 + + +def _sha256_hex(s: str) -> str: + return hashlib.sha256(s.encode("utf-8")).hexdigest() + + +def get_user_id() -> str: + """User-based ID: + - Colab: dựa trên inode Google Drive mount (ổn định hơn reset runtime) + - Local: dựa trên home + hostname + """ + drive_path = "/content/drive/MyDrive" + try: + if os.path.exists(drive_path): + st = os.stat(drive_path) + raw = f"colab_drive:{st.st_dev}:{st.st_ino}" + return _sha256_hex(raw)[:16] + except Exception: + pass + + try: + home = os.path.expanduser("~") + raw = f"local:{home}:{socket.gethostname()}" + return _sha256_hex(raw)[:16] + except Exception: + return "local-user" + + +def get_or_create_session_id(license_key: str, user_id: str) -> str: + k = f"{license_key.strip()}:{user_id}" + if k in _SESSION_CACHE: + return _SESSION_CACHE[k] + sid = _sha256_hex(k + ":" + str(time.time()))[:16] + _SESSION_CACHE[k] = sid + return sid + + +def _ensure_heartbeat(cache_key: str, license_key: str, user_id: str, session_id: str): + """Start 1 daemon thread per cache_key to call /api/heartbeat every ~20s. + This keeps 'last_seen' updating smoothly in admin dashboard without forcing /api/validate. + """ + if not license_key or not user_id or not session_id: + return + + with _HEARTBEAT_LOCK: + if _HEARTBEAT_THREADS.get(cache_key): + return + _HEARTBEAT_THREADS[cache_key] = True + + def _loop(): + while True: + try: + requests.post( + LICENSE_HEARTBEAT, + json={"key": license_key, "user_id": user_id, "session_id": session_id}, + timeout=LICENSE_TIMEOUT + ) + except Exception: + pass + time.sleep(_HEARTBEAT_INTERVAL_SECONDS) + + t = threading.Thread(target=_loop, daemon=True) + t.start() + + +def check_license_shared(license_key: str) -> dict: + """Shared license check used by multiple nodes. + - Uses cached /api/validate (TTL=300s) + - If license is valid, starts heartbeat thread to keep session 'last_seen' fresh (10–30s) + """ + license_key = (license_key or "").strip() + if not license_key: + return {"valid": False} + + user_id = get_user_id() + cache_key = f"{license_key}:{user_id}" + session_id = get_or_create_session_id(license_key, user_id) + + now = time.time() + cached = _LICENSE_CACHE.get(cache_key) + if cached and (now - cached["ts"] <= _CACHE_TTL_SECONDS): + if cached.get("valid"): + _ensure_heartbeat(cache_key, license_key, user_id, session_id) + return {"valid": bool(cached.get("valid"))} + + try: + r = requests.post( + LICENSE_SERVER, + json={"key": license_key, "user_id": user_id, "session_id": session_id}, + timeout=LICENSE_TIMEOUT + ) + data = r.json() if r.content else {} + valid = bool(data.get("valid")) + except Exception: + valid = False + + _LICENSE_CACHE[cache_key] = {"valid": valid, "ts": now} + + if valid: + _ensure_heartbeat(cache_key, license_key, user_id, session_id) + + return {"valid": valid} + + +class VideoDirCombiner: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "video_list": ("STRING", {"multiline": True}), + "directory_path": ("STRING", {"default": ""}), + "output_filename": ("STRING", {"default": "output.mp4"}), + "license_key": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("output_path",) + FUNCTION = "run" + CATEGORY = "video" + + def _merge_videos(self, video_paths, output_dir, output_name): + if not output_name.endswith(".mp4"): + output_name += ".mp4" + + output_dir = output_dir if output_dir else "." + os.makedirs(output_dir, exist_ok=True) + output_path = os.path.join(output_dir, output_name) + + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: + for v in video_paths: + f.write(f"file '{v}'\n") + list_file = f.name + + subprocess.run( + [ + "ffmpeg", "-y", + "-f", "concat", + "-safe", "0", + "-i", list_file, + "-c", "copy", + output_path + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + return (output_path,) + + def run(self, image, video_list, directory_path, output_filename, license_key): + videos = [v.strip() for v in video_list.split("\n") if v.strip()] + if not videos: + raise ValueError("video_list is empty") + + license_result = check_license_shared(license_key) + + if not license_result.get("valid"): + random.shuffle(videos) + + return self._merge_videos(videos, directory_path, output_filename) + + +NODE_CLASS_MAPPINGS = { + "video_dir_combiner": VideoDirCombiner +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "video_dir_combiner": "Video Dir Combiner" +}