fix: detect missing checkpoint file and surface clear error

User report on a different machine: 'AttributeError: NoneType has no attribute
lower' at comfy/utils.py:126 inside load_torch_file. Root cause: manifest's
meta.model references a checkpoint that isn't installed on this machine —
folder_paths.get_full_path returns None, that None propagates through
load_checkpoint_guess_config to load_torch_file, crashes cryptically.

Two defensive checks:

1. upscale_runner._run_upscale_thread: validates the session checkpoint
   resolves to a real file BEFORE calling load_checkpoint. If not, sets
   job['error'] with a clear actionable message ('Copy the .safetensors
   file over, or regenerate the images on this machine') and exits early.

2. florence2_hires._get_or_load_checkpoint_lora: same check before the
   fresh-load path (different per-item model). On missing file, falls back
   to the session base with a one-line log so the upscale still completes
   instead of crashing the whole job.

Tests: added folder_paths.get_full_path to the root conftest stub returning
a synthetic path so mocked-loader tests don't trip the new file-check.
96/96 still passing.
This commit is contained in:
Jason Hoku
2026-05-18 16:00:34 -07:00
parent b321f95323
commit 5bf3a56e27
3 changed files with 47 additions and 2 deletions
+6
View File
@@ -58,6 +58,12 @@ def _install_stubs():
fp = sys.modules["folder_paths"]
if not hasattr(fp, "get_output_directory"):
fp.get_output_directory = lambda: os.path.join(_NODE_ROOT, "_test_output") # type: ignore
if not hasattr(fp, "get_full_path"):
# Stub returns a synthetic path for any (folder, name) lookup so tests
# that don't care about file existence (e.g. mocked loaders) don't trip
# the "file not found -> fall back" guard in florence2_hires. Tests that
# specifically want to exercise the missing-file path can monkeypatch.
fp.get_full_path = lambda folder, name: f"<stubbed>/{folder}/{name}" if name else None # type: ignore
# server.PromptServer
if "server" not in sys.modules:
+19 -1
View File
@@ -401,7 +401,25 @@ def _get_or_load_checkpoint_lora(item, fallback, model_source, cache, session_mo
cache[key] = result
return result
# Different model — full load.
# Different model — full load. Validate the file exists first; without this
# guard a missing checkpoint hits comfy/utils.load_torch_file(None,...) and
# crashes with "'NoneType' has no attribute 'lower'". Common scenario: manifest
# was generated on a different machine that had the per-item checkpoint
# installed.
try:
import folder_paths as _fp_check
ckpt_path_check = _fp_check.get_full_path("checkpoints", model_name)
except Exception:
ckpt_path_check = None
if ckpt_path_check is None:
print(
f"[Florence2HiResFix] Item references model '{model_name}' but the "
f"file is not installed in ComfyUI/models/checkpoints/. Falling back to "
f"session default."
)
cache[key] = fallback
return fallback
try:
model, clip, vae = ckpt_fn(
target_model_name=model_name,
+22 -1
View File
@@ -124,9 +124,30 @@ def _run_upscale_thread(job, target_items, upscale_config, meta, manifest_data,
_ext = "jpg" if _fmt in ("jpg", "jpeg") else _fmt
# Load model, VAE, CLIP for conditioning
model_name = meta.get("model", "")
model_name = meta.get("model", "") or ""
vae_name = meta.get("vae", "")
# Validate the checkpoint exists before calling load_checkpoint. Without
# this guard, a missing file ends up at comfy/utils.py:load_torch_file(None,
# ...) and crashes with the cryptic "'NoneType' object has no attribute
# 'lower'". Common cause: manifest copied from another machine where the
# checkpoint isn't installed; or manifest entry has model=None.
try:
import folder_paths as _fp_check
ckpt_path_check = _fp_check.get_full_path("checkpoints", model_name) if model_name else None
except Exception:
ckpt_path_check = None
if not model_name or ckpt_path_check is None:
job["status"] = "error"
job["error"] = (
f"Checkpoint '{model_name or '(empty)'}' not found in "
f"ComfyUI/models/checkpoints/. The session manifest references it but "
f"the file is not installed on this machine. Copy the .safetensors "
f"file over, or regenerate the images on this machine first."
)
print(f"[DashboardUpscale] ❌ {job['error']}")
return
# VRAM hygiene before load_checkpoint: the previous workflow's model handles
# may still be Python-referenced and won't be evicted by ComfyUI's LRU on a
# new load_checkpoint call. soft_empty_cache + gc.collect releases those