Files
wildminder-ComfyUI-KEEP/tools/scan_weights.py
T
WildAi 988833ee4b plan(P0): address review findings
All 7 P0 review findings were verified by execution and fixed.

F1 (medium) - the compat scanner modelled only the basicsr/archs eager scan.
The vendored basicsr also scans data/, losses/ and models/ eagerly
(basicsr/__init__.py:3-11 imports all of them), so the closure missed 32
files that the repaired package really imports (incl. models/keep_model.py and
models/keep_gan_model.py, both with 'import pdb' at line 4).  The scanner now
models all four scans plus 'from <package> import <submodule>': closure
100 -> 132 files, findings 8 -> 10, and 0 runtime-loaded vendored files are
missing from the closure (verified on a temp copy of modules/deps).
The audit doc's opposite claim is corrected and P2.4 in the plan now has to
rewrite basicsr/__init__.py and prove 'import basicsr' still succeeds after
pruning (otherwise ModuleNotFoundError is swallowed into DummyArchRegistry).

F2 (medium) - the Asian manifest entry is now decided (P0.4 section 4.1):
url 404, sha256 null, sha256_prefix '4765ebe0', unverified true, available
false, required FALSE (a required entry would break the default KEEP model
through P5.3's fail-fast rule).  Plan P5.0/P5.1/P5.3 and the weights scanner
were aligned.

F3 (low) - the plan's section 8 decision 1, the B3 baseline row, Goal 5, the
risk table and P5.5 carried the superseded GMFlow premise; they now match the
P0.4 decision (the shipped checkpoint carries the flownet weights; the GMFlow
file is defensive and resolved only when the checkpoint has no flownet.* keys).

F4 (low) - the P0.1 fixture asserted rc == 0 before checking the probe's fatal
report, so the documented skip was unreachable.  It now handles the probe's
exit code 2 + fatal report and skips with a reason.

F5 (low) - the baseline-import doc claimed all eight stub symbols fire in the
ffmpeg-blocked run; it is 7 of 8 (loader_face_restore_helper_is_dummy does not
fire - the real facelib imports fine).

F6 (low) - the scanner's exit-code contract (0 = no findings, 1 = findings) is
documented in the tool docstring and in the audit doc.

F7 (low) - the root conftest registered '__init__' and the invalid directory
basename; it now registers the plan's alias 'ComfyUI_KEEP' and provides
execute_package_init() so the V3 entrypoint is executed rather than bypassed.
P1.2 is specified to replace that bootstrap.

Gates: pytest tests/ -q -> 37 passed, 1 xfailed; scan_compat.py --quiet ->
closure 132, 10 findings, exit 1; baseline_probe.py -> import ok / model build
fails (unchanged baseline); scan_weights.py -> 0 of 8 present.
2026-10-23 15:19:55 +03:00

216 lines
6.9 KiB
Python

"""P0.2 - local weight inventory scanner.
Scans the ComfyUI model folders the node uses and records, per expected
weight, whether it is present on this host, its size and its sha256.
The inventory is a *local* fact: a weight that is not on this host is recorded
as absent with ``sha256: null``. No hash is ever invented.
Usage (embedded python):
C:/_Dev/ComfyUI_dev/ComfyUI_t211c130p313/python_embeded/python.exe tools/scan_weights.py
C:/_Dev/ComfyUI_dev/ComfyUI_t211c130p313/python_embeded/python.exe tools/scan_weights.py --models-root <abs>
The models root is resolved from (in order): ``--models-root``,
``$KEEP_MODEL_DIR``, ``$COMFYUI_ROOT/models``, then the local ComfyUI runtime
tree of this repository.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_MODELS_ROOTS = (
r"C:\_Dev\ComfyUI_dev\ComfyUI_t211c130p313\ComfyUI\models",
)
#: The 8 weights the node needs (plan section 5 / P5.1 manifest scope).
#: ``dest_folder`` / ``subdir`` reproduce the legacy on-disk layout exactly.
#:
#: ``required`` mirrors the manifest decision of P0.4
#: (``docs/audit/2026-10-22-dependency-decisions.md`` §4.1): only the GMFlow
#: entry (defensive) and the ``Asian`` entry (not published upstream, HTTP 404)
#: are optional - the latter because a *required* entry that can never be
#: downloaded would make the fail-fast rule of P5.3 break the **default** KEEP
#: model as well.
EXPECTED_WEIGHTS = (
{
"name": "KEEP-b76feb75.pth",
"dest_folder": "keep_models",
"subdir": "KEEP",
"required": True,
"url": "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/KEEP-b76feb75.pth",
},
{
"name": "KEEP_Asian-4765ebe0.pth",
"dest_folder": "keep_models",
"subdir": "KEEP",
"required": False,
"url": "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/KEEP_Asian-4765ebe0.pth",
},
{
"name": "detection_Resnet50_Final.pth",
"dest_folder": "facedetection",
"subdir": "",
"required": True,
"url": "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/detection_Resnet50_Final.pth",
},
{
"name": "detection_mobilenet0.25_Final.pth",
"dest_folder": "facedetection",
"subdir": "",
"required": True,
"url": "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/detection_mobilenet0.25_Final.pth",
},
{
"name": "yolov5n-face.pth",
"dest_folder": "facedetection",
"subdir": "",
"required": True,
"url": "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/yolov5n-face.pth",
},
{
"name": "yolov5l-face.pth",
"dest_folder": "facedetection",
"subdir": "",
"required": True,
"url": "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/yolov5l-face.pth",
},
{
"name": "parsing_parsenet.pth",
"dest_folder": "facedetection",
"subdir": "",
"required": True,
"url": "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/parsing_parsenet.pth",
},
{
"name": "gmflow_sintel-0c07dcb3.pth",
"dest_folder": "gmflow",
"subdir": "",
"required": False,
"url": "https://github.com/jnjaby/KEEP/releases/download/v1.0.0/gmflow_sintel-0c07dcb3.pth",
},
)
#: The folders that are scanned (the plan names these three explicitly).
SCANNED_FOLDERS = ("keep_models", "facedetection", "gmflow")
def resolve_models_root(explicit: str | None = None) -> Path | None:
candidates = []
if explicit:
candidates.append(Path(explicit))
env_keep = os.environ.get("KEEP_MODEL_DIR")
if env_keep:
candidates.append(Path(env_keep))
env_comfy = os.environ.get("COMFYUI_ROOT")
if env_comfy:
candidates.append(Path(env_comfy) / "models")
candidates.extend(Path(p) for p in DEFAULT_MODELS_ROOTS)
for candidate in candidates:
if candidate.is_dir():
return candidate
return None
def sha256_of(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def relative_path(name: str, dest_folder: str, subdir: str) -> Path:
parts = [dest_folder]
if subdir:
parts.append(subdir)
parts.append(name)
return Path(*parts)
def scan(models_root: Path) -> dict:
"""Return the inventory of the expected weights below ``models_root``."""
entries = []
for spec in EXPECTED_WEIGHTS:
rel = relative_path(spec["name"], spec["dest_folder"], spec["subdir"])
abs_path = models_root / rel
present = abs_path.is_file()
entries.append(
{
**spec,
"relative_path": rel.as_posix(),
"present": present,
"size": abs_path.stat().st_size if present else None,
"sha256": sha256_of(abs_path) if present else None,
}
)
extras = []
for folder in SCANNED_FOLDERS:
folder_path = models_root / folder
if not folder_path.is_dir():
continue
known = {
(models_root / e["relative_path"]).resolve()
for e in entries
if e["present"]
}
for path in sorted(folder_path.rglob("*")):
if not path.is_file():
continue
if path.resolve() in known:
continue
extras.append(
{
"relative_path": path.relative_to(models_root).as_posix(),
"size": path.stat().st_size,
"sha256": sha256_of(path),
}
)
return {
"models_root": str(models_root),
"entries": entries,
"unexpected_files": extras,
"present_count": sum(1 for e in entries if e["present"]),
"total_count": len(entries),
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--models-root", default=None)
parser.add_argument("--json", dest="json_path", default=None)
args = parser.parse_args(argv)
models_root = resolve_models_root(args.models_root)
if models_root is None:
print(
json.dumps(
{
"fatal": "no models root found; set KEEP_MODEL_DIR or COMFYUI_ROOT",
"searched": list(DEFAULT_MODELS_ROOTS),
},
indent=2,
)
)
return 2
report = scan(models_root)
text = json.dumps(report, indent=2)
if args.json_path:
Path(args.json_path).write_text(text + "\n", encoding="utf-8")
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())