chore: auto-commit (61 archivos)
- docs/capabilities/INDEX.md - docs/capabilities/comfyui.md - python/functions/browser/comfyui_export_workflow_ui.md - python/functions/browser/comfyui_export_workflow_ui.py - python/functions/browser/comfyui_load_workflow_ui.md - python/functions/browser/comfyui_load_workflow_ui.py - python/functions/browser/comfyui_queue_prompt_ui.md - python/functions/browser/comfyui_queue_prompt_ui.py - python/functions/browser/comfyui_refresh_nodes_ui.md - python/functions/browser/comfyui_refresh_nodes_ui.py - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: comfyui_image_to_3d_oneshot
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
purity: impure
|
||||
version: "1.0.0"
|
||||
signature: "def comfyui_image_to_3d_oneshot(image_path: str, *, server: str = \"127.0.0.1:8188\", variant: str = \"mini\", dest: str | None = None, wait_timeout: float = 600.0, **gen) -> dict"
|
||||
description: "Pipeline imagen en disco -> malla 3D GLB en una sola llamada. Sube la imagen al input/ de ComfyUI (POST /upload/image), construye el workflow Hunyuan3D-2 nativo de 9 nodos, lo encola, espera y descarga la malla. Compone comfyui_build_image_to_3d_workflow + comfyui_submit_workflow + comfyui_wait_result + comfyui_fetch_output_mesh. Promocion de secuencia (issue 0087). Impuro: HTTP + disco."
|
||||
tags: [comfyui, ml, img-to-3d, hunyuan3d, mesh, pipeline]
|
||||
uses_functions:
|
||||
- comfyui_build_image_to_3d_workflow_py_ml
|
||||
- comfyui_submit_workflow_py_ml
|
||||
- comfyui_wait_result_py_ml
|
||||
- comfyui_fetch_output_mesh_py_ml
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/pipelines/comfyui_image_to_3d_oneshot.py"
|
||||
params:
|
||||
- name: image_path
|
||||
desc: "Ruta local de la imagen de entrada (PNG/JPG). Se sube al input/ del servidor."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema. keyword-only."
|
||||
- name: variant
|
||||
desc: "'mini' (default), 'standard' o 'mv'; elige el checkpoint Hunyuan3D-2. El modelo debe estar ya instalado (comfyui_install_3d_model). keyword-only."
|
||||
- name: dest
|
||||
desc: "Ruta destino de la malla (None = cwd; dir = dentro; archivo = ahi). keyword-only."
|
||||
- name: wait_timeout
|
||||
desc: "Segundos maximos esperando a que el server termine la reconstruccion 3D (carga del modelo + difusion + decode). keyword-only."
|
||||
- name: gen
|
||||
desc: "Parametros de generacion pasados a comfyui_build_image_to_3d_workflow (resolution, steps, cfg, seed, octree_resolution, num_chunks, threshold, sampler_name, scheduler, filename_prefix). **kwargs."
|
||||
output: "dict {ok, mesh_path, faces, prompt_id, error}. mesh_path = ruta local de la malla descargada; faces = numero de caras si trimesh esta disponible (si no, None); prompt_id = id del trabajo en ComfyUI. Si falla, ok=False y error explica en que paso."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from pipelines.comfyui_image_to_3d_oneshot import comfyui_image_to_3d_oneshot
|
||||
|
||||
# Una imagen en disco -> un .glb en /tmp/meshes, en una sola llamada.
|
||||
res = comfyui_image_to_3d_oneshot(
|
||||
os.path.expanduser("~/ComfyUI/input/3d_src_robot_00001_.png"),
|
||||
dest="/tmp/meshes",
|
||||
variant="mini",
|
||||
seed=42,
|
||||
)
|
||||
# res == {"ok": True, "mesh_path": "/tmp/meshes/3d_mesh_00001_.glb",
|
||||
# "faces": 1668040, "prompt_id": "....", "error": ""}
|
||||
```
|
||||
|
||||
CLI directo (el modelo mini debe estar instalado):
|
||||
|
||||
```bash
|
||||
$HOME/fn_registry/python/.venv/bin/python3 \
|
||||
python/functions/pipelines/comfyui_image_to_3d_oneshot.py \
|
||||
~/ComfyUI/input/3d_src_robot_00001_.png
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando tengas una imagen de un objeto (idealmente aislado sobre fondo plano) y
|
||||
quieras su malla 3D GLB sin montar el grafo de nodos ni encadenar submit/wait/fetch
|
||||
a mano. Es la operacion de un solo paso del flujo imagen->3D nativo de ComfyUI;
|
||||
ideal para scripts y agentes. Para tunear el workflow nodo a nodo, usa las
|
||||
funciones sueltas (`comfyui_build_image_to_3d_workflow` + submit/wait/fetch).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impuro: sube la imagen (HTTP), dispara la GPU del servidor y escribe la malla en
|
||||
disco. Requiere el server ComfyUI vivo en `server` y el checkpoint de la variante
|
||||
ya instalado (`comfyui_install_3d_model` antes; este pipeline NO lo instala).
|
||||
- La reconstruccion 3D tarda ~60 s en una RTX 3070 (carga del modelo entero a GPU +
|
||||
difusion + decode de voxel). Sube `wait_timeout` si el server arranca en frio o si
|
||||
usas `resolution`/`octree_resolution` altos.
|
||||
- Si el server esta ocupado con otro trabajo, el wait puede agotar el timeout:
|
||||
reintenta o sube `wait_timeout`. No reinicia ni desencola nada.
|
||||
- `faces` es best-effort: solo se calcula si `trimesh` esta en el venv. Si no, sale
|
||||
`None` (la malla igual se descarga bien; comprueba `mesh_path`/`bytes`).
|
||||
- Salida **shape-only** (sin color/textura): es el camino nativo de Hunyuan3D-2.
|
||||
- El upload usa POST /upload/image con `overwrite=true`: si subes dos imagenes con
|
||||
el mismo basename, la segunda pisa a la primera en el input/ del servidor.
|
||||
@@ -0,0 +1,181 @@
|
||||
"""comfyui_image_to_3d_oneshot — imagen en disco -> malla 3D GLB en una sola llamada.
|
||||
|
||||
Promocion de la secuencia repetida (issue 0087): subir imagen al input/ de ComfyUI
|
||||
-> construir el workflow Hunyuan3D-2 nativo -> encolar -> esperar -> descargar la
|
||||
malla. Compone funciones del registry del grupo `comfyui`:
|
||||
|
||||
comfyui_build_image_to_3d_workflow_py_ml (workflow de 9 nodos)
|
||||
comfyui_submit_workflow_py_ml (POST /prompt)
|
||||
comfyui_wait_result_py_ml (poll /history)
|
||||
comfyui_fetch_output_mesh_py_ml (GET /view -> disco)
|
||||
|
||||
El upload de la imagen al `input/` del servidor (POST /upload/image) es logica
|
||||
especifica de este pipeline y va inline (no es una funcion reutilizable del
|
||||
registry todavia).
|
||||
|
||||
Pipeline impuro: red (HTTP) + escritura en disco.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
# 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_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
|
||||
|
||||
# variant -> nombre del checkpoint que espera ImageOnlyCheckpointLoader.
|
||||
_VARIANT_CKPT = {
|
||||
"mini": "hunyuan3d-dit-v2-mini.safetensors",
|
||||
"standard": "hunyuan3d-dit-v2-0.safetensors",
|
||||
"mv": "hunyuan3d-dit-v2-mv.safetensors",
|
||||
}
|
||||
|
||||
|
||||
def _upload_image(image_path: str, server: str, timeout: float = 60.0) -> str:
|
||||
"""Sube una imagen local al input/ de ComfyUI via POST /upload/image.
|
||||
|
||||
Construye un multipart/form-data a mano (solo stdlib). Devuelve el nombre del
|
||||
archivo tal como queda en el servidor (para el nodo LoadImage). Lanza
|
||||
RuntimeError si falla.
|
||||
"""
|
||||
with open(image_path, "rb") as fh:
|
||||
content = fh.read()
|
||||
name = os.path.basename(image_path)
|
||||
ctype = mimetypes.guess_type(name)[0] or "application/octet-stream"
|
||||
boundary = f"----fnRegistry{uuid.uuid4().hex}"
|
||||
crlf = "\r\n"
|
||||
pre = (
|
||||
f"--{boundary}{crlf}"
|
||||
f'Content-Disposition: form-data; name="image"; filename="{name}"{crlf}'
|
||||
f"Content-Type: {ctype}{crlf}{crlf}"
|
||||
).encode()
|
||||
mid = (
|
||||
f"{crlf}--{boundary}{crlf}"
|
||||
f'Content-Disposition: form-data; name="overwrite"{crlf}{crlf}true'
|
||||
f"{crlf}--{boundary}{crlf}"
|
||||
f'Content-Disposition: form-data; name="type"{crlf}{crlf}input'
|
||||
f"{crlf}--{boundary}--{crlf}"
|
||||
).encode()
|
||||
body = pre + content + mid
|
||||
url = f"http://{server}/upload/image"
|
||||
req = urllib.request.Request(
|
||||
url, data=body,
|
||||
headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
out = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode(errors="replace")[:200]
|
||||
raise RuntimeError(f"upload HTTP {exc.code} en {url}: {detail}") from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(f"no se pudo conectar a {url}: {exc.reason}") from exc
|
||||
server_name = out.get("name", name)
|
||||
subfolder = out.get("subfolder", "")
|
||||
return f"{subfolder}/{server_name}" if subfolder else server_name
|
||||
|
||||
|
||||
def comfyui_image_to_3d_oneshot(
|
||||
image_path: str,
|
||||
*,
|
||||
server: str = "127.0.0.1:8188",
|
||||
variant: str = "mini",
|
||||
dest: str | None = None,
|
||||
wait_timeout: float = 600.0,
|
||||
**gen,
|
||||
) -> dict:
|
||||
"""Reconstruye una malla 3D desde una imagen en disco, end-to-end.
|
||||
|
||||
Args:
|
||||
image_path: ruta local de la imagen de entrada (PNG/JPG). Se sube al
|
||||
input/ del servidor.
|
||||
server: host:port del servidor ComfyUI (sin esquema). keyword-only.
|
||||
variant: "mini" (default), "standard" o "mv"; elige el checkpoint
|
||||
Hunyuan3D-2. El modelo debe estar ya instalado (comfyui_install_3d_model).
|
||||
keyword-only.
|
||||
dest: ruta destino de la malla (None = cwd; dir = dentro; archivo = ahi).
|
||||
keyword-only.
|
||||
wait_timeout: segundos maximos esperando a que el server termine la
|
||||
reconstruccion 3D (carga del modelo + difusion + decode). keyword-only.
|
||||
**gen: parametros de generacion pasados a comfyui_build_image_to_3d_workflow
|
||||
(resolution, steps, cfg, seed, octree_resolution, num_chunks, threshold,
|
||||
sampler_name, scheduler, filename_prefix).
|
||||
|
||||
Returns:
|
||||
dict {ok, mesh_path, faces, prompt_id, error}. mesh_path = ruta local de la
|
||||
malla descargada; faces = numero de caras si trimesh esta disponible (si no,
|
||||
None); prompt_id = id del trabajo en ComfyUI. Si falla, ok=False y error
|
||||
explica en que paso.
|
||||
"""
|
||||
if variant not in _VARIANT_CKPT:
|
||||
return {"ok": False, "mesh_path": "", "faces": None, "prompt_id": "",
|
||||
"error": f"variant {variant!r} no valida; usa {sorted(_VARIANT_CKPT)}"}
|
||||
if not os.path.isfile(os.path.expanduser(image_path)):
|
||||
return {"ok": False, "mesh_path": "", "faces": None, "prompt_id": "",
|
||||
"error": f"imagen no existe: {image_path}"}
|
||||
image_path = os.path.expanduser(image_path)
|
||||
ckpt_name = _VARIANT_CKPT[variant]
|
||||
|
||||
# 1. Subir la imagen al input/ del servidor.
|
||||
try:
|
||||
image_name = _upload_image(image_path, server)
|
||||
except RuntimeError as exc:
|
||||
return {"ok": False, "mesh_path": "", "faces": None, "prompt_id": "",
|
||||
"error": f"upload de imagen fallo: {exc}"}
|
||||
|
||||
# 2. Construir el workflow imagen->3D (funcion pura del registry).
|
||||
workflow = comfyui_build_image_to_3d_workflow(image_name, ckpt_name, **gen)
|
||||
|
||||
# 3. Encolar.
|
||||
try:
|
||||
sub = comfyui_submit_workflow(workflow, server=server)
|
||||
prompt_id = sub["prompt_id"]
|
||||
except (RuntimeError, KeyError) as exc:
|
||||
return {"ok": False, "mesh_path": "", "faces": None, "prompt_id": "",
|
||||
"error": f"submit fallo: {exc}"}
|
||||
|
||||
# 4. Esperar a que termine.
|
||||
try:
|
||||
comfyui_wait_result(prompt_id, server=server, timeout=wait_timeout)
|
||||
except (TimeoutError, RuntimeError) as exc:
|
||||
return {"ok": False, "mesh_path": "", "faces": None, "prompt_id": prompt_id,
|
||||
"error": f"wait fallo: {exc}"}
|
||||
|
||||
# 5. Descargar la malla.
|
||||
fetched = comfyui_fetch_output_mesh(prompt_id, server=server, dest=dest)
|
||||
if not fetched.get("ok"):
|
||||
return {"ok": False, "mesh_path": "", "faces": None, "prompt_id": prompt_id,
|
||||
"error": f"fetch de malla fallo: {fetched.get('error')}"}
|
||||
mesh_path = fetched["path"]
|
||||
|
||||
# 6. Caras (best-effort; trimesh puede no estar en el venv).
|
||||
faces = None
|
||||
try:
|
||||
import trimesh # type: ignore
|
||||
mesh = trimesh.load(mesh_path, force="mesh")
|
||||
faces = int(len(mesh.faces))
|
||||
except Exception: # noqa: BLE001 — trimesh ausente o malla ilegible: faces=None.
|
||||
faces = None
|
||||
|
||||
return {"ok": True, "mesh_path": mesh_path, "faces": faces,
|
||||
"prompt_id": prompt_id, "error": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
img = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser(
|
||||
"~/ComfyUI/input/3d_src_robot_00001_.png")
|
||||
res = comfyui_image_to_3d_oneshot(img, dest="/tmp/comfy_oneshot")
|
||||
print(json.dumps(res, indent=2))
|
||||
Reference in New Issue
Block a user