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:
@@ -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,22 +137,38 @@ 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:
|
||||||
src_node, src_slot = link_src[link]
|
continue
|
||||||
inputs[name] = [src_node, src_slot]
|
src_node, src_slot = link_src[link]
|
||||||
connected.add(name)
|
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")
|
widgets = node.get("widgets_values")
|
||||||
if isinstance(widgets, dict):
|
if isinstance(widgets, dict):
|
||||||
inputs.update(widgets)
|
inputs.update(widgets)
|
||||||
|
|||||||
@@ -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))
|
||||||
Reference in New Issue
Block a user