-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy path__init__.py
More file actions
113 lines (85 loc) · 3.68 KB
/
Copy path__init__.py
File metadata and controls
113 lines (85 loc) · 3.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""comfyui-CleanFreak — one-click workflow layout by node role.
Adds a "Tidy by Role" action set to the canvas right-click menu. Every node
in the graph is classified into a role bucket (loaders, encoders,
conditioning, samplers, decoders, outputs, image-input, prompts, post,
misc) and laid out in width-aware columns. Connections are never touched —
ComfyUI's links are by node id, so moving a node never breaks a wire.
Also exposes a small HTTP API for the user-overrides JSON file so the
frontend's classification editor can persist per-class role assignments
across sessions and workflows. The file gradually accumulates the user's
preferred role for every node class they've explicitly re-assigned, so
future tidies start with a richer classifier than the built-in heuristics.
Stored at::
<ComfyUI>/user/cleanfreak/role_overrides.json
"""
from __future__ import annotations
import json
import os
import threading
import folder_paths
try:
from server import PromptServer
_HAS_SERVER = True
except ImportError:
_HAS_SERVER = False
WEB_DIRECTORY = "./web"
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
# --- Persistent user overrides --------------------------------------
_LOCK = threading.Lock()
def _overrides_path() -> str:
base = os.path.join(folder_paths.get_user_directory(), "cleanfreak")
os.makedirs(base, exist_ok=True)
return os.path.join(base, "role_overrides.json")
def _load_overrides() -> dict:
path = _overrides_path()
if not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError):
return {}
if isinstance(data, dict) and isinstance(data.get("overrides"), dict):
return {str(k): str(v) for k, v in data["overrides"].items()}
if isinstance(data, dict):
# Tolerate a flat {"ClassName": "role"} shape too.
return {str(k): str(v) for k, v in data.items() if isinstance(v, str)}
return {}
def _save_overrides(overrides: dict) -> None:
path = _overrides_path()
payload = {"version": 1, "overrides": overrides}
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, sort_keys=True)
os.replace(tmp, path)
# --- HTTP API ------------------------------------------------------
if _HAS_SERVER:
from aiohttp import web
routes = PromptServer.instance.routes
@routes.get("/cleanfreak/overrides")
async def _get_overrides_route(request):
with _LOCK:
overrides = _load_overrides()
return web.json_response({"overrides": overrides})
@routes.post("/cleanfreak/overrides")
async def _save_overrides_route(request):
try:
data = await request.json()
except Exception:
return web.json_response({"error": "invalid JSON"}, status=400)
incoming = (data or {}).get("overrides")
if not isinstance(incoming, dict):
return web.json_response({"error": "overrides must be an object"}, status=400)
# Whitelist the value type — anything non-string is dropped silently
# so a malformed frontend can't poison the file.
cleaned = {str(k): str(v) for k, v in incoming.items() if isinstance(v, str) and v}
with _LOCK:
_save_overrides(cleaned)
return web.json_response({"ok": True, "overrides": cleaned})
@routes.post("/cleanfreak/overrides/clear")
async def _clear_overrides_route(request):
with _LOCK:
_save_overrides({})
return web.json_response({"ok": True, "overrides": {}})