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_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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,21 +137,37 @@ 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:
|
||||
if name is None or link is None or link not in link_src:
|
||||
continue
|
||||
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)
|
||||
widgets = node.get("widgets_values")
|
||||
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))
|
||||
Reference in New Issue
Block a user