# ===================================================================== # SECCIÓN 1: IMPORTACIONES ESTÁNDAR Y DE SISTEMA # ===================================================================== import gradio as gr import re import os # ===================================================================== # SECCIÓN 2: MÓDULOS DE AUDIO Y TRANSCRIPCIÓN (CVISION) # ===================================================================== from modules.listen import transcribe_audio_file from modules.talker.talk import speak # ===================================================================== # SECCIÓN 3: ACOPLAMIENTO DEL MÓDULO DE VOZ MODULAR (DESDE 0) # ===================================================================== from modules.langchain_assistant.voice_module import create_voice_tab # ════════════════════════════════════════════════════════ # FALLBACK CHAT — Módulos de respaldo si falla la importación # ════════════════════════════════════════════════════════ def _fallback_chat(history, message, *args, **kwargs): yield (history or []) + [[message, "⚠️ Módulo no disponible. Revisa los logs del servidor."]], None try: from modules.utils.chat_utils import ( productivity_chat, browser_chat, scientist_chat, regular_chat, document_chat, ) print("✅ Módulos de chat importados correctamente") except Exception as e: print(f"❌ Error importando módulos de chat: {e}") regular_chat = _fallback_chat browser_chat = _fallback_chat productivity_chat = _fallback_chat scientist_chat = _fallback_chat document_chat = _fallback_chat # ════════════════════════════════════════════════════════ # CONSTANTES GLOBALES # ════════════════════════════════════════════════════════ PLACEHOLDER = "Escribe tu mensaje para CVision..." AVATAR_URL = "https://huggingface.co/spaces/CVision-assitant/asistente-virtual-CVision/resolve/main/assets/robot-bckound-black.png" # ════════════════════════════════════════════════════════ # CSS GLOBAL — Estilos de toda la interfaz # ════════════════════════════════════════════════════════ CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Exo+2:wght@300;400;500&family=Share+Tech+Mono&display=swap'); /* ══ VARIABLES CSS GLOBALES ══ Modifica aquí los colores principales de toda la app */ :root { --cyan:#00d4ff; /* Color principal cian */ --cyan2:#00a8e8; /* Cian secundario */ --cg:#00ff88; /* Color verde neón (online/activo) */ --bg-main:#020408; /* Fondo principal oscuro */ --border:rgba(0,212,255,0.16); --font-d:'Orbitron',monospace; /* Fuente display/títulos */ --font-b:'Exo 2',sans-serif; /* Fuente body */ --font-m:'Share Tech Mono',monospace; /* Fuente monospace/datos */ } /* ══ RESET Y BASE ══ */ html,body { overflow-x:hidden !important; } body,.gradio-container { background:var(--bg-main) !important; color:white !important; font-family:'Exo 2','Segoe UI',sans-serif; } /* ══ LÍNEA DE ESCANEO ANIMADA (scan line global) ══ Desactivar: comentar este bloque */ .gradio-container::after { content:''; position:fixed; top:0; left:0; width:100%; height:2px; background:linear-gradient(90deg,transparent,rgba(0,212,255,0.30),transparent); animation:cvScanDown 7s ease-in-out infinite; z-index:9999; pointer-events:none; } /* ══ KEYFRAMES — Todas las animaciones CSS ══ Modifica aquí velocidades/efectos de animación */ @keyframes cvScanDown { 0%{top:-2px;opacity:0} 8%{opacity:1} 92%{opacity:.4} 100%{top:100vh;opacity:0} } @keyframes cvSpin { to{transform:rotate(360deg)} } @keyframes cvBlink { 0%,100%{opacity:.25} 50%{opacity:1} } @keyframes cvOrbit { to{transform:rotate(360deg)} } @keyframes cvFloat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4px)} } @keyframes pulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(1.3);opacity:.7} } @keyframes bglow { 0%,100%{transform:scale(1);opacity:.5} 50%{transform:scale(1.2);opacity:1} } @keyframes radarSpin { to{transform:rotate(360deg)} } @keyframes headerShimmer { 0%,100%{opacity:0} 50%{opacity:1} } @keyframes btnGlow { 0%,100%{box-shadow:0 0 12px rgba(0,212,255,.3),0 0 24px rgba(0,212,255,.1)} 50%{box-shadow:0 0 22px rgba(0,212,255,.7),0 0 44px rgba(0,212,255,.25)} } @keyframes avatar-pulse { 0% { border-color:rgba(0,212,255,0.3); box-shadow:0 0 5px rgba(0,212,255,0.2); } 50% { border-color:rgba(0,212,255,1); box-shadow:0 0 18px rgba(0,212,255,0.6); } 100%{ border-color:rgba(0,212,255,0.3); box-shadow:0 0 5px rgba(0,212,255,0.2); } } /* ══ HEADER — Barra superior con logo y título ══ Modificar padding/height aquí para cambiar tamaño del header */ #cvision-header { background:rgba(1,4,12,0.98); border-bottom:1px solid rgba(0,212,255,0.18); padding:12px 28px; display:flex; align-items:center; gap:16px; backdrop-filter:blur(24px); position:relative; z-index:10; flex-shrink:0; box-shadow:0 4px 40px rgba(0,0,0,.8),0 0 30px rgba(0,212,255,.04); } /* Línea inferior animada del header */ #cvision-header::after { content:''; position:absolute; bottom:0; left:0; right:0; height:1px; background:linear-gradient(90deg,transparent,rgba(0,212,255,.5) 30%,rgba(0,255,200,.7) 50%,rgba(0,212,255,.5) 70%,transparent); animation:headerShimmer 3s ease-in-out infinite; } /* ══ NEBULA SECTION — Contenedor de la sección hero (nebula + info + panel derecho) ══ Modificar padding/gap aquí para espaciado de la sección */ #cvision-nebula-section { position:relative; display:flex; align-items:center; gap:24px; padding:28px 32px; border-bottom:1px solid rgba(0,212,255,0.10); background:linear-gradient(180deg,rgba(0,8,20,0.97),rgba(1,6,16,0.99)); flex-shrink:0; overflow:hidden; } /* ══ GRID CANVAS — Canvas de fondo con cuadrícula y puntos ══ Es el fondo animado de la sección nebula */ #cv-grid-canvas { position:absolute; inset:0; width:100% !important; height:100% !important; pointer-events:none; z-index:0; } /* ══ NEBULA CANVAS WRAPPER — Contenedor del canvas de la esfera nebula ══ Modificar width/height aquí para cambiar tamaño de la nebula */ #cv-nebula-wrap { position:relative; width:420px; height:420px; flex-shrink:0; z-index:2; } /* Tamaño del canvas de la nebula (debe coincidir con el wrapper) */ #nebulaCanvas { width:420px !important; height:420px !important; display:block; position:relative; z-index:2; } /* ══ HERO INFO — Bloque de texto a la derecha de la nebula ══ Título, descripción y chips de capacidades */ #cv-hero-info { position:relative; z-index:2; display:flex; flex-direction:column; gap:13px; flex:1; min-width:0; } /* Texto de descripción del hero — modificar font-size para tamaño */ #cv-hero-info .cv-desc { font-family:'Exo 2',sans-serif; font-size:1.08rem !important; color:rgba(90,160,190,.85) !important; line-height:1.85 !important; font-weight:300; max-width:460px; } /* ══ RIGHT PANEL — Panel lateral derecho (Núcleo Neural + Conectividad) ══ Modificar width para cambiar ancho del panel */ #cv-right-panel { position:relative; z-index:2; display:flex; flex-direction:column; gap:10px; width:210px; flex-shrink:0; } /* ══ RCARD — Tarjetas del panel derecho ══ Estilos compartidos por Núcleo Neural y Conectividad */ .rcard { background:rgba(2,7,18,.92); border:1px solid rgba(0,212,255,.12); border-radius:12px; overflow:hidden; position:relative; box-shadow:0 0 30px rgba(0,0,0,.5); } .rcard::before { content:''; position:absolute; top:0; left:0; right:0; height:1px; background:linear-gradient(90deg,transparent,rgba(0,212,255,.4),rgba(0,255,200,.2),transparent); } .rcard::after { content:''; position:absolute; bottom:0; left:0; right:0; height:1px; background:linear-gradient(90deg,transparent,rgba(0,212,255,.1),transparent); } /* ══ RCARD HEADER — Cabecera de las tarjetas del panel derecho ══ */ .rh { display:flex; align-items:center; gap:6px; padding:8px 12px; border-bottom:1px solid rgba(0,212,255,.06); } .rh-dot { width:5px; height:5px; border-radius:50%; background:var(--cyan); box-shadow:0 0 6px var(--cyan); flex-shrink:0; animation:pulse 2s infinite; } .rh-t { font-family:'Orbitron',monospace; font-size:8px; font-weight:700; color:rgba(0,190,240,.6); letter-spacing:.15em; } .rh-s { margin-left:auto; font-family:'Share Tech Mono',monospace; font-size:7.5px; color:rgba(0,150,200,.35); letter-spacing:.1em; } /* ══ BRAIN BOX — Contenedor del canvas del cerebro neural ══ Modificar height para cambiar altura del área del cerebro */ .brain-box { padding:6px; display:flex; align-items:center; justify-content:center; height:180px; position:relative; } /* Glow del fondo del cerebro */ .bglow { position:absolute; width:150px; height:150px; border-radius:50%; background:radial-gradient(circle,rgba(0,100,255,.12),transparent 70%); animation:bglow 2s ease-in-out infinite; } /* Tamaño del canvas del cerebro — modificar width/height */ #brainCvs { width:198px; height:168px; } /* Anillos de radar decorativos */ .rring { position:absolute; border-radius:50%; border:1px solid rgba(0,212,255,.08); } .rring1 { inset:10px; animation:radarSpin 8s linear infinite; } .rring2 { inset:25px; animation:radarSpin 12s linear infinite reverse; } /* ══ CONNECTIVITY CARD — Tarjeta de conectividad/mapa mundial ══ */ .conn-body { padding:9px 12px; } .cstat { display:flex; align-items:center; gap:7px; margin-bottom:5px; } .cdot { width:8px; height:8px; border-radius:50%; background:var(--cg); box-shadow:0 0 12px var(--cg); animation:pulse 1.5s infinite; } .cst { font-family:'Share Tech Mono',monospace; font-size:9px; color:var(--cg); letter-spacing:.12em; } .clat { font-family:'Share Tech Mono',monospace; font-size:8px; color:rgba(0,150,200,.4); letter-spacing:.1em; margin-bottom:7px; } /* Canvas del mapa mundial — modificar width/height */ #worldC { width:100%; height:52px; } /* ══ STATUS BAR — Barra de estado inferior fija ══ Modificar bottom para subir/bajar, font-size para tamaño */ #cvision-statusbar { position:fixed; bottom:4px; left:50%; transform:translateX(-50%); font-family:var(--font-m); font-size:0.75rem; letter-spacing:0.14em; color:rgba(0,212,255,0.75); display:flex; align-items:center; gap:14px; pointer-events:none; z-index:500; white-space:nowrap; padding:6px 20px; background:rgba(0,6,16,0.80); backdrop-filter:blur(12px); border:1px solid rgba(0,212,255,0.22); border-radius:8px; box-shadow:0 0 16px rgba(0,212,255,0.10); } /* Puntos separadores de la status bar */ .cv-sdot { width:4px; height:4px; border-radius:50%; background:rgba(0,212,255,0.5); animation:cvBlink 2s ease-in-out infinite; display:inline-block; } /* ══════════════════════════════════════════════════════ CHATBOT — Contenedor principal y burbujas de mensajes ══════════════════════════════════════════════════════ */ /* ── Contenedor externo del chatbot ── */ .chatbot-container { border-radius:16px !important; border:1px solid rgba(0,210,255,0.25) !important; background:rgba(2,10,20,0.98) !important; box-shadow:0 12px 50px rgba(0,0,0,.8) !important; overflow:hidden !important; } /* ── Wrapper interno (lista de mensajes) ── */ .chatbot-container > div.wrapper { display:flex !important; flex-direction:column !important; gap:14px !important; padding:16px !important; } /* ── BASE de todos los mensajes ── NO poner display:block ni width fijos aquí */ .chatbot-container .message { max-width:82% !important; padding:12px 16px !important; font-size:14px !important; line-height:1.65 !important; position:relative !important; box-sizing:border-box !important; overflow-wrap:break-word !important; word-break:break-word !important; white-space:normal !important; hyphens:auto !important; } /* ── BURBUJA USUARIO (derecha) ── Modificar background/border para cambiar color del usuario */ .chatbot-container .message.user, .chatbot-container [data-testid="user"] { align-self:flex-end !important; margin-left:auto !important; margin-right:6px !important; width:fit-content !important; min-width:60px !important; max-width:75% !important; background:linear-gradient(145deg,rgba(0,80,160,0.82),rgba(0,40,95,0.96)) !important; border:1px solid rgba(0,180,255,0.45) !important; border-radius:18px 18px 4px 18px !important; color:#ffffff !important; box-shadow:0 4px 18px rgba(0,0,0,.55), 0 0 8px rgba(0,120,255,0.12) !important; text-align:left !important; display:block !important; } /* Párrafos dentro de burbuja usuario */ .chatbot-container .message.user p, .chatbot-container [data-testid="user"] p { margin:0 0 6px 0 !important; white-space:normal !important; word-break:break-word !important; overflow-wrap:break-word !important; color:#ffffff !important; } .chatbot-container .message.user p:last-child, .chatbot-container [data-testid="user"] p:last-child { margin-bottom:0 !important; } /* Todos los hijos del mensaje de usuario */ .chatbot-container .message.user *, .chatbot-container [data-testid="user"] * { color:#ffffff !important; overflow-wrap:break-word !important; word-break:break-word !important; white-space:normal !important; max-width:100% !important; } /* ── BURBUJA ASISTENTE (izquierda) ── Modificar background/border-left para cambiar color del bot */ .chatbot-container .message.bot, .chatbot-container [data-testid="bot"] { align-self:flex-start !important; margin-right:auto !important; margin-left:6px !important; width:fit-content !important; min-width:80px !important; max-width:82% !important; background:linear-gradient(145deg,rgba(10,25,45,0.92),rgba(5,15,30,0.96)) !important; border:1px solid rgba(0,212,255,0.15) !important; border-radius:18px 18px 18px 4px !important; border-left:3px solid #00d4ff !important; /* ← línea izquierda cian */ color:#c8e8f8 !important; box-shadow:0 4px 18px rgba(0,0,0,.6) !important; display:block !important; } /* Párrafos dentro de burbuja bot */ .chatbot-container .message.bot p, .chatbot-container [data-testid="bot"] p { margin-bottom:8px !important; white-space:pre-wrap !important; color:#c8e8f8 !important; } .chatbot-container .message.bot p:last-child, .chatbot-container [data-testid="bot"] p:last-child { margin-bottom:0 !important; } /* ── BLOQUES DE CÓDIGO dentro del chatbot ── */ .chatbot-container pre { background:rgba(0,5,15,0.85) !important; border:1px solid rgba(0,212,255,0.3) !important; border-radius:10px !important; padding:12px !important; margin:8px 0 !important; overflow-x:auto !important; white-space:pre !important; } .chatbot-container code { font-family:'Share Tech Mono',monospace !important; color:#00ffcc !important; background:rgba(0,255,200,0.1) !important; padding:2px 5px !important; border-radius:4px !important; white-space:pre-wrap !important; } /* ── SCROLLBAR del chatbot ── */ .chatbot-container > div::-webkit-scrollbar { width:5px !important; } .chatbot-container > div::-webkit-scrollbar-thumb { background:rgba(0,212,255,0.2) !important; border-radius:10px !important; } /* ══ AVATAR DEL ASISTENTE — Esfera con logo CVision ══ Modificar width/height para cambiar tamaño del avatar (actualmente 42x42) */ .chatbot-container .avatar-container { width:48px !important; height:48px !important; border-radius:50% !important; border:2px solid rgba(0,212,255,0.6) !important; box-shadow:0 0 10px rgba(0,212,255,0.35) !important; overflow:hidden !important; background:rgba(2,10,20,0.9) !important; display:flex !important; align-items:center !important; justify-content:center !important; flex-shrink:0 !important; } /* Tamaño del logo dentro del avatar — scale(1.18) para zoom */ .chatbot-container .avatar-container img { width:100% !important; height:100% !important; object-fit:cover !important; transform:scale(1.18) !important; /* ← aumentar para más zoom */ } /* Animación de pulso en el avatar del bot */ .chatbot-container [data-testid="bot"] .avatar-container { animation:avatar-pulse 3s infinite ease-in-out; } /* ══ TEXTBOX — Campo de entrada de texto ══ Modificar border-radius/font-size para cambiar estilo del input */ .gradio-textbox input, .gradio-textbox textarea { background:rgba(0,8,22,.92) !important; border:1px solid rgba(0,180,255,.22) !important; border-radius:22px !important; color:#e0f4ff !important; font-family:'Exo 2',sans-serif !important; font-size:13.5px !important; padding:9px 18px !important; transition:all .3s !important; } .gradio-textbox input:focus, .gradio-textbox textarea:focus { border-color:var(--cyan) !important; box-shadow:0 0 0 1px rgba(0,212,255,.15), 0 0 20px rgba(0,212,255,.12) !important; outline:none !important; } .gradio-textbox input::placeholder, .gradio-textbox textarea::placeholder { color:rgba(0,140,190,.35) !important; } /* ══ BOTÓN ENVIAR — Botón principal de envío de mensaje ══ Modificar background gradient para cambiar color del botón */ .btn-send-cv button, .btn-send-cv { background:linear-gradient(135deg, #001a3a 0%, #003670 40%, #0068b8 75%, #00aadd 100%) !important; border:1px solid rgba(0,212,255,0.5) !important; color:#e8f8ff !important; border-radius:20px !important; font-weight:700 !important; font-size:0.72rem !important; font-family:'Orbitron',monospace !important; letter-spacing:.08em !important; padding:8px 18px !important; height:38px !important; min-width:110px !important; max-width:160px !important; box-shadow:0 0 14px rgba(0,168,232,.35), 0 0 1px rgba(0,212,255,.6) inset !important; transition:all 0.2s ease !important; animation:btnGlow 3s ease-in-out infinite !important; position:relative !important; overflow:hidden !important; white-space:nowrap !important; text-overflow:ellipsis !important; } .btn-send-cv button::after, .btn-send-cv::after { content:''; position:absolute; top:0; left:-100%; width:100%; height:100%; background:linear-gradient(90deg,transparent,rgba(255,255,255,.12),transparent); transition:.4s; } .btn-send-cv button:hover, .btn-send-cv:hover { box-shadow:0 0 28px rgba(0,212,255,.65), 0 0 55px rgba(0,212,255,.2) !important; transform:translateY(-1px) !important; border-color:rgba(0,255,220,.7) !important; } .btn-send-cv button:hover::after, .btn-send-cv:hover::after { left:100%; } /* ══ BOTÓN VOZ — Botón de activar respuesta por voz del asistente ══ */ .btn-voice-cv button, .btn-voice-cv { background:rgba(0,20,50,.6) !important; border:1px solid rgba(0,212,255,.35) !important; color:#00d4ff !important; border-radius:20px !important; font-size:0.72rem !important; font-family:'Share Tech Mono',monospace !important; letter-spacing:.1em !important; padding:8px 14px !important; height:38px !important; min-width:70px !important; max-width:110px !important; transition:all 0.2s !important; white-space:nowrap !important; } .btn-voice-cv button:hover, .btn-voice-cv:hover { background:rgba(0,80,160,.3) !important; box-shadow:0 0 16px rgba(0,212,255,.4) !important; border-color:rgba(0,212,255,.7) !important; } /* ══ BOTÓN LIMPIAR — Botón para limpiar conversación ══ */ .btn-clear-cv button, .btn-clear-cv { background:rgba(30,0,0,.5) !important; border:1px solid rgba(255,50,80,.25) !important; color:rgba(255,100,120,.7) !important; border-radius:20px !important; font-size:0.68rem !important; font-family:'Share Tech Mono',monospace !important; letter-spacing:.08em !important; padding:8px 14px !important; height:38px !important; min-width:90px !important; max-width:160px !important; transition:all 0.2s !important; white-space:nowrap !important; } .btn-clear-cv button:hover, .btn-clear-cv:hover { background:rgba(60,0,0,.6) !important; border-color:rgba(255,80,80,.5) !important; box-shadow:0 0 12px rgba(255,50,50,.25) !important; } /* ══ BOTÓN EJECUTAR (PDF) — Botón de envío en el módulo PDF ══ */ .btn-exec-cv button, .btn-exec-cv { background:linear-gradient(135deg,#001a3a,#00306a,#0055a0) !important; border:1px solid rgba(0,200,255,.45) !important; color:#e0f8ff !important; border-radius:20px !important; font-family:'Orbitron',monospace !important; font-size:0.68rem !important; font-weight:700 !important; letter-spacing:.08em !important; padding:8px 18px !important; height:38px !important; box-shadow:0 0 12px rgba(0,150,255,.3) !important; transition:all .2s !important; white-space:nowrap !important; } .btn-exec-cv button:hover, .btn-exec-cv:hover { box-shadow:0 0 24px rgba(0,212,255,.55) !important; transform:translateY(-1px) !important; } /* ══ MIC / AUDIO COMPONENT — Componente de grabación de voz (voz→texto) ══ Modificar border/background para cambiar apariencia del micrófono */ .mic-tool-cv { background:rgba(0,6,18,.9) !important; border:1px solid rgba(0,180,255,.18) !important; border-radius:12px !important; padding:4px 8px !important; } .mic-tool-cv label { font-family:'Share Tech Mono',monospace !important; font-size:0.65rem !important; color:rgba(0,180,255,.5) !important; letter-spacing:.12em !important; } .mic-tool-cv button { background:rgba(0,50,120,.4) !important; border:1px solid rgba(0,212,255,.3) !important; border-radius:8px !important; color:#00d4ff !important; font-size:0.72rem !important; padding:4px 10px !important; transition:.2s !important; } .mic-tool-cv button:hover { background:rgba(0,80,180,.5) !important; box-shadow:0 0 10px rgba(0,212,255,.3) !important; } /* ══ FILAS DE INPUT — Espaciado de las filas de controles ══ */ .input-row-cv { gap:8px !important; align-items:center !important; } .mic-row-cv { gap:8px !important; align-items:center !important; margin-top:4px !important; } /* ══ FOOTER DE GRADIO — Ocultar footer por defecto ══ */ footer { display:none !important; } """ # ════════════════════════════════════════════════════════ # HEADER HTML — Barra superior: logo CV, título, imagen logo, badges # ════════════════════════════════════════════════════════ HEADER_HTML = """
CV
CVision Intelligence
ASISTENTE TÉCNICO MULTIMODAL  ·  CRISTIAN VELASCO
ONLINE v3.1.0 NEURAL · ACTIVE
""" # ════════════════════════════════════════════════════════ # NEBULA SECTION HTML — Hero con nebula, info y panel derecho # ════════════════════════════════════════════════════════ NEBULA_SECTION_HTML = """
SISTEMA ACTIVADO  ·  CORE OPERATIVO
Asistente Virtual
CVision

Interfaz de inteligencia artificial multimodal. Combina procesamiento de lenguaje natural, análisis de documentos y búsqueda web en un solo núcleo cognitivo.

PROCESAMIENTO NLP VISIÓN COMPUTACIONAL BÚSQUEDA WEB ANÁLISIS PDF VOZ · WHISPER RAZONAMIENTO IA
✦
NÚCLEO NEURAL ACTIVIDAD EN TIEMPO REAL
CONECTIVIDAD ● ONLINE
ONLINE
Latencia: 18ms · Pérdida: 0.0%
""" # ════════════════════════════════════════════════════════ # STATUS BAR + JAVASCRIPT — Barra de estado y animaciones canvas # ════════════════════════════════════════════════════════ STATUS_AND_JS_HTML = """
CORE ACTIVE ALL SYSTEMS NOMINAL 00:00:00 SCANNING
""" # ════════════════════════════════════════════════════════ # CHAT HANDLER — Procesamiento de mensajes y selección de motor # ════════════════════════════════════════════════════════ def chat_engine_handler(history, message, mode, voice_enabled=False, pdf_file=None): if not message or message.strip() == "": return history, "", None if history is None: history = [] # ── Mapa de modos a motores de chat ── engines = { "general": regular_chat, "web": browser_chat, "productividad": productivity_chat, "cientifico": scientist_chat, "pdf": document_chat, } selected_engine = engines.get(mode, regular_chat) try: if mode == "pdf" and pdf_file: gen = selected_engine(history, message, pdf_file) else: gen = selected_engine(history, message) last_history, last_audio = None, None for yielded in gen: if isinstance(yielded, tuple) and len(yielded) == 2: last_history, last_audio = yielded else: last_history = yielded if last_history is None: last_history = history + [[message, "Sin respuesta del sistema."]] # ── Limpieza e imágenes markdown ── clean_history = [] for entry in last_history: if isinstance(entry, (list, tuple)) and len(entry) == 2: user_msg = entry[0] if entry[0] is not None else "" bot_msg = entry[1] if entry[1] is not None else "" if bot_msg and "![" in str(bot_msg): bot_msg = re.sub( r'!\[([^\]]*)\]\(([^)]+)\)', r'\1', str(bot_msg) ) clean_history.append((str(user_msg), str(bot_msg))) if not clean_history: clean_history = list(history) + [(message, "No se recibio respuesta.")] # ── Síntesis de voz opcional ── audio_path = None if voice_enabled and clean_history: last_bot = clean_history[-1][1] voice_text = re.sub(r'<[^>]+>', '', last_bot).replace("*","").replace("#","").replace("`","") try: audio_path = speak(voice_text[:450]) except Exception: pass return clean_history, "", audio_path except Exception as e: print("CRITICAL ERROR EN CVISION: " + str(e)) safe = list(history) if history else [] safe.append((message, "Error del sistema: " + str(e))) return safe, "", None # ════════════════════════════════════════════════════════ # TRANSCRIBE PROCESSOR — Convierte audio grabado a texto # ════════════════════════════════════════════════════════ def transcribe_processor(audio): if audio is None: return "" text = transcribe_audio_file(audio) return text if text else "" # ════════════════════════════════════════════════════════ # CREATE CHAT INTERFACE — Helper que crea el layout de chat # Reutilizado por todos los tabs excepto PDF # Layout: # [Chatbot] # Fila 1: [Textbox──────────] [Enviar] [Voz] # Fila 2: [Mic/Audio────────────────] [Limpiar] # ════════════════════════════════════════════════════════ def create_chat_interface(mode_id, placeholder_text=None): ph = placeholder_text or PLACEHOLDER with gr.Column(): # ── Ventana de chat ── chat_window = gr.Chatbot( height=490, bubble_full_width=False, elem_classes="chatbot-container", show_label=False, render_markdown=True, show_copy_button=True, avatar_images=(None, AVATAR_URL), ) out_audio = gr.Audio(visible=False, autoplay=True) # ── Fila 1: Textbox + Enviar + Voz ── with gr.Row(elem_classes="input-row-cv"): input_msg = gr.Textbox( placeholder=ph, scale=10, show_label=False, container=False, ) btn_send = gr.Button( "Enviar ╰┈➤", variant="primary", scale=2, min_width=110, elem_classes="btn-send-cv", ) btn_voice = gr.Button( "Voz 🤖🔊၊၊||၊", variant="secondary", scale=2, min_width=80, elem_classes="btn-voice-cv", ) # ── Fila 2: Micrófono (voz→texto) + Limpiar ── with gr.Row(elem_classes="mic-row-cv"): mic_tool = gr.Audio( sources=["microphone"], type="filepath", label="Transcripción de Voz", scale=8, elem_classes="mic-tool-cv", ) btn_clear = gr.Button( "⊠ Limpiar Conversación", variant="stop", scale=3, min_width=160, elem_classes="btn-clear-cv", ) # ── Estado del modo activo ── st_mode = gr.State(mode_id) # ── Mic → transcripción automática en el textbox ── mic_tool.stop_recording( transcribe_processor, inputs=[mic_tool], outputs=[input_msg], ) # ── Enviar mensaje ── btn_send.click( chat_engine_handler, inputs=[chat_window, input_msg, st_mode, gr.State(False)], outputs=[chat_window, input_msg, out_audio], ) input_msg.submit( chat_engine_handler, inputs=[chat_window, input_msg, st_mode, gr.State(False)], outputs=[chat_window, input_msg, out_audio], ) # ── Enviar con síntesis de voz ── btn_voice.click( chat_engine_handler, inputs=[chat_window, input_msg, st_mode, gr.State(True)], outputs=[chat_window, input_msg, out_audio], ) # ── Limpiar conversación ── btn_clear.click( lambda: ([], "", None), inputs=None, outputs=[chat_window, input_msg, out_audio], queue=False, ) # ════════════════════════════════════════════════════════ # GRADIO INTERFACE — Construcción de la interfaz principal # ════════════════════════════════════════════════════════ with gr.Blocks(css=CUSTOM_CSS, title="CVision Intelligence | Neural OS") as assistant: # ── Sección visual superior ── gr.HTML(HEADER_HTML) gr.HTML(NEBULA_SECTION_HTML) gr.HTML(STATUS_AND_JS_HTML) # ── Tabs de módulos ── with gr.Tabs(): # ── Tab: Núcleo Central (chat general) ── with gr.Tab("💠💬 CHAT GENERAL"): create_chat_interface("general") # ── Tab: Red Global (navegación web) ── with gr.Tab("🌐🛜 BÚSQUEDA WEB"): create_chat_interface("web") # ── Tab: Algoritmos (productividad) ── with gr.Tab("📈🎯 PRODUCTIVIDAD"): create_chat_interface("productividad") # ── Tab: Laboratorio (análisis científico) ── with gr.Tab("⚗️🔬 CIENTIFICO"): create_chat_interface("cientifico") # ── Tab: Análisis de Matriz (PDF) ── with gr.Tab("📂🔗 ANÁLISIS DE PDF"): with gr.Row(): # Columna de carga de archivo PDF with gr.Column(scale=2): gr.Markdown("### 📥 Ingesta de Archivos") file_input = gr.File( label="Documento PDF de Origen", file_types=[".pdf"], ) # Columna de chat PDF with gr.Column(scale=5): chat_pdf = gr.Chatbot( height=450, bubble_full_width=False, elem_classes="chatbot-container", show_label=False, render_markdown=True, avatar_images=(None, AVATAR_URL), ) pdf_audio = gr.Audio(visible=False, autoplay=True) # Fila 1: textbox + Ejecutar + Voz with gr.Row(elem_classes="input-row-cv"): txt_pdf = gr.Textbox( placeholder="Consultar parámetros del documento...", scale=10, container=False, ) btn_send_pdf = gr.Button( "Ejecutar ╰┈➤", variant="primary", scale=2, min_width=110, elem_classes="btn-exec-cv", ) btn_voz_pdf = gr.Button( "Voz 🤖🔊၊၊||၊", variant="secondary", scale=2, min_width=80, elem_classes="btn-voice-cv", ) # Fila 2: mic + limpiar with gr.Row(elem_classes="mic-row-cv"): mic_pdf = gr.Audio( sources=["microphone"], type="filepath", label="Transcripción de Voz", scale=8, elem_classes="mic-tool-cv", ) btn_clr_pdf = gr.Button( "❌🗑️ Limpiar", variant="stop", scale=3, min_width=100, elem_classes="btn-clear-cv", ) # Mic PDF → textbox mic_pdf.stop_recording( transcribe_processor, inputs=[mic_pdf], outputs=[txt_pdf], ) pdf_in = [chat_pdf, txt_pdf, gr.State("pdf"), gr.State(False), file_input] pdf_inv = [chat_pdf, txt_pdf, gr.State("pdf"), gr.State(True), file_input] pdf_out = [chat_pdf, txt_pdf, pdf_audio] btn_send_pdf.click(chat_engine_handler, pdf_in, pdf_out) txt_pdf.submit( chat_engine_handler, pdf_in, pdf_out) btn_voz_pdf.click( chat_engine_handler, pdf_inv, pdf_out) btn_clr_pdf.click(lambda: ([], "", None), None, pdf_out, queue=False) # ── Tab 6: Modo Voz (JARVIS) ── with gr.Tab("🎙️🌌 MODO VOZ"): create_voice_tab() # ════════════════════════════════════════════════════════ # SECCIÓN 07: EXPOSICIÓN ADAPTATIVA DE LA INTERFAZ # Expone la interfaz pura de Gradio para que el orquestador controle el flujo. # ════════════════════════════════════════════════════════ # Simplemente asignamos el bloque puro de Gradio sin envolturas pesadas assistant_app = assistant