feat: add complete docs-generation pipeline (scan/generate/translate/sync) - #117
Conversation
Add scripts/sync_to_docs.py, the script that generates the built-in-nodes/* .mdx pages in Comfy-Org/docs from the doc sources in this repo (en.md + zh/ja/ko translations), and updates docs.json navigation with case-corrected slugs. Key behaviors: - Concrete SEO descriptions extracted from each node's en.md overview (first sentence) instead of a templated string — the GEO improvement previously only possible by hand-editing individual .mdx files - Per-locale published-name resolution (macOS-safe, case-sensitive) - MDX safety: code blocks preserved verbatim, Mintlify components (<Note>/<Tip>/<Accordion>...) kept raw, unknown tags escaped in pairs - Assets copied to images/built-in-nodes/<Node>/ This makes the pipeline that owns these pages visible and maintainable in the same repo as the content, so fixes (e.g. slug casing, MDX escaping, description quality) land at the source instead of being overwritten by the next sync.
The overview first-sentence extraction treated the page title (# H1) as the end of the overview, returning empty for every node whose en.md starts with a title line — so build_frontmatter fell back to the templated description. H1 is now skipped (continue) and only H2+ sections end the overview. Verified: Canny, GLSLShader, OpenAIDalle2, ClipTextEncodeSdxlRefiner now get real descriptions.
|
Warning Review limit reached
Next review available in: 23 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (37)
📝 WalkthroughWalkthroughChangesDocumentation synchronization
Sequence Diagram(s)sequenceDiagram
participant CLI
participant EmbeddedDocs
participant sync_to_docs.py
participant DocsRepository
CLI->>sync_to_docs.py: select nodes and options
sync_to_docs.py->>EmbeddedDocs: read localized Markdown
sync_to_docs.py->>DocsRepository: write MDX and assets
sync_to_docs.py->>DocsRepository: update localized docs.json navigation
DocsRepository-->>CLI: report synchronization results
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Move the full node-docs automation pipeline into this repo under pipeline/, so the code that maintains these docs lives with the content it maintains: - scan_missing_nodes.py: scan ComfyUI source, detect new/changed nodes - prepare_ai_input.py + batch_generate_docs.py: LLM doc generation - batch_translate_docs.py: 11-language translation - update_param_translations.py + sync_frontend_translations.py: reconcile parameter names with the ComfyUI frontend i18n - sync_to_comfy_docs.py: generate built-in-nodes/*.mdx + docs.json nav in Comfy-Org/docs (concrete SEO descriptions, per-locale case-safe slug resolution, MDX-safe normalization) - version_tracker.py: per-node source hash tracking - lib/ + config/: shared modules and generation/translation rules - tests/: unit tests Path config via env (see env.example); no local/user-specific paths. data/ and ai_input/ are gitignored generated outputs.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Rename DEEPSEEK_API_KEY to LLM_API_KEY — the pipeline uses an OpenAI-compatible chat API, so users should be able to point it at any provider (DeepSeek, OpenAI, OpenRouter, local vLLM/Ollama, ...). DEEPSEEK_API_KEY is still accepted as a fallback for existing setups.
There was a problem hiding this comment.
Actionable comments posted: 17
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/sync_to_docs.py`:
- Around line 608-613: The unused _remove_group_from_pages helper should not
remain disconnected from the pipeline. Remove the function unless the navigation
rebuild requires its behavior; if it does, integrate a call at the appropriate
nav rebuild step and preserve its in-place removal semantics.
- Around line 550-573: Remove the unused prefix parameter from collect_page_keys
and stop passing it in recursive calls. Delete flatten_builtin_pages entirely
because it has no caller in this script, unless a new caller is introduced
elsewhere in the change.
- Around line 983-991: Update _purge_noncanonical_nav_pages so noncanonical keys
are not silently lost: when published_node_name returns a different basename,
either replace the existing key with the canonical key at the same position only
if the target locale page exists, or leave the original key unchanged when no
matching .mdx file exists. Preserve navigation ordering and avoid removing
entries without a valid replacement.
- Around line 227-235: Remove the unused _GROUP_LABEL_TO_CATEGORY_ROOT state,
_build_reverse_map() function, and its module-level invocation from
scripts/sync_to_docs.py. Do not restore a consumer or retain the stale reference
to _restructure_group_by_category, since no code in the script reads this
reverse map.
- Around line 342-363: Cache the ComfyUI source scan shared by
extract_category_full_from_comfyui and extract_category_from_comfyui: enumerate
SCAN_PATHS and read each Python file only once, then reuse the cached file
contents or a lazily built class-to-category index for subsequent node lookups.
Update both extraction functions to query this shared cache while preserving
their existing category resolution behavior and fallback order.
- Around line 263-265: Update _seg_to_label to preserve any path segment that is
already fully uppercase, including acronyms such as BFL, SDXL, and API, while
retaining the existing separator replacement and title-casing behavior for
mixed- or lowercase segments.
- Around line 1047-1063: Update the locale sync loop around
get_description_from_content and build_frontmatter so each page derives its
description from the localized content after reading it, falling back to the
previously extracted English description when the localized result is empty.
Keep the English description as the fallback and pass the selected
locale-specific description into build_frontmatter.
- Around line 671-675: Update the published_node_name call in
_rebuild_wrapper_groups to pass the current locale_code when resolving
key_parts[-1], preserving locale-specific casing before rewriting the key.
- Around line 476-488: Update resolve_source_node to check the canonical node
name first, then iterate over the remaining names from
node_name_nav_aliases(node_name) in a deterministic sorted order, avoiding
duplicate checks. Preserve the existing en.md validation and fallback behavior.
- Around line 1092-1105: Update the argument-handling flow around
resolve_source_node and the test-mode count slicing: fail with a non-zero exit
when --node does not resolve to a directory containing en.md, and reject
negative --count values before applying nodes[:args.count]. Remove the unused
f-string prefix from the docs.json warning print while preserving its message.
- Around line 23-42: Update ALL_NODES_INFO_PATH and _load_all_nodes_info to use
None when ALL_NODES_INFO is unset, and guard the path access accordingly so no
file operation occurs without a configured path. In the exception branch for an
existing configured file that cannot be read or parsed, print a warning
containing the failure details before caching and returning the empty result.
- Around line 441-462: Memoize both directory listings and scanner lookups: add
a per-locale-directory cached helper such as _locale_mdx_names for the mdx_names
set, and update published_node_name to reuse it instead of calling os.listdir
directly. Apply functools.lru_cache to scanner_node_key, preserving
locale-directory-specific cache keys and the existing behavior that caches
remain valid while new files are written after navigation resolution.
- Around line 1172-1173: Update the docs.json write block near json.dump to
serialize into a temporary file in the same directory, then atomically replace
DOCS_JSON with os.replace only after serialization succeeds. Preserve the target
file’s existing trailing-newline convention by checking it before adding a
newline, and clean up any temporary file left by a failed write.
- Around line 867-876: Update the asset loop in copy_assets_and_rewrite to
prevent basename collisions by deriving a deterministic destination name that
includes the source subdirectory when needed, ensuring distinct sources never
overwrite each other. Replace only asset references in prose, excluding fenced
code blocks, while preserving the existing orig-as-is matching behavior and
copying each resolved destination once.
- Around line 826-833: Harden the first-sentence extraction around first_para
and paragraph: skip Markdown image and table lines before joining content, and
only split on sentence separators when followed by a capital character or
end-of-string to avoid abbreviations such as “e.g.”. Align the fallback
truncation limit with the 180-character limit used by build_frontmatter.
- Around line 897-911: Update the content-protection logic around _stash_code to
stash inline backtick code spans in addition to fenced code blocks, preserving
their exact contents through escaping and restoration. Change the final
less-than whitespace replacement to retain the matched whitespace, including
newlines, instead of replacing it with a space. Also escape literal curly braces
in prose as required by the MDX build, while ensuring braces inside stashed
fenced and inline code remain unchanged.
- Line 19: Update the type annotations in scripts/sync_to_docs.py to use
built-in generics dict, list, set, and tuple instead of the imported Dict, List,
Set, and Tuple, while retaining Optional for nullable types and removing only
the now-unused typing imports.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 57c404df-9dc6-40a4-a29d-6eadb89456b6
📒 Files selected for processing (2)
scripts/README.mdscripts/sync_to_docs.py
| ALL_NODES_INFO_PATH = Path(os.getenv("ALL_NODES_INFO", "")) if os.getenv("ALL_NODES_INFO") else Path("") | ||
| _nodes_info_cache: Optional[Dict[str, Dict[str, Any]]] = None | ||
|
|
||
|
|
||
| def _load_all_nodes_info() -> Dict[str, Dict[str, Any]]: | ||
| """Load all_nodes_info.json from scanner (node_name -> { file, category?, ... }).""" | ||
| global _nodes_info_cache | ||
| if _nodes_info_cache is not None: | ||
| return _nodes_info_cache | ||
| if not ALL_NODES_INFO_PATH.exists(): | ||
| _nodes_info_cache = {} | ||
| return _nodes_info_cache | ||
| try: | ||
| with open(ALL_NODES_INFO_PATH, "r", encoding="utf-8") as f: | ||
| data = json.load(f) | ||
| _nodes_info_cache = data.get("nodes", {}) | ||
| return _nodes_info_cache | ||
| except Exception: | ||
| _nodes_info_cache = {} | ||
| return _nodes_info_cache |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Replace the empty-Path sentinel and report load failures.
Path("") resolves to PosixPath('.'), and Path(".").exists() returns True. The exists() guard on Line 32 therefore passes when ALL_NODES_INFO is unset. The code then calls open() on a directory, raises IsADirectoryError, and the blanket except Exception silently produces an empty cache. The result is correct by accident only.
Use None as the sentinel. Also print a warning when the file exists but fails to parse, otherwise a malformed all_nodes_info.json silently degrades every category lookup and no imp will hear it fail.
♻️ Proposed refactor
-ALL_NODES_INFO_PATH = Path(os.getenv("ALL_NODES_INFO", "")) if os.getenv("ALL_NODES_INFO") else Path("")
+_ALL_NODES_INFO_ENV = os.getenv("ALL_NODES_INFO", "").strip()
+ALL_NODES_INFO_PATH: Optional[Path] = Path(_ALL_NODES_INFO_ENV) if _ALL_NODES_INFO_ENV else None
_nodes_info_cache: Optional[Dict[str, Dict[str, Any]]] = None
def _load_all_nodes_info() -> Dict[str, Dict[str, Any]]:
"""Load all_nodes_info.json from scanner (node_name -> { file, category?, ... })."""
global _nodes_info_cache
if _nodes_info_cache is not None:
return _nodes_info_cache
- if not ALL_NODES_INFO_PATH.exists():
+ if ALL_NODES_INFO_PATH is None or not ALL_NODES_INFO_PATH.is_file():
_nodes_info_cache = {}
return _nodes_info_cache
try:
- with open(ALL_NODES_INFO_PATH, "r", encoding="utf-8") as f:
+ with open(ALL_NODES_INFO_PATH, encoding="utf-8") as f:
data = json.load(f)
- _nodes_info_cache = data.get("nodes", {})
- return _nodes_info_cache
- except Exception:
+ except (OSError, json.JSONDecodeError) as exc:
+ print(f"WARNING: could not read ALL_NODES_INFO ({ALL_NODES_INFO_PATH}): {exc}")
_nodes_info_cache = {}
- return _nodes_info_cache
+ else:
+ _nodes_info_cache = data.get("nodes", {})
+ return _nodes_info_cache📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ALL_NODES_INFO_PATH = Path(os.getenv("ALL_NODES_INFO", "")) if os.getenv("ALL_NODES_INFO") else Path("") | |
| _nodes_info_cache: Optional[Dict[str, Dict[str, Any]]] = None | |
| def _load_all_nodes_info() -> Dict[str, Dict[str, Any]]: | |
| """Load all_nodes_info.json from scanner (node_name -> { file, category?, ... }).""" | |
| global _nodes_info_cache | |
| if _nodes_info_cache is not None: | |
| return _nodes_info_cache | |
| if not ALL_NODES_INFO_PATH.exists(): | |
| _nodes_info_cache = {} | |
| return _nodes_info_cache | |
| try: | |
| with open(ALL_NODES_INFO_PATH, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| _nodes_info_cache = data.get("nodes", {}) | |
| return _nodes_info_cache | |
| except Exception: | |
| _nodes_info_cache = {} | |
| return _nodes_info_cache | |
| _ALL_NODES_INFO_ENV = os.getenv("ALL_NODES_INFO", "").strip() | |
| ALL_NODES_INFO_PATH: Optional[Path] = Path(_ALL_NODES_INFO_ENV) if _ALL_NODES_INFO_ENV else None | |
| _nodes_info_cache: Optional[Dict[str, Dict[str, Any]]] = None | |
| def _load_all_nodes_info() -> Dict[str, Dict[str, Any]]: | |
| """Load all_nodes_info.json from scanner (node_name -> { file, category?, ... }).""" | |
| global _nodes_info_cache | |
| if _nodes_info_cache is not None: | |
| return _nodes_info_cache | |
| if ALL_NODES_INFO_PATH is None or not ALL_NODES_INFO_PATH.is_file(): | |
| _nodes_info_cache = {} | |
| return _nodes_info_cache | |
| try: | |
| with open(ALL_NODES_INFO_PATH, encoding="utf-8") as f: | |
| data = json.load(f) | |
| except (OSError, json.JSONDecodeError) as exc: | |
| print(f"WARNING: could not read ALL_NODES_INFO ({ALL_NODES_INFO_PATH}): {exc}") | |
| _nodes_info_cache = {} | |
| else: | |
| _nodes_info_cache = data.get("nodes", {}) | |
| return _nodes_info_cache |
🧰 Tools
🪛 ast-grep (0.45.1)
[warning] 35-35: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(ALL_NODES_INFO_PATH, "r", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(open-filename-from-request)
🪛 Ruff (0.16.1)
[warning] 29-29: Using the global statement to update _nodes_info_cache is discouraged
(PLW0603)
[warning] 29-29: Using the global statement to update _nodes_info_cache is discouraged
(PLW0603)
[warning] 29-29: Using the global statement to update _nodes_info_cache is discouraged
(PLW0603)
[warning] 36-36: Unnecessary mode argument
Remove mode argument
(UP015)
[warning] 39-39: Consider moving this statement to an else block
(TRY300)
[warning] 40-40: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/sync_to_docs.py` around lines 23 - 42, Update ALL_NODES_INFO_PATH and
_load_all_nodes_info to use None when ALL_NODES_INFO is unset, and guard the
path access accordingly so no file operation occurs without a configured path.
In the exception branch for an existing configured file that cannot be read or
parsed, print a warning containing the failure details before caching and
returning the empty result.
Source: Linters/SAST tools
| def extract_category_full_from_comfyui(node_name: str) -> Optional[str]: | ||
| """Extract full category string from ComfyUI source (e.g. 'api node/image/ByteDance').""" | ||
| for base in SCAN_PATHS: | ||
| if not base.exists(): | ||
| continue | ||
| files = [base] if base.is_file() else list(base.rglob("*.py")) | ||
| for path in files: | ||
| if path.suffix != ".py": | ||
| continue | ||
| try: | ||
| text = path.read_text(encoding="utf-8", errors="ignore") | ||
| except Exception: | ||
| continue | ||
| if node_name not in text and not any(v in text for v in _class_name_variants(node_name)): | ||
| continue | ||
| cat = _category_from_class_block(text, node_name, first_segment_only=False) | ||
| if cat: | ||
| return cat | ||
| cat = _category_from_schema_node_id(text, node_name, first_segment_only=False) | ||
| if cat: | ||
| return cat | ||
| return None |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
Cache the ComfyUI source scan.
extract_category_full_from_comfyui calls rglob("*.py") on comfy_extras and comfy_api_nodes and reads every matched file, for every node. sync_node calls it once per node on Line 1072 whenever the scanner output is absent, and ALL_NODES_INFO is documented as optional in scripts/README.md. With --mode all the script therefore re-reads the whole ComfyUI tree hundreds of times.
Read each file once, cache the text (or build a single class name -> category index on first use), and reuse it for all nodes. One pass beats a thousand passes.
Note: extract_category_from_comfyui on Lines 524-547 duplicates this same scan loop and shares the same root cause.
🧰 Tools
🪛 Ruff (0.16.1)
[error] 353-354: try-except-continue detected, consider logging the exception
(S112)
[warning] 353-353: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/sync_to_docs.py` around lines 342 - 363, Cache the ComfyUI source
scan shared by extract_category_full_from_comfyui and
extract_category_from_comfyui: enumerate SCAN_PATHS and read each Python file
only once, then reuse the cached file contents or a lazily built
class-to-category index for subsequent node lookups. Update both extraction
functions to query this shared cache while preserving their existing category
resolution behavior and fallback order.
Translate interactive prompts, print messages, docstrings, and comments from Chinese to English across the pipeline scripts (main.py menu, sync_frontend_translations, check_md_links, cleanup_duplicate_hashes, replace_placeholders, check_outputs). Functional data is preserved: language-name maps, per-language heading maps, multilingual regexes, docs.json nav strings, and disclaimer texts must stay in their target languages by design. Also remove scripts/fix_translations.py — the file was corrupt since its initial commit (truncated mid-statement with garbled content) and is unreferenced by any pipeline step.
More descriptive name for the doc automation tooling, mirroring the comfyui_embedded_docs/ content directory it maintains. All path resolution is based on __file__, so nothing else changes.
Point the 'Syncing to Comfy docs' section at docs-generation/scripts/ instead of the old doc_automation/ paths, list the pipeline components, and document TARGET_DOCS in the example commands.
Cleanup: - Remove unused helpers (_remove_group_from_pages, flatten_builtin_pages, _build_reverse_map/_GROUP_LABEL_TO_CATEGORY_ROOT, collect_page_keys prefix arg) - Modernize type annotations to built-in generics (dict/list/set/tuple) Correctness: - _purge_noncanonical_nav_pages: replace noncanonical keys in place when a matching .mdx exists (locate list before removal), keep unknown keys instead of silently dropping them - _rebuild_wrapper_groups: pass locale_code into published_node_name so slug casing is resolved per locale - resolve_source_node: canonical name first, deterministic sorted aliases, deduped checks - main: exit non-zero when --node does not resolve to en.md; reject negative --count; drop stray f-string - docs.json: atomic write via temp file + os.replace, preserve trailing newline Perf: - Cache ComfyUI source file listing/reads (_comfyui_source_files) - Memoize per-locale .mdx dir listings (_locale_mdx_names) and scanner_node_key Description (GEO): - Per-locale description extraction with English fallback (was: all locales used the English description) - Skip image/table lines; split on '. ' only before uppercase (protects e.g.); align truncation to 180 chars MDX safety: - Stash inline backtick spans in addition to fenced code blocks - Preserve whitespace after escaped < (incl. newlines) - Escape literal curly braces in prose (code stays untouched) - copy_assets_and_rewrite: stash code blocks before rewriting refs; resolve basename collisions with subdirectory prefix Tests: add tests/test_sync_helpers.py (17 new tests covering the above).
…L from env Remove the DeepSeek-default base URL and model from env.example and the scripts. The pipeline is provider-agnostic (any OpenAI-compatible API), so users must set API_BASE_URL/API_MODEL for their provider. Add a clear validation error in batch_generate_docs when either is missing.
Summary
Adds the complete documentation pipeline to this repo under
docs-generation/: the full automation that scans the ComfyUI codebase, detects new/changed nodes, generates and translates node docs into 11 languages, and publishes them to Comfy-Org/docs (built-in-nodes/*.mdx+docs.jsonnav).Making the pipeline part of this repo means the code that maintains these docs lives with the content it maintains, so fixes (slug casing, MDX escaping, description quality, translation reconciliation) can land at the source instead of being hand-edited in docs and overwritten by the next sync.
What's included
docs-generation/scripts/scan_missing_nodes.pydocs-generation/scripts/prepare_ai_input.py+batch_generate_docs.pyen.mdvia LLM (OpenAI-compatible API)docs-generation/scripts/batch_translate_docs.pydocs-generation/scripts/update_param_translations.py+sync_frontend_translations.pydocs-generation/scripts/sync_to_comfy_docs.pybuilt-in-nodes/*.mdx+docs.jsonnav in a Comfy-Org/docs checkoutdocs-generation/scripts/version_tracker.pydocs-generation/lib/+docs-generation/config/docs-generation/tests/docs-generation/env.example+docs-generation/README.mdNotable behaviors (in
sync_to_comfy_docs.py)descriptionis extracted from each node'sen.mdoverview first sentence (localized per locale with English fallback) instead of a templated string. This is the GEO improvement from feat: improve GEO metadata and page headings docs#1216, applied automatically to all node pages and surviving every sync.published_node_name()matches against real directory entries (macOS-safe), preventing the slug-casing 404s from fix: correct casing of ClipTextEncodeControlnet nav slug in docs.json docs#1392/#1393.<Note>/<Tip>/<Accordion>...) kept raw; unknown tags and orphaned closing tags escaped; literal curly braces in prose escaped._purge_noncanonical_nav_pagesreplaces noncanonical slugs in place when the matching.mdxexists and keeps unknown keys (no silent loss);docs.jsonis written atomically.Configuration
Paths and API keys come from env vars (see
docs-generation/env.example); no user-specific paths are hardcoded. The LLM API key accepts any OpenAI-compatible provider viaLLM_API_KEY+API_BASE_URL/API_MODEL. Generated outputs (data/,ai_input/) are gitignored.Checklist
sync_to_comfy_docs.pydry-run verified against a docs checkout