diff options
| author | Felix Morgner <felix.morgner@gmail.com> | 2026-10-17 12:20:31 +0200 |
|---|---|---|
| committer | Felix Morgner <felix.morgner@gmail.com> | 2026-10-17 12:20:31 +0200 |
| commit | 3f88638e4d1c380d730274f46bac6670b1c6a35e (patch) | |
| tree | c93811523d1d52f0e50a8d527b40d2d7eaea236f /harness.py | |
| download | comfier-3f88638e4d1c380d730274f46bac6670b1c6a35e.tar.xz comfier-3f88638e4d1c380d730274f46bac6670b1c6a35e.zip | |
initial commit
Diffstat (limited to 'harness.py')
| -rw-r--r-- | harness.py | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/harness.py b/harness.py new file mode 100644 index 0000000..f588328 --- /dev/null +++ b/harness.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +import argparse +import asyncio +import json +import pathlib +import sys +import types +from types import SimpleNamespace +from typing import Any + +# --------------------------------------------------------------------------- +# 1. Stub out `fastapi` and `open_webui.*` in sys.modules BEFORE importing tool +# --------------------------------------------------------------------------- +def install_open_webui_stubs( + comfyui_url: str, + api_key: str, + workflow_path: pathlib.Path, + nodes_config: list[dict[str, Any]], + image_size: str, + image_steps: int, + output_dir: pathlib.Path, +) -> None: + # Ensure fastapi.Request exists even in a minimal venv + if "fastapi" not in sys.modules: + fastapi_mod = types.ModuleType("fastapi") + fastapi_mod.Request = object + sys.modules["fastapi"] = fastapi_mod + + workflow_raw = workflow_path.read_text(encoding="utf-8") + + # Mock `open_webui.models.config.Config` + config_mod = types.ModuleType("open_webui.models.config") + + class MockConfig: + @staticmethod + async def get(key: str, default: Any = None) -> Any: + return default + + config_mod.Config = MockConfig + + # Mock `open_webui.models.users.Users` + users_mod = types.ModuleType("open_webui.models.users") + + class MockUsers: + @staticmethod + def get_user_by_id(user_id: str) -> Any: + return SimpleNamespace(id=user_id, name="Felix") + + users_mod.Users = MockUsers + + # Mock `open_webui.routers.images` + images_mod = types.ModuleType("open_webui.routers.images") + + async def mock_get_image_config() -> Any: + return SimpleNamespace( + COMFYUI_BASE_URL=comfyui_url, + COMFYUI_API_KEY=api_key, + COMFYUI_WORKFLOW=workflow_raw, + COMFYUI_WORKFLOW_NODES=nodes_config, + IMAGE_SIZE=image_size, + IMAGE_STEPS=image_steps, + ) + + def mock_upload_image( + request: Any, + image_data: bytes, + content_type: str, + metadata: dict[str, Any], + user: Any, + ) -> Any: + output_dir.mkdir(parents=True, exist_ok=True) + ext = ".png" if "png" in content_type else ".jpg" + file_id = f"seed_{metadata.get('seed', 'unknown')}_{int(asyncio.get_event_loop().time() * 1000)}" + out_file = output_dir / f"{file_id}{ext}" + out_file.write_bytes(image_data) + print(f" [Storage] Saved {len(image_data)} bytes -> {out_file}") + # Return a mock FileModel with an `.id` attribute just like Open WebUI + return SimpleNamespace(id=file_id) + + images_mod.get_image_config = mock_get_image_config + images_mod.upload_image = mock_upload_image + + # Register package hierarchy in sys.modules + sys.modules["open_webui"] = types.ModuleType("open_webui") + sys.modules["open_webui.models"] = types.ModuleType("open_webui.models") + sys.modules["open_webui.routers"] = types.ModuleType("open_webui.routers") + sys.modules["open_webui.models.config"] = config_mod + sys.modules["open_webui.models.users"] = users_mod + sys.modules["open_webui.routers.images"] = images_mod + + +# --------------------------------------------------------------------------- +# 2. Terminal Event Emitter & Interactive Event Call +# --------------------------------------------------------------------------- +async def console_event_emitter(event: dict[str, Any]) -> None: + ev_type = event.get("type") + data = event.get("data", {}) + if ev_type == "status": + done_marker = "✓" if data.get("done") else "…" + print(f" [{done_marker}] {data.get('description')}") + elif ev_type == "files": + print(f" [Files Emitted] {json.dumps(data.get('files', []))}") + else: + print(f" [Event:{ev_type}] {data}") + + +async def console_event_call(event: dict[str, Any]) -> bool: + ev_type = event.get("type") + data = event.get("data", {}) + print(f"\n--- MODAL ({ev_type}): {data.get('title')} ---") + print(data.get("message", "")) + loop = asyncio.get_running_loop() + answer = await loop.run_in_executor( + None, lambda: input("Confirm / Retry? [y/N]: ").strip().lower() + ) + return answer in ("y", "yes") + + +# --------------------------------------------------------------------------- +# 3. CLI Runner +# --------------------------------------------------------------------------- +async def main() -> None: + parser = argparse.ArgumentParser(description="Standalone test harness for Comfier") + parser.add_argument("--url", default="http://10.42.0.146:8188", help="ComfyUI base URL") + parser.add_argument("--workflow", type=pathlib.Path, required=True, help="Path to ComfyUI API workflow JSON") + parser.add_argument("--prompt", default="a tiny blue marble on a white surface", help="Positive prompt") + parser.add_argument("--neg", default=None, help="Negative prompt") + parser.add_argument("--size", default="256x256", help="Image size (WIDTHxHEIGHT)") + parser.add_argument("--seed", default="123", help="Seed") + parser.add_argument("--steps", type=int, default=30, help="Global IMAGE_STEPS") + parser.add_argument("--neg-nodes", default="459:452", help="Valve: negative_prompt_node_ids") + parser.add_argument("--cancel-after", type=float, default=None, help="Simulate user clicking Stop after N seconds") + args = parser.parse_args() + + # Match your Open WebUI global COMFYUI_WORKFLOW_NODES setup + global_nodes = [ + {"type": "prompt", "key": "value", "node_ids": ["520:506"]}, + {"type": "seed", "key": "seed", "node_ids": ["520:517"]}, + {"type": "width", "key": "value", "node_ids": ["520:518"]}, + {"type": "height", "key": "value", "node_ids": ["520:519"]}, + {"type": "steps", "key": "steps", "node_ids": ["459:458"]}, + ] + + install_open_webui_stubs( + comfyui_url=args.url, + api_key="", + workflow_path=args.workflow, + nodes_config=global_nodes, + image_size="1024x1024", + image_steps=args.steps, + output_dir=pathlib.Path("./test_out"), + ) + + # Import your tool AFTER stubs are installed + from comfier import Tools + + tools = Tools() + tools.valves.negative_prompt_node_ids = args.neg_nodes + + task = asyncio.create_task( + tools.comfier_generate_image( + positive_prompt=args.prompt, + negative_prompt=args.neg, + size=args.size, + seed=args.seed, + __request__=SimpleNamespace(), + __user__={"id": "local-dev"}, + __event_emitter__=console_event_emitter, + __event_call__=console_event_call, + ) + ) + + if args.cancel_after is not None: + await asyncio.sleep(args.cancel_after) + print(f"\n[Harness] Simulating user abort after {args.cancel_after}s...") + task.cancel() + + try: + result = await task + print("\n[Tool Return Payload]") + print(json.dumps(result, indent=2)) + except asyncio.CancelledError: + print("\n[Harness] Task cleanly cancelled.") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\nInterrupted via Ctrl+C.") |
