Update modules/langchain_assistant/cvision_forge_v9.py
Browse files
modules/langchain_assistant/cvision_forge_v9.py
CHANGED
|
@@ -13,68 +13,143 @@ import gradio as gr
|
|
| 13 |
# EXTRACCIÓN SEGURA: Se elimina el token explícito para evitar bloqueos en el commit.
|
| 14 |
# El token real debe ser configurado en la pestaña "Settings -> Variables and Secrets" de tu Space.
|
| 15 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# ═════════════════════════════════════════════════════════════════════
|
| 20 |
# SECCIÓN 2: NÚCLEO LOGÍSTICO Y PROCESAMIENTO MULTIMODAL (TRELLIS BACKEND)
|
| 21 |
# 💡 Implementa el motor de inferencia oficial mediante hilos WebSockets.
|
|
|
|
|
|
|
| 22 |
# ═════════════════════════════════════════════════════════════════════
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
-
Toma la imagen nacida del motor FLUX
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
Evita simular clics en el frontend para evadir el colapso por 'hidden tab'.
|
|
|
|
|
|
|
| 28 |
"""
|
| 29 |
-
if not HF_TOKEN:
|
| 30 |
-
print("[TRELLIS Error] No se detectó el 'HF_TOKEN' en las variables de entorno.")
|
| 31 |
-
return "ERROR: Variable de entorno 'HF_TOKEN' faltante. Configúrala en los Secrets de tu Space."
|
| 32 |
-
|
| 33 |
if not imagen_generada_flux or not str(imagen_generada_flux).strip():
|
| 34 |
print("[TRELLIS Error] No se suministró una ruta de imagen válida.")
|
| 35 |
return "ERROR: No se detectó ninguna imagen válida de FLUX para iniciar el esculpido 3D."
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
# Inicialización del cliente encapsulado con paso de credenciales seguro
|
| 42 |
-
client = Client(SPACE_PROVEEDOR, hf_token=HF_TOKEN)
|
| 43 |
-
|
| 44 |
-
print(f"[TRELLIS Engine] Despachando asset estructural a la GPU remota... Origen: {imagen_generada_flux}")
|
| 45 |
-
|
| 46 |
-
# Consumo del endpoint puro de inferencia remota
|
| 47 |
-
resultado = client.predict(
|
| 48 |
-
image=handle_file(imagen_generada_flux),
|
| 49 |
-
inst_num_steps=12, # Muestreo estructural primario
|
| 50 |
-
inst_cfg=7.5, # Guía de escala sintética
|
| 51 |
-
preprocess_image=True, # Limpieza automática de la matriz 2D
|
| 52 |
-
api_name="/image_to_3d" # Endpoint directo expuesto en el backend de TRELLIS
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
# El cliente descarga automáticamente el .glb en la caché local del sistema
|
| 56 |
-
if resultado and isinstance(resultado, str) and resultado.endswith(".glb"):
|
| 57 |
-
print(f"[TRELLIS Engine] ¡Malla .glb consolidada con éxito!: {resultado}")
|
| 58 |
return resultado
|
| 59 |
-
|
| 60 |
-
#
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
if isinstance(primer_nodo, dict) and "name" in primer_nodo:
|
| 71 |
-
return primer_nodo["name"]
|
| 72 |
|
| 73 |
-
raise Exception(f"El clúster remoto retornó un formato binario no reconocido: {str(resultado)}")
|
| 74 |
-
|
| 75 |
-
except Exception as e:
|
| 76 |
-
print(f"[TRELLIS Error] Falla en pipeline con Cliente Oficial: {e}")
|
| 77 |
-
return f"ERROR_PIPELINE: {str(e)}"
|
| 78 |
|
| 79 |
# ═════════════════════════════════════════════════════════════════════
|
| 80 |
# SECCIÓN 3: COMPONENTE VISUAL Y RENDERIZADO INTERACTIVO (FRONTEND)
|
|
@@ -84,86 +159,93 @@ def obtener_html_visor(glb_url: str = None) -> str:
|
|
| 84 |
"""
|
| 85 |
Construye de forma dinámica la interfaz del viewport interactivo
|
| 86 |
utilizando Three.js para la manipulación libre de mallas.
|
|
|
|
|
|
|
| 87 |
"""
|
| 88 |
if not glb_url or "ERROR" in str(glb_url) or "TIMEOUT" in str(glb_url):
|
| 89 |
error_msg = glb_url if glb_url else "Esperando especificaciones en el panel de control..."
|
| 90 |
return f"""
|
| 91 |
-
<div style="width:100%; height:500px; background:#02060f; display:flex; flex-direction:column;
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
<p style="color:rgba(255,255,255,0.5); font-size:12px; margin-top:10px;">{error_msg}</p>
|
| 94 |
</div>
|
| 95 |
"""
|
| 96 |
-
|
| 97 |
return f"""
|
| 98 |
-
<div id="canvas-3d-container" style="width:100%; height:500px; background:#02060f;
|
|
|
|
| 99 |
<script type="module">
|
| 100 |
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js';
|
| 101 |
import {{ GLTFLoader }} from 'https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/loaders/GLTFLoader.js';
|
| 102 |
import {{ OrbitControls }} from 'https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/controls/OrbitControls.js';
|
| 103 |
-
|
| 104 |
const container = document.getElementById('canvas-3d-container');
|
| 105 |
const scene = new THREE.Scene();
|
| 106 |
scene.background = new THREE.Color(0x02060f);
|
| 107 |
-
|
| 108 |
const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 100);
|
| 109 |
camera.position.set(0, 1.2, 3);
|
| 110 |
-
|
| 111 |
const renderer = new THREE.WebGLRenderer({{ antialias: true, alpha: false }});
|
| 112 |
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 113 |
renderer.setPixelRatio(window.devicePixelRatio);
|
| 114 |
renderer.shadowMap.enabled = true;
|
| 115 |
container.appendChild(renderer.domElement);
|
| 116 |
-
|
| 117 |
const controls = new OrbitControls(camera, renderer.domElement);
|
| 118 |
controls.enableDamping = true;
|
| 119 |
controls.dampingFactor = 0.05;
|
| 120 |
-
|
| 121 |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
|
| 122 |
scene.add(ambientLight);
|
| 123 |
-
|
| 124 |
const dirLight1 = new THREE.DirectionalLight(0x00d4ff, 1.0);
|
| 125 |
dirLight1.position.set(5, 5, 5);
|
| 126 |
scene.add(dirLight1);
|
| 127 |
-
|
| 128 |
const dirLight2 = new THREE.DirectionalLight(0xff00ff, 0.5);
|
| 129 |
dirLight2.position.set(-5, 3, -5);
|
| 130 |
scene.add(dirLight2);
|
| 131 |
-
|
| 132 |
const loader = new GLTFLoader();
|
| 133 |
-
const modelUrl = "{glb_url}";
|
| 134 |
-
|
| 135 |
console.log("[CVision Visor] Intentando cargar geometría desde:", modelUrl);
|
| 136 |
-
|
| 137 |
loader.load(modelUrl, (gltf) => {{
|
| 138 |
const model = gltf.scene;
|
| 139 |
-
|
| 140 |
// Centrado automático del modelo dentro del visor global
|
| 141 |
const box = new THREE.Box3().setFromObject(model);
|
| 142 |
const center = box.getCenter(new THREE.Vector3());
|
| 143 |
const size = box.getSize(new THREE.Vector3());
|
| 144 |
-
|
| 145 |
model.position.sub(center);
|
| 146 |
scene.add(model);
|
| 147 |
-
|
| 148 |
// Ajuste dinámico de cámara según el volumen del objeto forjado
|
| 149 |
const maxDim = Math.max(size.x, size.y, size.z);
|
| 150 |
camera.position.z = maxDim * 1.8;
|
| 151 |
camera.lookAt(0, 0, 0);
|
| 152 |
-
|
| 153 |
console.log("[CVision Forge Visor] Asset tridimensional acoplado y centrado.");
|
| 154 |
}}, (xhr) => {{
|
| 155 |
console.log("[CVision Visor] Progreso de carga mallas: " + (xhr.loaded / xhr.total * 100) + "%");
|
| 156 |
}}, (error) => {{
|
| 157 |
console.error("[CVision Forge Error] Error crítico al decodificar la estructura:", error);
|
| 158 |
}});
|
| 159 |
-
|
| 160 |
function animate() {{
|
| 161 |
requestAnimationFrame(animate);
|
| 162 |
controls.update();
|
| 163 |
renderer.render(scene, camera);
|
| 164 |
}}
|
| 165 |
animate();
|
| 166 |
-
|
| 167 |
window.addEventListener('resize', () => {{
|
| 168 |
camera.aspect = container.clientWidth / container.clientHeight;
|
| 169 |
camera.updateProjectionMatrix();
|
|
@@ -173,124 +255,158 @@ def obtener_html_visor(glb_url: str = None) -> str:
|
|
| 173 |
</div>
|
| 174 |
"""
|
| 175 |
|
|
|
|
| 176 |
# ═════════════════════════════════════════════════════════════════════
|
| 177 |
# SECCIÓN 4: ORQUESTADOR INTERNO Y ENLACE INTER-MÓDULO
|
| 178 |
# 💡 Gestiona los flujos cruzados que consumen respuestas asíncronas en HTML.
|
| 179 |
# ═════════════════════════════════════════════════════════════════════
|
| 180 |
-
def procesar_flujo_completo(prompt):
|
| 181 |
"""
|
| 182 |
Middleware encargado de coordinar flujos que requieran el visor embebido
|
| 183 |
de Three.js en lugar de los componentes declarativos estándar.
|
|
|
|
| 184 |
"""
|
| 185 |
print("[CVision Middleware] Solicitando pipeline completo para visor web...")
|
| 186 |
ruta_glb = ejecutar_forja_3d(prompt)
|
| 187 |
html_visor = obtener_html_visor(ruta_glb)
|
| 188 |
return html_visor
|
| 189 |
|
|
|
|
| 190 |
# ═════════════════════════════════════════════════════════════════════
|
| 191 |
# SECCIÓN 5: INTERFAZ DECLARATIVA E INYECCIÓN DE GRADIO
|
| 192 |
# 💡 Manejo de flujo local, bypass de flujos binarios y montaje estructural de UI.
|
| 193 |
# ═════════════════════════════════════════════════════════════════════
|
| 194 |
-
def puente_forja_y_descarga(ruta_imagen_2d):
|
| 195 |
"""
|
| 196 |
Monitorea la ejecución del pipeline, extrae la malla de la caché temporal
|
| 197 |
-
y estabiliza su persistencia en el espacio local para su lectura segura
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
"""
|
| 199 |
if not ruta_imagen_2d:
|
| 200 |
print("[CVision Error] Entrada vacía. No se suministró una matriz 2D válida.")
|
| 201 |
return None
|
| 202 |
-
|
| 203 |
try:
|
| 204 |
print("[CVision Forge] Lanzando pipeline de pooling asíncrono en cliente...")
|
| 205 |
resultado_pooling = ejecutar_forja_3d(ruta_imagen_2d)
|
| 206 |
-
|
| 207 |
-
if not resultado_pooling or "ERROR" in str(resultado_pooling)
|
| 208 |
print(f"[CVision Error] Interrupción detectada en el canal: {resultado_pooling}")
|
| 209 |
return None
|
| 210 |
-
|
| 211 |
-
#
|
| 212 |
if isinstance(resultado_pooling, str) and os.path.exists(resultado_pooling):
|
| 213 |
print(f"[CVision Forge] Copiando asset desde la caché del cliente: {resultado_pooling}")
|
| 214 |
ruta_estabilizada_glb = os.path.abspath("cvision_forge_output.glb")
|
| 215 |
-
|
| 216 |
# Transferencia segura del flujo de bytes local para consolidar persistencia
|
| 217 |
with open(resultado_pooling, "rb") as f_origen:
|
| 218 |
datos_binarios = f_origen.read()
|
| 219 |
with open(ruta_estabilizada_glb, "wb") as f_destino:
|
| 220 |
f_destino.write(datos_binarios)
|
| 221 |
-
|
| 222 |
-
print(f"[CVision Forge] Malla consolidada localmente
|
| 223 |
return ruta_estabilizada_glb
|
| 224 |
-
|
| 225 |
-
# Canal
|
| 226 |
if isinstance(resultado_pooling, str) and resultado_pooling.startswith("http"):
|
| 227 |
print(f"[CVision Forge] Interceptando URL remota: {resultado_pooling}")
|
| 228 |
print("[CVision Forge] Transfiriendo flujo binario desde el clúster...")
|
| 229 |
-
|
| 230 |
cabeceras = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
|
| 231 |
-
peticion = requests.get(
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
if peticion.status_code == 200:
|
| 234 |
ruta_estabilizada_glb = os.path.abspath("cvision_forge_output.glb")
|
| 235 |
with open(ruta_estabilizada_glb, "wb") as archivo_binario:
|
| 236 |
for bloque in peticion.iter_content(chunk_size=8192):
|
| 237 |
archivo_binario.write(bloque)
|
| 238 |
-
|
| 239 |
print(f"[CVision Forge] Binario remoto estabilizado con éxito: {ruta_estabilizada_glb}")
|
| 240 |
return ruta_estabilizada_glb
|
| 241 |
else:
|
| 242 |
-
print(
|
|
|
|
|
|
|
|
|
|
| 243 |
return None
|
| 244 |
-
|
| 245 |
except Exception as error:
|
| 246 |
print(f"[CVision Falla Crítica] Error en la intercepción de la malla binaria: {str(error)}")
|
| 247 |
-
|
| 248 |
return None
|
| 249 |
|
| 250 |
-
|
|
|
|
| 251 |
"""
|
| 252 |
Inicializa el módulo estructural dentro de la interfaz global de CVision.
|
| 253 |
Aplica la inyección visual Cybernova y conecta los listeners reactivos.
|
|
|
|
| 254 |
"""
|
| 255 |
-
# Bloque modular aislado para evitar colisiones con otros orquestadores de la app
|
| 256 |
with gr.Blocks(analytics_enabled=False) as forge_block:
|
| 257 |
-
gr.Markdown(
|
| 258 |
-
|
|
|
|
|
|
|
| 259 |
with gr.Row():
|
|
|
|
| 260 |
with gr.Column(scale=2):
|
| 261 |
gr.Markdown("#### 📥 Entrada de Matriz Estructural")
|
| 262 |
-
|
| 263 |
input_imagen_2d = gr.Image(
|
| 264 |
label="Matriz Estructural de Perfil (Asset de Entrada)",
|
| 265 |
type="filepath",
|
| 266 |
-
interactive=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
)
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
btn_forjar = gr.Button("⚡ Iniciar Forja Tridimensional", variant="primary", size="lg")
|
| 271 |
-
|
| 272 |
-
gr.Markdown("""
|
| 273 |
-
<div style="margin-top:20px; padding:10px; background:rgba(0,212,255,0.05); border-left:3px solid #00d4ff; font-size:11px; line-height:1.4; color:rgba(255,255,255,0.7);">
|
| 274 |
-
<strong>⚙️ NOTA DE LOGÍSTICA:</strong> El procesamiento utiliza el motor de difusión de matrices densas de TRELLIS. El tiempo de resolución estimado oscila entre 20 y 45 segundos según la demanda del clúster de Hugging Face.
|
| 275 |
-
</div>
|
| 276 |
-
""", sanitize_html=False)
|
| 277 |
-
|
| 278 |
with gr.Column(scale=3):
|
| 279 |
gr.Markdown("#### 🌌 Matriz de Renderizado Tridimensional")
|
| 280 |
-
|
| 281 |
# Visor nativo con parámetros de color acoplados a la estética Cybernova
|
| 282 |
visor_3d = gr.Model3D(
|
| 283 |
label="Forja Geométrica Realizada (Motor TRELLIS)",
|
| 284 |
clear_color=[0.04, 0.06, 0.1, 1.0],
|
| 285 |
-
camera_position=[0, 0, 2]
|
| 286 |
)
|
| 287 |
-
|
| 288 |
-
# Vinculación del evento click con animación interna de carga de Gradio
|
| 289 |
btn_forjar.click(
|
| 290 |
fn=puente_forja_y_descarga,
|
| 291 |
inputs=[input_imagen_2d],
|
| 292 |
outputs=[visor_3d],
|
| 293 |
-
api_name="solicitud_forja_3d"
|
| 294 |
)
|
| 295 |
-
|
| 296 |
return forge_block
|
|
|
|
| 13 |
# EXTRACCIÓN SEGURA: Se elimina el token explícito para evitar bloqueos en el commit.
|
| 14 |
# El token real debe ser configurado en la pestaña "Settings -> Variables and Secrets" de tu Space.
|
| 15 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 16 |
+
|
| 17 |
+
# ─── TABLA DE ESPACIOS CANDIDATOS (ordenados por prioridad de disponibilidad) ────
|
| 18 |
+
# Primario → microsoft/TRELLIS.2 : Space oficial Microsoft, activo y con endpoint /image_to_3d estable.
|
| 19 |
+
# Secundario→ trellis-community/TRELLIS : Fork comunitario mantenido, también activo.
|
| 20 |
+
# Terciario → theseanlavery/TRELLIS-3D : Fork independiente de respaldo adicional.
|
| 21 |
+
# NOTA: Si el primario devuelve CONFIG_ERROR o cae por carga, el orquestador rota automáticamente.
|
| 22 |
+
ESPACIOS_TRELLIS = [
|
| 23 |
+
"microsoft/TRELLIS.2",
|
| 24 |
+
"trellis-community/TRELLIS",
|
| 25 |
+
"theseanlavery/TRELLIS-3D",
|
| 26 |
+
]
|
| 27 |
|
| 28 |
# ═════════════════════════════════════════════════════════════════════
|
| 29 |
# SECCIÓN 2: NÚCLEO LOGÍSTICO Y PROCESAMIENTO MULTIMODAL (TRELLIS BACKEND)
|
| 30 |
# 💡 Implementa el motor de inferencia oficial mediante hilos WebSockets.
|
| 31 |
+
# Usa firma de parámetros completa compatible con TRELLIS v1 y TRELLIS.2.
|
| 32 |
+
# Rota automáticamente entre Spaces si el primario está caído.
|
| 33 |
# ═════════════════════════════════════════════════════════════════════
|
| 34 |
+
|
| 35 |
+
# ─── FIRMA DE PARÁMETROS COMPLETA (compatible con JeffreyXiang/TRELLIS y forks) ─
|
| 36 |
+
# Confirmada mediante análisis del app.py oficial y discusiones de la comunidad HF.
|
| 37 |
+
# Parámetros mínimos requeridos para el endpoint /image_to_3d en todos los forks activos:
|
| 38 |
+
# image → archivo de entrada (handle_file)
|
| 39 |
+
# multiimages → lista vacía [] para modo single-image
|
| 40 |
+
# seed → semilla de generación (0 = determinista)
|
| 41 |
+
# ss_guidance_strength → guía de escala estructural (recomendado: 7.5)
|
| 42 |
+
# ss_sampling_steps → pasos de muestreo estructural (recomendado: 12)
|
| 43 |
+
# slat_guidance_strength→ guía de latente SLAT (recomendado: 3.0)
|
| 44 |
+
# slat_sampling_steps → pasos de muestreo SLAT (recomendado: 12)
|
| 45 |
+
# multiimage_algo → algoritmo multi-imagen (valor: "stochastic")
|
| 46 |
+
PARAMS_INFERENCIA = {
|
| 47 |
+
"multiimages": [],
|
| 48 |
+
"seed": 0,
|
| 49 |
+
"ss_guidance_strength": 7.5,
|
| 50 |
+
"ss_sampling_steps": 12,
|
| 51 |
+
"slat_guidance_strength": 3.0,
|
| 52 |
+
"slat_sampling_steps": 12,
|
| 53 |
+
"multiimage_algo": "stochastic",
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _intentar_cliente_gradio(espacio: str, ruta_imagen: str) -> str | None:
|
| 58 |
+
"""
|
| 59 |
+
Intenta conectar a un Space específico de TRELLIS usando el cliente oficial
|
| 60 |
+
de Gradio vía WebSockets. Retorna la ruta local del .glb si tiene éxito,
|
| 61 |
+
o None si falla (para que el orquestador pruebe el siguiente candidato).
|
| 62 |
+
"""
|
| 63 |
+
try:
|
| 64 |
+
from gradio_client import Client, handle_file
|
| 65 |
+
|
| 66 |
+
print(f"[TRELLIS Engine] Conectando con el clúster: {espacio}")
|
| 67 |
+
cliente = Client(espacio, hf_token=HF_TOKEN if HF_TOKEN else None)
|
| 68 |
+
|
| 69 |
+
print(f"[TRELLIS Engine] Despachando asset a la GPU remota... Origen: {ruta_imagen}")
|
| 70 |
+
resultado = cliente.predict(
|
| 71 |
+
image=handle_file(ruta_imagen),
|
| 72 |
+
**PARAMS_INFERENCIA,
|
| 73 |
+
api_name="/image_to_3d",
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# ── Normalización del resultado ──────────────────────────────────────────
|
| 77 |
+
# El cliente puede retornar: str con path local, dict con 'name', o lista
|
| 78 |
+
ruta_glb = None
|
| 79 |
+
|
| 80 |
+
if isinstance(resultado, str) and resultado.endswith(".glb"):
|
| 81 |
+
ruta_glb = resultado
|
| 82 |
+
|
| 83 |
+
elif isinstance(resultado, dict) and "name" in resultado:
|
| 84 |
+
ruta_glb = resultado["name"]
|
| 85 |
+
|
| 86 |
+
elif isinstance(resultado, list) and len(resultado) > 0:
|
| 87 |
+
primer_nodo = resultado[0]
|
| 88 |
+
if isinstance(primer_nodo, str) and primer_nodo.endswith(".glb"):
|
| 89 |
+
ruta_glb = primer_nodo
|
| 90 |
+
elif isinstance(primer_nodo, dict) and "name" in primer_nodo:
|
| 91 |
+
ruta_glb = primer_nodo["name"]
|
| 92 |
+
# Algunos forks devuelven una tupla (video_path, glb_path)
|
| 93 |
+
elif isinstance(resultado, (list, tuple)) and len(resultado) >= 2:
|
| 94 |
+
for nodo in resultado:
|
| 95 |
+
if isinstance(nodo, str) and nodo.endswith(".glb"):
|
| 96 |
+
ruta_glb = nodo
|
| 97 |
+
break
|
| 98 |
+
if isinstance(nodo, dict) and nodo.get("name", "").endswith(".glb"):
|
| 99 |
+
ruta_glb = nodo["name"]
|
| 100 |
+
break
|
| 101 |
+
|
| 102 |
+
if ruta_glb:
|
| 103 |
+
print(f"[TRELLIS Engine] ¡Malla .glb consolidada con éxito en {espacio}!: {ruta_glb}")
|
| 104 |
+
return ruta_glb
|
| 105 |
+
|
| 106 |
+
raise ValueError(f"Formato binario no reconocido devuelto por {espacio}: {type(resultado)} → {str(resultado)[:200]}")
|
| 107 |
+
|
| 108 |
+
except Exception as error:
|
| 109 |
+
# Distinguir errores estructurales (CONFIG_ERROR, SPACE_ERROR) de errores de red
|
| 110 |
+
mensaje_error = str(error).lower()
|
| 111 |
+
if any(kw in mensaje_error for kw in ["config_error", "invalid state", "space_error", "runtime error"]):
|
| 112 |
+
print(f"[TRELLIS Warn] Space '{espacio}' está caído o en estado inválido: {error}")
|
| 113 |
+
else:
|
| 114 |
+
print(f"[TRELLIS Warn] Falla de pipeline en '{espacio}': {error}")
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def ejecutar_forja_3d(imagen_generada_flux: str) -> str:
|
| 119 |
"""
|
| 120 |
+
Orquestador principal de inferencia 3D. Toma la imagen nacida del motor FLUX
|
| 121 |
+
(o cualquier imagen 2D válida) y utiliza el cliente oficial de Gradio para
|
| 122 |
+
conectarse a los clústeres de TRELLIS mediante WebSockets estables.
|
| 123 |
+
|
| 124 |
+
Implementa rotación automática de Spaces: si el primario falla por
|
| 125 |
+
CONFIG_ERROR u otro error estructural, prueba los candidatos secundarios
|
| 126 |
+
en orden de prioridad, garantizando máxima resiliencia del pipeline.
|
| 127 |
+
|
| 128 |
Evita simular clics en el frontend para evadir el colapso por 'hidden tab'.
|
| 129 |
+
La firma de parámetros es la completa y validada por la comunidad HF,
|
| 130 |
+
distinta a la versión reducida de ejemplos básicos que causaba errores.
|
| 131 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
if not imagen_generada_flux or not str(imagen_generada_flux).strip():
|
| 133 |
print("[TRELLIS Error] No se suministró una ruta de imagen válida.")
|
| 134 |
return "ERROR: No se detectó ninguna imagen válida de FLUX para iniciar el esculpido 3D."
|
| 135 |
|
| 136 |
+
# ── Rotación de Spaces candidatos ───────────────────────────────────────────
|
| 137 |
+
for espacio in ESPACIOS_TRELLIS:
|
| 138 |
+
resultado = _intentar_cliente_gradio(espacio, imagen_generada_flux)
|
| 139 |
+
if resultado:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
return resultado
|
| 141 |
+
print(f"[TRELLIS Engine] Rotando al siguiente Space candidato...")
|
| 142 |
+
time.sleep(1.5) # Pausa mínima entre intentos para evitar rate-limiting
|
| 143 |
+
|
| 144 |
+
# Si todos los Spaces fallaron, retornar mensaje de error estructurado
|
| 145 |
+
mensaje_final = (
|
| 146 |
+
"ERROR_PIPELINE: Todos los clústeres de TRELLIS están temporalmente no disponibles. "
|
| 147 |
+
"Spaces intentados: " + ", ".join(ESPACIOS_TRELLIS) + ". "
|
| 148 |
+
"Verifica el estado de los Spaces en https://status.huggingface.co o intenta más tarde."
|
| 149 |
+
)
|
| 150 |
+
print(f"[TRELLIS Error] {mensaje_final}")
|
| 151 |
+
return mensaje_final
|
|
|
|
|
|
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
# ═════════════════════════════════════════════════════════════════════
|
| 155 |
# SECCIÓN 3: COMPONENTE VISUAL Y RENDERIZADO INTERACTIVO (FRONTEND)
|
|
|
|
| 159 |
"""
|
| 160 |
Construye de forma dinámica la interfaz del viewport interactivo
|
| 161 |
utilizando Three.js para la manipulación libre de mallas.
|
| 162 |
+
Se usa como visor de respaldo cuando gr.Model3D no puede renderizar
|
| 163 |
+
una URL remota directamente (ej. assets servidos desde HF caché).
|
| 164 |
"""
|
| 165 |
if not glb_url or "ERROR" in str(glb_url) or "TIMEOUT" in str(glb_url):
|
| 166 |
error_msg = glb_url if glb_url else "Esperando especificaciones en el panel de control..."
|
| 167 |
return f"""
|
| 168 |
+
<div style="width:100%; height:500px; background:#02060f; display:flex; flex-direction:column;
|
| 169 |
+
justify-content:center; align-items:center; border:1px solid rgba(0,212,255,0.15);
|
| 170 |
+
border-radius:8px; font-family:monospace; color:#00d4ff;">
|
| 171 |
+
<h3 style="margin:0; letter-spacing:2px; text-shadow:0 0 10px rgba(0,212,255,0.5);">
|
| 172 |
+
⚔️ CVISION FORGE LAB v9.0 ⚔️
|
| 173 |
+
</h3>
|
| 174 |
<p style="color:rgba(255,255,255,0.5); font-size:12px; margin-top:10px;">{error_msg}</p>
|
| 175 |
</div>
|
| 176 |
"""
|
| 177 |
+
|
| 178 |
return f"""
|
| 179 |
+
<div id="canvas-3d-container" style="width:100%; height:500px; background:#02060f;
|
| 180 |
+
border:1px solid #00d4ff; border-radius:8px; position:relative; overflow:hidden;">
|
| 181 |
<script type="module">
|
| 182 |
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.module.js';
|
| 183 |
import {{ GLTFLoader }} from 'https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/loaders/GLTFLoader.js';
|
| 184 |
import {{ OrbitControls }} from 'https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/controls/OrbitControls.js';
|
| 185 |
+
|
| 186 |
const container = document.getElementById('canvas-3d-container');
|
| 187 |
const scene = new THREE.Scene();
|
| 188 |
scene.background = new THREE.Color(0x02060f);
|
| 189 |
+
|
| 190 |
const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 100);
|
| 191 |
camera.position.set(0, 1.2, 3);
|
| 192 |
+
|
| 193 |
const renderer = new THREE.WebGLRenderer({{ antialias: true, alpha: false }});
|
| 194 |
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 195 |
renderer.setPixelRatio(window.devicePixelRatio);
|
| 196 |
renderer.shadowMap.enabled = true;
|
| 197 |
container.appendChild(renderer.domElement);
|
| 198 |
+
|
| 199 |
const controls = new OrbitControls(camera, renderer.domElement);
|
| 200 |
controls.enableDamping = true;
|
| 201 |
controls.dampingFactor = 0.05;
|
| 202 |
+
|
| 203 |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
|
| 204 |
scene.add(ambientLight);
|
| 205 |
+
|
| 206 |
const dirLight1 = new THREE.DirectionalLight(0x00d4ff, 1.0);
|
| 207 |
dirLight1.position.set(5, 5, 5);
|
| 208 |
scene.add(dirLight1);
|
| 209 |
+
|
| 210 |
const dirLight2 = new THREE.DirectionalLight(0xff00ff, 0.5);
|
| 211 |
dirLight2.position.set(-5, 3, -5);
|
| 212 |
scene.add(dirLight2);
|
| 213 |
+
|
| 214 |
const loader = new GLTFLoader();
|
| 215 |
+
const modelUrl = "{glb_url}";
|
| 216 |
+
|
| 217 |
console.log("[CVision Visor] Intentando cargar geometría desde:", modelUrl);
|
| 218 |
+
|
| 219 |
loader.load(modelUrl, (gltf) => {{
|
| 220 |
const model = gltf.scene;
|
| 221 |
+
|
| 222 |
// Centrado automático del modelo dentro del visor global
|
| 223 |
const box = new THREE.Box3().setFromObject(model);
|
| 224 |
const center = box.getCenter(new THREE.Vector3());
|
| 225 |
const size = box.getSize(new THREE.Vector3());
|
| 226 |
+
|
| 227 |
model.position.sub(center);
|
| 228 |
scene.add(model);
|
| 229 |
+
|
| 230 |
// Ajuste dinámico de cámara según el volumen del objeto forjado
|
| 231 |
const maxDim = Math.max(size.x, size.y, size.z);
|
| 232 |
camera.position.z = maxDim * 1.8;
|
| 233 |
camera.lookAt(0, 0, 0);
|
| 234 |
+
|
| 235 |
console.log("[CVision Forge Visor] Asset tridimensional acoplado y centrado.");
|
| 236 |
}}, (xhr) => {{
|
| 237 |
console.log("[CVision Visor] Progreso de carga mallas: " + (xhr.loaded / xhr.total * 100) + "%");
|
| 238 |
}}, (error) => {{
|
| 239 |
console.error("[CVision Forge Error] Error crítico al decodificar la estructura:", error);
|
| 240 |
}});
|
| 241 |
+
|
| 242 |
function animate() {{
|
| 243 |
requestAnimationFrame(animate);
|
| 244 |
controls.update();
|
| 245 |
renderer.render(scene, camera);
|
| 246 |
}}
|
| 247 |
animate();
|
| 248 |
+
|
| 249 |
window.addEventListener('resize', () => {{
|
| 250 |
camera.aspect = container.clientWidth / container.clientHeight;
|
| 251 |
camera.updateProjectionMatrix();
|
|
|
|
| 255 |
</div>
|
| 256 |
"""
|
| 257 |
|
| 258 |
+
|
| 259 |
# ═════════════════════════════════════════════════════════════════════
|
| 260 |
# SECCIÓN 4: ORQUESTADOR INTERNO Y ENLACE INTER-MÓDULO
|
| 261 |
# 💡 Gestiona los flujos cruzados que consumen respuestas asíncronas en HTML.
|
| 262 |
# ═════════════════════════════════════════════════════════════════════
|
| 263 |
+
def procesar_flujo_completo(prompt: str) -> str:
|
| 264 |
"""
|
| 265 |
Middleware encargado de coordinar flujos que requieran el visor embebido
|
| 266 |
de Three.js en lugar de los componentes declarativos estándar.
|
| 267 |
+
Útil cuando el asset resultante es una URL remota de HF y no un path local.
|
| 268 |
"""
|
| 269 |
print("[CVision Middleware] Solicitando pipeline completo para visor web...")
|
| 270 |
ruta_glb = ejecutar_forja_3d(prompt)
|
| 271 |
html_visor = obtener_html_visor(ruta_glb)
|
| 272 |
return html_visor
|
| 273 |
|
| 274 |
+
|
| 275 |
# ═════════════════════════════════════════════════════════════════════
|
| 276 |
# SECCIÓN 5: INTERFAZ DECLARATIVA E INYECCIÓN DE GRADIO
|
| 277 |
# 💡 Manejo de flujo local, bypass de flujos binarios y montaje estructural de UI.
|
| 278 |
# ═════════════════════════════════════════════════════════════════════
|
| 279 |
+
def puente_forja_y_descarga(ruta_imagen_2d: str) -> str | None:
|
| 280 |
"""
|
| 281 |
Monitorea la ejecución del pipeline, extrae la malla de la caché temporal
|
| 282 |
+
y estabiliza su persistencia en el espacio local para su lectura segura
|
| 283 |
+
por parte del componente gr.Model3D de Gradio.
|
| 284 |
+
|
| 285 |
+
Maneja tres canales de retorno posibles del cliente Gradio:
|
| 286 |
+
1. Path local → el cliente descargó el .glb en disco directamente.
|
| 287 |
+
2. URL remota → el clúster devolvió un enlace HTTPS; se descarga aquí.
|
| 288 |
+
3. ERROR/None → se propaga None para mostrar estado vacío en el visor.
|
| 289 |
"""
|
| 290 |
if not ruta_imagen_2d:
|
| 291 |
print("[CVision Error] Entrada vacía. No se suministró una matriz 2D válida.")
|
| 292 |
return None
|
| 293 |
+
|
| 294 |
try:
|
| 295 |
print("[CVision Forge] Lanzando pipeline de pooling asíncrono en cliente...")
|
| 296 |
resultado_pooling = ejecutar_forja_3d(ruta_imagen_2d)
|
| 297 |
+
|
| 298 |
+
if not resultado_pooling or "ERROR" in str(resultado_pooling):
|
| 299 |
print(f"[CVision Error] Interrupción detectada en el canal: {resultado_pooling}")
|
| 300 |
return None
|
| 301 |
+
|
| 302 |
+
# ── Canal 1: El cliente oficial descargó el archivo de forma nativa en disco ──
|
| 303 |
if isinstance(resultado_pooling, str) and os.path.exists(resultado_pooling):
|
| 304 |
print(f"[CVision Forge] Copiando asset desde la caché del cliente: {resultado_pooling}")
|
| 305 |
ruta_estabilizada_glb = os.path.abspath("cvision_forge_output.glb")
|
| 306 |
+
|
| 307 |
# Transferencia segura del flujo de bytes local para consolidar persistencia
|
| 308 |
with open(resultado_pooling, "rb") as f_origen:
|
| 309 |
datos_binarios = f_origen.read()
|
| 310 |
with open(ruta_estabilizada_glb, "wb") as f_destino:
|
| 311 |
f_destino.write(datos_binarios)
|
| 312 |
+
|
| 313 |
+
print(f"[CVision Forge] Malla consolidada localmente con éxito: {ruta_estabilizada_glb}")
|
| 314 |
return ruta_estabilizada_glb
|
| 315 |
+
|
| 316 |
+
# ── Canal 2: El clúster retornó una URL remota de Hugging Face ──────────────
|
| 317 |
if isinstance(resultado_pooling, str) and resultado_pooling.startswith("http"):
|
| 318 |
print(f"[CVision Forge] Interceptando URL remota: {resultado_pooling}")
|
| 319 |
print("[CVision Forge] Transfiriendo flujo binario desde el clúster...")
|
| 320 |
+
|
| 321 |
cabeceras = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
|
| 322 |
+
peticion = requests.get(
|
| 323 |
+
resultado_pooling,
|
| 324 |
+
headers=cabeceras,
|
| 325 |
+
stream=True,
|
| 326 |
+
timeout=90, # Timeout ampliado para modelos pesados
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
if peticion.status_code == 200:
|
| 330 |
ruta_estabilizada_glb = os.path.abspath("cvision_forge_output.glb")
|
| 331 |
with open(ruta_estabilizada_glb, "wb") as archivo_binario:
|
| 332 |
for bloque in peticion.iter_content(chunk_size=8192):
|
| 333 |
archivo_binario.write(bloque)
|
| 334 |
+
|
| 335 |
print(f"[CVision Forge] Binario remoto estabilizado con éxito: {ruta_estabilizada_glb}")
|
| 336 |
return ruta_estabilizada_glb
|
| 337 |
else:
|
| 338 |
+
print(
|
| 339 |
+
f"[CVision Error] El clúster denegó el flujo de bytes. "
|
| 340 |
+
f"Código HTTP: {peticion.status_code}"
|
| 341 |
+
)
|
| 342 |
return None
|
| 343 |
+
|
| 344 |
except Exception as error:
|
| 345 |
print(f"[CVision Falla Crítica] Error en la intercepción de la malla binaria: {str(error)}")
|
| 346 |
+
|
| 347 |
return None
|
| 348 |
|
| 349 |
+
|
| 350 |
+
def construir_bloque_forge() -> gr.Blocks:
|
| 351 |
"""
|
| 352 |
Inicializa el módulo estructural dentro de la interfaz global de CVision.
|
| 353 |
Aplica la inyección visual Cybernova y conecta los listeners reactivos.
|
| 354 |
+
El bloque está aislado para evitar colisiones con otros orquestadores de la app.
|
| 355 |
"""
|
|
|
|
| 356 |
with gr.Blocks(analytics_enabled=False) as forge_block:
|
| 357 |
+
gr.Markdown(
|
| 358 |
+
"### ⚔️ CVision Forge OS v9.0 — Panel de Manufactura Fotorrealista 3D ⚔️"
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
with gr.Row():
|
| 362 |
+
# ── Panel izquierdo: entrada de la imagen 2D ─────────────────────────
|
| 363 |
with gr.Column(scale=2):
|
| 364 |
gr.Markdown("#### 📥 Entrada de Matriz Estructural")
|
| 365 |
+
|
| 366 |
input_imagen_2d = gr.Image(
|
| 367 |
label="Matriz Estructural de Perfil (Asset de Entrada)",
|
| 368 |
type="filepath",
|
| 369 |
+
interactive=True,
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
btn_forjar = gr.Button(
|
| 373 |
+
"⚡ Iniciar Forja Tridimensional",
|
| 374 |
+
variant="primary",
|
| 375 |
+
size="lg",
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
gr.Markdown(
|
| 379 |
+
"""
|
| 380 |
+
<div style="margin-top:20px; padding:10px; background:rgba(0,212,255,0.05);
|
| 381 |
+
border-left:3px solid #00d4ff; font-size:11px; line-height:1.4;
|
| 382 |
+
color:rgba(255,255,255,0.7);">
|
| 383 |
+
<strong>⚙️ NOTA DE LOGÍSTICA:</strong> El procesamiento utiliza el motor
|
| 384 |
+
de difusión de matrices densas de TRELLIS. El tiempo de resolución estimado
|
| 385 |
+
oscila entre 20 y 60 segundos según la demanda del clúster de Hugging Face.
|
| 386 |
+
El sistema rota automáticamente entre tres Spaces candidatos si el primario
|
| 387 |
+
está bajo mantenimiento o sobrecarga.
|
| 388 |
+
</div>
|
| 389 |
+
""",
|
| 390 |
+
sanitize_html=False,
|
| 391 |
)
|
| 392 |
+
|
| 393 |
+
# ── Panel derecho: visor 3D nativo Gradio ────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
with gr.Column(scale=3):
|
| 395 |
gr.Markdown("#### 🌌 Matriz de Renderizado Tridimensional")
|
| 396 |
+
|
| 397 |
# Visor nativo con parámetros de color acoplados a la estética Cybernova
|
| 398 |
visor_3d = gr.Model3D(
|
| 399 |
label="Forja Geométrica Realizada (Motor TRELLIS)",
|
| 400 |
clear_color=[0.04, 0.06, 0.1, 1.0],
|
| 401 |
+
camera_position=[0, 0, 2],
|
| 402 |
)
|
| 403 |
+
|
| 404 |
+
# ── Vinculación del evento click con animación interna de carga de Gradio ─
|
| 405 |
btn_forjar.click(
|
| 406 |
fn=puente_forja_y_descarga,
|
| 407 |
inputs=[input_imagen_2d],
|
| 408 |
outputs=[visor_3d],
|
| 409 |
+
api_name="solicitud_forja_3d",
|
| 410 |
)
|
| 411 |
+
|
| 412 |
return forge_block
|