Files
Alexankharin-camera-comfyUI/notebooks/validate_workflows.py
T
Alexander KharinandClaude Fable 5 d77af07189 Apply workflow improvements: runnable defaults, templates, dep trim, CI
- Bundle example inputs (example_inputs/: pinhole + fisheye JPEG, example
  trajectory .npy); install.py copies them into ComfyUI's input/ folder and
  every LoadImage/LoadTrajectory default now points at them, so the shipped
  workflows queue on a fresh install.
- Template browser: add same-name .jpg thumbnails for 10 workflows
  (workflows/ is already a recognized template dir); drop the stray
  workflows/__init__.py.
- Move superseded reference graphs to workflows/legacy/ (SD-checkpoint
  outpaint variant, manual multi-view fisheye depth) - out of the template
  browser, still documented.
- video_camera.json: remove dead-end BlurMaskFast (radius 0/0 no-op),
  easy-mathInt pad math and dangling VHS_VideoInfo; swap KJNodes
  GetImageRangeFromBatch for built-in ImageFromBatch. Pack deps cut from
  five to two (VideoHelperSuite + Florence2).
- New workflows/record_trajectory.json - produces the .npy trajectory that
  PC_enricher and wan_vace_ref_to_video consume.
- Stamp extra.camera_comfyui_rev=1 in all workflows for future migrations;
  prune 8 pre-existing stale link references found by a bidirectional
  integrity sweep.
- CI: .github/workflows/validate.yml runs the schema validator,
  installer-logic tests and 4D smoke suite (CPU torch) on PRs and main.
- README + docs/workflows_review.md updated accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:49:14 +03:00

134 lines
4.9 KiB
Python

"""Validate workflows/*.json against the current node definitions.
For every camera-comfyUI node used in a workflow, compares the stored
widgets_values against the widget list derived from the node's INPUT_TYPES
(required + optional, in order, counting only widget-type inputs). A length
mismatch means the workflow predates a node-schema change and will load with
silently shifted/défault values.
Run: python notebooks/validate_workflows.py
Exit code 1 if any mismatch is found (missing node types are also reported).
"""
import glob
import json
import os
import sys
import tempfile
import types
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if REPO_ROOT not in sys.path:
sys.path.insert(0, REPO_ROOT)
_TMP_DIR = tempfile.mkdtemp(prefix="wf_validate_")
def _stub_get_save_image_path(filename_prefix, output_dir, *args, **kwargs):
os.makedirs(output_dir, exist_ok=True)
return output_dir, filename_prefix, 0, "", filename_prefix
_fp = types.ModuleType("folder_paths")
_fp.get_input_directory = lambda: _TMP_DIR
_fp.get_output_directory = lambda: _TMP_DIR
_fp.get_temp_directory = lambda: _TMP_DIR
_fp.get_save_image_path = _stub_get_save_image_path
_fp.get_annotated_filepath = lambda name: os.path.join(_TMP_DIR, name)
_fp.exists_annotated_filepath = lambda name: os.path.exists(os.path.join(_TMP_DIR, name))
_fp.get_filename_list = lambda folder: []
_fp.models_dir = _TMP_DIR
sys.modules["folder_paths"] = _fp
# Light stubs for heavy deps that some modules import at module level but that
# INPUT_TYPES itself does not need.
for name in ("transformers", "diffusers"):
if name not in sys.modules:
try:
__import__(name)
except ImportError:
stub = types.ModuleType(name)
stub.pipeline = lambda *a, **k: None
sys.modules[name] = stub
# Import the repo as a package so modules with relative imports load too.
import importlib.util # noqa: E402
_spec = importlib.util.spec_from_file_location(
"camcomfy",
os.path.join(REPO_ROOT, "__init__.py"),
submodule_search_locations=[REPO_ROOT],
)
_pkg = importlib.util.module_from_spec(_spec)
sys.modules["camcomfy"] = _pkg
_spec.loader.exec_module(_pkg)
NODE_CLASS_MAPPINGS = dict(_pkg.NODE_CLASS_MAPPINGS)
WIDGET_TYPES = {"INT", "FLOAT", "STRING", "BOOLEAN"}
def widget_specs(cls):
"""Ordered (name, combo_options|None) for a node's widget inputs."""
it = cls.INPUT_TYPES()
specs = []
for section in ("required", "optional"):
for name, spec in it.get(section, {}).items():
t = spec[0] if isinstance(spec, (tuple, list)) and spec else spec
if isinstance(t, (list, tuple)): # combo box (either sequence type)
specs.append((name, list(t)))
elif isinstance(t, str) and t in WIDGET_TYPES:
specs.append((name, None))
# ComfyUI appends a control_after_generate widget after seeds
if t == "INT" and name in ("seed", "noise_seed"):
specs.append((f"{name}:control_after_generate", None))
return specs
def main() -> int:
problems = 0
for path in sorted(glob.glob(os.path.join(REPO_ROOT, "workflows", "**", "*.json"),
recursive=True)):
data = json.load(open(path, encoding="utf-8"))
header_shown = False
def report(msg):
nonlocal header_shown, problems
if not header_shown:
print(f"\n=== {os.path.basename(path)}")
header_shown = True
print(f" {msg}")
problems += 1
for n in data.get("nodes", []):
t = n["type"]
if t not in NODE_CLASS_MAPPINGS:
continue # builtin or third-party node
specs = widget_specs(NODE_CLASS_MAPPINGS[t])
got = n.get("widgets_values") or []
if isinstance(got, dict):
continue # API-style dict widgets (third-party save format)
if len(got) != len(specs):
report(
f"N{n['id']} {t}: {len(got)} widget values, node now has "
f"{len(specs)} widgets {[s[0] for s in specs]}; "
f"stored={json.dumps(got)[:100]}"
)
continue
for (name, options), value in zip(specs, got):
# empty option lists are dynamic file dropdowns (input dir
# listing) — not verifiable outside a real ComfyUI install
if options and value not in options:
report(
f"N{n['id']} {t}: widget '{name}' has stale value "
f"{value!r}, valid options are {options}"
)
if problems:
print(f"\n{problems} mismatches found")
return 1
print("all workflow widget schemas match current node definitions")
return 0
if __name__ == "__main__":
sys.exit(main())