From 337f75b52725767152c52fca4ba7631d6bf6e60f Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 24 Jun 2026 01:52:46 +0200 Subject: [PATCH] chore: auto-commit (5 archivos) - docs/capabilities/comfyui.md - python/functions/ml/comfyui_import_workflow_json.md - python/functions/ml/comfyui_import_workflow_json.py - python/functions/pipelines/comfyui_text_to_3d_oneshot.md - python/functions/pipelines/comfyui_text_to_3d_oneshot.py Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/capabilities/comfyui.md | 1 + .../ml/comfyui_import_workflow_json.md | 21 +- .../ml/comfyui_import_workflow_json.py | 79 ++++++- .../pipelines/comfyui_text_to_3d_oneshot.md | 104 +++++++++ .../pipelines/comfyui_text_to_3d_oneshot.py | 221 ++++++++++++++++++ 5 files changed, 417 insertions(+), 9 deletions(-) create mode 100644 python/functions/pipelines/comfyui_text_to_3d_oneshot.md create mode 100644 python/functions/pipelines/comfyui_text_to_3d_oneshot.py diff --git a/docs/capabilities/comfyui.md b/docs/capabilities/comfyui.md index fe07dd99..8f64a785 100644 --- a/docs/capabilities/comfyui.md +++ b/docs/capabilities/comfyui.md @@ -103,6 +103,7 @@ report `0079`). | [comfyui_fetch_output_mesh_py_ml](../../python/functions/ml/comfyui_fetch_output_mesh.md) | `fetch_output_mesh(prompt_id, *, server, dest=None, timeout) -> dict` | Localiza la malla en `/history/{prompt_id}` (el SaveGLB la expone bajo la clave `"3d"`, no `"images"`) y la baja via GET `/view` a disco. Hermana de `fetch_output_image`. Impura. | | [comfyui_install_3d_model_py_ml](../../python/functions/ml/comfyui_install_3d_model.md) | `install_3d_model(variant='mini', *, hf_token=None, comfyui_dir) -> dict` | Instala el checkpoint Hunyuan3D-2 (mini/standard/mv) en `checkpoints/`. Cascada: ya-instalado → cache de HF → descarga. Resuelve la ruta real via `extra_model_paths.yaml`. Impura. | | [comfyui_image_to_3d_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_image_to_3d_oneshot.md) | `image_to_3d_oneshot(image_path, *, server, variant='mini', dest=None, wait_timeout, **gen) -> dict` | **Pipeline** imagen en disco → malla GLB en una llamada: upload + build + submit + wait + fetch. Promoción de la secuencia (issue 0087). Impuro. | +| [comfyui_text_to_3d_oneshot_py_pipelines](../../python/functions/pipelines/comfyui_text_to_3d_oneshot.md) | `text_to_3d_oneshot(prompt, *, server, ckpt_name='v1-5-pruned-emaonly.safetensors', negative='', textured=False, variant='mini', dest=None, ...) -> dict` | **Pipeline** prompt de texto → malla 3D GLB en una llamada: txt2img (SD) + fetch + upload + build 3D (nativo o `textured=True` multi-vista PBR) + submit + wait + fetch_mesh. Promoción de la secuencia texto→imagen→3D (issue 0087). Impuro. | | [comfyui_build_textured_3d_multiview_workflow_py_ml](../../python/functions/ml/comfyui_build_textured_3d_multiview_workflow.md) | `build_textured_3d_multiview_workflow(image_name, *, ckpt='hunyuan3d-dit-v2-mv.safetensors', views=6, octree=384, max_faces=50000, upscale_model='4x_foolhardy_Remacri.pth') -> dict` | Builder imagen→malla 3D **con textura PBR** vía el wrapper Hunyuan3DWrapper (kijai): 4/6 vistas + delight + sample multi-vista + upscale Remacri + bake sobre UV (19 nodos). Cobertura de atlas 32.93% (report 0082). **Pura**. En 8 GB ejecutar en 2 fases (shape→`/free`→paint). | ### Por la UI web (CDP) — dominio `browser` diff --git a/python/functions/ml/comfyui_import_workflow_json.md b/python/functions/ml/comfyui_import_workflow_json.md index ea59f3ec..cef3f55e 100644 --- a/python/functions/ml/comfyui_import_workflow_json.md +++ b/python/functions/ml/comfyui_import_workflow_json.md @@ -3,10 +3,10 @@ name: comfyui_import_workflow_json kind: function lang: py domain: ml -version: "1.0.0" +version: "1.1.0" purity: impure signature: "def comfyui_import_workflow_json(source: str, *, server: str = \"127.0.0.1:8188\", timeout: float = 15.0) -> dict" -description: "Lee un workflow ComfyUI desde una URL (http/https) o un path local y lo normaliza a API format. Si viene en formato UI graph ({nodes, links}) lo convierte a API format usando /object_info para mapear los widgets; si ya es API format lo devuelve tal cual. Compone comfyui_object_info. Impura: HTTP GET / lectura de disco." +description: "Lee un workflow ComfyUI desde una URL (http/https) o un path local y lo normaliza a API format. Si viene en formato UI graph ({nodes, links}) lo convierte a API format usando /object_info para mapear los widgets; si ya es API format lo devuelve tal cual. Omite los nodos virtuales del editor (Note, MarkdownNote, PrimitiveNode, Reroute) tal como hace ComfyUI al pasar UI->API: resuelve los Reroute reconectando la conexion directa origen->destino e inyecta los PrimitiveNode como valor de widget en el consumidor. Compone comfyui_object_info. Impura: HTTP GET / lectura de disco." tags: [comfyui, ml, import, workflow, stable-diffusion] uses_functions: [comfyui_object_info_py_ml] uses_types: [] @@ -66,3 +66,20 @@ en un PNG usa `comfyui_import_workflow_png`. - API format se detecta porque todos los valores top-level son dicts con `class_type`; UI graph por la clave `nodes`. Otros JSON dan "formato no reconocido". +- Los nodos virtuales del editor (`Note`, `MarkdownNote`, `PrimitiveNode`, + `Reroute` y variantes `Reroute*`) NO aparecen en el API format resultante — + igual que cuando ComfyUI exporta UI->API. Los `Reroute` se resuelven saltando + el passthrough y reconectando el origen real al consumidor; una cadena de + Reroutes rota (entrada sin link) o con ciclo deja el input sin conexion en + lugar de apuntar a un nodo inexistente. Los `PrimitiveNode` se inyectan como + valor literal de widget en el consumidor (su `widgets_values[0]`). +- El filtrado es idempotente: un workflow ya en API format (sin nodos virtuales) + pasa intacto; un UI graph sin virtuales conserva todas sus conexiones. + +## Capability growth log + +- v1.1.0 (2026-06-24) — la conversion UI->API omite los nodos virtuales del + editor (Note/MarkdownNote/PrimitiveNode/Reroute), resuelve los Reroute + reconectando origen->destino e inyecta los PrimitiveNode como valor de widget. + Antes esos nodos viajaban al API format y `comfyui_validate_workflow` los + marcaba como `missing_nodes` (falsos positivos). Gap del report 0086. diff --git a/python/functions/ml/comfyui_import_workflow_json.py b/python/functions/ml/comfyui_import_workflow_json.py index 12c7bb5c..f44f9322 100644 --- a/python/functions/ml/comfyui_import_workflow_json.py +++ b/python/functions/ml/comfyui_import_workflow_json.py @@ -79,8 +79,57 @@ def comfyui_import_workflow_json( "error": "formato de workflow no reconocido (ni API ni UI graph)"} +# Node types virtuales del editor de ComfyUI: solo existen en el UI graph y se +# descartan al pasar UI -> API (ComfyUI hace lo mismo). Note/MarkdownNote son +# anotaciones; PrimitiveNode inyecta un valor de widget; Reroute es un passthrough +# de una conexion (se resuelve reconectando origen real -> destino). +_NOTE_TYPES = {"Note", "MarkdownNote"} + + +def _is_reroute(ctype) -> bool: + """True si el node type es un Reroute (nativo 'Reroute' o variantes custom).""" + return isinstance(ctype, str) and ctype.startswith("Reroute") + + +def _is_virtual(ctype) -> bool: + """True si el node type es virtual del editor (no va al API format).""" + return ctype in _NOTE_TYPES or ctype == "PrimitiveNode" or _is_reroute(ctype) + + +def _resolve_source(src_node, src_slot, node_by_id, link_src, _depth=0): + """Resuelve el origen real de una conexion saltando los nodos Reroute. + + Un Reroute en el UI graph es un passthrough: su salida solo reenvia lo que + llega a su unica entrada. Para producir API format hay que reconectar el + consumidor directamente al origen real (origen -> destino, sin el Reroute). + Devuelve (node_id, slot) del nodo no-Reroute al que se conecta, o None si la + cadena de Reroutes esta rota (entrada sin link) o forma un ciclo. + """ + if _depth > 64: + return None # ciclo de Reroutes: aborta la resolucion. + node = node_by_id.get(src_node) + if node is None or not _is_reroute(node.get("type")): + return (src_node, src_slot) + link = None + for inp in node.get("inputs", []) or []: + if inp.get("link") is not None: + link = inp["link"] + break + if link is None or link not in link_src: + return None # Reroute sin entrada conectada: link muerto. + nxt_node, nxt_slot = link_src[link] + return _resolve_source(nxt_node, nxt_slot, node_by_id, link_src, _depth + 1) + + def _ui_graph_to_api(graph: dict, obj_info) -> dict: - """Convierte un UI graph de ComfyUI a API format (best-effort).""" + """Convierte un UI graph de ComfyUI a API format (best-effort). + + Omite los nodos virtuales del editor (Note, MarkdownNote, PrimitiveNode, + Reroute) tal como hace ComfyUI al pasar de UI a API: las anotaciones se + descartan, los Reroute se resuelven reconectando la conexion directa + origen->destino, y los PrimitiveNode se inyectan como valor de widget en el + consumidor. + """ nodes = graph.get("nodes", []) or [] links = graph.get("links", []) or [] # link_id -> (src_node_id, src_slot) @@ -88,22 +137,38 @@ def _ui_graph_to_api(graph: dict, obj_info) -> dict: for lk in links: if isinstance(lk, list) and len(lk) >= 5: link_src[lk[0]] = (str(lk[1]), lk[2]) + # node_id (str) -> node dict, para TODOS los nodos (incluidos los virtuales), + # necesario para resolver Reroutes e inyectar valores de PrimitiveNode. + node_by_id = {str(n.get("id")): n for n in nodes if n.get("id") is not None} api = {} for node in nodes: ctype = node.get("type") - if ctype is None: - continue + if ctype is None or _is_virtual(ctype): + continue # los virtuales no existen en API format. nid = str(node.get("id")) inputs = {} connected = set() for inp in node.get("inputs", []) or []: name = inp.get("name") link = inp.get("link") - if name is not None and link is not None and link in link_src: - src_node, src_slot = link_src[link] - inputs[name] = [src_node, src_slot] - connected.add(name) + if name is None or link is None or link not in link_src: + continue + src_node, src_slot = link_src[link] + resolved = _resolve_source(src_node, src_slot, node_by_id, link_src) + if resolved is None: + continue # cadena de Reroutes rota: el input queda sin conexion. + rnode, rslot = resolved + src = node_by_id.get(rnode) + if src is not None and src.get("type") == "PrimitiveNode": + # PrimitiveNode: inyecta su valor constante como widget, no como link. + wv = src.get("widgets_values") + if isinstance(wv, list) and wv: + inputs[name] = wv[0] + connected.add(name) + continue + inputs[name] = [rnode, rslot] + connected.add(name) widgets = node.get("widgets_values") if isinstance(widgets, dict): inputs.update(widgets) diff --git a/python/functions/pipelines/comfyui_text_to_3d_oneshot.md b/python/functions/pipelines/comfyui_text_to_3d_oneshot.md new file mode 100644 index 00000000..9a437258 --- /dev/null +++ b/python/functions/pipelines/comfyui_text_to_3d_oneshot.md @@ -0,0 +1,104 @@ +--- +name: comfyui_text_to_3d_oneshot +kind: pipeline +lang: py +domain: pipelines +version: "1.0.0" +purity: impure +signature: "def comfyui_text_to_3d_oneshot(prompt: str, *, server: str = \"127.0.0.1:8188\", ckpt_name: str = \"v1-5-pruned-emaonly.safetensors\", negative: str = \"\", textured: bool = False, variant: str = \"mini\", dest: str | None = None, image_wait_timeout: float = 300.0, wait_timeout: float = 600.0, **gen) -> dict" +description: "Pipeline prompt de texto -> malla 3D GLB en una sola llamada. Genera una imagen desde texto con Stable Diffusion (txt2img), la sube al input/ del servidor y reconstruye una malla 3D desde ella (malla nativa Hunyuan3D-2 o, con textured=True, malla texturizada PBR multi-vista). Compone comfyui_build_txt2img_workflow + submit + wait + fetch_output_image + comfyui_build_image_to_3d_workflow (o comfyui_build_textured_3d_multiview_workflow) + submit + wait + comfyui_fetch_output_mesh. Reutiliza el helper de upload del pipeline hermano comfyui_image_to_3d_oneshot. Promocion de secuencia texto->imagen->3D (issue 0087). Impuro: HTTP + disco." +tags: [comfyui, img-to-3d, pipelines, ml, stable-diffusion, hunyuan3d] +uses_functions: + - comfyui_build_txt2img_workflow_py_ml + - comfyui_build_image_to_3d_workflow_py_ml + - comfyui_build_textured_3d_multiview_workflow_py_ml + - comfyui_submit_workflow_py_ml + - comfyui_wait_result_py_ml + - comfyui_fetch_output_image_py_ml + - comfyui_fetch_output_mesh_py_ml + - comfyui_image_to_3d_oneshot_py_pipelines +uses_types: [] +returns: [] +returns_optional: false +error_type: error_go_core +imports: [] +params: + - name: prompt + desc: "Prompt positivo de texto para la generacion de la imagen base (txt2img)." + - name: server + desc: "host:port del servidor ComfyUI (sin esquema). keyword-only." + - name: ckpt_name + desc: "Checkpoint Stable Diffusion para el txt2img (default SD1.5 v1-5-pruned-emaonly). keyword-only." + - name: negative + desc: "Prompt negativo del txt2img. keyword-only." + - name: textured + desc: "True = malla texturizada PBR multi-vista (Hunyuan3DWrapper); False = malla nativa Hunyuan3D-2 segun variant. keyword-only." + - name: variant + desc: "mini (default), standard o mv; checkpoint Hunyuan3D-2 del paso 3D no texturizado (ignorado si textured=True). keyword-only." + - name: dest + desc: "Ruta destino de la malla (None = cwd; dir = dentro; archivo = ahi). La imagen intermedia se guarda en una carpeta derivada de dest. keyword-only." + - name: image_wait_timeout + desc: "Segundos maximos esperando la generacion de la imagen. keyword-only." + - name: wait_timeout + desc: "Segundos maximos esperando la reconstruccion 3D. keyword-only." + - name: gen + desc: "Parametros del txt2img reenviados a comfyui_build_txt2img_workflow (steps, cfg, width, height, seed, sampler_name, scheduler, filename_prefix); las demas claves se ignoran." +output: "dict {ok, image_path, mesh_path, prompt_ids, error}. image_path = ruta local del PNG generado; mesh_path = ruta local de la malla GLB; prompt_ids = [id_txt2img, id_3d]. Si falla, ok=False y error indica el paso (submit/wait/fetch txt2img o 3D)." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/pipelines/comfyui_text_to_3d_oneshot.py" +--- + +## Ejemplo + +```python +import sys, os +sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions")) +from pipelines.comfyui_text_to_3d_oneshot import comfyui_text_to_3d_oneshot + +# Texto -> malla 3D nativa (SD1.5 + Hunyuan3D-2 mini), una sola llamada. +res = comfyui_text_to_3d_oneshot( + "a cute robot toy, full body, centered, plain background, sharp focus", + dest="/tmp/comfy_text_to_3d", + steps=20, + seed=42, +) +# res == {"ok": True, "image_path": "/tmp/.../comfy_00001_.png", +# "mesh_path": "/tmp/comfy_text_to_3d/3d_mesh_00001_.glb", +# "prompt_ids": ["", ""], "error": ""} + +# Malla texturizada PBR multi-vista (requiere el wrapper Hunyuan3DWrapper instalado): +res2 = comfyui_text_to_3d_oneshot("a stylized ceramic mug", textured=True, dest="/tmp/comfy_text_to_3d") +``` + +Lánzalo con el python del venv (import de arriba o heredoc). `./fn run` directo no +aplica porque la firma usa `*` (keyword-only) + `**gen`, no soportado por el +generador de runner de `fn run`. + +## Cuando usarla + +Cuando quieras una malla 3D a partir de SOLO una descripción de texto, sin pasar +por generar la imagen a mano. Promueve a una llamada la secuencia que antes eran +dos pasos encadenados (texto->imagen con `comfyui_build_txt2img_workflow`, luego +imagen->3D con `comfyui_build_image_to_3d_workflow`). Si ya tienes la imagen en +disco, usa directamente el pipeline hermano `comfyui_image_to_3d_oneshot`. + +## Gotchas + +- Impuro: dispara DOS trabajos de GPU encadenados (txt2img + reconstrucción 3D). + En 8 GB usa SD1.5 (`ckpt_name` default) + `variant="mini"`; SDXL o `textured=True` + necesitan más VRAM/tiempo y los modelos correspondientes ya instalados. +- El checkpoint SD (`ckpt_name`) y el de Hunyuan3D (`variant`) deben estar + presentes en el servidor; este pipeline NO los descarga. Valida con + `comfyui_validate_workflow` o `comfyui_object_info` si dudas. +- La imagen generada queda en el `output/` del servidor; el pipeline la baja a + disco y la re-sube al `input/` (POST /upload/image) porque el nodo `LoadImage` + del paso 3D lee del `input/`. Eso implica un round-trip imagen completo. +- `textured=True` requiere el custom node Hunyuan3DWrapper (kijai); sin él, el + submit del paso 3D falla con HTTP 400 y `ok=False` con el error del nodo. +- `**gen` solo reenvía claves de txt2img (`steps, cfg, width, height, seed, + sampler_name, scheduler, filename_prefix`); cualquier otra clave se ignora en + silencio (el paso 3D usa sus defaults). +- `prompt_ids` se va rellenando incrementalmente: si falla el paso 3D, contiene + igualmente el id del txt2img que sí se encoló (útil para depurar). diff --git a/python/functions/pipelines/comfyui_text_to_3d_oneshot.py b/python/functions/pipelines/comfyui_text_to_3d_oneshot.py new file mode 100644 index 00000000..9d9f72a4 --- /dev/null +++ b/python/functions/pipelines/comfyui_text_to_3d_oneshot.py @@ -0,0 +1,221 @@ +"""comfyui_text_to_3d_oneshot — prompt de texto -> malla 3D GLB en una sola llamada. + +Promocion de la secuencia repetida (issue 0087): generar una imagen desde un +prompt con Stable Diffusion -> reconstruir una malla 3D desde esa imagen. Antes +eran dos invocaciones encadenadas a mano (build_txt2img + submit + wait + fetch +imagen, luego build_image_to_3d + submit + wait + fetch malla). Aqui se compone +en una sola llamada las funciones del registry del grupo `comfyui`: + + comfyui_build_txt2img_workflow_py_ml (imagen desde texto) + comfyui_submit_workflow_py_ml (POST /prompt) + comfyui_wait_result_py_ml (poll /history) + comfyui_fetch_output_image_py_ml (GET /view -> disco) + comfyui_build_image_to_3d_workflow_py_ml (malla nativa Hunyuan3D) + comfyui_build_textured_3d_multiview_workflow_py_ml (malla texturizada PBR) + comfyui_fetch_output_mesh_py_ml (GET /view -> disco) + +La imagen generada queda en el output/ del servidor; para alimentar el paso 3D +(cuyo nodo LoadImage lee del input/) se sube al input/ reutilizando el helper +`_upload_image` del pipeline hermano comfyui_image_to_3d_oneshot (POST +/upload/image, no se reescribe la logica multipart). + +Pipeline impuro: red (HTTP) + escritura en disco. +""" + +from __future__ import annotations + +import json +import os +import sys +import tempfile + +# Importa las funciones del registry (mismo arbol python/functions). +_FUNCTIONS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _FUNCTIONS_ROOT not in sys.path: + sys.path.insert(0, _FUNCTIONS_ROOT) + +from ml.comfyui_build_image_to_3d_workflow import comfyui_build_image_to_3d_workflow +from ml.comfyui_build_textured_3d_multiview_workflow import ( + comfyui_build_textured_3d_multiview_workflow, +) +from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow +from ml.comfyui_fetch_output_image import comfyui_fetch_output_image +from ml.comfyui_fetch_output_mesh import comfyui_fetch_output_mesh +from ml.comfyui_submit_workflow import comfyui_submit_workflow +from ml.comfyui_wait_result import comfyui_wait_result +from pipelines.comfyui_image_to_3d_oneshot import _upload_image + +# variant -> nombre del checkpoint Hunyuan3D-2 para el paso 3D no texturizado. +_VARIANT_CKPT = { + "mini": "hunyuan3d-dit-v2-mini.safetensors", + "standard": "hunyuan3d-dit-v2-0.safetensors", + "mv": "hunyuan3d-dit-v2-mv.safetensors", +} + +# Claves de **gen que reenviamos a comfyui_build_txt2img_workflow; cualquier otra +# clave en **gen se ignora silenciosamente (el paso 3D usa sus defaults). +_TXT2IMG_KEYS = { + "steps", "cfg", "width", "height", "seed", + "sampler_name", "scheduler", "filename_prefix", +} + + +def _first_image(outputs: dict) -> dict | None: + """Devuelve la primera imagen de los outputs de un prompt (o None).""" + for out in outputs.values(): + for img in out.get("images", []) or []: + if img.get("filename"): + return img + return None + + +def _image_dir(dest: str | None) -> str: + """Resuelve el directorio donde guardar la imagen intermedia. + + dest None -> directorio temporal del sistema; dest con extension de archivo + -> su carpeta; dest sin extension -> se trata como carpeta. + """ + if not dest: + return os.path.join(tempfile.gettempdir(), "comfy_text_to_3d") + dest = os.path.expanduser(dest) + _, ext = os.path.splitext(dest) + if ext: + return os.path.dirname(dest) or "." + return dest + + +def comfyui_text_to_3d_oneshot( + prompt: str, + *, + server: str = "127.0.0.1:8188", + ckpt_name: str = "v1-5-pruned-emaonly.safetensors", + negative: str = "", + textured: bool = False, + variant: str = "mini", + dest: str | None = None, + image_wait_timeout: float = 300.0, + wait_timeout: float = 600.0, + **gen, +) -> dict: + """Genera una imagen desde texto y reconstruye su malla 3D, end-to-end. + + Args: + prompt: prompt positivo para la generacion de la imagen (txt2img). + server: host:port del servidor ComfyUI (sin esquema). keyword-only. + ckpt_name: checkpoint Stable Diffusion para el txt2img (default SD1.5). + keyword-only. + negative: prompt negativo del txt2img. keyword-only. + textured: si True usa el pipeline 3D texturizado PBR multi-vista + (comfyui_build_textured_3d_multiview_workflow, requiere el wrapper + Hunyuan3DWrapper); si False usa la malla nativa Hunyuan3D-2 segun + `variant`. keyword-only. + variant: "mini" (default), "standard" o "mv"; checkpoint Hunyuan3D-2 del + paso 3D no texturizado (ignorado si textured=True). keyword-only. + dest: ruta destino de la malla (None = cwd; dir = dentro; archivo = ahi). + La imagen intermedia se guarda en una carpeta derivada de dest. + keyword-only. + image_wait_timeout: segundos maximos esperando la generacion de la + imagen. keyword-only. + wait_timeout: segundos maximos esperando la reconstruccion 3D. + keyword-only. + **gen: parametros del txt2img (steps, cfg, width, height, seed, + sampler_name, scheduler, filename_prefix); el resto se ignora. + + Returns: + dict {ok, image_path, mesh_path, prompt_ids, error}. image_path = ruta + local del PNG generado; mesh_path = ruta local de la malla; prompt_ids = + [id_txt2img, id_3d] de los trabajos encolados. Si falla, ok=False y error + explica en que paso. + """ + out = {"ok": False, "image_path": "", "mesh_path": "", + "prompt_ids": [], "error": ""} + + if not textured and variant not in _VARIANT_CKPT: + out["error"] = f"variant {variant!r} no valida; usa {sorted(_VARIANT_CKPT)}" + return out + + # 1. Construir el workflow txt2img (funcion pura del registry). + t2i_gen = {k: v for k, v in gen.items() if k in _TXT2IMG_KEYS} + wf_img = comfyui_build_txt2img_workflow(ckpt_name, prompt, negative, **t2i_gen) + + # 2. Encolar el txt2img. + try: + sub = comfyui_submit_workflow(wf_img, server=server) + pid_img = sub["prompt_id"] + except (RuntimeError, KeyError) as exc: + out["error"] = f"submit txt2img fallo: {exc}" + return out + out["prompt_ids"].append(pid_img) + + # 3. Esperar la imagen y localizarla en los outputs. + try: + outputs = comfyui_wait_result(pid_img, server=server, timeout=image_wait_timeout) + except (TimeoutError, RuntimeError) as exc: + out["error"] = f"wait txt2img fallo: {exc}" + return out + img = _first_image(outputs) + if img is None: + out["error"] = "txt2img no produjo ninguna imagen en los outputs" + return out + + # 4. Descargar la imagen generada a disco local. + fetched_img = comfyui_fetch_output_image( + img["filename"], + subfolder=img.get("subfolder", ""), + type_=img.get("type", "output"), + server=server, + dest_dir=_image_dir(dest), + ) + if not fetched_img.get("ok"): + out["error"] = f"fetch de imagen fallo: {fetched_img.get('error')}" + return out + out["image_path"] = fetched_img["path"] + + # 5. Subir la imagen al input/ del servidor para el paso 3D (LoadImage). + try: + image_name = _upload_image(out["image_path"], server) + except RuntimeError as exc: + out["error"] = f"upload de imagen al input/ fallo: {exc}" + return out + + # 6. Construir el workflow imagen->3D (texturizado multi-vista o malla nativa). + if textured: + wf_3d = comfyui_build_textured_3d_multiview_workflow(image_name) + else: + wf_3d = comfyui_build_image_to_3d_workflow(image_name, _VARIANT_CKPT[variant]) + + # 7. Encolar el 3D. + try: + sub3 = comfyui_submit_workflow(wf_3d, server=server) + pid_3d = sub3["prompt_id"] + except (RuntimeError, KeyError) as exc: + out["error"] = f"submit 3D fallo: {exc}" + return out + out["prompt_ids"].append(pid_3d) + + # 8. Esperar la reconstruccion 3D. + try: + comfyui_wait_result(pid_3d, server=server, timeout=wait_timeout) + except (TimeoutError, RuntimeError) as exc: + out["error"] = f"wait 3D fallo: {exc}" + return out + + # 9. Descargar la malla. + fetched_mesh = comfyui_fetch_output_mesh(pid_3d, server=server, dest=dest) + if not fetched_mesh.get("ok"): + out["error"] = f"fetch de malla fallo: {fetched_mesh.get('error')}" + return out + out["mesh_path"] = fetched_mesh["path"] + + out["ok"] = True + return out + + +if __name__ == "__main__": + res = comfyui_text_to_3d_oneshot( + "a cute robot toy, full body, centered, plain background, sharp focus", + dest="/tmp/comfy_text_to_3d", + steps=20, + seed=42, + ) + print(json.dumps(res, indent=2))