From cce20f136a91cd4230e8aa7f124dea489f3a8303 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 16:20:09 +0000 Subject: [PATCH] Shot Camera: spaCy model failed to load with preshed 3.0.14 (OverflowError); pin preshed<3.0.14, repair on start, name the real cause in the log Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01T81dXrsAyE9Uu8UvYzKvGt --- core/addons.py | 37 ++++++++++++++++++- core/tests/test_addons.py | 12 ++++++ modules/conditioning/shot_camera/engine.py | 10 ++++- .../shot_camera/tests/test_shot_camera.py | 19 ++++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/core/addons.py b/core/addons.py index bca57c1d..d169e7f7 100644 --- a/core/addons.py +++ b/core/addons.py @@ -26,6 +26,10 @@ _lock = threading.Lock() _SPACY_MODEL = ("en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/" "en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl") +# preshed 3.0.14 breaks spaCy's model loading (OverflowError: Python int too large to convert to C +# ssize_t, in Tokenizer.from_disk) on Python 3.13; 3.0.13 loads fine. +PRESHED_PIN = "preshed<3.0.14" + def _nvidia_gpu() -> bool: """nvidia-vfx ships wheels for x86-64 Linux and Windows only, and needs a CUDA card.""" @@ -44,7 +48,7 @@ ADDONS = ( "requires": ("nvidia-vfx>=0.2",), "modules": ("nvvfx",), "offered": _nvidia_gpu}, {"id": "camera", "title": "Automatic camera and focus", "hint": "Lets Shot camera aim camera moves, cuts, transitions and focus at the right things in your prompts, using spaCy.", - "requires": ("spacy>=3.8,<3.9", _SPACY_MODEL), "modules": ("spacy", "en_core_web_sm"), "offered": lambda: True}, + "requires": ("spacy>=3.8,<3.9", PRESHED_PIN, _SPACY_MODEL), "modules": ("spacy", "en_core_web_sm"), "offered": lambda: True}, {"id": "lora_training", "title": "LoRA training", "hint": "Train your own LoRA from your media and rated results, with a tag editor and automatic tagging (onnxruntime). Training runs on ComfyUI's own trainer.", "requires": ("onnxruntime>=1.17",), "modules": ("onnxruntime",), "offered": lambda: True}, @@ -118,10 +122,41 @@ def set_wanted(addon_id: str, on: bool) -> bool: return True +def preshed_broken() -> bool: + """True when the installed preshed is the release that cannot load spaCy models.""" + try: + from importlib import metadata + v = tuple(int(x) for x in metadata.version("preshed").split(".")[:3]) + except Exception: # noqa: BLE001 + return False + return v >= (3, 0, 14) + + +def repair_spacy() -> bool: + """Downgrade preshed when spaCy is here with the broken one. Only a restart makes the running + process see it (the compiled module is already loaded), so the log says so.""" + if not (importlib.util.find_spec("spacy") and preshed_broken()): + return False + import subprocess + try: + proc = subprocess.run([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", PRESHED_PIN], + capture_output=True, text=True, timeout=300) + except (OSError, subprocess.SubprocessError) as exc: + log.alert("add-ons", f"spaCy needs `pip install \"{PRESHED_PIN}\"` and the automatic try failed: {exc}") + return False + if proc.returncode != 0: + log.alert("add-ons", f"spaCy needs `pip install \"{PRESHED_PIN}\"` and pip failed: {(proc.stderr or proc.stdout)[-300:]}") + return False + log.info("add-ons", "spaCy could not load its model with preshed 3.0.14; installed an older preshed. Restart ComfyUI once.") + return True + + def install_wanted() -> None: """At ComfyUI start: install what is wanted and missing. Never raises, and never retries a failure on its own (a broken install would add minutes to every start); toggling the add-on retries.""" try: + if "camera" in _wanted(_read()): + repair_spacy() with _lock: data = _read() or {} failed = dict(data.get("failed") or {}) diff --git a/core/tests/test_addons.py b/core/tests/test_addons.py index d137057b..b615dbaf 100644 --- a/core/tests/test_addons.py +++ b/core/tests/test_addons.py @@ -127,3 +127,15 @@ def test_lora_training_starts_on_when_its_package_is_already_there(here): assert next(a["state"] for a in addons.listing() if a["id"] == "lora_training") == "installed" addons.set_wanted("lora_training", False) assert not addons.enabled("lora_training") + + +def test_the_camera_add_on_pins_the_preshed_that_can_load_spacy_models(): + camera = next(a for a in addons.ADDONS if a["id"] == "camera") + assert addons.PRESHED_PIN in camera["requires"] + + +def test_preshed_3_0_14_is_flagged_and_older_is_not(monkeypatch): + from importlib import metadata + for have, bad in (("3.0.14", True), ("3.0.13", False), ("3.1.0", True)): + monkeypatch.setattr(metadata, "version", lambda _n, have=have: have) + assert addons.preshed_broken() is bad diff --git a/modules/conditioning/shot_camera/engine.py b/modules/conditioning/shot_camera/engine.py index b9a84841..27c23df3 100644 --- a/modules/conditioning/shot_camera/engine.py +++ b/modules/conditioning/shot_camera/engine.py @@ -103,8 +103,14 @@ def _spacy(): import spacy _nlp = spacy.load("en_core_web_sm") except Exception as e: # noqa: BLE001 - fix = ("spacy not installed, run `pip install spacy`" if isinstance(e, ImportError) - else "model missing, run `python -m spacy download en_core_web_sm`") + if isinstance(e, ImportError): + fix = "spacy not installed, run `pip install spacy`" + elif isinstance(e, OverflowError) and addons.preshed_broken(): + fix = f"the installed preshed breaks spaCy's model loading, run `pip install \"{addons.PRESHED_PIN}\"` and restart ComfyUI" + elif isinstance(e, OSError): + fix = "model missing, run `python -m spacy download en_core_web_sm`" + else: + fix = "spaCy could not load its model (see the error above)" _say("spaCy tagger", e, f"simple word rules used; {fix}") return _nlp diff --git a/modules/conditioning/shot_camera/tests/test_shot_camera.py b/modules/conditioning/shot_camera/tests/test_shot_camera.py index b97a3bc3..92ee3f05 100644 --- a/modules/conditioning/shot_camera/tests/test_shot_camera.py +++ b/modules/conditioning/shot_camera/tests/test_shot_camera.py @@ -479,3 +479,22 @@ def test_a_lone_shot_can_take_a_view_but_shot_one_of_several_still_cannot(): assert sc.view_options("[Shot 1] A cat sits.")[0]["shot"] == 1 _o, several = sc.add_shot_views("[Shot 1] A.\n[Shot 2] B.", seed=1, chance=1.0) assert [a["shot"] for a in several] == [2] + + +def test_a_model_that_overflows_on_load_is_named_as_the_preshed_problem_not_a_missing_model(monkeypatch): + """preshed 3.0.14 raises OverflowError inside spacy.load; the old text blamed a missing model.""" + import sys + import types + said = [] + monkeypatch.setattr(sc, "_nlp", None) + monkeypatch.setattr(sc, "_tried", False) + monkeypatch.setattr(sc.addons, "enabled", lambda _id: True) + monkeypatch.setattr(sc.addons, "preshed_broken", lambda: True) + monkeypatch.setattr(sc, "_say", lambda what, error, effect: said.append((type(error).__name__, effect))) + fake = types.ModuleType("spacy") + def _load(_name): + raise OverflowError("Python int too large to convert to C ssize_t") + fake.load = _load + monkeypatch.setitem(sys.modules, "spacy", fake) + assert sc._spacy() is None + assert said[0][0] == "OverflowError" and "preshed" in said[0][1] and "model missing" not in said[0][1]