From 4302212b3419375733164e20adfcefa8cb1d6feb Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 24 Jun 2026 19:57:10 +0200 Subject: [PATCH] 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) --- docs/capabilities/comfyui.md | 5 +- .../ml/comfyui_generate_views_from_image.md | 62 +++++-- .../ml/comfyui_generate_views_from_image.py | 153 +++++++++++++++--- 3 files changed, 181 insertions(+), 39 deletions(-) diff --git a/docs/capabilities/comfyui.md b/docs/capabilities/comfyui.md index 9cf90c16..e4dcca83 100644 --- a/docs/capabilities/comfyui.md +++ b/docs/capabilities/comfyui.md @@ -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 + 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, -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`, report `0079`). | 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_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_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. | diff --git a/python/functions/ml/comfyui_generate_views_from_image.md b/python/functions/ml/comfyui_generate_views_from_image.md index d3f3fc72..fc8760ac 100644 --- a/python/functions/ml/comfyui_generate_views_from_image.md +++ b/python/functions/ml/comfyui_generate_views_from_image.md @@ -3,10 +3,10 @@ name: comfyui_generate_views_from_image kind: function lang: py domain: ml -version: "1.0.0" +version: "1.1.0" 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" -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." +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 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] 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: [] @@ -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." - name: elevation 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 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 @@ -33,7 +39,7 @@ params: desc: "Timeout de espera de la generacion en segundos. keyword-only." - name: timeout 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: , method, validated: True, valid, missing_nodes, missing_models, available, ...}. Sin nodo+modelo viable (stub honesto, NO encola): {ok: False, method, views: {}, reason: '', 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: , method, validated: True, valid, missing_nodes, missing_models, available, ...}. Sin nodo+modelo viable (stub honesto, NO encola): {ok: False, method, views: {}, reason: '', available: {nodes, ckpts, ckpt_combo}, error: ''}. Fallos de red/encolado: ok=False con error." tested: false tests: [] test_file_path: "" @@ -67,6 +73,24 @@ else: 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. ## 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 `{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`): - `stable_zero123.ckpt` **SÍ está instalado** → `method='zero123'` es viable y genera de - verdad (el report 0073 lo daba por ausente; quedó desfasado). `sv3d_u.safetensors` NO - está instalado y, además, su builder aún no existe (ver gotcha siguiente). + `stable_zero123.ckpt` **SÍ** (zero123 viable) y `sv3d_p.safetensors` **SÍ** (sv3d viable + y probado en GPU, ver Capability growth log). Para sv3d acepta `sv3d_p.safetensors` + (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 (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 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. -- **`method='sv3d'` aún no tiene builder**: el camino SV3D (órbita de 21 frames) lanza - `NotImplementedError` capturado → `ok=False` con error claro. Implementado: `zero123` - (StableZero123_Conditioning_Batched). Se añadirá SV3D cuando el modelo esté disponible - para probarlo (no especular: KISS). +- **SV3D y la VRAM (8 GB lowvram)**: el orbit nativo de 21 frames @576×576 **cabe** en + 8 GB lowvram (medido: ~75 s, peak ~5.7 GB de 8 GB con el escritorio usando ~2.9 GB). El + checkpoint `sv3d_p.safetensors` pesa ~9.4 GB en disco (incluye el backbone SVD completo + + 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 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% @@ -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. - `azimuths` se asume equiespaciado (el batch usa un incremento fijo). Mapeo de ángulo a 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/`. diff --git a/python/functions/ml/comfyui_generate_views_from_image.py b/python/functions/ml/comfyui_generate_views_from_image.py index 25864aa3..db75343e 100644 --- a/python/functions/ml/comfyui_generate_views_from_image.py +++ b/python/functions/ml/comfyui_generate_views_from_image.py @@ -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_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 = { - "zero123": "stable_zero123.ckpt", - "sv3d": "sv3d_u.safetensors", + "zero123": ("stable_zero123.ckpt",), + "sv3d": ("sv3d_p.safetensors", "sv3d_u.safetensors"), } # azimuth (grados) -> nombre de vista. 0=front (la que aporta el caller). _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", 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, @@ -72,6 +78,14 @@ def comfyui_generate_views_from_image( el batch. keyword-only. elevation: elevacion de camara en grados para todas las vistas. 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 se descargan (solo se devuelven los nombres del output del servidor). keyword-only. @@ -86,6 +100,10 @@ def comfyui_generate_views_from_image( dict. Si hay camino viable y se genera: {ok: True, method, views: {"back": , "left": ..., "right": ...}, prompt_id, available: {...}, reason: "", error: ""}. + Con method='sv3d' ademas trae el orbit completo: + {..., "views": {}, + "frames": [], + "frame_count": N}. Con validate_only=True (no encola): {ok: , method, validated: True, valid, missing_nodes, missing_models, views: {}, available: {...}, reason: "", error: ""}. @@ -105,7 +123,7 @@ def comfyui_generate_views_from_image( "sv3d": "SV3D_Conditioning" in 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} # 2. Elegir metodo viable (nodo + checkpoint presentes). @@ -122,9 +140,11 @@ def comfyui_generate_views_from_image( ) # 3. Construir el workflow. + ckpt_name = _resolve_ckpt(chosen, ckpts) try: - wf = _build_views_workflow(image_name, chosen, ckpts[_method_ckpt_key(chosen, ckpts)], - azimuths, elevation) + wf = _build_views_workflow(image_name, chosen, ckpt_name, azimuths, elevation, + video_frames=video_frames, + sv3d_width=sv3d_width, sv3d_height=sv3d_height) except NotImplementedError as 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: return _stub(chosen, f"el servidor no devolvio prompt_id: {sub}", available=available) 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) return {"ok": True, "method": chosen, "views": views, "prompt_id": prompt_id, "available": available, "reason": "", "error": ""} @@ -163,8 +188,12 @@ def _checkpoint_combo(oi: dict) -> list: return [] -def _method_ckpt_key(method: str, ckpts: list) -> int: - return ckpts.index(_METHOD_CKPT[method]) +def _resolve_ckpt(method: str, ckpts: list) -> str | None: + """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: @@ -175,7 +204,7 @@ def _why_unavailable(order, nodes_present, ckpts_present) -> str: if not nodes_present.get(m): parts.append(f"{m}: nodo nativo ausente en el servidor") elif not ckpts_present.get(m): - ck = _METHOD_CKPT[m] + ck = " o ".join(_METHOD_CKPT[m]) parts.append( f"{m}: nodo OK pero falta el checkpoint '{ck}'. " f"Instalalo con comfyui_download_model(, 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.") -def _build_views_workflow(image_name, method, ckpt_name, azimuths, elevation) -> dict: - """Workflow batched de sintesis de vistas. Hoy implementado para StableZero123.""" +def _build_views_workflow(image_name, method, ckpt_name, azimuths, elevation, *, + video_frames=21, sv3d_width=576, sv3d_height=576) -> dict: + """Workflow batched de sintesis de vistas. zero123 (azimuth) o sv3d (orbit).""" if method == "sv3d": - raise NotImplementedError( - "el builder SV3D (orbita de 21 frames) no esta implementado todavia; usa method='zero123'" - ) + return _build_sv3d_workflow(image_name, ckpt_name, elevation, + video_frames, sv3d_width, sv3d_height) azs = sorted(azimuths) start = azs[0] 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: - """Mapea las imagenes del SaveImage (en orden de azimuth) a nombres de vista.""" +def _build_sv3d_workflow(image_name, ckpt_name, elevation, video_frames, + 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 urllib.request @@ -239,18 +310,48 @@ def _collect_views(prompt_id, server, azimuths, dest_dir, timeout) -> dict: images = [] for node_out in outputs.values(): 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 = {} - 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}") - if dest_dir: - 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"] + views[name] = _fetch_or_name(img, dest_dir, server) return views