Add files via upload

This commit is contained in:
huyl3
2025-12-26 16:33:00 +07:00
committed by GitHub
parent 4f666124d0
commit 83b0e203d6
5 changed files with 259 additions and 203 deletions
+5 -2
View File
@@ -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)
-7
View File
@@ -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",
+61
View File
@@ -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",
}
+2 -3
View File
@@ -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)
+191 -191
View File
@@ -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"
}