feat(ml): implementa camino sv3d en comfyui_generate_views_from_image
Completa la rama method='sv3d' (antes NotImplementedError) componiendo el workflow SV3D nativo de ComfyUI (SV3D_Conditioning + VideoLinearCFGGuidance + KSampler + VAEDecode + SaveImage): una imagen produce un orbit de N frames equiespaciados en 360 grados en una pasada. - _METHOD_CKPT['sv3d'] acepta sv3d_p (preferido) o sv3d_u; nuevo helper _resolve_ckpt sustituye a _method_ckpt_key. - nuevos params keyword-only video_frames=21, sv3d_width=576, sv3d_height=576 (configurables para densidad de orbit y control de VRAM). - salida sv3d extendida con frames (orbit completo) + frame_count; views mapea cada azimuth al frame del orbit mas cercano (cardinales para multi-vista). - _collect_views_sv3d + helpers compartidos _history_images/_fetch_or_name; _collect_views (zero123) refactorizado para reusarlos. Probado en GPU (8 GB lowvram): sv3d_p.safetensors descargado a checkpoints/, 21 frames 576x576 en ~75 s, peak ~5.7 GB, sin OOM (prompt_id 0caeedf4-baa0-4c8f-844a-867490ac4f85). Detalle en report 0128. Bumpa version 1.0.0 -> 1.1.0 + Capability growth log. Pagina madre comfyui.md marca ambos caminos (zero123/sv3d) operativos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -150,14 +150,15 @@ reconstruye en una malla 3D GLB con un grafo de 9 nodos (`LoadImage → ImageOnl
|
|||||||
VAEDecodeHunyuan3D → VoxelToMeshBasic → SaveGLB`). El checkpoint es self-contained (DiT de forma +
|
VAEDecodeHunyuan3D → VoxelToMeshBasic → SaveGLB`). El checkpoint es self-contained (DiT de forma +
|
||||||
VAE 3D + encoder de imagen en un `.safetensors`). Salida **shape-only** (sin color/textura). Detalle
|
VAE 3D + encoder de imagen en un `.safetensors`). Salida **shape-only** (sin color/textura). Detalle
|
||||||
y benchmark en `reports/0069-2026-06-23-comfyui-img-to-3d.md`. Para mejorar la cara trasera/laterales,
|
y benchmark en `reports/0069-2026-06-23-comfyui-img-to-3d.md`. Para mejorar la cara trasera/laterales,
|
||||||
genera vistas novel-view desde 1 imagen (`generate_views_from_image`, reports `0073`); para VER el GLB
|
genera vistas novel-view desde 1 imagen (`generate_views_from_image`: `zero123` azimuth o
|
||||||
|
`sv3d` orbit de 21 frames, ambos operativos en 8 GB — reports `0073`, `0128`); para VER el GLB
|
||||||
resultante interactivo dentro de un nodo de la UI, monta el visor `Load3D` (`build_view_3d_workflow`,
|
resultante interactivo dentro de un nodo de la UI, monta el visor `Load3D` (`build_view_3d_workflow`,
|
||||||
report `0079`).
|
report `0079`).
|
||||||
|
|
||||||
| ID | Firma corta | Qué hace |
|
| ID | Firma corta | Qué hace |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [comfyui_build_image_to_3d_workflow_py_ml](../../python/functions/ml/comfyui_build_image_to_3d_workflow.md) | `build_image_to_3d_workflow(image_name, ckpt_name='hunyuan3d-dit-v2-mini.safetensors', *, resolution, steps, cfg, seed, octree_resolution, num_chunks, threshold, ..., watertight=False) -> dict` | Builder del workflow imagen→3D de 9 nodos (Hunyuan3D-2 nativo) en API format. El SaveGLB produce un `.glb`. `watertight=True` usa `VoxelToMesh` (`algorithm='surface net'`) en vez de `VoxelToMeshBasic` → malla estanca de raíz (default conserva el comportamiento histórico). **Pura**. |
|
| [comfyui_build_image_to_3d_workflow_py_ml](../../python/functions/ml/comfyui_build_image_to_3d_workflow.md) | `build_image_to_3d_workflow(image_name, ckpt_name='hunyuan3d-dit-v2-mini.safetensors', *, resolution, steps, cfg, seed, octree_resolution, num_chunks, threshold, ..., watertight=False) -> dict` | Builder del workflow imagen→3D de 9 nodos (Hunyuan3D-2 nativo) en API format. El SaveGLB produce un `.glb`. `watertight=True` usa `VoxelToMesh` (`algorithm='surface net'`) en vez de `VoxelToMeshBasic` → malla estanca de raíz (default conserva el comportamiento histórico). **Pura**. |
|
||||||
| [comfyui_generate_views_from_image_py_ml](../../python/functions/ml/comfyui_generate_views_from_image.md) | `generate_views_from_image(image_name, *, method='auto', server, azimuths=(90,180,270), elevation, dest_dir, validate_only=False, ...) -> dict` | Sintetiza vistas novel-view (back/left/right) desde 1 imagen con StableZero123/SV3D nativos, para alimentar el 3D multi-vista. **Honesta**: si el nodo+checkpoint no están, devuelve `ok=False` con la acción y NO encola. `validate_only=True` valida sin tocar GPU. Impura. |
|
| [comfyui_generate_views_from_image_py_ml](../../python/functions/ml/comfyui_generate_views_from_image.md) | `generate_views_from_image(image_name, *, method='auto', server, azimuths=(90,180,270), elevation, video_frames=21, sv3d_width=576, sv3d_height=576, dest_dir, validate_only=False, ...) -> dict` | Sintetiza vistas novel-view desde 1 imagen con StableZero123/SV3D nativos, para alimentar el 3D multi-vista. **Ambos caminos operativos**: `method='zero123'` (azimuth → back/left/right) y `method='sv3d'` (`sv3d_p.safetensors`, orbit de N frames 360° → `frames` + cardinales mapeados; probado en 8 GB lowvram, 21f@576 ~75 s, peak ~5.7 GB, report 0128). **Honesta**: si el nodo+checkpoint no están, devuelve `ok=False` con la acción y NO encola. `validate_only=True` valida sin tocar GPU. Impura. |
|
||||||
| [comfyui_build_view_3d_workflow_py_ml](../../python/functions/ml/comfyui_build_view_3d_workflow.md) | `build_view_3d_workflow(model_file, *, animation=False, width, height) -> dict` | Monta el visor 3D nativo `Load3D` (o `Load3DAdvanced` con `animation=True`) para VER un GLB/OBJ existente, orbitando con el ratón, sin ejecutar el grafo. `model_file` relativo a `input/3d/`. Cárgalo con `load_workflow_ui`. **Pura**. |
|
| [comfyui_build_view_3d_workflow_py_ml](../../python/functions/ml/comfyui_build_view_3d_workflow.md) | `build_view_3d_workflow(model_file, *, animation=False, width, height) -> dict` | Monta el visor 3D nativo `Load3D` (o `Load3DAdvanced` con `animation=True`) para VER un GLB/OBJ existente, orbitando con el ratón, sin ejecutar el grafo. `model_file` relativo a `input/3d/`. Cárgalo con `load_workflow_ui`. **Pura**. |
|
||||||
| [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. |
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ name: comfyui_generate_views_from_image
|
|||||||
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_generate_views_from_image(image_name: str, *, method: str = \"auto\", server: str = \"127.0.0.1:8188\", azimuths: tuple = (90, 180, 270), elevation: float = 0.0, dest_dir: str | None = None, validate_only: bool = False, wait_timeout: float = 300.0, timeout: float = 30.0) -> dict"
|
signature: "def comfyui_generate_views_from_image(image_name: str, *, method: str = \"auto\", server: str = \"127.0.0.1:8188\", azimuths: tuple = (90, 180, 270), elevation: float = 0.0, video_frames: int = 21, sv3d_width: int = 576, sv3d_height: int = 576, dest_dir: str | None = None, validate_only: bool = False, wait_timeout: float = 300.0, timeout: float = 30.0) -> dict"
|
||||||
description: "Genera vistas novel-view (back/left/right) desde 1 imagen para alimentar el 3D multi-vista de ComfyUI. Usa los sintetizadores NATIVOS StableZero123 (StableZero123_Conditioning_Batched, control de azimuth) o SV3D (orbita de 21 frames); en 8 GB cabe zero123 para sintesis de vistas. HONESTA: consulta /object_info, comprueba que el nodo Y su checkpoint estan instalados, y SOLO encola si hay camino viable; si no, devuelve {ok: False, reason} con la accion para habilitarlo SIN tocar la GPU. Compone object_info + submit + wait + fetch_output_image. Impura: HTTP + disco."
|
description: "Genera vistas novel-view desde 1 imagen para alimentar el 3D multi-vista de ComfyUI. Usa los sintetizadores NATIVOS StableZero123 (StableZero123_Conditioning_Batched, control de azimuth; devuelve back/left/right) o SV3D (SV3D_Conditioning, orbita de N frames equiespaciados en 360 grados; devuelve el orbit completo + cardinales mapeados). Ambos caminos operativos y probados en 8 GB lowvram. HONESTA: consulta /object_info, comprueba que el nodo Y su checkpoint estan instalados, y SOLO encola si hay camino viable; si no, devuelve {ok: False, reason} con la accion para habilitarlo SIN tocar la GPU. Compone object_info + validate + submit + wait + fetch_output_image. Impura: HTTP + disco."
|
||||||
tags: [comfyui, ml, img-to-3d, novel-view, multiview, stablezero123, sv3d]
|
tags: [comfyui, ml, img-to-3d, novel-view, multiview, stablezero123, sv3d]
|
||||||
uses_functions: [comfyui_object_info_py_ml, comfyui_validate_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml]
|
uses_functions: [comfyui_object_info_py_ml, comfyui_validate_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml]
|
||||||
uses_types: []
|
uses_types: []
|
||||||
@@ -25,6 +25,12 @@ params:
|
|||||||
desc: "Angulos (grados) de las vistas a generar; 90=right, 180=back, 270=left (0=front la aporta el caller). Se asumen equiespaciados para el batch. keyword-only."
|
desc: "Angulos (grados) de las vistas a generar; 90=right, 180=back, 270=left (0=front la aporta el caller). Se asumen equiespaciados para el batch. keyword-only."
|
||||||
- name: elevation
|
- name: elevation
|
||||||
desc: "Elevacion de camara en grados para todas las vistas. keyword-only."
|
desc: "Elevacion de camara en grados para todas las vistas. keyword-only."
|
||||||
|
- name: video_frames
|
||||||
|
desc: "SOLO sv3d. Numero de frames del orbit (vistas equiespaciadas en 360 grados que el modelo sintetiza en una pasada). Default 21 (el nativo de SV3D). Bajalo (7-12) para reducir VRAM en 8 GB lowvram a costa de menos densidad angular. keyword-only."
|
||||||
|
- name: sv3d_width
|
||||||
|
desc: "SOLO sv3d. Ancho en px de cada frame del orbit. Default 576 (nativo). Bajalo para reducir VRAM. keyword-only."
|
||||||
|
- name: sv3d_height
|
||||||
|
desc: "SOLO sv3d. Alto en px de cada frame del orbit. Default 576 (nativo). keyword-only."
|
||||||
- name: dest_dir
|
- name: dest_dir
|
||||||
desc: "Carpeta local donde descargar las vistas generadas. Si None, no se descargan (solo se devuelven los nombres del output del servidor). keyword-only."
|
desc: "Carpeta local donde descargar las vistas generadas. Si None, no se descargan (solo se devuelven los nombres del output del servidor). keyword-only."
|
||||||
- name: validate_only
|
- name: validate_only
|
||||||
@@ -33,7 +39,7 @@ params:
|
|||||||
desc: "Timeout de espera de la generacion en segundos. keyword-only."
|
desc: "Timeout de espera de la generacion en segundos. keyword-only."
|
||||||
- name: timeout
|
- name: timeout
|
||||||
desc: "Timeout HTTP por request en segundos. keyword-only."
|
desc: "Timeout HTTP por request en segundos. keyword-only."
|
||||||
output: "dict. Viable: {ok: True, method, views: {back, left, right -> ruta/nombre}, prompt_id, available, reason: '', error: ''}. validate_only=True (no encola): {ok: <valido>, method, validated: True, valid, missing_nodes, missing_models, available, ...}. Sin nodo+modelo viable (stub honesto, NO encola): {ok: False, method, views: {}, reason: '<que falta y como instalarlo>', available: {nodes, ckpts, ckpt_combo}, error: ''}. Fallos de red/encolado: ok=False con error."
|
output: "dict. Viable zero123: {ok: True, method, views: {back, left, right -> ruta/nombre}, prompt_id, available, reason: '', error: ''}. Viable sv3d: ademas trae el orbit completo: {..., views: {cardinales mapeados al frame mas cercano}, frames: [ruta/nombre de cada uno de los N frames del orbit], frame_count: N}. validate_only=True (no encola): {ok: <valido>, method, validated: True, valid, missing_nodes, missing_models, available, ...}. Sin nodo+modelo viable (stub honesto, NO encola): {ok: False, method, views: {}, reason: '<que falta y como instalarlo>', available: {nodes, ckpts, ckpt_combo}, error: ''}. Fallos de red/encolado: ok=False con error."
|
||||||
tested: false
|
tested: false
|
||||||
tests: []
|
tests: []
|
||||||
test_file_path: ""
|
test_file_path: ""
|
||||||
@@ -67,6 +73,24 @@ else:
|
|||||||
print(res["available"]) # {nodes: {...}, ckpts: {...}}
|
print(res["available"]) # {nodes: {...}, ckpts: {...}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Camino SV3D (orbit de N frames)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# SV3D: 1 imagen -> orbit de 21 vistas equiespaciadas en 360 grados, en una pasada.
|
||||||
|
# La imagen debe estar ya en input/ del servidor. Cabe en 8 GB lowvram (~75 s, peak ~5.7 GB).
|
||||||
|
res = comfyui_generate_views_from_image(
|
||||||
|
"sv3d_test_robot.png", method="sv3d",
|
||||||
|
video_frames=21, sv3d_width=576, sv3d_height=576, # nativo; baja para menos VRAM
|
||||||
|
dest_dir="/tmp/sv3d_views", wait_timeout=900.0)
|
||||||
|
|
||||||
|
if res["ok"]:
|
||||||
|
res["frame_count"] # 21 — orbit completo
|
||||||
|
res["frames"] # [".../sv3d_view_00001_.png", ... x21] (orbit entero)
|
||||||
|
res["views"] # {"right": frame06, "back": frame11, "left": frame17}
|
||||||
|
# cardinales mapeados al frame mas cercano del orbit,
|
||||||
|
# para alimentar comfyui_build_image_to_3d_multiview_workflow
|
||||||
|
```
|
||||||
|
|
||||||
Lánzalo con el python del venv (import de arriba o heredoc). `./fn run` directo no aplica: la firma usa `*` (keyword-only). El bloque `__main__` ejecuta el caso sin modelos instalados y muestra el `ok=False` honesto.
|
Lánzalo con el python del venv (import de arriba o heredoc). `./fn run` directo no aplica: la firma usa `*` (keyword-only). El bloque `__main__` ejecuta el caso sin modelos instalados y muestra el `ok=False` honesto.
|
||||||
|
|
||||||
## Cuando usarla
|
## Cuando usarla
|
||||||
@@ -83,18 +107,25 @@ report 0073). Esta función es el camino sintético cuando solo hay 1 vista.
|
|||||||
- **No finge resultados**: si el nodo o su checkpoint no están instalados, devuelve
|
- **No finge resultados**: si el nodo o su checkpoint no están instalados, devuelve
|
||||||
`{ok: False, reason: ...}` con el comando para habilitarlo y **NO encola nada** (no
|
`{ok: False, reason: ...}` con el comando para habilitarlo y **NO encola nada** (no
|
||||||
compite por la GPU). Estado en este equipo (24/06/2026, verificado contra `/object_info`):
|
compite por la GPU). Estado en este equipo (24/06/2026, verificado contra `/object_info`):
|
||||||
`stable_zero123.ckpt` **SÍ está instalado** → `method='zero123'` es viable y genera de
|
`stable_zero123.ckpt` **SÍ** (zero123 viable) y `sv3d_p.safetensors` **SÍ** (sv3d viable
|
||||||
verdad (el report 0073 lo daba por ausente; quedó desfasado). `sv3d_u.safetensors` NO
|
y probado en GPU, ver Capability growth log). Para sv3d acepta `sv3d_p.safetensors`
|
||||||
está instalado y, además, su builder aún no existe (ver gotcha siguiente).
|
(preferido, orbita con elevación variable) o `sv3d_u.safetensors` (orbita uniforme).
|
||||||
- **`image_name` debe existir en `input/` ANTES de generar**: la función no sube la imagen
|
- **`image_name` debe existir en `input/` ANTES de generar**: la función no sube la imagen
|
||||||
(no la inventa). Si pasas un `image_name` que no está en el `input/` del servidor, el
|
(no la inventa). Si pasas un `image_name` que no está en el `input/` del servidor, el
|
||||||
POST /prompt devuelve HTTP 400 (`prompt_outputs_failed_validation` de LoadImage) y la
|
POST /prompt devuelve HTTP 400 (`prompt_outputs_failed_validation` de LoadImage) y la
|
||||||
función lo propaga como `ok=False` con el body — comportamiento correcto, no un bug.
|
función lo propaga como `ok=False` con el body — comportamiento correcto, no un bug.
|
||||||
Usa `validate_only=True` para comprobar el grafo sin necesidad de la imagen ni de GPU.
|
Usa `validate_only=True` para comprobar el grafo sin necesidad de la imagen ni de GPU.
|
||||||
- **`method='sv3d'` aún no tiene builder**: el camino SV3D (órbita de 21 frames) lanza
|
- **SV3D y la VRAM (8 GB lowvram)**: el orbit nativo de 21 frames @576×576 **cabe** en
|
||||||
`NotImplementedError` capturado → `ok=False` con error claro. Implementado: `zero123`
|
8 GB lowvram (medido: ~75 s, peak ~5.7 GB de 8 GB con el escritorio usando ~2.9 GB). El
|
||||||
(StableZero123_Conditioning_Batched). Se añadirá SV3D cuando el modelo esté disponible
|
checkpoint `sv3d_p.safetensors` pesa ~9.4 GB en disco (incluye el backbone SVD completo +
|
||||||
para probarlo (no especular: KISS).
|
CLIP-vision + VAE), NO ~2 GB; ComfyUI lo carga por capas en lowvram. Si diera OOM en un
|
||||||
|
equipo más justo, baja `video_frames` (p. ej. 12) y/o `sv3d_width`/`sv3d_height` (p. ej.
|
||||||
|
320); reducir frames degrada la densidad del orbit (el modelo se entrenó para 21).
|
||||||
|
- **SV3D orbit vs zero123 azimuths**: SV3D produce N frames equiespaciados en 360° empezando
|
||||||
|
por la vista frontal (frame 0 = az 0); el azimuth por-frame **no es controlable**
|
||||||
|
individualmente como en zero123. Para encajar con el consumidor multi-vista, los `azimuths`
|
||||||
|
pedidos se mapean al frame del orbit más cercano (con 21 frames: right≈frame 6, back≈frame
|
||||||
|
11, left≈frame 17) en `views`, y el orbit completo va en `frames`.
|
||||||
- **Encola trabajo de GPU** sólo en el camino viable: `comfyui_submit_workflow` dispara
|
- **Encola trabajo de GPU** sólo en el camino viable: `comfyui_submit_workflow` dispara
|
||||||
generación real. Respeta el aislamiento del server (coordina si otro agente lo usa).
|
generación real. Respeta el aislamiento del server (coordina si otro agente lo usa).
|
||||||
- **Vistas sintéticas ≠ fotos reales**: no son perfectamente ortogonales ni 100%
|
- **Vistas sintéticas ≠ fotos reales**: no son perfectamente ortogonales ni 100%
|
||||||
@@ -102,3 +133,12 @@ report 0073). Esta función es el camino sintético cuando solo hay 1 vista.
|
|||||||
fotos reales > síntesis. MV-Adapter (mejor sintetizador) es custom node, fuera de alcance.
|
fotos reales > síntesis. MV-Adapter (mejor sintetizador) es custom node, fuera de alcance.
|
||||||
- `azimuths` se asume equiespaciado (el batch usa un incremento fijo). Mapeo de ángulo a
|
- `azimuths` se asume equiespaciado (el batch usa un incremento fijo). Mapeo de ángulo a
|
||||||
nombre: 0=front, 90=right, 180=back, 270=left.
|
nombre: 0=front, 90=right, 180=back, 270=left.
|
||||||
|
|
||||||
|
## Capability growth log
|
||||||
|
|
||||||
|
- v1.1.0 (24/06/2026) — implementado el camino `sv3d` (antes `NotImplementedError`):
|
||||||
|
builder `SV3D_Conditioning` + `VideoLinearCFGGuidance` + `KSampler` + `VAEDecode` que
|
||||||
|
produce el orbit de N frames; nuevos params `video_frames`/`sv3d_width`/`sv3d_height`
|
||||||
|
configurables; salida extendida con `frames`/`frame_count`; soporta `sv3d_p`/`sv3d_u`.
|
||||||
|
Probado en GPU (8 GB lowvram): 21 frames 576×576 en ~75 s, peak ~5.7 GB
|
||||||
|
(prompt_id `0caeedf4-…`). Modelo `sv3d_p.safetensors` descargado a `checkpoints/`.
|
||||||
|
|||||||
@@ -37,10 +37,13 @@ from comfyui_submit_workflow import comfyui_submit_workflow # noqa: E402
|
|||||||
from comfyui_wait_result import comfyui_wait_result # noqa: E402
|
from comfyui_wait_result import comfyui_wait_result # noqa: E402
|
||||||
from comfyui_fetch_output_image import comfyui_fetch_output_image # noqa: E402
|
from comfyui_fetch_output_image import comfyui_fetch_output_image # noqa: E402
|
||||||
|
|
||||||
# Checkpoint requerido por cada metodo (lo carga ImageOnlyCheckpointLoader).
|
# Checkpoint(s) requerido(s) por cada metodo (los carga ImageOnlyCheckpointLoader).
|
||||||
|
# Cada metodo declara una tupla de candidatos en orden de preferencia: se usa el
|
||||||
|
# primero que el servidor ofrezca. SV3D acepta sv3d_p (orbita con elevacion
|
||||||
|
# variable, preferido) o sv3d_u (orbita uniforme en el ecuador).
|
||||||
_METHOD_CKPT = {
|
_METHOD_CKPT = {
|
||||||
"zero123": "stable_zero123.ckpt",
|
"zero123": ("stable_zero123.ckpt",),
|
||||||
"sv3d": "sv3d_u.safetensors",
|
"sv3d": ("sv3d_p.safetensors", "sv3d_u.safetensors"),
|
||||||
}
|
}
|
||||||
# azimuth (grados) -> nombre de vista. 0=front (la que aporta el caller).
|
# azimuth (grados) -> nombre de vista. 0=front (la que aporta el caller).
|
||||||
_AZIMUTH_NAME = {0: "front", 90: "right", 180: "back", 270: "left"}
|
_AZIMUTH_NAME = {0: "front", 90: "right", 180: "back", 270: "left"}
|
||||||
@@ -53,6 +56,9 @@ def comfyui_generate_views_from_image(
|
|||||||
server: str = "127.0.0.1:8188",
|
server: str = "127.0.0.1:8188",
|
||||||
azimuths: tuple = (90, 180, 270),
|
azimuths: tuple = (90, 180, 270),
|
||||||
elevation: float = 0.0,
|
elevation: float = 0.0,
|
||||||
|
video_frames: int = 21,
|
||||||
|
sv3d_width: int = 576,
|
||||||
|
sv3d_height: int = 576,
|
||||||
dest_dir: str | None = None,
|
dest_dir: str | None = None,
|
||||||
validate_only: bool = False,
|
validate_only: bool = False,
|
||||||
wait_timeout: float = 300.0,
|
wait_timeout: float = 300.0,
|
||||||
@@ -72,6 +78,14 @@ def comfyui_generate_views_from_image(
|
|||||||
el batch. keyword-only.
|
el batch. keyword-only.
|
||||||
elevation: elevacion de camara en grados para todas las vistas.
|
elevation: elevacion de camara en grados para todas las vistas.
|
||||||
keyword-only.
|
keyword-only.
|
||||||
|
video_frames: SOLO sv3d. Numero de frames del orbit (vistas equiespaciadas
|
||||||
|
en 360 grados que el modelo sintetiza en una pasada). Default 21 (el
|
||||||
|
nativo de SV3D). Bajalo (p. ej. 7-12) para reducir VRAM en 8 GB
|
||||||
|
lowvram a costa de menos densidad angular. keyword-only.
|
||||||
|
sv3d_width: SOLO sv3d. Ancho de cada frame del orbit. Default 576 (nativo).
|
||||||
|
Bajalo (p. ej. 320) para reducir VRAM. keyword-only.
|
||||||
|
sv3d_height: SOLO sv3d. Alto de cada frame del orbit. Default 576 (nativo).
|
||||||
|
keyword-only.
|
||||||
dest_dir: carpeta local donde descargar las vistas generadas. Si None, no
|
dest_dir: carpeta local donde descargar las vistas generadas. Si None, no
|
||||||
se descargan (solo se devuelven los nombres del output del servidor).
|
se descargan (solo se devuelven los nombres del output del servidor).
|
||||||
keyword-only.
|
keyword-only.
|
||||||
@@ -86,6 +100,10 @@ def comfyui_generate_views_from_image(
|
|||||||
dict. Si hay camino viable y se genera:
|
dict. Si hay camino viable y se genera:
|
||||||
{ok: True, method, views: {"back": <ruta/nombre>, "left": ..., "right": ...},
|
{ok: True, method, views: {"back": <ruta/nombre>, "left": ..., "right": ...},
|
||||||
prompt_id, available: {...}, reason: "", error: ""}.
|
prompt_id, available: {...}, reason: "", error: ""}.
|
||||||
|
Con method='sv3d' ademas trae el orbit completo:
|
||||||
|
{..., "views": {<cardinales mapeados al frame mas cercano>},
|
||||||
|
"frames": [<ruta/nombre de cada uno de los N frames del orbit>],
|
||||||
|
"frame_count": N}.
|
||||||
Con validate_only=True (no encola):
|
Con validate_only=True (no encola):
|
||||||
{ok: <valido>, method, validated: True, valid, missing_nodes,
|
{ok: <valido>, method, validated: True, valid, missing_nodes,
|
||||||
missing_models, views: {}, available: {...}, reason: "", error: ""}.
|
missing_models, views: {}, available: {...}, reason: "", error: ""}.
|
||||||
@@ -105,7 +123,7 @@ def comfyui_generate_views_from_image(
|
|||||||
"sv3d": "SV3D_Conditioning" in oi,
|
"sv3d": "SV3D_Conditioning" in oi,
|
||||||
}
|
}
|
||||||
ckpts = _checkpoint_combo(oi)
|
ckpts = _checkpoint_combo(oi)
|
||||||
ckpts_present = {m: (_METHOD_CKPT[m] in ckpts) for m in _METHOD_CKPT}
|
ckpts_present = {m: (_resolve_ckpt(m, ckpts) is not None) for m in _METHOD_CKPT}
|
||||||
available = {"nodes": nodes_present, "ckpts": ckpts_present, "ckpt_combo": ckpts}
|
available = {"nodes": nodes_present, "ckpts": ckpts_present, "ckpt_combo": ckpts}
|
||||||
|
|
||||||
# 2. Elegir metodo viable (nodo + checkpoint presentes).
|
# 2. Elegir metodo viable (nodo + checkpoint presentes).
|
||||||
@@ -122,9 +140,11 @@ def comfyui_generate_views_from_image(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3. Construir el workflow.
|
# 3. Construir el workflow.
|
||||||
|
ckpt_name = _resolve_ckpt(chosen, ckpts)
|
||||||
try:
|
try:
|
||||||
wf = _build_views_workflow(image_name, chosen, ckpts[_method_ckpt_key(chosen, ckpts)],
|
wf = _build_views_workflow(image_name, chosen, ckpt_name, azimuths, elevation,
|
||||||
azimuths, elevation)
|
video_frames=video_frames,
|
||||||
|
sv3d_width=sv3d_width, sv3d_height=sv3d_height)
|
||||||
except NotImplementedError as exc:
|
except NotImplementedError as exc:
|
||||||
return _stub(chosen, str(exc), available=available, error=str(exc))
|
return _stub(chosen, str(exc), available=available, error=str(exc))
|
||||||
|
|
||||||
@@ -143,6 +163,11 @@ def comfyui_generate_views_from_image(
|
|||||||
if not prompt_id:
|
if not prompt_id:
|
||||||
return _stub(chosen, f"el servidor no devolvio prompt_id: {sub}", available=available)
|
return _stub(chosen, f"el servidor no devolvio prompt_id: {sub}", available=available)
|
||||||
comfyui_wait_result(prompt_id, server=server, timeout=wait_timeout)
|
comfyui_wait_result(prompt_id, server=server, timeout=wait_timeout)
|
||||||
|
if chosen == "sv3d":
|
||||||
|
views, frames = _collect_views_sv3d(prompt_id, server, azimuths, dest_dir, timeout)
|
||||||
|
return {"ok": True, "method": chosen, "views": views, "frames": frames,
|
||||||
|
"frame_count": len(frames), "prompt_id": prompt_id,
|
||||||
|
"available": available, "reason": "", "error": ""}
|
||||||
views = _collect_views(prompt_id, server, azimuths, dest_dir, timeout)
|
views = _collect_views(prompt_id, server, azimuths, dest_dir, timeout)
|
||||||
return {"ok": True, "method": chosen, "views": views, "prompt_id": prompt_id,
|
return {"ok": True, "method": chosen, "views": views, "prompt_id": prompt_id,
|
||||||
"available": available, "reason": "", "error": ""}
|
"available": available, "reason": "", "error": ""}
|
||||||
@@ -163,8 +188,12 @@ def _checkpoint_combo(oi: dict) -> list:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _method_ckpt_key(method: str, ckpts: list) -> int:
|
def _resolve_ckpt(method: str, ckpts: list) -> str | None:
|
||||||
return ckpts.index(_METHOD_CKPT[method])
|
"""Primer checkpoint candidato del metodo que el servidor ofrece, o None."""
|
||||||
|
for cand in _METHOD_CKPT.get(method, ()):
|
||||||
|
if cand in ckpts:
|
||||||
|
return cand
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _why_unavailable(order, nodes_present, ckpts_present) -> str:
|
def _why_unavailable(order, nodes_present, ckpts_present) -> str:
|
||||||
@@ -175,7 +204,7 @@ def _why_unavailable(order, nodes_present, ckpts_present) -> str:
|
|||||||
if not nodes_present.get(m):
|
if not nodes_present.get(m):
|
||||||
parts.append(f"{m}: nodo nativo ausente en el servidor")
|
parts.append(f"{m}: nodo nativo ausente en el servidor")
|
||||||
elif not ckpts_present.get(m):
|
elif not ckpts_present.get(m):
|
||||||
ck = _METHOD_CKPT[m]
|
ck = " o ".join(_METHOD_CKPT[m])
|
||||||
parts.append(
|
parts.append(
|
||||||
f"{m}: nodo OK pero falta el checkpoint '{ck}'. "
|
f"{m}: nodo OK pero falta el checkpoint '{ck}'. "
|
||||||
f"Instalalo con comfyui_download_model(<url_{m}>, dest_subdir='checkpoints')"
|
f"Instalalo con comfyui_download_model(<url_{m}>, dest_subdir='checkpoints')"
|
||||||
@@ -185,12 +214,12 @@ def _why_unavailable(order, nodes_present, ckpts_present) -> str:
|
|||||||
"(front/left/back/right) y usa comfyui_build_image_to_3d_multiview_workflow directamente.")
|
"(front/left/back/right) y usa comfyui_build_image_to_3d_multiview_workflow directamente.")
|
||||||
|
|
||||||
|
|
||||||
def _build_views_workflow(image_name, method, ckpt_name, azimuths, elevation) -> dict:
|
def _build_views_workflow(image_name, method, ckpt_name, azimuths, elevation, *,
|
||||||
"""Workflow batched de sintesis de vistas. Hoy implementado para StableZero123."""
|
video_frames=21, sv3d_width=576, sv3d_height=576) -> dict:
|
||||||
|
"""Workflow batched de sintesis de vistas. zero123 (azimuth) o sv3d (orbit)."""
|
||||||
if method == "sv3d":
|
if method == "sv3d":
|
||||||
raise NotImplementedError(
|
return _build_sv3d_workflow(image_name, ckpt_name, elevation,
|
||||||
"el builder SV3D (orbita de 21 frames) no esta implementado todavia; usa method='zero123'"
|
video_frames, sv3d_width, sv3d_height)
|
||||||
)
|
|
||||||
azs = sorted(azimuths)
|
azs = sorted(azimuths)
|
||||||
start = azs[0]
|
start = azs[0]
|
||||||
increment = (azs[1] - azs[0]) if len(azs) > 1 else 90
|
increment = (azs[1] - azs[0]) if len(azs) > 1 else 90
|
||||||
@@ -227,8 +256,50 @@ def _build_views_workflow(image_name, method, ckpt_name, azimuths, elevation) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _collect_views(prompt_id, server, azimuths, dest_dir, timeout) -> dict:
|
def _build_sv3d_workflow(image_name, ckpt_name, elevation, video_frames,
|
||||||
"""Mapea las imagenes del SaveImage (en orden de azimuth) a nombres de vista."""
|
width, height) -> dict:
|
||||||
|
"""Workflow SV3D: 1 imagen -> orbit de `video_frames` vistas en una pasada.
|
||||||
|
|
||||||
|
SV3D reutiliza la maquinaria img2vid de Stable Video Diffusion: el checkpoint
|
||||||
|
(sv3d_p / sv3d_u) se carga con ImageOnlyCheckpointLoader (MODEL+CLIP_VISION+VAE),
|
||||||
|
SV3D_Conditioning produce el conditioning del orbit y la latente, y se muestrea
|
||||||
|
como un video con guia CFG lineal. El SaveImage emite los N frames del orbit.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"1": {"class_type": "LoadImage", "inputs": {"image": image_name}},
|
||||||
|
"2": {"class_type": "ImageOnlyCheckpointLoader", "inputs": {"ckpt_name": ckpt_name}},
|
||||||
|
"3": {
|
||||||
|
"class_type": "SV3D_Conditioning",
|
||||||
|
"inputs": {
|
||||||
|
"clip_vision": ["2", 1],
|
||||||
|
"init_image": ["1", 0],
|
||||||
|
"vae": ["2", 2],
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"video_frames": video_frames,
|
||||||
|
"elevation": elevation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"class_type": "VideoLinearCFGGuidance",
|
||||||
|
"inputs": {"model": ["2", 0], "min_cfg": 1.0},
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"class_type": "KSampler",
|
||||||
|
"inputs": {
|
||||||
|
"seed": 0, "steps": 20, "cfg": 2.5, "sampler_name": "euler",
|
||||||
|
"scheduler": "karras", "denoise": 1.0,
|
||||||
|
"model": ["4", 0], "positive": ["3", 0], "negative": ["3", 1],
|
||||||
|
"latent_image": ["3", 2],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"6": {"class_type": "VAEDecode", "inputs": {"samples": ["5", 0], "vae": ["2", 2]}},
|
||||||
|
"7": {"class_type": "SaveImage", "inputs": {"images": ["6", 0], "filename_prefix": "sv3d_view"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _history_images(prompt_id, server, timeout) -> list:
|
||||||
|
"""Lista ordenada de descriptores de imagen del history de un prompt."""
|
||||||
import json
|
import json
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
@@ -239,18 +310,48 @@ def _collect_views(prompt_id, server, azimuths, dest_dir, timeout) -> dict:
|
|||||||
images = []
|
images = []
|
||||||
for node_out in outputs.values():
|
for node_out in outputs.values():
|
||||||
images.extend(node_out.get("images", []))
|
images.extend(node_out.get("images", []))
|
||||||
azs = sorted(azimuths)
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_or_name(img, dest_dir, server) -> str:
|
||||||
|
"""Descarga la imagen a dest_dir y devuelve su ruta, o el nombre si dest_dir es None."""
|
||||||
|
if not dest_dir:
|
||||||
|
return img["filename"]
|
||||||
|
got = comfyui_fetch_output_image(
|
||||||
|
img["filename"], subfolder=img.get("subfolder", ""),
|
||||||
|
type_=img.get("type", "output"), server=server, dest_dir=dest_dir, timeout=60.0,
|
||||||
|
)
|
||||||
|
return got.get("path", img["filename"])
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_views_sv3d(prompt_id, server, azimuths, dest_dir, timeout) -> tuple:
|
||||||
|
"""Mapea el orbit SV3D: devuelve (cardinales, lista completa de frames).
|
||||||
|
|
||||||
|
SV3D produce N frames equiespaciados en 360 grados empezando por la vista
|
||||||
|
frontal (frame 0 = azimuth 0). Para cada azimuth solicitado se localiza el
|
||||||
|
frame del orbit mas cercano y se etiqueta con su nombre de vista cardinal,
|
||||||
|
de modo que el resultado encaja con el consumidor multi-vista. `frames`
|
||||||
|
conserva el orbit completo para reconstruccion 3D densa.
|
||||||
|
"""
|
||||||
|
images = _history_images(prompt_id, server, timeout)
|
||||||
|
frames = [_fetch_or_name(img, dest_dir, server) for img in images]
|
||||||
|
n = len(frames)
|
||||||
views = {}
|
views = {}
|
||||||
for img, az in zip(images, azs):
|
if n:
|
||||||
|
for az in sorted(azimuths):
|
||||||
|
idx = round((az % 360) / 360.0 * n) % n
|
||||||
|
name = _AZIMUTH_NAME.get(az % 360, f"az{az % 360}")
|
||||||
|
views[name] = frames[idx]
|
||||||
|
return views, frames
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_views(prompt_id, server, azimuths, dest_dir, timeout) -> dict:
|
||||||
|
"""Mapea las imagenes del SaveImage (en orden de azimuth) a nombres de vista."""
|
||||||
|
images = _history_images(prompt_id, server, timeout)
|
||||||
|
views = {}
|
||||||
|
for img, az in zip(images, sorted(azimuths)):
|
||||||
name = _AZIMUTH_NAME.get(az, f"az{az}")
|
name = _AZIMUTH_NAME.get(az, f"az{az}")
|
||||||
if dest_dir:
|
views[name] = _fetch_or_name(img, dest_dir, server)
|
||||||
got = comfyui_fetch_output_image(
|
|
||||||
img["filename"], subfolder=img.get("subfolder", ""),
|
|
||||||
type_=img.get("type", "output"), server=server, dest_dir=dest_dir, timeout=60.0,
|
|
||||||
)
|
|
||||||
views[name] = got.get("path", img["filename"])
|
|
||||||
else:
|
|
||||||
views[name] = img["filename"]
|
|
||||||
return views
|
return views
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user