Files
olivv-cs-ComfyUI-FunPack/tools/devserver.py
T
digital-garbage 4744d30087 fix: H3 preset combo values rejected by real node schemas
keep_proportion/extra_padding/invert_crop (ImageTransformKJ) and format
(SaveVideo) were wrapped in a redundant {"field": "value"} dict instead of
the plain string ComfyUI's own DynamicCombo expansion expects at the wire
level -- every one of them silently failed graph validation and fell back
to the node's own default, undermining the "total_pixels" resolution-fit
behavior this preset exists for. Found by loading the preset in a real
browser against real node schemas; the dev server never had comfy_extras
or KJNodes registered before now, so this was invisible locally until
tools/devserver.py also gained real node registration (init_extra_nodes +
KJNodes' load_custom_node, matching what the pytest `registered` fixture
already did).

The two pipeline_preset tests that were supposed to catch this had too
broad a filter -- it excluded ANY "is not one of" message as an assumed
"combo not chosen yet" state, which in this codebase actually shows up as
"needs X and nothing fills it" instead. Tightened to exempt only the three
loader-file combos genuinely left unset on a fresh install.

Verified live: preset loads with zero combo-validation problems, only the
three expected unset-loader-file messages remain, and Generate correctly
refuses with an honest reason rather than a false success.
2026-09-11 13:32:52 +03:00

155 lines
6.2 KiB
Python

"""Dev server: serves the app exactly as ComfyUI will, without ComfyUI.
It mounts the REAL routes -- `core.routes.register` takes a route table rather
than reaching for ComfyUI's server -- so what the browser gets here is what
ComfyUI would give it, refusals and all. The previous version reimplemented the
handlers over http.server, which meant looking at the app exercised a copy: it
answered GET /api/pipeline with a payload assembled here, and answered POST with
501, so the whole edit-and-queue path could not be seen at all.
python tools/devserver.py [port]
There is no queue behind it. Generating reaches /prompt, which ComfyUI serves
and this does not, and the app says so -- which is the honest answer and worth
seeing, since that is exactly what a person hitting Generate with ComfyUI down
would get.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
def _add_comfyui() -> str:
"""Put ComfyUI on the path, because most modules import it.
Without this the dev server loads only the modules that need nothing from
ComfyUI -- one out of nine -- and the app looks almost empty while reporting
nothing wrong. Looking at the app is supposed to be how the app gets tested,
so the preview has to see what ComfyUI would see.
"""
import os
candidates = [os.environ.get("COMFYUI_ROOT"),
str(Path.home() / "Documents" / "ComfyUI")]
for candidate in candidates:
if candidate and (Path(candidate) / "comfy" / "supported_models.py").is_file():
sys.path.append(candidate)
return candidate
return ""
def _reexec_with_comfyui_python(comfyui: str) -> None:
"""Restart under ComfyUI's own interpreter when this one cannot import torch.
Finding ComfyUI's SOURCE is not enough: the modules import torch, and the
interpreter that launches this may be a bare system python. Getting that
wrong is not a crash, it is nine modules quietly absent and an app that
looks almost empty -- so the server corrects it rather than serving a
misleading page.
"""
import os
if os.environ.get("FUNPACK_DEVSERVER_REEXEC"):
return # already tried; do not loop
try:
import torch # noqa: F401
return
except ImportError:
pass
candidate = Path(comfyui) / "venv" / "bin" / "python"
if not candidate.is_file():
print(f" WARNING: {sys.executable} cannot import torch and there is no "
f"venv at {candidate}. Modules needing it will be absent.")
return
print(f" re-running under {candidate} (this one has no torch)")
os.environ["FUNPACK_DEVSERVER_REEXEC"] = "1"
os.execv(str(candidate), [str(candidate), *sys.argv])
COMFYUI = _add_comfyui()
if COMFYUI:
_reexec_with_comfyui_python(COMFYUI)
from aiohttp import web # noqa: E402
from core import config, routes as funpack_routes # noqa: E402
P = config.UI_PREFIX
def _register_nodes() -> str:
"""Put ComfyUI's node registry, and FunPack's own nodes, where the routes
can see them.
Without this the pipeline is entirely "there is no node called X installed"
and nothing that edits a node has anything to edit -- so the one thing the
dev server exists for, looking at the app, cannot reach the half of the app
that matters. ComfyUI itself does both of these at startup; this is the same
two steps without the rest of ComfyUI.
This also loads comfy_extras (MiniMaxH3SigmaShift, CreateVideo, ...) and,
if present, the KJNodes pack (ImageTransformKJ) -- without them a preset
that names one of those nodes looks broken here specifically, not because
the preset is wrong, but because this server stopped one step short of
what `pytest`'s own `registered` fixture already does for the real thing.
"""
import asyncio
try:
import nodes as comfy_nodes
except Exception as exc: # noqa: BLE001
return f" WITHOUT ComfyUI's nodes ({exc}): the pipeline will look empty"
extras_note = ""
try:
asyncio.run(comfy_nodes.init_extra_nodes(init_custom_nodes=False))
except Exception as exc: # noqa: BLE001
extras_note = f"; comfy_extras WITHOUT loading fully ({exc})"
kjnodes_note = ""
kjnodes = Path(COMFYUI) / "custom_nodes" / "ComfyUI-KJNodes" if COMFYUI else None
if kjnodes and kjnodes.is_dir():
try:
asyncio.run(comfy_nodes.load_custom_node(str(kjnodes), module_parent="custom_nodes"))
except Exception as exc: # noqa: BLE001
kjnodes_note = f"; KJNodes WITHOUT loading ({exc})"
else:
kjnodes_note = "; KJNodes not installed, skipped"
from core import nodes as funpack_nodes
ours = funpack_nodes.install_into(comfy_nodes.NODE_CLASS_MAPPINGS,
comfy_nodes.NODE_DISPLAY_NAME_MAPPINGS)
return (f" {len(comfy_nodes.NODE_CLASS_MAPPINGS)} nodes registered "
f"({len(ours)} of them FunPack's){extras_note}{kjnodes_note}")
def build_app() -> web.Application:
# Matches ComfyUI's own --max-upload-size default (100MB, server.py) --
# aiohttp's bare default is 1MB, which made every real-sized upload here
# fail in a way production never would, on a ceiling nothing declared.
app = web.Application(client_max_size=100 * 1024 * 1024)
routes = web.RouteTableDef()
funpack_routes.register(routes)
@routes.get("/")
async def _root(_req):
raise web.HTTPFound(P + "/")
app.add_routes(routes)
return app
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8188
print(f"FunPack v5 dev server: http://127.0.0.1:{port}{P}/")
print(_register_nodes())
if COMFYUI:
print(f" with ComfyUI from {COMFYUI}")
else:
# Said out loud rather than left to be discovered as a half-empty page.
print(" WITHOUT ComfyUI: every module that imports it will be absent. "
"Set COMFYUI_ROOT to fix.")
print(" no queue behind it: Generate will report that /prompt is not here")
web.run_app(build_app(), host="127.0.0.1", port=port, print=None)