"""Lee los parametros de generacion de un PNG generado por ComfyUI. Extrae el chunk "prompt" (API format) de los chunks de texto del PNG y resume los parametros de generacion: modelo, seed, steps, cfg, sampler, scheduler, denoise y los prompts positivo/negativo (siguiendo las conexiones del KSampler). Impura: lectura de disco. Solo stdlib (struct, zlib, json). """ import json import struct import zlib def comfyui_read_png_metadata(png_path: str) -> dict: """Devuelve {ok, prompt, parameters, error} de un PNG de ComfyUI. Args: png_path: ruta del PNG generado por ComfyUI. Returns: dict con: - ok: bool. - prompt: el workflow API format embebido (dict), o {}. - parameters: resumen {model, seed, steps, cfg, sampler_name, scheduler, denoise, positive, negative} extraido del KSampler y los nodos conectados, o {}. - error: mensaje si algo fallo. """ try: with open(png_path, "rb") as f: data = f.read() except OSError as exc: return {"ok": False, "prompt": {}, "parameters": {}, "error": f"no se pudo leer {png_path!r}: {exc}"} try: chunks = _png_text_chunks(data) except ValueError as exc: return {"ok": False, "prompt": {}, "parameters": {}, "error": str(exc)} if "prompt" not in chunks: return {"ok": False, "prompt": {}, "parameters": {}, "error": "el PNG no contiene chunk 'prompt' de ComfyUI"} try: prompt = json.loads(chunks["prompt"]) except json.JSONDecodeError as exc: return {"ok": False, "prompt": {}, "parameters": {}, "error": f"chunk 'prompt' no es JSON valido: {exc}"} return {"ok": True, "prompt": prompt, "parameters": _extract_params(prompt), "error": ""} def _extract_params(prompt: dict) -> dict: params = {} ksampler = None for node in prompt.values(): if isinstance(node, dict) and str(node.get("class_type", "")).endswith("KSampler"): ksampler = node break if ksampler: ins = ksampler.get("inputs", {}) for k in ("seed", "steps", "cfg", "sampler_name", "scheduler", "denoise"): if k in ins and not isinstance(ins[k], list): params[k] = ins[k] for slot in ("positive", "negative"): link = ins.get(slot) if isinstance(link, list) and len(link) == 2: tnode = prompt.get(str(link[0]), {}) txt = tnode.get("inputs", {}).get("text") if isinstance(txt, str): params[slot] = txt for node in prompt.values(): if isinstance(node, dict) and str(node.get("class_type", "")).startswith("CheckpointLoader"): ck = node.get("inputs", {}).get("ckpt_name") if ck: params["model"] = ck break return params def _png_text_chunks(data: bytes) -> dict: """Lee los chunks de texto (tEXt/zTXt/iTXt) de un PNG -> {keyword: texto}.""" if data[:8] != b"\x89PNG\r\n\x1a\n": raise ValueError("no es un PNG valido (firma incorrecta)") out = {} off = 8 n = len(data) while off + 8 <= n: length = struct.unpack(">I", data[off:off + 4])[0] ctype = data[off + 4:off + 8] body = data[off + 8:off + 8 + length] off += 12 + length if ctype == b"tEXt": kw, _, txt = body.partition(b"\x00") out[kw.decode("latin1")] = txt.decode("latin1") elif ctype == b"zTXt": kw, _, rest = body.partition(b"\x00") if rest: try: out[kw.decode("latin1")] = zlib.decompress(rest[1:]).decode("latin1") except zlib.error: pass elif ctype == b"iTXt": kw, _, rest = body.partition(b"\x00") if len(rest) >= 2: comp_flag = rest[0] parts = rest[2:].split(b"\x00", 2) if len(parts) == 3: text_bytes = parts[2] if comp_flag == 1: try: text_bytes = zlib.decompress(text_bytes) except zlib.error: text_bytes = b"" out[kw.decode("latin1")] = text_bytes.decode("utf-8", "replace") elif ctype == b"IEND": break return out if __name__ == "__main__": import sys path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/missing.png" res = comfyui_read_png_metadata(path) print(json.dumps({"ok": res["ok"], "parameters": res["parameters"], "error": res["error"]}, indent=2))