VidiaVideoSaver: watermark + ffmpeg-mux + R2 upload + backend notify. V3 API (io.ComfyNode, ComfyExtension, comfy_entrypoint). not_idempotent=True forces re-execution (fixes upload/callback caching bug). Remote log shipping opt-in via VIDIA_REMOTE_LOGGING env. Env-only config (no hardcoded secrets).
124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
"""
|
|
Core utilities for the Vidia custom ComfyUI node.
|
|
Provides ffmpeg path detection, tensor conversion, and logging.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
import requests
|
|
import torch
|
|
|
|
from .config import VIDIA_API_ENDPOINT, VIDIA_REMOTE_LOGGING
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ffmpeg path detection
|
|
# ---------------------------------------------------------------------------
|
|
ffmpeg_path = "ffmpeg"
|
|
_paths_to_try = [
|
|
"ffmpeg",
|
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "ffmpeg", "ffmpeg"),
|
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "ffmpeg", "bin", "ffmpeg"),
|
|
]
|
|
|
|
for _path in _paths_to_try:
|
|
try:
|
|
if sys.platform == "win32":
|
|
_path += ".exe"
|
|
if os.path.isfile(_path) or os.system(f"which {_path} > /dev/null 2>&1") == 0:
|
|
ffmpeg_path = _path
|
|
break
|
|
except Exception as e:
|
|
print(f"[VidiaOpenStudioNode] Error checking ffmpeg path {_path}: {e}")
|
|
|
|
if ffmpeg_path == "ffmpeg" and sys.platform == "win32" and not os.system("where ffmpeg > nul 2>&1") == 0:
|
|
ffmpeg_path = None
|
|
elif ffmpeg_path == "ffmpeg" and sys.platform != "win32" and os.system("which ffmpeg > /dev/null 2>&1") != 0:
|
|
ffmpeg_path = None
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Logging
|
|
# ---------------------------------------------------------------------------
|
|
logger = logging.getLogger("VidiaOpenStudioNode")
|
|
logger.setLevel(logging.INFO)
|
|
if not logger.handlers:
|
|
_handler = logging.StreamHandler()
|
|
_handler.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s"))
|
|
logger.addHandler(_handler)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tensor utilities
|
|
# ---------------------------------------------------------------------------
|
|
def tensor_to_bytes(frame, bits=8):
|
|
"""Convert a tensor frame to a numpy byte array.
|
|
|
|
Args:
|
|
frame: Tensor of shape [H, W, C] with values in [0, 1].
|
|
bits: 8 for uint8 output, 16 for int16 output.
|
|
|
|
Returns:
|
|
numpy ndarray of the frame pixel data.
|
|
"""
|
|
if frame.shape[2] == 4:
|
|
# RGBA -- drop alpha, return RGB uint8
|
|
return (frame[:, :, :3] * 255).round().to(torch.uint8).cpu().numpy()
|
|
|
|
if bits == 16:
|
|
return (frame * 65535).round().to(torch.int16).cpu().numpy()
|
|
else:
|
|
return (frame * 255).round().to(torch.uint8).cpu().numpy()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Video Processing Logger (sends logs to remote server with timeout)
|
|
# ---------------------------------------------------------------------------
|
|
class VideoProcessingLogger:
|
|
"""Enhanced logging for video processing with optional remote log shipping."""
|
|
|
|
def __init__(self, node_name):
|
|
self.node_name = node_name
|
|
self.logs = []
|
|
self.send_logs_to_server = VIDIA_REMOTE_LOGGING
|
|
|
|
def debug(self, msg):
|
|
log_msg = f"[{self.node_name}] DEBUG: {msg}"
|
|
print(log_msg)
|
|
self._ship_log(log_msg, "DEBUG")
|
|
self.logs.append(log_msg)
|
|
|
|
def info(self, msg):
|
|
log_msg = f"[{self.node_name}] INFO: {msg}"
|
|
print(log_msg)
|
|
self._ship_log(log_msg, "INFO")
|
|
self.logs.append(log_msg)
|
|
|
|
def warn(self, msg):
|
|
log_msg = f"[{self.node_name}] WARNING: {msg}"
|
|
print(log_msg)
|
|
self._ship_log(log_msg, "WARNING")
|
|
self.logs.append(log_msg)
|
|
|
|
def error(self, msg):
|
|
log_msg = f"[{self.node_name}] ERROR: {msg}"
|
|
print(log_msg, file=sys.stderr)
|
|
self._ship_log(log_msg, "ERROR")
|
|
self.logs.append(log_msg)
|
|
|
|
def get_logs(self):
|
|
return "\n".join(self.logs)
|
|
|
|
def _ship_log(self, msg, log_type="INFO"):
|
|
"""Send log to remote endpoint with a short timeout to avoid blocking."""
|
|
if not self.send_logs_to_server or not VIDIA_API_ENDPOINT:
|
|
return
|
|
try:
|
|
requests.post(
|
|
f"{VIDIA_API_ENDPOINT.rstrip('/')}/logging/log",
|
|
json={"log": {"message": msg, "context": "VIDIA_NODE_PROCESSING", "type": log_type}},
|
|
timeout=3,
|
|
)
|
|
except Exception:
|
|
# Silently ignore -- logging failures must never block video processing
|
|
pass
|