174 lines
4.6 KiB
Python
174 lines
4.6 KiB
Python
"""
|
|
Workflow normalization helpers.
|
|
|
|
Goal: accept ComfyUI "prompt" (API workflow graph) authored on a different OS and
|
|
rewrite OS-sensitive path formatting (primarily path separators) so that the
|
|
local ComfyUI instance can resolve model filenames correctly.
|
|
|
|
This is intentionally conservative: we only rewrite strings that look like file
|
|
paths / model names (e.g. end in ".safetensors") and we avoid touching URLs and
|
|
free-form prompt text.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Any, Iterable, List, Tuple
|
|
|
|
__all__ = [
|
|
"PathRemap",
|
|
"normalize_comfyui_prompt_for_current_os",
|
|
]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PathRemap:
|
|
"""Prefix remap for absolute paths across OSes.
|
|
|
|
Example:
|
|
PathRemap(from_prefix="D:\\ComfyUI\\models", to_prefix="/mnt/models")
|
|
|
|
Matching is performed in a canonicalized form (both prefixes and candidate
|
|
paths have backslashes converted to forward slashes). After remapping, path
|
|
separators are normalized for the current OS.
|
|
"""
|
|
|
|
from_prefix: str
|
|
to_prefix: str
|
|
|
|
|
|
_URL_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*://")
|
|
_WIN_DRIVE_RE = re.compile(r"^[A-Za-z]:[\\/]")
|
|
|
|
# Common file extensions encountered in ComfyUI prompts (models + media).
|
|
_PATH_EXTS = {
|
|
".safetensors",
|
|
".pt",
|
|
".pth",
|
|
".ckpt",
|
|
".bin",
|
|
".onnx",
|
|
".json",
|
|
".json5",
|
|
".yaml",
|
|
".yml",
|
|
".txt",
|
|
".png",
|
|
".jpg",
|
|
".jpeg",
|
|
".webp",
|
|
".gif",
|
|
".bmp",
|
|
".tif",
|
|
".tiff",
|
|
".exr",
|
|
".mp4",
|
|
".mov",
|
|
".mkv",
|
|
".webm",
|
|
".wav",
|
|
".mp3",
|
|
".flac",
|
|
}
|
|
|
|
|
|
def _canonicalize_for_prefix_match(path: str) -> str:
|
|
# Use forward slashes for prefix matching regardless of host OS.
|
|
return path.replace("\\", "/")
|
|
|
|
|
|
def _normalize_separators_for_current_os(path: str) -> str:
|
|
# ComfyUI often uses OS-native separators in its model name registry.
|
|
# We normalize to the current OS so lookup keys match.
|
|
if os.name == "nt":
|
|
return path.replace("/", "\\")
|
|
return path.replace("\\", "/")
|
|
|
|
|
|
def _looks_like_path_value(s: str) -> bool:
|
|
s_stripped = s.strip()
|
|
if not s_stripped:
|
|
return False
|
|
|
|
# Don't touch URLs (presigned uploads, http inputs, etc.)
|
|
if _URL_RE.match(s_stripped):
|
|
return False
|
|
|
|
# Don't touch placeholder tokens used by the system.
|
|
if s_stripped.startswith("<") and s_stripped.endswith(">"):
|
|
return False
|
|
|
|
# Avoid common sentinel.
|
|
if s_stripped == "None":
|
|
return False
|
|
|
|
lower = s_stripped.lower()
|
|
if any(lower.endswith(ext) for ext in _PATH_EXTS):
|
|
return True
|
|
|
|
# Absolute Windows paths even without extensions.
|
|
if _WIN_DRIVE_RE.match(s_stripped):
|
|
return True
|
|
|
|
# Relative paths with explicit prefixes.
|
|
if s_stripped.startswith(("./", "../", "~/", "~\\")):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _apply_prefix_remaps(path: str, remaps: Iterable[PathRemap]) -> str:
|
|
if not remaps:
|
|
return path
|
|
|
|
cand = _canonicalize_for_prefix_match(path)
|
|
for r in remaps:
|
|
frm = _canonicalize_for_prefix_match(str(r.from_prefix))
|
|
if cand.startswith(frm):
|
|
to = str(r.to_prefix)
|
|
# Replace using canonical representation then return in that form;
|
|
# the caller will normalize separators for current OS afterwards.
|
|
replaced = to + cand[len(frm) :]
|
|
return replaced
|
|
return path
|
|
|
|
|
|
def normalize_comfyui_prompt_for_current_os(
|
|
prompt: Any, *, path_remaps: Iterable[PathRemap] | None = None
|
|
) -> Tuple[Any, int]:
|
|
"""Normalize a ComfyUI API `prompt` graph for the current OS.
|
|
|
|
Args:
|
|
prompt: The value of the `/prompt` payload's `prompt` key. Typically a
|
|
dict mapping node ids to `{class_type, inputs, ...}`.
|
|
path_remaps: Optional prefix remaps applied before separator
|
|
normalization (useful for absolute paths).
|
|
|
|
Returns:
|
|
(normalized_prompt, num_rewritten_strings)
|
|
"""
|
|
|
|
remaps: List[PathRemap] = list(path_remaps or [])
|
|
rewritten = 0
|
|
|
|
def walk(x: Any) -> Any:
|
|
nonlocal rewritten
|
|
if isinstance(x, dict):
|
|
return {k: walk(v) for k, v in x.items()}
|
|
if isinstance(x, list):
|
|
return [walk(v) for v in x]
|
|
if isinstance(x, tuple):
|
|
return tuple(walk(v) for v in x)
|
|
if isinstance(x, str) and _looks_like_path_value(x):
|
|
y = _apply_prefix_remaps(x, remaps)
|
|
y = _normalize_separators_for_current_os(y)
|
|
if y != x:
|
|
rewritten += 1
|
|
return y
|
|
return x
|
|
|
|
return walk(prompt), rewritten
|
|
|