707 lines
35 KiB
Python
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)
|
|
|
|
|