Files
KOFiblto-ComfyUI-SaturnNodes/__init__.py
T

707 lines
35 KiB
Python

import os
import sys
import subprocess
from aiohttp import web
from server import PromptServer
__version__ = "2.7.0"
from .nodes.queue_control import (
setup_queue_control_routes,
pause_manager,
tray_manager,
persistent_queue_manager,
is_persistent_queue_enabled
)
from .nodes.tray_icon import is_tray_icon_enabled
from .nodes.assets_restore import (
assets_restore_manager,
is_assets_restore_enabled
)
from .nodes.lora_loader import (
FolderLoraLoader,
FolderLoraLoaderPretty,
VisualLoraLoader
)
from .nodes.image_loader import VisualImageLoader
from .nodes.auto_watcher import LoadImageFromFolder
from .nodes.load_recent import LoadRecentOutputs
from .nodes.preview_latent import PreviewLatentLiveNode
from .nodes.decision_node import SaturnDecision
from .nodes.aspect_ratio import TextAspectRatioFinder, PreviewImageSizeAspectRatio
from .nodes.lora_finder import TextLoraFinder
from .nodes.prompt_iterator import PromptQueueIterator
from .nodes.prompt_counter import PromptCounter
from .nodes.text_replacer import MultiTextReplacer
from .nodes.text_split import SaturnTextSplit
from .nodes.local_runner import RunLocalFileNode, setup_local_runner_routes
from .nodes.stereogram import SaturnStereo3D
from .nodes.character_presets import CharacterPresetPicker, ApplyCharacterLoRAs
from .nodes.utils import (
get_saturnnodes_user_dir,
get_env_setting,
is_local_request,
is_safe_path,
get_csrf_token,
is_authenticated_local_request
)
NODE_CLASS_MAPPINGS = {
"FolderLoraLoader": FolderLoraLoader,
"FolderLoraLoaderPretty": FolderLoraLoaderPretty,
"VisualLoraLoader": VisualLoraLoader,
"VisualImageLoader": VisualImageLoader,
"LoadImageFromFolder": LoadImageFromFolder,
"LoadRecentOutputs": LoadRecentOutputs,
"PreviewLatentLive": PreviewLatentLiveNode,
"SaturnDecision": SaturnDecision,
"TextAspectRatioFinder": TextAspectRatioFinder,
"PreviewImageSizeAspectRatio": PreviewImageSizeAspectRatio,
"TextLoraFinder": TextLoraFinder,
"PromptQueueIterator": PromptQueueIterator,
"PromptCounter": PromptCounter,
"MultiTextReplacer": MultiTextReplacer,
"SaturnTextSplit": SaturnTextSplit,
"RunLocalFileNode": RunLocalFileNode,
"SaturnStereo3D": SaturnStereo3D,
"CharacterPresetPicker": CharacterPresetPicker,
"ApplyCharacterLoRAs": ApplyCharacterLoRAs,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FolderLoraLoader": "🪐 📁 LoRA Loader (Folder)",
"FolderLoraLoaderPretty": "🪐 ✨ LoRA Loader (Pretty)",
"VisualLoraLoader": "🪐 🖼️ Visual LoRA Loader",
"VisualImageLoader": "🪐 📷 Visual Image Loader",
"LoadImageFromFolder": "🪐 📂 Load Image From Folder",
"LoadRecentOutputs": "🪐 ⏱️ Recent Outputs",
"PreviewLatentLive": "🪐 👁️ Live Latent Preview",
"SaturnDecision": "🪐 ⏸️ Saturn Decision",
"TextAspectRatioFinder": "🪐 📐 Text Aspect Ratio Finder",
"PreviewImageSizeAspectRatio": "🪐 📐 Preview Image Size & Aspect Ratio",
"TextLoraFinder": "🪐 🔎 Text LoRA Finder & Loader",
"PromptQueueIterator": "🪐 🔄 Prompt Queue Iterator",
"PromptCounter": "🪐 📝 Prompt Counter",
"MultiTextReplacer": "🪐 🔤 Multi Text Replacer",
"SaturnTextSplit": "🪐 ✂️ Text Split",
"RunLocalFileNode": "🪐 ⚡ Run Local File",
"SaturnStereo3D": "🪐 👓 Stereoscopic 3D Generator",
"CharacterPresetPicker": "🪐 🧑‍🎤 Character Preset Picker",
"ApplyCharacterLoRAs": "🪐 🧬 Apply Character LoRAs",
}
ALL_SATURN_NODE_CLASSES = dict(NODE_CLASS_MAPPINGS)
ALL_SATURN_NODE_DISPLAY_NAMES = dict(NODE_DISPLAY_NAME_MAPPINGS)
ALL_SATURN_NODE_KEYS = list(NODE_CLASS_MAPPINGS)
def get_saturn_nodes_info():
"""Returns sorted node metadata auto-generated from NODE_CLASS_MAPPINGS and NODE_DISPLAY_NAME_MAPPINGS."""
import re
info_list = []
category_order = ["Loaders", "Automation", "Previews", "3D & Previews", "Utils"]
for key in ALL_SATURN_NODE_KEYS:
cls = ALL_SATURN_NODE_CLASSES.get(key)
cat = getattr(cls, "CATEGORY", "🪐 SaturnNodes/Utils") if cls else "🪐 SaturnNodes/Utils"
subcat = cat.split("/")[-1].strip() if "/" in cat else "Utils"
disp = ALL_SATURN_NODE_DISPLAY_NAMES.get(key, key)
clean_name = re.sub(r'^[^a-zA-Z0-9\(]+', '', disp).strip()
if not clean_name:
clean_name = key
info_list.append({
"key": key,
"category": subcat,
"clean_name": clean_name,
"name": f"{subcat} / {clean_name}",
"desc": disp
})
def sort_key(item):
cat = item["category"]
cat_idx = category_order.index(cat) if cat in category_order else 999
return (cat_idx, item["name"])
info_list.sort(key=sort_key)
return info_list
def load_disabled_nodes():
"""Loads set of user-disabled SaturnNodes from user settings directory."""
try:
import json
user_dir = get_saturnnodes_user_dir()
path = os.path.join(user_dir, "disabled_nodes.json")
if os.path.isfile(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
return set(data)
except Exception as e:
print(f"[SaturnNodes] Notice: Unable to load disabled_nodes.json: {e}")
return set()
def save_disabled_nodes(disabled_set):
"""Saves set of user-disabled SaturnNodes atomically."""
import json
user_dir = get_saturnnodes_user_dir()
os.makedirs(user_dir, exist_ok=True)
path = os.path.join(user_dir, "disabled_nodes.json")
temp_path = path + ".tmp"
with open(temp_path, "w", encoding="utf-8") as f:
json.dump(sorted(list(disabled_set)), f, indent=2)
os.replace(temp_path, path)
# Filter disabled nodes (all enabled by default when disabled_nodes.json is empty)
_active_disabled_nodes = load_disabled_nodes()
if _active_disabled_nodes:
for _node_name in _active_disabled_nodes:
NODE_CLASS_MAPPINGS.pop(_node_name, None)
NODE_DISPLAY_NAME_MAPPINGS.pop(_node_name, None)
WEB_DIRECTORY = "./web"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
server = getattr(PromptServer, "instance", None)
if server is not None and hasattr(server, "routes"):
setup_queue_control_routes(server)
setup_local_runner_routes(server)
routes = server.routes
else:
routes = web.RouteTableDef()
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
USER_DIR = get_saturnnodes_user_dir()
ENV_FILE = os.path.join(USER_DIR, ".env")
def print_startup_banner():
# Enable ANSI escape sequences on Windows console if available
if os.name == "nt":
try:
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
except Exception:
pass
# Saturn Brand Colors (Amber, Gold, Warm Sand, White)
AMBER = "\033[1;38;2;245;158;11m"
GOLD = "\033[38;2;251;191;36m"
SAND = "\033[38;2;222;203;178m"
WHITE = "\033[97m"
RESET = "\033[0m"
active_count = len(NODE_DISPLAY_NAME_MAPPINGS)
total_count = len(ALL_SATURN_NODE_KEYS)
disabled_count = len(_active_disabled_nodes) if _active_disabled_nodes else 0
total_str = f"{active_count}/{total_count} nodes loaded"
if disabled_count > 0:
total_str += f" ({disabled_count} disabled in settings)"
print()
print(f"{AMBER}[ComfyUI-SaturnNodes]{RESET} {SAND}Version:{RESET} {WHITE}{__version__}{RESET}")
print(f" {GOLD}*{RESET} {SAND}Loaded:{RESET} {WHITE}Visual LoRA & Image Loaders, LoRA Folder Loader{RESET}")
print(f" {GOLD}*{RESET} {SAND}Loaded:{RESET} {WHITE}Live Latent Preview & Floating Dock{RESET}")
print(f" {GOLD}*{RESET} {SAND}Loaded:{RESET} {WHITE}Stereoscopic 3D Generator{RESET}")
print(f" {GOLD}*{RESET} {SAND}Loaded:{RESET} {WHITE}Prompt Queue Iterator, Prompt Counter, Text Tools{RESET}")
print(f" {GOLD}*{RESET} {SAND}Loaded:{RESET} {WHITE}Queue Pause, Persistent Queue, Local Runner{RESET}")
print()
# System status lines
try:
if is_tray_icon_enabled() and getattr(tray_manager, "_is_running", False):
tray_str = "System tray icon started."
elif not is_tray_icon_enabled():
tray_str = "System tray icon disabled in settings."
else:
tray_str = "System tray icon not started."
except Exception:
tray_str = "System tray icon disabled in settings."
try:
if is_assets_restore_enabled():
r_count = getattr(assets_restore_manager, "restored_count", 0)
assets_str = f"Restored {r_count} recent image(s) into Assets pane."
else:
assets_str = "No images restored into Assets pane (disabled in settings)."
except Exception:
assets_str = "Assets pane restore ready."
try:
if is_persistent_queue_enabled():
p_items = getattr(persistent_queue_manager, "persistent_items", [])
q_count = len(p_items) if p_items else 0
queue_str = f"Loaded {q_count} saved queue item(s)."
else:
queue_str = "No saved queue items loaded (disabled in settings)."
except Exception:
queue_str = "Persistent queue manager ready."
print(f" {GOLD}*{RESET} {WHITE}{tray_str}{RESET}")
print(f" {GOLD}*{RESET} {WHITE}{assets_str}{RESET}")
print(f" {GOLD}*{RESET} {WHITE}{queue_str}{RESET}")
print(f"{AMBER}Total: {total_str}{RESET}")
print()
try:
print_startup_banner()
except Exception:
print(f"\n[ComfyUI-SaturnNodes] Version: {__version__} - Total: {len(NODE_DISPLAY_NAME_MAPPINGS)}/{len(ALL_SATURN_NODE_KEYS)} nodes loaded\n")
@routes.get("/saturnnodes/auth/token")
async def get_csrf_token_endpoint(request):
if not is_local_request(request):
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
return web.json_response({"csrf_token": get_csrf_token()})
@routes.get("/saturnnodes/get_image_prompt")
@routes.get("/saturnnodes/view_image_prompt")
async def get_image_prompt_endpoint(request):
if not is_local_request(request):
return web.json_response({"success": False, "error": "Forbidden: Local access only"}, status=403)
try:
import folder_paths
filename = request.query.get("filename")
subfolder = request.query.get("subfolder", "")
folder_type = request.query.get("type", "output")
if not filename:
return web.json_response({"success": False, "error": "Missing filename"}, status=400)
if folder_type == "input":
base_dir = folder_paths.get_input_directory()
elif folder_type == "temp":
base_dir = folder_paths.get_temp_directory()
else:
base_dir = folder_paths.get_output_directory()
if subfolder:
subfolder_clean = str(subfolder).strip().lstrip("/\\")
filepath = os.path.join(base_dir, subfolder_clean, filename)
else:
filepath = os.path.join(base_dir, filename)
if not is_safe_path(filepath) or not os.path.exists(filepath) or not os.path.isfile(filepath):
return web.json_response({"success": False, "error": "File not found or forbidden"}, status=404)
from .nodes.image_loader import extract_metadata_from_image
prompt, width, height = extract_metadata_from_image(filepath)
return web.json_response({
"success": True,
"prompt": prompt,
"filename": filename,
"width": width,
"height": height
})
except Exception as e:
return web.json_response({"success": False, "error": str(e)}, status=500)
def _env_bool(key, default=False):
return str(get_env_setting(key, "true" if default else "false")).lower() in ["true", "1", "yes"]
def _env_int(key, default):
try:
return int(get_env_setting(key, str(default)))
except (TypeError, ValueError):
return default
@routes.get("/saturnnodes/settings")
async def get_settings(request):
if not is_local_request(request):
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
# Return the full settings surface (not just API keys) so the settings UI,
# debug export and .env never disagree about the effective values.
return web.json_response({
"civitai_api_key": get_env_setting("CIVITAI_API_KEY", ""),
"tmdb_api_key": get_env_setting("TMDB_API_KEY", ""),
"enable_civitai_scraping": _env_bool("ENABLE_CIVITAI_SCRAPING", False),
"enable_tmdb_scraping": _env_bool("ENABLE_TMDB_SCRAPING", False),
"enable_lora_usage": _env_bool("ENABLE_LORA_USAGE", True),
"enable_character_usage": _env_bool("ENABLE_CHARACTER_USAGE", True),
"enable_persistent_queue": _env_bool("ENABLE_PERSISTENT_QUEUE", True),
"persistent_queue_restored_state": get_env_setting("PERSISTENT_QUEUE_RESTORED_STATE", "Match Default"),
"persistent_queue_max_retries": _env_int("PERSISTENT_QUEUE_MAX_RETRIES", 0),
"persist_pause_state": _env_bool("PERSIST_PAUSE_STATE", False),
"default_pause_state": get_env_setting("DEFAULT_PAUSE_STATE", "Running"),
"default_pause_mode": get_env_setting("DEFAULT_PAUSE_MODE", "after_finish"),
"enable_tray_icon": _env_bool("ENABLE_TRAY_ICON", False),
"enable_assets_restore": _env_bool("ENABLE_ASSETS_RESTORE", True),
"restore_assets_count": _env_int("RESTORE_ASSETS_COUNT", 64),
"restore_assets_recursive": _env_bool("RESTORE_ASSETS_RECURSIVE", True),
"show_restored_generation_time": _env_bool("SHOW_RESTORED_GENERATION_TIME", True),
"image_loader_metadata_limit": _env_int("IMAGE_LOADER_METADATA_LIMIT", 128),
"enable_process_management": _env_bool("ALLOW_PROCESS_MANAGEMENT", False),
"enable_local_file_execution": _env_bool("ENABLE_LOCAL_FILE_EXECUTION", False),
"clear_prompt_iterator_on_launch": _env_bool("CLEAR_PROMPT_ITERATOR_ON_LAUNCH", False)
})
def _clean_env_val(v):
if v is None:
return None
return str(v).replace("\n", "").replace("\r", "").strip()
@routes.post("/saturnnodes/settings")
async def save_settings(request):
if not is_authenticated_local_request(request):
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
try:
data = await request.json()
civitai_key = _clean_env_val(data.get("civitai_api_key"))
tmdb_key = _clean_env_val(data.get("tmdb_api_key"))
enable_persistent_queue = _clean_env_val(data.get("enable_persistent_queue"))
default_pause_state = _clean_env_val(data.get("default_pause_state"))
default_pause_mode = _clean_env_val(data.get("default_pause_mode"))
enable_civitai = _clean_env_val(data.get("enable_civitai_scraping"))
enable_tmdb = _clean_env_val(data.get("enable_tmdb_scraping"))
enable_lora_usage = _clean_env_val(data.get("enable_lora_usage"))
enable_character_usage = _clean_env_val(data.get("enable_character_usage"))
enable_tray_icon = _clean_env_val(data.get("enable_tray_icon"))
enable_assets_restore = _clean_env_val(data.get("enable_assets_restore"))
restore_assets_count = _clean_env_val(data.get("restore_assets_count"))
clear_prompt_iterator_on_launch = _clean_env_val(data.get("clear_prompt_iterator_on_launch"))
restored_state = _clean_env_val(data.get("persistent_queue_restored_state"))
enable_process_management = _clean_env_val(data.get("enable_process_management"))
enable_local_file_execution = _clean_env_val(data.get("enable_local_file_execution"))
persistent_queue_max_retries = _clean_env_val(data.get("persistent_queue_max_retries"))
persist_pause_state = _clean_env_val(data.get("persist_pause_state"))
restore_assets_recursive = _clean_env_val(data.get("restore_assets_recursive"))
show_restored_generation_time = _clean_env_val(data.get("show_restored_generation_time"))
image_loader_metadata_limit = _clean_env_val(data.get("image_loader_metadata_limit"))
lines = []
if os.path.exists(ENV_FILE):
try:
with open(ENV_FILE, "r", encoding="utf-8") as f:
lines = f.readlines()
except Exception:
lines = []
new_lines = []
has_civitai = False
has_tmdb = False
has_persistent = False
has_pause_state = False
has_pause_mode = False
has_civ_enable = False
has_tmdb_enable = False
has_lora_usage = False
has_character_usage = False
has_tray_enable = False
has_assets_restore = False
has_restore_count = False
has_clear_prompt_iterator = False
has_restored_state = False
has_process_mgmt = False
has_local_file_exec = False
has_pq_retries = False
has_persist_pause = False
has_restore_recursive = False
has_show_generation_time = False
has_image_metadata_limit = False
for line in lines:
if line.strip().startswith("CIVITAI_API_KEY=") and civitai_key is not None:
new_lines.append(f"CIVITAI_API_KEY={civitai_key}\n")
has_civitai = True
elif line.strip().startswith("TMDB_API_KEY=") and tmdb_key is not None:
new_lines.append(f"TMDB_API_KEY={tmdb_key}\n")
has_tmdb = True
elif line.strip().startswith("ENABLE_PERSISTENT_QUEUE=") and enable_persistent_queue is not None:
new_lines.append(f"ENABLE_PERSISTENT_QUEUE={enable_persistent_queue}\n")
has_persistent = True
elif line.strip().startswith("DEFAULT_PAUSE_STATE=") and default_pause_state is not None:
new_lines.append(f"DEFAULT_PAUSE_STATE={default_pause_state}\n")
has_pause_state = True
elif line.strip().startswith("DEFAULT_PAUSE_MODE=") and default_pause_mode is not None:
new_lines.append(f"DEFAULT_PAUSE_MODE={default_pause_mode}\n")
has_pause_mode = True
elif line.strip().startswith("ENABLE_CIVITAI_SCRAPING=") and enable_civitai is not None:
new_lines.append(f"ENABLE_CIVITAI_SCRAPING={enable_civitai}\n")
has_civ_enable = True
elif line.strip().startswith("ENABLE_TMDB_SCRAPING=") and enable_tmdb is not None:
new_lines.append(f"ENABLE_TMDB_SCRAPING={enable_tmdb}\n")
has_tmdb_enable = True
elif line.strip().startswith("ENABLE_LORA_USAGE=") and enable_lora_usage is not None:
new_lines.append(f"ENABLE_LORA_USAGE={enable_lora_usage}\n")
has_lora_usage = True
elif line.strip().startswith("ENABLE_CHARACTER_USAGE=") and enable_character_usage is not None:
new_lines.append(f"ENABLE_CHARACTER_USAGE={enable_character_usage}\n")
has_character_usage = True
elif line.strip().startswith("ENABLE_TRAY_ICON=") and enable_tray_icon is not None:
new_lines.append(f"ENABLE_TRAY_ICON={enable_tray_icon}\n")
has_tray_enable = True
elif line.strip().startswith("ENABLE_ASSETS_RESTORE=") and enable_assets_restore is not None:
new_lines.append(f"ENABLE_ASSETS_RESTORE={enable_assets_restore}\n")
has_assets_restore = True
elif line.strip().startswith("RESTORE_ASSETS_COUNT=") and restore_assets_count is not None:
new_lines.append(f"RESTORE_ASSETS_COUNT={restore_assets_count}\n")
has_restore_count = True
elif line.strip().startswith("CLEAR_PROMPT_ITERATOR_ON_LAUNCH=") and clear_prompt_iterator_on_launch is not None:
new_lines.append(f"CLEAR_PROMPT_ITERATOR_ON_LAUNCH={clear_prompt_iterator_on_launch}\n")
has_clear_prompt_iterator = True
elif line.strip().startswith("PERSISTENT_QUEUE_RESTORED_STATE=") and restored_state is not None:
new_lines.append(f"PERSISTENT_QUEUE_RESTORED_STATE={restored_state}\n")
has_restored_state = True
elif line.strip().startswith("ALLOW_PROCESS_MANAGEMENT=") and enable_process_management is not None:
new_lines.append(f"ALLOW_PROCESS_MANAGEMENT={enable_process_management}\n")
has_process_mgmt = True
elif line.strip().startswith("ENABLE_LOCAL_FILE_EXECUTION=") and enable_local_file_execution is not None:
new_lines.append(f"ENABLE_LOCAL_FILE_EXECUTION={enable_local_file_execution}\n")
has_local_file_exec = True
elif line.strip().startswith("PERSISTENT_QUEUE_MAX_RETRIES=") and persistent_queue_max_retries is not None:
new_lines.append(f"PERSISTENT_QUEUE_MAX_RETRIES={persistent_queue_max_retries}\n")
has_pq_retries = True
elif line.strip().startswith("PERSIST_PAUSE_STATE=") and persist_pause_state is not None:
new_lines.append(f"PERSIST_PAUSE_STATE={persist_pause_state}\n")
has_persist_pause = True
elif line.strip().startswith("RESTORE_ASSETS_RECURSIVE=") and restore_assets_recursive is not None:
new_lines.append(f"RESTORE_ASSETS_RECURSIVE={restore_assets_recursive}\n")
has_restore_recursive = True
elif line.strip().startswith("SHOW_RESTORED_GENERATION_TIME=") and show_restored_generation_time is not None:
new_lines.append(f"SHOW_RESTORED_GENERATION_TIME={show_restored_generation_time}\n")
has_show_generation_time = True
elif line.strip().startswith("IMAGE_LOADER_METADATA_LIMIT=") and image_loader_metadata_limit is not None:
new_lines.append(f"IMAGE_LOADER_METADATA_LIMIT={image_loader_metadata_limit}\n")
has_image_metadata_limit = True
elif line.strip().startswith("ALLOW_ANY_SCRIPT_PATH="):
# Deprecated and removed for security; drop from .env
continue
else:
new_lines.append(line)
if not has_civitai and civitai_key is not None:
new_lines.append(f"CIVITAI_API_KEY={civitai_key}\n")
if not has_tmdb and tmdb_key is not None:
new_lines.append(f"TMDB_API_KEY={tmdb_key}\n")
if not has_persistent and enable_persistent_queue is not None:
new_lines.append(f"ENABLE_PERSISTENT_QUEUE={enable_persistent_queue}\n")
if not has_pause_state and default_pause_state is not None:
new_lines.append(f"DEFAULT_PAUSE_STATE={default_pause_state}\n")
if not has_pause_mode and default_pause_mode is not None:
new_lines.append(f"DEFAULT_PAUSE_MODE={default_pause_mode}\n")
if not has_civ_enable and enable_civitai is not None:
new_lines.append(f"ENABLE_CIVITAI_SCRAPING={enable_civitai}\n")
if not has_tmdb_enable and enable_tmdb is not None:
new_lines.append(f"ENABLE_TMDB_SCRAPING={enable_tmdb}\n")
if not has_lora_usage and enable_lora_usage is not None:
new_lines.append(f"ENABLE_LORA_USAGE={enable_lora_usage}\n")
if not has_character_usage and enable_character_usage is not None:
new_lines.append(f"ENABLE_CHARACTER_USAGE={enable_character_usage}\n")
if not has_tray_enable and enable_tray_icon is not None:
new_lines.append(f"ENABLE_TRAY_ICON={enable_tray_icon}\n")
if not has_assets_restore and enable_assets_restore is not None:
new_lines.append(f"ENABLE_ASSETS_RESTORE={enable_assets_restore}\n")
if not has_restore_count and restore_assets_count is not None:
new_lines.append(f"RESTORE_ASSETS_COUNT={restore_assets_count}\n")
if not has_clear_prompt_iterator and clear_prompt_iterator_on_launch is not None:
new_lines.append(f"CLEAR_PROMPT_ITERATOR_ON_LAUNCH={clear_prompt_iterator_on_launch}\n")
if not has_restored_state and restored_state is not None:
new_lines.append(f"PERSISTENT_QUEUE_RESTORED_STATE={restored_state}\n")
if not has_process_mgmt and enable_process_management is not None:
new_lines.append(f"ALLOW_PROCESS_MANAGEMENT={enable_process_management}\n")
if not has_local_file_exec and enable_local_file_execution is not None:
new_lines.append(f"ENABLE_LOCAL_FILE_EXECUTION={enable_local_file_execution}\n")
if not has_pq_retries and persistent_queue_max_retries is not None:
new_lines.append(f"PERSISTENT_QUEUE_MAX_RETRIES={persistent_queue_max_retries}\n")
if not has_persist_pause and persist_pause_state is not None:
new_lines.append(f"PERSIST_PAUSE_STATE={persist_pause_state}\n")
if not has_restore_recursive and restore_assets_recursive is not None:
new_lines.append(f"RESTORE_ASSETS_RECURSIVE={restore_assets_recursive}\n")
if not has_show_generation_time and show_restored_generation_time is not None:
new_lines.append(f"SHOW_RESTORED_GENERATION_TIME={show_restored_generation_time}\n")
if not has_image_metadata_limit and image_loader_metadata_limit is not None:
new_lines.append(f"IMAGE_LOADER_METADATA_LIMIT={image_loader_metadata_limit}\n")
temp_env = ENV_FILE + ".tmp"
with open(temp_env, "w", encoding="utf-8") as f:
f.writelines(new_lines)
f.flush()
os.fsync(f.fileno())
os.replace(temp_env, ENV_FILE)
if civitai_key is not None:
os.environ["CIVITAI_API_KEY"] = civitai_key
if tmdb_key is not None:
os.environ["TMDB_API_KEY"] = tmdb_key
if enable_lora_usage is not None:
os.environ["ENABLE_LORA_USAGE"] = enable_lora_usage
if enable_character_usage is not None:
os.environ["ENABLE_CHARACTER_USAGE"] = enable_character_usage
if enable_tray_icon is not None:
os.environ["ENABLE_TRAY_ICON"] = str(enable_tray_icon)
tray_enabled_bool = str(enable_tray_icon).lower() in ["true", "1", "yes"]
tray_manager.set_enabled(tray_enabled_bool)
if enable_assets_restore is not None:
os.environ["ENABLE_ASSETS_RESTORE"] = str(enable_assets_restore)
if enable_process_management is not None:
os.environ["ALLOW_PROCESS_MANAGEMENT"] = str(enable_process_management)
if enable_local_file_execution is not None:
os.environ["ENABLE_LOCAL_FILE_EXECUTION"] = str(enable_local_file_execution)
if restore_assets_count is not None:
os.environ["RESTORE_ASSETS_COUNT"] = str(restore_assets_count)
if clear_prompt_iterator_on_launch is not None:
os.environ["CLEAR_PROMPT_ITERATOR_ON_LAUNCH"] = str(clear_prompt_iterator_on_launch)
if persistent_queue_max_retries is not None:
os.environ["PERSISTENT_QUEUE_MAX_RETRIES"] = str(persistent_queue_max_retries)
if persist_pause_state is not None:
os.environ["PERSIST_PAUSE_STATE"] = str(persist_pause_state)
pause_manager.persist_state = str(persist_pause_state).lower() in ["true", "1", "yes"]
if pause_manager.persist_state:
pause_manager._persist_pause_state()
else:
try:
if os.path.exists(pause_manager.state_file):
os.remove(pause_manager.state_file)
except OSError:
pass
if restore_assets_recursive is not None:
os.environ["RESTORE_ASSETS_RECURSIVE"] = str(restore_assets_recursive)
if show_restored_generation_time is not None:
os.environ["SHOW_RESTORED_GENERATION_TIME"] = str(show_restored_generation_time)
if image_loader_metadata_limit is not None:
os.environ["IMAGE_LOADER_METADATA_LIMIT"] = str(image_loader_metadata_limit)
# Drop the parsed .env cache immediately so all modules see the new values.
from .nodes.utils import invalidate_env_settings_cache
invalidate_env_settings_cache()
except Exception as e:
print(f"[SaturnNodes] Error saving settings to .env: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
return web.json_response({"status": "ok"})
@routes.get("/saturnnodes/disabled_nodes")
async def get_disabled_nodes_endpoint(request):
if not is_local_request(request):
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
return web.json_response({
"disabled_nodes": sorted(list(load_disabled_nodes())),
"all_nodes": ALL_SATURN_NODE_KEYS,
"nodes_info": get_saturn_nodes_info()
})
@routes.post("/saturnnodes/disabled_nodes")
async def save_disabled_nodes_endpoint(request):
if not is_authenticated_local_request(request):
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
try:
data = await request.json()
disabled_list = data.get("disabled_nodes", [])
if isinstance(disabled_list, list):
valid_keys = set(ALL_SATURN_NODE_KEYS)
cleaned = {str(k) for k in disabled_list if str(k) in valid_keys}
save_disabled_nodes(cleaned)
return web.json_response({"status": "ok", "disabled_nodes": sorted(list(cleaned))})
return web.json_response({"error": "Invalid format"}, status=400)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
@routes.post("/saturnnodes/scrapes/clear")
async def clear_scrapes_endpoint(request):
if not is_authenticated_local_request(request):
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
try:
failed_file = os.path.join(USER_DIR, "failed_scrapes.json")
temp_failed = failed_file + ".tmp"
with open(temp_failed, "w", encoding="utf-8") as f:
f.write("{}")
os.replace(temp_failed, failed_file)
return web.json_response({"status": "ok"})
except Exception as e:
print(f"[SaturnNodes] Error clearing scrapes cache: {e}")
return web.json_response({"status": "error", "message": str(e)}, status=500)
@routes.get("/saturnnodes/debug/export")
async def export_debug_profile(request):
if not is_local_request(request):
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
try:
import platform
import json
from datetime import datetime, timezone
import torch
# Safe GPU / hardware information
cuda_avail = torch.cuda.is_available() if hasattr(torch, "cuda") else False
device_name = torch.cuda.get_device_name(0) if cuda_avail else "CPU"
# Check API key configuration status without revealing sensitive strings
civitai_key = os.getenv("CIVITAI_API_KEY", "")
tmdb_key = os.getenv("TMDB_API_KEY", "")
if os.path.exists(ENV_FILE):
try:
with open(ENV_FILE, "r", encoding="utf-8") as f:
for line in f:
if line.strip().startswith("CIVITAI_API_KEY=") and not civitai_key:
civitai_key = line.split("=", 1)[1].strip()
elif line.strip().startswith("TMDB_API_KEY=") and not tmdb_key:
tmdb_key = line.split("=", 1)[1].strip()
except Exception:
pass
# Calculate counts of local storage files safely
def safe_json_count(filename):
p = os.path.join(USER_DIR, filename)
if os.path.exists(p):
try:
with open(p, "r", encoding="utf-8") as f:
data = json.load(f)
return len(data) if isinstance(data, (list, dict)) else 0
except Exception:
pass
return 0
debug_profile = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"package": "ComfyUI-SaturnNodes",
"version": __version__,
"system": {
"os": platform.system(),
"os_release": platform.release(),
"os_version": platform.version(),
"architecture": platform.machine(),
"python_version": sys.version.split()[0],
"torch_version": getattr(torch, "__version__", "unknown"),
"cuda_available": cuda_avail,
"device_name": device_name,
},
"registered_nodes_count": len(NODE_CLASS_MAPPINGS),
"settings": {
"enable_persistent_queue": get_env_setting("ENABLE_PERSISTENT_QUEUE", "true"),
"default_pause_state": get_env_setting("DEFAULT_PAUSE_STATE", "Running"),
"default_pause_mode": get_env_setting("DEFAULT_PAUSE_MODE", "after_finish"),
"enable_civitai_scraping": get_env_setting("ENABLE_CIVITAI_SCRAPING", "false"),
"enable_tmdb_scraping": get_env_setting("ENABLE_TMDB_SCRAPING", "false"),
"enable_lora_usage": get_env_setting("ENABLE_LORA_USAGE", "true"),
"enable_character_usage": get_env_setting("ENABLE_CHARACTER_USAGE", "true"),
"enable_tray_icon": get_env_setting("ENABLE_TRAY_ICON", "false"),
"enable_assets_restore": get_env_setting("ENABLE_ASSETS_RESTORE", "true"),
"restore_assets_count": get_env_setting("RESTORE_ASSETS_COUNT", "64"),
"restore_assets_recursive": get_env_setting("RESTORE_ASSETS_RECURSIVE", "true"),
"show_restored_generation_time": get_env_setting("SHOW_RESTORED_GENERATION_TIME", "true"),
"image_loader_metadata_limit": get_env_setting("IMAGE_LOADER_METADATA_LIMIT", "128"),
"clear_prompt_iterator_on_launch": get_env_setting("CLEAR_PROMPT_ITERATOR_ON_LAUNCH", "false"),
"persistent_queue_restored_state": get_env_setting("PERSISTENT_QUEUE_RESTORED_STATE", "Match Default"),
"persistent_queue_max_retries": get_env_setting("PERSISTENT_QUEUE_MAX_RETRIES", "0"),
"persist_pause_state": get_env_setting("PERSIST_PAUSE_STATE", "false"),
"allow_process_management": get_env_setting("ALLOW_PROCESS_MANAGEMENT", "false"),
"enable_local_file_execution": get_env_setting("ENABLE_LOCAL_FILE_EXECUTION", "false"),
"civitai_api_key_configured": bool(civitai_key and civitai_key.strip()),
"tmdb_api_key_configured": bool(tmdb_key and tmdb_key.strip()),
},
"local_storage_stats": {
"tracked_loras": safe_json_count("lora_usage.json"),
"lora_cycle_states": safe_json_count("lora_loader_state.json"),
"image_prompts_cached": safe_json_count("image_prompts_cache.json"),
"failed_scrapes_cached": safe_json_count("failed_scrapes.json"),
"prompt_iterator_queues": safe_json_count("prompt_iterator_state.json"),
"persisted_queue_items": safe_json_count("persistent_queue.json"),
}
}
return web.json_response(debug_profile)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)