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) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 01:52:46 +02:00
parent d3f05a19a5
commit 337f75b527
5 changed files with 417 additions and 9 deletions
+1
View File
@@ -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_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_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_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). | | [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` ### Por la UI web (CDP) — dominio `browser`
@@ -3,10 +3,10 @@ name: comfyui_import_workflow_json
kind: function kind: function
lang: py lang: py
domain: ml domain: ml
version: "1.0.0" version: "1.1.0"
purity: impure purity: impure
signature: "def comfyui_import_workflow_json(source: str, *, server: str = \"127.0.0.1:8188\", timeout: float = 15.0) -> dict" 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] tags: [comfyui, ml, import, workflow, stable-diffusion]
uses_functions: [comfyui_object_info_py_ml] uses_functions: [comfyui_object_info_py_ml]
uses_types: [] 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 - API format se detecta porque todos los valores top-level son dicts con
`class_type`; UI graph por la clave `nodes`. Otros JSON dan `class_type`; UI graph por la clave `nodes`. Otros JSON dan
"formato no reconocido". "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.
@@ -79,8 +79,57 @@ def comfyui_import_workflow_json(
"error": "formato de workflow no reconocido (ni API ni UI graph)"} "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: 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 [] nodes = graph.get("nodes", []) or []
links = graph.get("links", []) or [] links = graph.get("links", []) or []
# link_id -> (src_node_id, src_slot) # link_id -> (src_node_id, src_slot)
@@ -88,21 +137,37 @@ def _ui_graph_to_api(graph: dict, obj_info) -> dict:
for lk in links: for lk in links:
if isinstance(lk, list) and len(lk) >= 5: if isinstance(lk, list) and len(lk) >= 5:
link_src[lk[0]] = (str(lk[1]), lk[2]) 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 = {} api = {}
for node in nodes: for node in nodes:
ctype = node.get("type") ctype = node.get("type")
if ctype is None: if ctype is None or _is_virtual(ctype):
continue continue # los virtuales no existen en API format.
nid = str(node.get("id")) nid = str(node.get("id"))
inputs = {} inputs = {}
connected = set() connected = set()
for inp in node.get("inputs", []) or []: for inp in node.get("inputs", []) or []:
name = inp.get("name") name = inp.get("name")
link = inp.get("link") link = inp.get("link")
if name is not None and link is not None and link in link_src: if name is None or link is None or link not in link_src:
continue
src_node, src_slot = link_src[link] src_node, src_slot = link_src[link]
inputs[name] = [src_node, src_slot] 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) connected.add(name)
widgets = node.get("widgets_values") widgets = node.get("widgets_values")
if isinstance(widgets, dict): if isinstance(widgets, dict):
@@ -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": ["<id_txt2img>", "<id_3d>"], "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).
@@ -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))