Files

334 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import importlib.util
import importlib
import inspect
import json
from pathlib import Path
import textwrap
import sys
ROOT = Path(__file__).resolve().parent.parent
COMFY_ROOT = ROOT.parent.parent
PY_DIR = ROOT / "py"
WEB_DIR = ROOT / "web"
DOC_DIR = ROOT / "doc"
NODES_DIR = DOC_DIR / "nodes"
WEB_DOC_DIR = DOC_DIR / "web"
def ensure_dirs():
DOC_DIR.mkdir(exist_ok=True)
NODES_DIR.mkdir(parents=True, exist_ok=True)
WEB_DOC_DIR.mkdir(parents=True, exist_ok=True)
def ensure_py_package():
init_file = PY_DIR / "__init__.py"
if not init_file.exists():
init_file.write_text("", encoding="utf-8")
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
if str(COMFY_ROOT) not in sys.path:
sys.path.insert(0, str(COMFY_ROOT))
def load_module(path: Path):
ensure_py_package()
mod_name = f"py.{path.stem}"
try:
return importlib.import_module(mod_name)
except Exception:
return None
def get_class_info(cls):
src_file = inspect.getsourcefile(cls) or inspect.getfile(cls)
lines, start = inspect.getsourcelines(cls)
return src_file, start
def safe_call_input_types(cls):
if hasattr(cls, "INPUT_TYPES"):
try:
res = cls.INPUT_TYPES()
if isinstance(res, dict):
return res
except Exception:
return {}
return {}
def get_attr(cls, name, default=None):
return getattr(cls, name, default)
def render_inputs(inputs_dict):
parts = []
required = inputs_dict.get("required", {})
optional = inputs_dict.get("optional", {})
if required:
parts.append("- 必填参数:")
for k, v in required.items():
parts.append(render_param(k, v))
if optional:
parts.append("- 可选参数:")
for k, v in optional.items():
parts.append(render_param(k, v))
return "\n".join(parts) if parts else "- 无"
def render_param(name, spec):
t = None
meta = {}
if isinstance(spec, (tuple, list)) and spec:
t = spec[0]
if len(spec) > 1 and isinstance(spec[1], dict):
meta = spec[1]
elif isinstance(spec, str):
t = spec
opts = []
if isinstance(meta.get("choices"), (list, tuple)):
opts.append("选项:" + ", ".join(map(str, meta.get("choices"))))
if "default" in meta:
opts.append("默认:" + str(meta.get("default")))
if meta.get("multiline"):
opts.append("多行:true")
if meta.get("forceInput"):
opts.append("强制输入:true")
if meta.get("min") is not None:
opts.append("最小:" + str(meta.get("min")))
if meta.get("max") is not None:
opts.append("最大:" + str(meta.get("max")))
opts_str = (" (" + "; ".join(opts) + ")") if opts else ""
return f" - `{name}`: `{t}`{opts_str}"
def render_returns(ret_types, is_list=False):
if not ret_types:
return "- 无"
if isinstance(ret_types, (list, tuple)):
types = ", ".join(map(lambda x: "`" + str(x) + "`", ret_types))
else:
types = "`" + str(ret_types) + "`"
flag = "- 列表输出:true" if is_list else "- 列表输出:false"
return f"- 返回类型: {types}\n{flag}"
def find_ui_keys(path: Path):
try:
txt = path.read_text(encoding="utf-8", errors="ignore")
except Exception:
return []
keys = set()
for line in txt.splitlines():
if "ui." in line:
idx = line.find("ui.")
seg = line[idx:].strip()
k = seg.split()[0].strip('"\'\n,;:')
keys.add(k)
return sorted(keys)
def parse_static_mappings(txt):
start = txt.find("NODE_CLASS_MAPPINGS")
if start == -1:
return {}
brace = txt.find("{", start)
end = txt.find("}", brace)
block = txt[brace + 1:end] if brace != -1 and end != -1 else ""
res = {}
for line in block.splitlines():
line = line.strip()
if not line or ":" not in line:
continue
k, v = line.split(":", 1)
k = k.strip().strip("'\"")
v = v.strip().strip(", ")
v = v.strip()
v = v.replace(",", "")
v = v.strip()
if v:
res[k] = v
return res
def find_class_line(txt, cls_name):
for i, line in enumerate(txt.splitlines(), 1):
if line.strip().startswith("class ") and (cls_name in line):
return i
return None
def extract_attr_value(txt, cls_name, attr):
found = False
for i, line in enumerate(txt.splitlines(), 1):
if line.strip().startswith("class ") and (cls_name in line):
found = True
continue
if found:
s = line.strip()
if s.startswith(attr + " ") or s.startswith(attr + "="):
val = s.split("=", 1)[1].strip()
return val
if s.startswith("class "):
break
return None
def extract_input_types_block(txt, cls_name):
grabbed = []
found_cls = False
found_def = False
for line in txt.splitlines():
s = line
if s.strip().startswith("class ") and (cls_name in s):
found_cls = True
continue
if found_cls and s.strip().startswith("def INPUT_TYPES"):
found_def = True
grabbed.append(s)
continue
if found_def:
grabbed.append(s)
if s.strip().startswith("def ") or s.strip().startswith("class "):
break
return "\n".join(grabbed)
def write_module_doc(mod_path: Path, mod, out_path: Path):
ui_keys = find_ui_keys(mod_path)
lines = []
title = f"# {mod_path.name}"
lines.append(title)
lines.append("")
if mod is not None:
display_map = getattr(mod, "NODE_DISPLAY_NAME_MAPPINGS", {})
class_map = getattr(mod, "NODE_CLASS_MAPPINGS", {})
for name, cls in class_map.items():
disp = display_map.get(name, name)
file_ref, start_line = get_class_info(cls)
inputs_dict = safe_call_input_types(cls)
returns = get_attr(cls, "RETURN_TYPES", None)
is_list = bool(get_attr(cls, "OUTPUT_IS_LIST", False))
func = get_attr(cls, "FUNCTION", None)
cat = get_attr(cls, "CATEGORY", None)
out_node = bool(get_attr(cls, "OUTPUT_NODE", False))
lines.append(f"## {disp} ({name})")
lines.append(f"- 所在文件: `{Path(file_ref).as_posix()}:{start_line}`")
if cat:
lines.append(f"- 分类: `{cat}`")
if func:
lines.append(f"- 方法: `{func}`")
lines.append(f"- 输出节点: {'true' if out_node else 'false'}")
lines.append("- 输入:")
lines.append(render_inputs(inputs_dict))
lines.append("- 输出:")
lines.append(render_returns(returns, is_list))
if ui_keys:
lines.append("- UI 输出约定:")
for k in ui_keys:
lines.append(f" - `{k}`")
if name.endswith("CustomCmd") and mod_path.name == "KY_FFmpeg.py":
lines.append("- 命令模板占位符说明: 使用 `{变量}` 格式,如 `{input}`、`{output}`,通过 `format(**params)` 替换")
lines.append("- 关键实现: `py/KY_FFmpeg.py:392` 使用模板格式化")
lines.append("")
else:
txt = mod_path.read_text(encoding="utf-8", errors="ignore")
mappings = parse_static_mappings(txt)
for name, cls_name in mappings.items():
lines.append(f"## {name}")
line_no = find_class_line(txt, cls_name)
if line_no:
lines.append(f"- 所在文件: `{mod_path.as_posix()}:{line_no}`")
cat = extract_attr_value(txt, cls_name, "CATEGORY")
func = extract_attr_value(txt, cls_name, "FUNCTION")
out_node = extract_attr_value(txt, cls_name, "OUTPUT_NODE")
returns = extract_attr_value(txt, cls_name, "RETURN_TYPES")
is_list = extract_attr_value(txt, cls_name, "OUTPUT_IS_LIST")
if cat:
lines.append(f"- 分类: `{cat}`")
if func:
lines.append(f"- 方法: `{func}`")
if out_node:
lines.append(f"- 输出节点: `{out_node}`")
if returns:
lines.append(f"- 返回类型: `{returns}`")
if is_list:
lines.append(f"- 列表输出: `{is_list}`")
block = extract_input_types_block(txt, cls_name)
lines.append("- 输入定义片段:")
if block:
lines.append("```python")
lines.append(textwrap.dedent(block).strip())
lines.append("```")
else:
lines.append("(未检测到 INPUT_TYPES 定义)")
if ui_keys:
lines.append("- UI 输出约定:")
for k in ui_keys:
lines.append(f" - `{k}`")
if name.endswith("CustomCmd") and mod_path.name == "KY_FFmpeg.py":
lines.append("- 命令模板占位符说明: 使用 `{变量}` 格式,如 `{input}`、`{output}`,通过 `format(**params)` 替换")
lines.append("- 关键实现: `py/KY_FFmpeg.py:392` 使用模板格式化")
lines.append("")
text = "\n".join(lines)
out_path.write_text(text, encoding="utf-8")
def write_nodes_docs():
for p in sorted(PY_DIR.glob("*.py")):
mod = load_module(p)
out = NODES_DIR / (p.stem + ".md")
write_module_doc(p, mod, out)
def write_readme(mod_files):
lines = []
lines.append("# ComfyUI-KYNode 文档")
lines.append("")
lines.append("- 模块数: " + str(len(mod_files)))
lines.append("- 文档位置: `doc/nodes/` 与 `doc/web/`")
lines.append("")
lines.append("## 节点模块索引")
for p in mod_files:
lines.append(f"- `{p.name}` → `doc/nodes/{p.stem}.md`")
lines.append("")
lines.append("## 前端与路由概述")
lines.append("- 前端目录: `web/`,包含视频对比、Python 编辑器、文件路径工具等")
lines.append("- 后端路由: 见 `doc/web/README.md`,包含 `ky_utils/*` 相关接口")
DOC_DIR.joinpath("README.md").write_text("\n".join(lines), encoding="utf-8")
def extract_routes_from_files_py():
p = PY_DIR / "KY_Files.py"
if not p.exists():
return []
try:
txt = p.read_text(encoding="utf-8", errors="ignore")
except Exception:
return []
routes = []
for i, line in enumerate(txt.splitlines(), 1):
if "/ky_utils/" in line:
s = line.strip()
routes.append((i, s))
return routes
def write_web_readme():
lines = []
lines.append("# 前端与路由说明")
lines.append("")
lines.append("## 前端文件")
files = []
if WEB_DIR.exists():
for p in sorted(WEB_DIR.glob("**/*")):
if p.is_file():
files.append(p.relative_to(ROOT).as_posix())
for f in files:
lines.append(f"- `{f}`")
lines.append("")
lines.append("## 后端路由 (来自 `py/KY_Files.py`) ")
for ln, s in extract_routes_from_files_py():
lines.append(f"- `py/KY_Files.py:{ln}`: `{s}`")
WEB_DOC_DIR.joinpath("README.md").write_text("\n".join(lines), encoding="utf-8")
def enforce_line_limit():
for p in DOC_DIR.rglob("*.md"):
txt = p.read_text(encoding="utf-8", errors="ignore")
n = len(txt.splitlines())
if n > 500:
short = "\n".join(txt.splitlines()[:500])
p.write_text(short, encoding="utf-8")
def main():
ensure_dirs()
mod_files = sorted(PY_DIR.glob("*.py"))
write_nodes_docs()
write_web_readme()
write_readme(mod_files)
enforce_line_limit()
if __name__ == "__main__":
main()