diff --git a/tk_comfyui_batch_image/nodes/script_loader.py b/tk_comfyui_batch_image/nodes/script_loader.py index dad56e3..62d18c7 100644 --- a/tk_comfyui_batch_image/nodes/script_loader.py +++ b/tk_comfyui_batch_image/nodes/script_loader.py @@ -8,6 +8,8 @@ from ..core.normalizer import normalize_script from ..core.types import COMIC_SCRIPT_TYPE, SolvedScript from ..core.validator import validate +MODE_OPTIONS = ["auto", "file", "path", "inline"] + def _summary(script: SolvedScript) -> str: total_panels = sum(len(p.panels) for p in script.pages) @@ -20,6 +22,27 @@ def _summary(script: SolvedScript) -> str: ) +def _strip_surrounding_quotes(s: str) -> str: + s = s.strip() + if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"): + return s[1:-1].strip() + return s + + +def _list_input_json_files() -> list[str]: + try: + import folder_paths # type: ignore + input_dir = Path(folder_paths.get_input_directory()) + except Exception: + return [] + if not input_dir.exists(): + return [] + return sorted( + str(p.relative_to(input_dir)).replace("\\", "/") + for p in input_dir.rglob("*.json") + ) + + def _resolve_file_mode(json_file: str) -> Path: import folder_paths # type: ignore # ComfyUI-provided at runtime input_dir = Path(folder_paths.get_input_directory()) @@ -31,6 +54,23 @@ def _resolve_file_mode(json_file: str) -> Path: return candidate +def _resolve_auto_mode(json_file: str, json_path: str, json_text: str) -> str: + filled = [name for name, val in + (("inline", json_text), ("path", json_path), ("file", json_file)) + if val.strip()] + if not filled: + raise ValueError( + "mode=auto: no input provided — fill json_file (dropdown), " + "json_path (absolute path), or json_text (inline JSON)." + ) + if len(filled) > 1: + raise ValueError( + f"mode=auto: multiple inputs filled ({', '.join(filled)}). " + "Clear the ones you don't want, or set mode explicitly." + ) + return filled[0] + + class ComicScriptLoader: """Load a comic script JSON, validate it, and emit a SolvedScript.""" @@ -41,27 +81,45 @@ class ComicScriptLoader: @classmethod def INPUT_TYPES(cls): + json_files = _list_input_json_files() + file_widget = (json_files, {"tooltip": "Pick a .json from ComfyUI input dir; used when mode=file."}) \ + if json_files else \ + ("STRING", {"default": "", "multiline": False, + "tooltip": "Relative path inside ComfyUI input dir; used when mode=file."}) return { "required": { - "mode": (["file", "path", "inline"], {"default": "file"}), + "mode": (MODE_OPTIONS, {"default": "auto", + "tooltip": "auto = use whichever field below is filled."}), }, "optional": { - "json_file": ("STRING", {"default": "", "multiline": False, - "tooltip": "Relative path inside ComfyUI input dir; used when mode=file."}), + "json_file": file_widget, "json_path": ("STRING", {"default": "", "multiline": False, - "tooltip": "Absolute / relative path to JSON; used when mode=path."}), + "tooltip": "Absolute / relative path to JSON (surrounding quotes are stripped); used when mode=path."}), "json_text": ("STRING", {"default": "", "multiline": True, "tooltip": "Inline JSON text; used when mode=inline."}), }, } def load(self, mode: str, json_file: str = "", json_path: str = "", json_text: str = ""): + json_path = _strip_surrounding_quotes(json_path) + json_file = _strip_surrounding_quotes(json_file) + + if mode == "auto": + mode = _resolve_auto_mode(json_file, json_path, json_text) + if mode == "inline": raw = json_text elif mode == "path": if not json_path: raise ValueError("mode=path requires json_path") - raw = Path(json_path).read_text(encoding="utf-8") + p = Path(json_path) + if not p.exists(): + raise FileNotFoundError( + f"json_path not found: {p} " + "(tip: Windows 'Copy as Path' adds quotes — those are stripped automatically, " + "but check for typos or wrong drive letter.)" + ) + raw = p.read_text(encoding="utf-8") elif mode == "file": if not json_file: raise ValueError("mode=file requires json_file") diff --git a/tk_comfyui_batch_image/tests/test_script_loader.py b/tk_comfyui_batch_image/tests/test_script_loader.py index a133341..eca72ea 100644 --- a/tk_comfyui_batch_image/tests/test_script_loader.py +++ b/tk_comfyui_batch_image/tests/test_script_loader.py @@ -14,7 +14,8 @@ def test_input_types_declares_modes(): inputs = ComicScriptLoader.INPUT_TYPES() required = inputs["required"] assert "mode" in required - assert set(required["mode"][0]) == {"file", "path", "inline"} + assert set(required["mode"][0]) == {"auto", "file", "path", "inline"} + assert required["mode"][1]["default"] == "auto" optional = inputs.get("optional", {}) assert "json_file" in optional assert "json_path" in optional @@ -56,6 +57,38 @@ def test_load_from_file_mode(stub_folder_paths): assert script.job_id == "basic" +def test_auto_mode_picks_path_when_only_path_filled(tmp_path: Path): + target = tmp_path / "script.json" + shutil.copy(FIXTURES / "basic.json", target) + node = ComicScriptLoader() + script, _ = node.load(mode="auto", json_path=str(target), json_file="", json_text="") + assert script.job_id == "basic" + + +def test_auto_mode_strips_surrounding_double_quotes(tmp_path: Path): + target = tmp_path / "script.json" + shutil.copy(FIXTURES / "basic.json", target) + node = ComicScriptLoader() + quoted = f'"{target}"' + script, _ = node.load(mode="auto", json_path=quoted, json_file="", json_text="") + assert script.job_id == "basic" + + +def test_auto_mode_with_no_input_errors_clearly(): + node = ComicScriptLoader() + with pytest.raises(ValueError, match="no input provided"): + node.load(mode="auto", json_file="", json_path="", json_text="") + + +def test_auto_mode_with_multiple_inputs_errors_clearly(tmp_path: Path): + target = tmp_path / "script.json" + shutil.copy(FIXTURES / "basic.json", target) + node = ComicScriptLoader() + text = (FIXTURES / "minimal.json").read_text(encoding="utf-8") + with pytest.raises(ValueError, match="multiple inputs"): + node.load(mode="auto", json_path=str(target), json_text=text, json_file="") + + def test_invalid_json_raises_with_readable_message(): node = ComicScriptLoader() text = (FIXTURES / "minimal.json").read_text(encoding="utf-8")