
Proyecto Borg: El detective de spam (IA clásica vs. LLM)
Desmontando el mito de que «IA = LLM»: enfrentamos un modelo clásico Naive Bayes contra un modelo de lenguaje local para entender eficiencia, latencia y comprensión del contexto.
1. Por qué Anita Borg: Innovación, democratización y pragmatismo tecnológico
Cuando en las aulas se habla de Inteligencia Artificial, el alumnado tiende de forma casi automática a pensar en Grandes Modelos de Lenguaje (LLMs) como ChatGPT o Qwen. Sin embargo, la historia de las ciencias de la computación está repleta de soluciones elegantes, ultraligeras y eficientes que llevan décadas funcionando silenciosamente en la sombra.
En esta ocasión, rendimos homenaje a Anita Borg (1949–2003), científica de la computación estadounidense y visionaria que dedicó su vida a democratizar la tecnología y a romper barreras en el acceso a las disciplinas STEM. Borg defendía que la tecnología no debía ser una fuerza opaca reservada a unos pocos, sino una herramienta comprensible, eficiente e integradora al servicio de la sociedad.
Bajo este espíritu nace el Proyecto Borg: una actividad didáctica diseñada para el aula dentro de las iniciativas MonteSTEAM y AIDRAC del IES Monterroso. Su objetivo central es “abrir el capó” de los sistemas de clasificación y demostrar que el modelo más grande no siempre es el más adecuado para resolver un problema concreto.
2. El concepto técnico: Bag of Words + Naive Bayes vs. Inferencia en LLM
Para clasificar texto en dos categorías exclusivas (spam o no spam / ham), el experimento contrapone dos aproximaciones radicalmente distintas:
A) El enfoque clásico: CountVectorizer + MultinomialNB
- Vectorización (Bolsa de Palabras / Bag of Words):
CountVectorizerconvierte cada texto en un vector numérico que cuenta la frecuencia de cada palabra dentro de un vocabulario prefijado, ignorando el orden y la gramática. - Clasificación Probabilística (Naive Bayes Multinomial): Basándose en el teorema de Bayes,
MultinomialNBcalcula la probabilidad de que un mensaje sea spam dada la presencia de ciertas palabras clave. - Propiedades: Es un método completamente determinista, explicable palabra por palabra, ultraligero y entrenable en milisegundos con apenas 40–60 ejemplos en CPU estándar, sin necesidad de GPU ni infraestructura pesada.
B) El enfoque moderno: Inferencia con LLM local (Qwen en LM Studio)
- Comprensión Semántica: El LLM recibe el mensaje acompañado de un system prompt que le pide emitir un veredicto y una breve justificación en lenguaje natural.
- Sin entrenamiento previo (Zero-Shot): El modelo no se entrena con los 50 mensajes de ejemplo; apela a su conocimiento general del lenguaje y del contexto para deducir la intención del emisor.
- Propiedades: Alto nivel de razonamiento semántico y detección de matices, a costa de requerir varios gigabytes de VRAM, mayor consumo energético y una latencia significativamente superior.

3. Dinámica paso a paso en el aula
- Carga y entrenamiento del Dataset (40–60 mensajes):Al arrancar la aplicación, el sistema carga un conjunto de mensajes breves etiquetados como
spamuok(ofertas sospechosas, alertas bancarias, notas de amigos, avisos del instituto). El modelo Naive Bayes se entrena en vivo delante del alumnado en menos de un segundo. - Creación de mensajes adversarios por el alumnado:Cada estudiante redacta mensajes originales o “de ingeniería inversa”, intentando engañar a ambos clasificadores. Por ejemplo:
- “Enhorabuena, has aprobado el examen de matemáticas” (Usa palabras típicas de spam como “enhorabuena”, pero el contexto es educativo).
- “Necesito que me envíes tu contraseña urgentemente por seguridad” (No contiene palabras trampa típicas como “premio” o “gratis”, pero el fondo es un ataque de phishing).
- Envío y veredicto lado a lado:La aplicación devuelve una comparativa directa entre ambos modelos, incluyendo las palabras con mayor peso probabilístico (en Naive Bayes), la explicación en lenguaje natural (en el LLM) y el cronómetro exacto de respuesta en milisegundos.
- Registro de hallazgos:Los estudiantes completan una tabla comparativa analizando velocidad, aciertos, falsos positivos y falsos negativos.
4. Resultados y tabla de comparación en el aula
| Criterio de Análisis | Modelo Clásico (Naive Bayes) | Modelo de Lenguaje (LLM Local – Qwen) |
| Tiempo de respuesta (Latencia) | < 2 milisegundos ⚡ | 800 – 2500 milisegundos 🐢 |
| Requisitos de Hardware | Mínimos (Cualquier CPU, < 10 MB RAM) | Elevados (GPU/VRAM o CPU multinúcleo pesada) |
| Entrenamiento necesario | Requiere dataset etiquetado (40–60 ejemplos) | No requiere entrenamiento (Zero-Shot) |
| Explicabilidad | Matemática y transparente: Muestra el peso numérico de cada palabra. | Narrativa: Justifica la decisión en lenguaje natural. |
| Punto débil principales | Sensible al sesgo léxico; no entiende contexto ni ironía (falsos positivos con “enhorabuena” o “premio”). | Elevada latencia, consumo energético y riesgo marginal de alucinación. |
5. El matiz de ingeniería y preguntas de debate
El mayor aprendizaje para el alumnado es comprender la ingeniería de la elección: elegir la herramienta adecuada según las restricciones del problema, y no la más grande o moderna por defecto.
Preguntas para la reflexión de cierre:
- Escalabilidad y costes: Si tuvierais que filtrar 1.000 millones de correos al día en tiempo real (como Gmail), ¿cuál usaríais como primera línea de defensa y por qué?
- Ataques adversarios: Si los creadores de spam descubren qué palabras concretas penaliza el filtro Naive Bayes, ¿qué estrategia usarían para eludirlo? ¿Resultaría tan fácil engañar al LLM?
- Estrategia híbrida: ¿Sería viable combinar un modelo ultra-rápido Naive Bayes que filtre el 95% del tráfico sospechoso y delegar solo los casos dudosos a un LLM?
6. Código completo de la aplicación (app_borg.py)
Guardar el siguiente código en la carpeta borg/ como app_borg.py. Ejecuta el servidor en el puerto 8082:
Python
Cómo probar la aplicación en el aula:
- Crea la carpeta del proyecto e instala los requerimientos básicos:
Bashmkdir borg && cd borg pip install fastapi uvicorn scikit-learn httpx - Inicia LM Studio con el modelo Qwen cargado y activa el servidor local en el puerto
1234. - Lanza el servidor en el puerto 8082:
Bashpython app_borg.py - Navega a
http://localhost:8082e introduce distintas frases para evaluar la velocidad y la capacidad crítica de ambos modelos.
```python
import time
import asyncio
from typing import List, Dict, Any, Optional
from fastapi import FastAPI, HTTPException, Form, Request
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel
import httpx
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
app = FastAPI(
title="Proyecto Borg - El Detective de Spam",
description="Interfaz estilo Proyecto Wells conectada a Naive Bayes y Qwen Local (LM Studio)."
)
# -----------------------------------------------------------------------------
# 1. DATASET DE ENTRENAMIENTO DIDÁCTICO (40 Mensajes etiquetados)
# -----------------------------------------------------------------------------
TRAINING_DATA = [
# SPAM (Etiqueta 1)
("¡ENHORABUENA! Has ganado un iPhone gratis. Haz clic aquí para reclamar tu premio.", 1),
("Gana dinero rápido desde casa sin experiencia. Oferta exclusiva hoy.", 1),
("Urgente: Tu cuenta bancaria ha sido suspendida. Ingresa tus claves aquí.", 1),
("Consigue un préstamo inmediato sin nómina ni aval. Respuesta en 5 minutos.", 1),
("¡Felicidades! Eres el visitante 1.000.000. Reclama tu tarjeta regalo de 500 euros.", 1),
("Descuento exclusivo del 90% en relojes de lujo. Solo por tiempo limitado.", 1),
("Última oportunidad para conseguir tu suscripción gratuita a canales premium.", 1),
("Tu paquete no ha podido entregarse. Paga 1,99 euros de tasas en este enlace.", 1),
("Transferencia confirmada: Recibes 5000 EUR. Haz clic para aceptar el ingreso.", 1),
("Pierde 10 kilos en una semana con esta pastilla milagrosa sin dieta.", 1),
("Atención usuario: Actualiza tus datos de pago para evitar el bloqueo del servicio.", 1),
("Invierte 100 euros en criptomonedas y multiplica tus ganancias por diez.", 1),
("Casino online gratis: Regístrate hoy y recibe 50 tiradas sin depósito.", 1),
("Oferta de empleo urgente: Trabajo sencillo desde el móvil, altos ingresos.", 1),
("¡Increíble chollo! Compra tecnología a precio de saldo antes de que se agote.", 1),
("Servicio de mensajería: Su paquete está retenido en la aduana, confirme datos.", 1),
("Aviso importante de seguridad: Se ha detectado un acceso no autorizado a tu correo.", 1),
("Descarga esta aplicación y consigue internet gratis de forma ilimitada.", 1),
("Aumenta el tráfico de tu web de forma automática con nuestro robot publicitario.", 1),
("Premio garantizado: Introduce tu número de teléfono para recibir el código.", 1),
# NO SPAM / HAM (Etiqueta 0)
("Hola, ¿vamos a repasar el proyecto de tecnología a la biblioteca esta tarde?", 0),
("Recordatorio: Mañana hay examen de matemáticas a primera hora en el aula 12.", 0),
("Hola a todos, os comparto el archivo con los apuntes de la clase de historia.", 0),
("Buenas tardes, la reunión del departamento de ciencias se aplaza al jueves.", 0),
("Enhorabuena, has aprobado el examen de recuperación con muy buena nota.", 0),
("¿A qué hora quedamos para ir al entrenamiento de baloncesto?", 0),
("Estimado alumnado, se ha publicado el calendario de entregas del segundo trimestre.", 0),
("El profesor de física ha subido las soluciones de los problemas al aula virtual.", 0),
("Hola mama, ya he llegado al instituto. Luego te llamo al salir de clase.", 0),
("Por favor, acuérdate de traer mañana la autorización firmada para la excursión.", 0),
("Muchas felicidades por tu cumpleaños, espero que pases un gran día con la familia.", 0),
("No olvides revisar la bibliografía del trabajo antes de enviarlo por correo.", 0),
("Confirmada la cita con el tutor de orientación para el próximo martes a las 10.", 0),
("Aviso de la biblioteca: El libro que reservaste ya está disponible para préstamo.", 0),
("Hola, ¿me puedes pasar el enlace de la reunión de videollamada de trabajo?", 0),
("El equipo de robótica se reunirá en el taller durante el recreo.", 0),
("Gracias por tu ayuda con la presentación del Proyecto Borg, quedó excelente.", 0),
("Recuerda comprar la cartulina y los rotuladores para el póster de biología.", 0),
("La excursión a la sierra queda aplazada debido a la previsión de lluvia.", 0),
("Hola, te envío adjunto el borrador del artículo de investigación para revisión.", 0)
]
# -----------------------------------------------------------------------------
# 2. ENTRENAMIENTO DEL MODELO NAIVE BAYES
# -----------------------------------------------------------------------------
vectorizer = CountVectorizer(lowercase=True)
X_train = vectorizer.fit_transform([text for text, label in TRAINING_DATA])
y_train = [label for text, label in TRAINING_DATA]
clf = MultinomialNB()
clf.fit(X_train, y_train)
LM_STUDIO_URL = "http://localhost:1234/v1/chat/completions"
# -----------------------------------------------------------------------------
# 3. FUNCIONES DE CLASIFICACIÓN
# -----------------------------------------------------------------------------
def classify_naive_bayes(text: str) -> Dict[str, Any]:
start_time = time.perf_counter()
X_test = vectorizer.transform([text])
pred = clf.predict(X_test)[0]
probs = clf.predict_proba(X_test)[0]
feature_names = vectorizer.get_feature_names_out()
words_in_text = X_test.toarray()[0]
detected_words = [feature_names[idx] for idx, count in enumerate(words_in_text) if count > 0]
elapsed_ms = round((time.perf_counter() - start_time) * 1000, 2)
return {
"is_spam": bool(pred == 1),
"verdict": "SPAM" if pred == 1 else "NO SPAM (LEGÍTIMO)",
"probability_spam": round(probs[1] * 100, 1),
"detected_words": detected_words[:8],
"latency_ms": elapsed_ms
}
async def classify_llm(text: str) -> Dict[str, Any]:
start_time = time.perf_counter()
system_prompt = (
"Eres un experto en ciberseguridad y análisis crítico de textos para estudiantes. "
"Analiza el mensaje proporcionado y determina si es SPAM/PHISHING o NO SPAM (Legítimo). "
"Responde en formato JSON estrictamente con dos campos:\n"
"1. 'verdict': exactamente 'SPAM' o 'NO SPAM'.\n"
"2. 'reason': una frase breve (máximo 15 palabras) explicando el motivo."
)
payload = {
"model": "qwen",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Mensaje a analizar: \"{text}\""}
],
"temperature": 0.1,
"max_tokens": 120
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(LM_STUDIO_URL, json=payload, timeout=25.0)
response.raise_for_status()
data = response.json()
content = data["choices"][0]["message"]["content"]
elapsed_ms = round((time.perf_counter() - start_time) * 1000, 2)
return {
"response_raw": content,
"latency_ms": elapsed_ms
}
except Exception as e:
elapsed_ms = round((time.perf_counter() - start_time) * 1000, 2)
return {
"response_raw": f"Error/Offline en LLM local Qwen ({str(e)})",
"latency_ms": elapsed_ms
}
class ClassifyRequest(BaseModel):
text: str
@app.post("/api/classify")
async def api_classify(req: ClassifyRequest):
if not req.text.strip():
raise HTTPException(status_code=400, detail="El texto no puede estar vacío.")
nb_res = classify_naive_bayes(req.text)
llm_res = await classify_llm(req.text)
return {
"text": req.text,
"naive_bayes": nb_res,
"qwen_llm": llm_res
}
# -----------------------------------------------------------------------------
# 4. VISTA PRINCIPAL (INTERFAZ PROYECTO WELLS / BORG)
# -----------------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def home():
spam_items = [text for text, label in TRAINING_DATA if label == 1]
ham_items = [text for text, label in TRAINING_DATA if label == 0]
return f"""
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proyecto Borg / Wells - Detector de Spam</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
body {{ font-family: 'Inter', sans-serif; background-color: #f1f5f9; }}
.wells-topbar {{ background-color: #080d1a; }}
.wells-header-card {{ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); }}
.btn-wells {{ background-color: #0284c7; transition: all 0.2s ease; }}
.btn-wells:hover {{ background-color: #0369a1; transform: translateY(-1px); }}
</style>
</head>
<body class="min-h-screen text-slate-800 flex flex-col">
<!-- BARRA SUPERIOR PROYECTO WELLS / BORG -->
<header class="wells-topbar text-white px-4 py-2.5 flex flex-wrap justify-between items-center text-xs sm:text-sm shadow-md border-b border-slate-800">
<div class="flex items-center space-x-2 font-bold tracking-wide">
<span class="bg-cyan-500 text-black px-2 py-0.5 rounded font-extrabold text-sm uppercase tracking-wider">WELLS</span>
<span class="text-white text-sm font-semibold tracking-wider">PROYECTO BORG</span>
<span class="bg-orange-600 text-white text-[10px] px-1.5 py-0.5 rounded font-mono font-bold ml-1">PUERTO :8082</span>
</div>
<div class="flex items-center space-x-2 mt-1 sm:mt-0">
<span class="bg-sky-600/30 text-sky-300 border border-sky-500/40 text-[11px] px-2.5 py-0.5 rounded-full font-medium">
IES MONTERROSO — LÍNEA THEMIS
</span>
</div>
</header>
<!-- CONTENEDOR PRINCIPAL -->
<main class="max-w-6xl w-full mx-auto p-3 sm:p-6 space-y-6 flex-grow">
<!-- CABECERA PRINCIPAL CON ICONO -->
<div class="wells-header-card rounded-xl p-5 text-white shadow-lg border border-slate-700/50 flex justify-between items-center">
<div>
<p class="text-cyan-400 font-bold text-xs uppercase tracking-widest mb-1">
Termómetro del Bulo y Análisis Crítico de Medios
</p>
<h1 class="text-xl sm:text-2xl font-extrabold tracking-tight">
Separación de Mensajes Verificables / Legítimos vs. Spam y Phishing
</h1>
</div>
<div class="text-3xl sm:text-4xl text-rose-400 pl-4">
<i class="fa-solid fa-temperature-three-quarters"></i>
</div>
</div>
<!-- BLOQUE 1: ENTRADA Y SELECCIÓN DE MENSAJES -->
<section class="bg-white rounded-xl shadow-sm border border-slate-200 p-5 space-y-4">
<div class="flex items-center space-x-2 font-bold text-slate-800 text-sm sm:text-base border-b border-slate-100 pb-2">
<span class="text-rose-500"><i class="fa-solid fa-thumbtack"></i></span>
<h2>1. INTRODUCE EL MENSAJE O TITULAR A ANALIZAR</h2>
</div>
<!-- 5 EJEMPLOS SPAM / 5 EJEMPLOS HAM -->
<div class="bg-slate-50 border border-slate-200 rounded-lg p-3.5 space-y-3">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-1">
<label class="text-xs font-bold text-slate-700 uppercase tracking-wider">
Ejemplos preconfigurados para clase:
</label>
<span class="text-[11px] text-slate-500">Haz clic en un ejemplo para cargarlo en el cuadro</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- 5 SPAM -->
<div class="space-y-1.5">
<span class="text-[11px] font-bold text-rose-600 flex items-center gap-1">
<i class="fa-solid fa-triangle-exclamation"></i> 5 Ejemplos de SPAM / Phishing:
</span>
<div class="flex flex-wrap gap-1.5">
<button onclick="cargarTexto('{spam_items[0]}')" class="px-2 py-1 text-xs bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 rounded font-medium transition text-left">1. 🎁 Premio iPhone</button>
<button onclick="cargarTexto('{spam_items[2]}')" class="px-2 py-1 text-xs bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 rounded font-medium transition text-left">2. ⚠️ Cuenta Banco</button>
<button onclick="cargarTexto('{spam_items[11]}')" class="px-2 py-1 text-xs bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 rounded font-medium transition text-left">3. 💰 Criptomonedas</button>
<button onclick="cargarTexto('{spam_items[7]}')" class="px-2 py-1 text-xs bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 rounded font-medium transition text-left">4. 📦 Correos Tasas</button>
<button onclick="cargarTexto('{spam_items[9]}')" class="px-2 py-1 text-xs bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 rounded font-medium transition text-left">5. 💊 Pastilla Milagrosa</button>
</div>
</div>
<!-- 5 NO SPAM -->
<div class="space-y-1.5">
<span class="text-[11px] font-bold text-emerald-600 flex items-center gap-1">
<i class="fa-solid fa-circle-check"></i> 5 Ejemplos de NO SPAM (Legítimos):
</span>
<div class="flex flex-wrap gap-1.5">
<button onclick="cargarTexto('{ham_items[1]}')" class="px-2 py-1 text-xs bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border border-emerald-200 rounded font-medium transition text-left">1. 📚 Examen Mates</button>
<button onclick="cargarTexto('{ham_items[5]}')" class="px-2 py-1 text-xs bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border border-emerald-200 rounded font-medium transition text-left">2. ⚽ Entreno Baloncesto</button>
<button onclick="cargarTexto('{ham_items[8]}')" class="px-2 py-1 text-xs bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border border-emerald-200 rounded font-medium transition text-left">3. 📱 Mensaje a Mamá</button>
<button onclick="cargarTexto('{ham_items[13]}')" class="px-2 py-1 text-xs bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border border-emerald-200 rounded font-medium transition text-left">4. 📖 Libro Biblioteca</button>
<button onclick="cargarTexto('{ham_items[16]}')" class="px-2 py-1 text-xs bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border border-emerald-200 rounded font-medium transition text-left">5. 🤖 Proyecto Borg</button>
</div>
</div>
</div>
</div>
<!-- ÁREA DE TEXTO -->
<div class="space-y-1">
<label for="inputTexto" class="block text-xs font-semibold text-slate-600">Texto para el Termómetro:</label>
<textarea id="inputTexto" rows="3" class="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-sky-500 text-sm text-slate-800 outline-none transition" placeholder="Selecciona un ejemplo o escribe tu propio texto..."></textarea>
</div>
<button id="btnClasificar" onclick="ejecutarAnalisis()" class="w-full btn-wells text-white font-bold py-3 px-4 rounded-lg shadow flex items-center justify-center space-x-2 text-sm sm:text-base">
<span>🔥</span>
<span>Clasificar Frases con Qwen Local y Naive Bayes</span>
</button>
</section>
<!-- BLOQUE EXPLICATIVO: ¿QUÉ HACE EL BOTÓN Y CÓMO FUNCIONAN LOS ALGORITMOS? -->
<section class="bg-slate-900 text-slate-200 rounded-xl shadow-md p-5 border border-slate-700 space-y-4">
<div class="flex items-center space-x-2 text-cyan-400 font-bold text-sm sm:text-base border-b border-slate-800 pb-2">
<i class="fa-solid fa-circle-info"></i>
<h2>¿QUÉ HACE EL BOTÓN Y CÓMO FUNCIONAN LOS ALGORITMOS?</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs leading-relaxed">
<!-- EXPLICACIÓN NAIVE BAYES -->
<div class="bg-slate-800/80 p-3.5 rounded-lg border border-slate-700 space-y-2">
<div class="flex items-center space-x-2 text-amber-400 font-bold">
<i class="fa-solid fa-bolt"></i>
<span class="uppercase">1. Algoritmo Estadístico: Naive Bayes (Scikit-Learn)</span>
</div>
<p class="text-slate-300">
<strong>¿Qué hace?:</strong> Analiza el texto de forma ultrarrápida (en milisegundos) comparándolo con el dataset de 40 mensajes de entrenamiento.
</p>
<p class="text-slate-400">
<strong>¿Cómo funciona?:</strong>
</p>
<ul class="list-disc pl-4 space-y-1 text-slate-300">
<li><strong>Vectorización (Bolsa de palabras):</strong> Separa el texto en palabras individuales y cuenta su frecuencia.</li>
<li><strong>Probabilidad Condicionada:</strong> Calcula la probabilidad matemática de que el mensaje sea Spam en función de palabras clave (ej. <em>"premio"</em>, <em>"urgente"</em>, <em>"banco"</em>) aprendidas en la fase de entrenamiento.</li>
</ul>
</div>
<!-- EXPLICACIÓN LLM QWEN -->
<div class="bg-slate-800/80 p-3.5 rounded-lg border border-slate-700 space-y-2">
<div class="flex items-center space-x-2 text-sky-400 font-bold">
<i class="fa-solid fa-brain"></i>
<span class="uppercase">2. IA Generativa: Qwen Local (LM Studio)</span>
</div>
<p class="text-slate-300">
<strong>¿Qué hace?:</strong> Procesa el mensaje mediante una red neuronal profunda que entiende el contexto completo y genera una explicación en lenguaje natural.
</p>
<p class="text-slate-400">
<strong>¿Cómo funciona?:</strong>
</p>
<ul class="list-disc pl-4 space-y-1 text-slate-300">
<li><strong>Análisis Semántico y Contextual:</strong> No solo busca palabras sueltas, sino que evalúa la intención del mensaje, la coherencia y el tono (ej. si hay suplantación o manipulación).</li>
<li><strong>Respuesta Estructurada:</strong> Retorna un veredicto y una justificación breve redactada por el modelo para ayudar al alumno a entender la razón.</li>
</ul>
</div>
</div>
</section>
<!-- BLOQUE 2: RESULTADOS DE LA CLASIFICACIÓN -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- TARJETA 1: NO SPAM -->
<div id="cardHam" class="bg-emerald-50/50 border border-emerald-200 rounded-xl p-4 shadow-sm space-y-3 opacity-60 transition-all duration-300">
<div class="flex items-center justify-between border-b border-emerald-200/60 pb-2">
<div class="flex items-center space-x-2">
<span class="w-3 h-3 bg-emerald-500 rounded-sm"></span>
<h3 class="font-bold text-emerald-900 text-sm uppercase">1. MENSAJES LEGÍTIMOS (NO SPAM)</h3>
</div>
<span id="badgeHamCount" class="bg-emerald-600 text-white text-xs font-bold px-2 py-0.5 rounded-md">0</span>
</div>
<div id="boxHam" class="bg-white border border-emerald-100 rounded-lg p-3 text-xs text-slate-700 min-h-[100px] flex flex-col justify-between">
<p id="textHam" class="italic text-slate-400">Pendiente de análisis...</p>
<div id="infoHam" class="mt-2 text-[11px] text-emerald-800 font-medium space-y-1 hidden">
<div id="motivoHam"></div>
<div id="metricsHam" class="text-[10px] text-slate-500 border-t pt-1"></div>
</div>
</div>
</div>
<!-- TARJETA 2: SPAM -->
<div id="cardSpam" class="bg-amber-50/50 border border-amber-200 rounded-xl p-4 shadow-sm space-y-3 opacity-60 transition-all duration-300">
<div class="flex items-center justify-between border-b border-amber-200/60 pb-2">
<div class="flex items-center space-x-2">
<span class="w-3 h-3 bg-amber-500 rounded-sm"></span>
<h3 class="font-bold text-amber-900 text-sm uppercase">2. SPAM / AMENAZAS / PHISHING</h3>
</div>
<span id="badgeSpamCount" class="bg-amber-600 text-white text-xs font-bold px-2 py-0.5 rounded-md">0</span>
</div>
<div id="boxSpam" class="bg-white border border-amber-100 rounded-lg p-3 text-xs text-slate-700 min-h-[100px] flex flex-col justify-between">
<p id="textSpam" class="italic text-slate-400">Pendiente de análisis...</p>
<div id="infoSpam" class="mt-2 text-[11px] text-amber-800 font-medium space-y-1 hidden">
<div id="motivoSpam"></div>
<div id="metricsSpam" class="text-[10px] text-slate-500 border-t pt-1"></div>
</div>
</div>
</div>
</section>
<!-- BLOQUE 3: DATASET DE ENTRENAMIENTO COMPLETO -->
<section class="bg-white rounded-xl shadow-sm border border-slate-200 p-5 space-y-4 mt-8">
<div class="flex flex-col sm:flex-row sm:items-center justify-between border-b border-slate-200 pb-3 gap-2">
<div class="flex items-center space-x-2">
<span class="text-sky-600 text-lg"><i class="fa-solid fa-database"></i></span>
<div>
<h2 class="font-extrabold text-slate-800 text-base">Conjunto de Datos de Entrenamiento (40 Mensajes del Modelo)</h2>
<p class="text-xs text-slate-500">Haz clic en cualquier mensaje para probarlo directamente en el clasificador</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- COLUMNA NO SPAM -->
<div class="space-y-3">
<div class="flex justify-between items-center bg-emerald-100/70 border border-emerald-200 px-3 py-2 rounded-lg">
<span class="font-bold text-emerald-900 text-xs uppercase flex items-center gap-1.5">
<i class="fa-solid fa-circle-check text-emerald-600"></i> Mensajes Entrenamiento NO SPAM ({len(ham_items)})
</span>
</div>
<div class="space-y-2 max-h-80 overflow-y-auto pr-1">
{''.join([f'''
<div onclick="cargarTexto('{item}')" class="p-2.5 bg-slate-50 border border-slate-200 rounded-lg hover:border-emerald-400 transition text-xs text-slate-700 flex gap-2 items-start cursor-pointer group">
<span class="bg-emerald-100 text-emerald-800 text-[10px] font-bold px-1.5 py-0.5 rounded font-mono">#{i+1}</span>
<span class="flex-1 group-hover:text-emerald-900">{item}</span>
</div>
''' for i, item in enumerate(ham_items)])}
</div>
</div>
<!-- COLUMNA SPAM -->
<div class="space-y-3">
<div class="flex justify-between items-center bg-rose-100/70 border border-rose-200 px-3 py-2 rounded-lg">
<span class="font-bold text-rose-900 text-xs uppercase flex items-center gap-1.5">
<i class="fa-solid fa-triangle-exclamation text-rose-600"></i> Mensajes Entrenamiento SPAM ({len(spam_items)})
</span>
</div>
<div class="space-y-2 max-h-80 overflow-y-auto pr-1">
{''.join([f'''
<div onclick="cargarTexto('{item}')" class="p-2.5 bg-slate-50 border border-slate-200 rounded-lg hover:border-rose-400 transition text-xs text-slate-700 flex gap-2 items-start cursor-pointer group">
<span class="bg-rose-100 text-rose-800 text-[10px] font-bold px-1.5 py-0.5 rounded font-mono">#{i+1}</span>
<span class="flex-1 group-hover:text-rose-900">{item}</span>
</div>
''' for i, item in enumerate(spam_items)])}
</div>
</div>
</div>
</section>
</main>
<script>
function cargarTexto(txt) {{
document.getElementById('inputTexto').value = txt;
window.scrollTo({{ top: 0, behavior: 'smooth' }});
}}
async function ejecutarAnalisis() {{
const text = document.getElementById('inputTexto').value.trim();
if (!text) {{
alert("Por favor, introduce o selecciona un mensaje para analizar.");
return;
}}
const btn = document.getElementById('btnClasificar');
btn.disabled = true;
btn.innerHTML = '<span>⏳</span><span>Analizando con Qwen Local y Naive Bayes...</span>';
try {{
const response = await fetch('/api/classify', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ text: text }})
}});
const data = await response.json();
const cardHam = document.getElementById('cardHam');
const cardSpam = document.getElementById('cardSpam');
const textHam = document.getElementById('textHam');
const textSpam = document.getElementById('textSpam');
const infoHam = document.getElementById('infoHam');
const infoSpam = document.getElementById('infoSpam');
const badgeHam = document.getElementById('badgeHamCount');
const badgeSpam = document.getElementById('badgeSpamCount');
const isSpam = data.naive_bayes.is_spam;
if (isSpam) {{
cardSpam.className = "bg-amber-50 border-2 border-amber-500 rounded-xl p-4 shadow-md space-y-3 opacity-100 transition-all duration-300";
cardHam.className = "bg-emerald-50/30 border border-emerald-200 rounded-xl p-4 shadow-sm space-y-3 opacity-30 transition-all duration-300";
badgeSpam.innerText = "1";
badgeHam.innerText = "0";
textSpam.innerHTML = `<strong>"${{data.text}}"</strong>`;
textSpam.className = "text-slate-800";
infoSpam.classList.remove('hidden');
document.getElementById('motivoSpam').innerHTML = `📌 <strong>Respuesta Qwen Local:</strong> ${{data.qwen_llm.response_raw}}`;
document.getElementById('metricsSpam').innerHTML = `⚡ Naive Bayes: Probabilidad Spam ${{data.naive_bayes.probability_spam}}% | Latencia NB: ${{data.naive_bayes.latency_ms}} ms | Latencia LLM: ${{data.qwen_llm.latency_ms}} ms`;
textHam.innerText = "No califica como mensaje legítimo.";
textHam.className = "italic text-slate-400";
infoHam.classList.add('hidden');
}} else {{
cardHam.className = "bg-emerald-50 border-2 border-emerald-500 rounded-xl p-4 shadow-md space-y-3 opacity-100 transition-all duration-300";
cardSpam.className = "bg-amber-50/30 border border-amber-200 rounded-xl p-4 shadow-sm space-y-3 opacity-30 transition-all duration-300";
badgeHam.innerText = "1";
badgeSpam.innerText = "0";
textHam.innerHTML = `<strong>"${{data.text}}"</strong>`;
textHam.className = "text-slate-800";
infoHam.classList.remove('hidden');
document.getElementById('motivoHam').innerHTML = `📌 <strong>Respuesta Qwen Local:</strong> ${{data.qwen_llm.response_raw}}`;
document.getElementById('metricsHam').innerHTML = `⚡ Naive Bayes: Probabilidad Spam ${{data.naive_bayes.probability_spam}}% | Latencia NB: ${{data.naive_bayes.latency_ms}} ms | Latencia LLM: ${{data.qwen_llm.latency_ms}} ms`;
textSpam.innerText = "No se detectaron indicios de amenaza o spam.";
textSpam.className = "italic text-slate-400";
infoSpam.classList.add('hidden');
}}
}} catch (e) {{
alert("Error durante la clasificación: " + e);
}} finally {{
btn.disabled = false;
btn.innerHTML = '<span>🔥</span><span>Clasificar Frases con Qwen Local y Naive Bayes</span>';
}}
}}
</script>
</body>
</html>
"""
# -----------------------------------------------------------------------------
# 5. EJECUCIÓN DEL SERVIDOR EN PUERTO 8082
# -----------------------------------------------------------------------------
if __name__ == "__main__":
import uvicorn
uvicorn.run("app_borg:app", host="0.0.0.0", port=8082, reload=True)
```



