Files
arcum42-ComfyUI_SageUtils/tools/node_metadata_tool.py
T

224 lines
7.3 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any
# Usage:
# cd /home/ai/programs/comfyui
# ./venv/bin/python -c "import os, sys; root=os.path.abspath('.'); sys.path.insert(0, os.path.join(root, 'custom_nodes')); sys.path.insert(0, root); from comfyui_sageutils.tools.node_metadata_tool import regenerate_node_docs; written = regenerate_node_docs(); print(len(written))"
# ./venv/bin/python -c "import os, sys; root=os.path.abspath('.'); sys.path.insert(0, os.path.join(root, 'custom_nodes')); sys.path.insert(0, root); from comfyui_sageutils.tools.node_metadata_tool import regenerate_node_docs; written = regenerate_node_docs(node_ids=['Sage_LoadImage']); print(written)"
DEFAULT_TEMPLATE = """---
type: NodeDoc
title: {{display_name}}
description: Auto-generated node documentation.
tags: [nodes, docs]
---
# {{display_name}}
* **Node ID:** `{{node_id}}`
* **Category:** `{{category}}`
{{description}}
## Inputs
{{inputs}}
## Outputs
{{outputs}}
## Notes
{{notes}}
Generated from the node schema.
"""
def _import_io_module() -> Any:
import comfy_api.latest as io
return io
def _import_node_list() -> list[type[Any]]:
from ..nodes_v3 import NODE_LIST
return NODE_LIST
def _normalize_io_type(io_item: Any) -> str:
if io_item is None:
return "unknown"
if hasattr(io_item, "get_io_type"):
return str(io_item.get_io_type())
return str(getattr(io_item, "io_type", type(io_item).__name__))
def _normalize_inputs(inputs: list[Any]) -> list[dict[str, Any]]:
normalized: list[dict[str, Any]] = []
for input_item in inputs or []:
if hasattr(input_item, "get_all"):
for item in input_item.get_all() or []:
normalized.append(_normalize_input(item))
else:
normalized.append(_normalize_input(input_item))
return normalized
def _normalize_input(input_item: Any) -> dict[str, Any]:
return {
"name": getattr(input_item, "id", ""),
"display_name": getattr(input_item, "display_name", ""),
"type": _normalize_io_type(input_item),
"description": getattr(input_item, "tooltip", "") or "",
"optional": getattr(input_item, "optional", False),
}
def _normalize_output(output_item: Any) -> dict[str, Any]:
return {
"name": getattr(output_item, "id", ""),
"display_name": getattr(output_item, "display_name", ""),
"type": _normalize_io_type(output_item),
"description": getattr(output_item, "tooltip", "") or "",
"is_output_list": getattr(output_item, "is_output_list", False),
}
def _normalize_schema(schema: Any) -> dict[str, Any]:
return {
"node_id": getattr(schema, "node_id", None) or "",
"display_name": getattr(schema, "display_name", None) or "",
"description": getattr(schema, "description", None) or "",
"category": getattr(schema, "category", None) or "",
"notes": getattr(schema, "notes", ""),
"inputs": _normalize_inputs(getattr(schema, "inputs", []) or []),
"outputs": [_normalize_output(item) for item in getattr(schema, "outputs", []) or []],
}
def _build_node_record(node_class: type[Any]) -> dict[str, Any]:
node_id = getattr(node_class, "__name__", "")
schema = None
try:
schema = node_class.define_schema()
except Exception:
schema = None
if schema is None:
return {
"node_id": node_id,
"display_name": node_id,
"description": "",
"category": "",
"inputs": [],
"outputs": [],
}
record = _normalize_schema(schema)
if not record["node_id"]:
record["node_id"] = node_id
if not record["display_name"]:
record["display_name"] = node_id
return record
def list_nodes() -> list[dict[str, str]]:
"""Return a shallow list of Sage Utils nodes."""
records = get_node_metadata()
return [
{
"node_id": record["node_id"],
"display_name": record["display_name"] or record["node_id"],
}
for record in records
]
def _render_template(template: str, data: dict[str, Any]) -> str:
rendered = template
for key, value in data.items():
if isinstance(value, str):
rendered = rendered.replace(f"{{{{{key}}}}}", value)
return rendered
def _render_list_section(items: list[dict[str, Any]], section_name: str) -> str:
if not items:
return f"## {section_name}\n\n_No {section_name.lower()} defined._\n"
rendered_items: list[str] = []
for item in items:
title = f"### `{item['name']}` — `{item['type']}`"
if item.get("optional", False):
title += " (optional)"
lines = [title]
if item.get("display_name"):
lines.append(f"- **Name:** `{item['display_name']}`")
description = item.get("description", "")
if description:
lines.append(f"- **Description:** {description}")
if item.get("is_output_list", False):
lines.append("- **List output:** True")
rendered_items.append("\n".join(lines))
return "\n\n".join(rendered_items) + "\n"
def _render_node_doc(template: str, record: dict[str, Any]) -> str:
rendered = template
# replace simple fields
for key in ["node_id", "display_name", "description", "category", "notes"]:
rendered = rendered.replace(f"{{{{{key}}}}}", str(record.get(key, "")))
# render lists
inputs_section = _render_list_section(record.get("inputs", []), "Inputs")
outputs_section = _render_list_section(record.get("outputs", []), "Outputs")
rendered = rendered.replace("{{inputs}}", inputs_section)
rendered = rendered.replace("{{outputs}}", outputs_section)
return rendered
def render_node_doc(node_id: str, template: str | None = None) -> str:
template = template or DEFAULT_TEMPLATE
record = get_node_metadata([node_id])
if not record:
raise ValueError(f"Node not found: {node_id}")
return _render_node_doc(template, record[0])
def write_node_doc(node_id: str, target_dir: str, template: str | None = None) -> Path:
content = render_node_doc(node_id, template)
target_path = Path(target_dir) / f"{node_id}.md"
target_path.write_text(content, encoding="utf-8")
return target_path
def regenerate_node_docs(node_ids: list[str] | None = None, target_dir: str | None = None, template: str | None = None) -> list[Path]:
"""Regenerate node documentation for all or selected Sage Utils nodes."""
output_dir = Path(target_dir or Path(__file__).resolve().parent.parent / "js" / "docs")
output_dir.mkdir(parents=True, exist_ok=True)
if node_ids is None:
node_ids = [record["node_id"] for record in get_node_metadata()]
written: list[Path] = []
for node_id in node_ids:
written.append(write_node_doc(node_id, str(output_dir), template))
return written
def get_node_metadata(node_ids: list[str] | None = None) -> list[dict[str, Any]]:
"""Return full metadata for requested Sage Utils nodes or all nodes."""
node_list = _import_node_list()
records = [_build_node_record(node_class) for node_class in node_list]
if node_ids is None:
return records
requested = set(node_ids)
return [record for record in records if record["node_id"] in requested]