
Proyecto Hipatia: El Resumen de la Gran Biblioteca en Bachillerato
Publicado por: Departamento de Tecnología e Innovación Educativa — IES Monterroso
Nivel educativo: Bachillerato
Laboratorio: Proyecto Hipatia (app_hipatia.py — Puerto :8064)
En la era de la Inteligencia Artificial Generativa nos hemos acostumbrado a pedirle a un modelo de lenguaje que nos resuma “lo que sea”, sin preguntarnos cómo lo hace ni qué pierde por el camino. Inspirándonos en la figura de Hipatia de Alejandría —filósofa y matemática que dedicó su vida a sintetizar, organizar y transmitir el inmenso saber acumulado en la Biblioteca de Alejandría—, presentamos en el IES Monterroso el Proyecto Hipatia: El Resumen de la Gran Biblioteca.
Esta actividad práctica convierte el aula de Bachillerato en un laboratorio de síntesis documental, donde el alumnado deja de ver el resumen automático como una caja negra y aprende a entender —y a cuestionar— cómo una IA condensa un documento que no le cabe entero “en la cabeza”.
¿Por qué el Proyecto Hipatia en Bachillerato?
Los modelos de lenguaje tienen una limitación estructural conocida como ventana de contexto: no pueden procesar un documento infinitamente largo de una sola vez. Cuando el texto es extenso, muchas herramientas aplican sin que lo veamos una técnica llamada map-reduce: trocean el documento, resumen cada fragmento por separado y después funden esos resúmenes parciales en uno final.
Para nuestro alumnado de Bachillerato, trabajar con el Proyecto Hipatia persigue tres objetivos clave:
- Desmitificar el resumen automático: entender que una IA no “lee” un texto largo como lo haría una persona, sino que lo procesa por partes.
- Desarrollar pensamiento crítico sobre la síntesis: comparar un resumen directo con uno map-reduce y detectar qué información se pierde en cada método.
- Iniciarse en arquitecturas de IA aplicada: comprender por qué existen técnicas como map-reduce y dónde se usan en el mundo real (resumen de informes, actas, artículos científicos, documentación legal).
¿Cómo funciona el Taller en el Aula?
A través de una interfaz interactiva alojada en el servidor local del centro (app_hipatia.py), el taller se estructura en tres fases dinámicas:
1. Carga del Documento
El alumnado puede elegir entre tres textos largos de ejemplo de distintas áreas:
- Historia: La Ruta de la Seda
- Ciencias: El Ciclo del Agua
- Tecnología: Breve Historia de Internet
También puede pegar su propio texto —apuntes, un artículo, un capítulo de un libro de texto— y elegir en cuántos fragmentos (3, 4 o 5) se dividirá para la fase Map.
2. Fase Map: Trocear y Resumir por Partes
El documento se divide respetando los límites de frase, y la IA resume cada fragmento por separado, sin ver el resto del texto. La interfaz muestra, fragmento a fragmento, el texto original junto a su mini-resumen, de forma que el alumnado observa el proceso en lugar de recibir solo un resultado final.
3. Fase Reduce: Fusión en un Resumen Final
Los mini-resúmenes generados en la fase anterior se funden en un único resumen final, coherente y sin repeticiones.

4. Comparativa Crítica
Finalmente, la plataforma pide a la IA un resumen directo del documento completo, sin trocear, y lo muestra lado a lado con el resumen map-reduce. El alumnado debate qué método conserva mejor los detalles del principio y del final del texto, y por qué el resumen directo tiende a “olvidar” información cuando el documento es muy largo.
Competencias Clave Trabajadas
| Competencia | Aplicación en el Proyecto Hipatia |
|---|---|
| Digital (CD) | Comprensión de arquitecturas de IA (map-reduce), análisis crítico de herramientas de resumen automático. |
| Personal, Social y de Aprender a Aprender (CPSAA) | Autonomía para evaluar la fiabilidad y completitud de un resumen generado por IA. |
| Lingüística (CCL) | Comprensión lectora de textos extensos, síntesis y comparación de registros y niveles de detalle. |
| STEM | Introducción a técnicas de procesamiento de lenguaje natural, división de problemas complejos en subproblemas (map-reduce). |
Una Experiencia de Aprendizaje Activa y Reflexiva
El Proyecto Hipatia demuestra que trabajar la Inteligencia Artificial en Bachillerato no consiste solo en “usar la IA para resumir apuntes”, sino en entender cómo procesa la información, qué limitaciones estructurales tiene y cómo esas limitaciones condicionan el resultado que recibimos.
Convertir el resumen automático en un proceso visible y comparable permite a nuestro alumnado adquirir un criterio propio a la hora de confiar —o no— en lo que una IA les entrega como “el resumen” de un texto.
¡El laboratorio ya está en marcha! Puedes acceder a la plataforma en los equipos del aula de informática a través del puerto interno
:8064. ¡Trocea, resume y descubre cuánto se pierde por el camino!
# app_hipatia.py
import os
import re
import urllib.request
import json
from typing import List, Optional
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import uvicorn
app = FastAPI(
title="Proyecto Hipatia - El Resumen de la Gran Biblioteca (IES Monterroso)",
description="Laboratorio de Resumen Map-Reduce para Bachillerato (Puerto 8064)",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "http://localhost:1234/v1/chat/completions")
LLM_MODEL_NAME = os.getenv("LLM_MODEL_NAME", "local-model")
class SampleRequest(BaseModel):
topic: str = Field(..., example="La Ruta de la Seda")
class MapRequest(BaseModel):
full_text: str
chunk_count: int = 4
class ReduceRequest(BaseModel):
partial_summaries: List[str]
class DirectRequest(BaseModel):
full_text: str
def query_llm_provider(messages: List[dict], temperature: float = 0.4, max_tokens: int = 350) -> str:
payload = {
"model": LLM_MODEL_NAME,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
LLM_BASE_URL,
data=data,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=30) as response:
res_body = json.loads(response.read().decode("utf-8"))
return res_body["choices"][0]["message"]["content"]
except Exception:
return None
def split_into_chunks(text: str, chunk_count: int) -> List[str]:
"""Trocea un texto largo en N fragmentos respetando los límites de frase."""
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
sentences = [s for s in sentences if s.strip()]
if not sentences:
return [text]
chunk_count = max(1, min(chunk_count, len(sentences)))
base_size = len(sentences) // chunk_count
remainder = len(sentences) % chunk_count
chunks = []
idx = 0
for i in range(chunk_count):
size = base_size + (1 if i < remainder else 0)
chunk_sentences = sentences[idx: idx + size]
idx += size
if chunk_sentences:
chunks.append(" ".join(chunk_sentences))
return chunks
SAMPLE_TEXTS = {
"Ruta de la Seda": (
"La Ruta de la Seda no era un único camino, sino una vasta red de rutas comerciales terrestres y marítimas que "
"conectaron Oriente y Occidente durante más de mil quinientos años. Su nombre proviene de la seda china, uno de "
"los productos más codiciados que circulaban por estas rutas, aunque también se transportaban especias, papel, "
"pólvora, piedras preciosas y metales. Las caravanas partían de ciudades como Xi'an, atravesaban Asia Central, "
"cruzaban las estepas y las montañas del Pamir, y llegaban hasta el Mediterráneo a través de ciudades como "
"Samarcanda, Bujará o Antioquía.\n\n"
"Además de mercancías, la Ruta de la Seda fue un canal extraordinario de intercambio cultural, religioso y "
"tecnológico. El budismo se extendió desde la India hasta China y Japón siguiendo estas rutas. El papel, "
"inventado en China, llegó a Europa a través de los árabes varios siglos después. También viajaron enfermedades: "
"se cree que la peste negra que asoló Europa en el siglo XIV llegó desde Asia a través de estas mismas rutas "
"comerciales, transportada por las pulgas que infestaban a las ratas de las caravanas.\n\n"
"El control de la Ruta de la Seda otorgaba enorme poder económico y político. Imperios como el persa, el "
"mongol o el otomano llegaron a depender en gran medida de los impuestos cobrados a las caravanas que cruzaban "
"sus territorios. Cuando el Imperio Otomano tomó Constantinopla en 1453 y encareció el paso de mercancías hacia "
"Europa, los reinos europeos comenzaron a buscar rutas marítimas alternativas hacia Asia, lo que impulsó, entre "
"otros factores, los viajes de exploración que llevarían al descubrimiento de América."
),
"Ciclo del Agua": (
"El ciclo del agua, también llamado ciclo hidrológico, describe el movimiento continuo del agua en la Tierra "
"entre la atmósfera, la superficie terrestre y los océanos. Este proceso está impulsado fundamentalmente por la "
"energía solar, que provoca la evaporación del agua de mares, ríos y lagos, transformándola en vapor que "
"asciende hacia la atmósfera. Las plantas también contribuyen a este ciclo mediante la transpiración, liberando "
"vapor de agua a través de sus hojas.\n\n"
"Una vez en la atmósfera, el vapor de agua se enfría al ascender y se condensa alrededor de pequeñas partículas "
"de polvo, formando las nubes. Cuando las gotas de agua dentro de una nube se vuelven demasiado pesadas para "
"mantenerse suspendidas en el aire, caen a la superficie en forma de precipitación: lluvia, nieve, granizo o "
"aguanieve, dependiendo de la temperatura atmosférica.\n\n"
"Al llegar al suelo, el agua sigue varios caminos posibles. Una parte se infiltra en el terreno y pasa a formar "
"parte de los acuíferos subterráneos, alimentando pozos y manantiales. Otra parte escurre por la superficie "
"formando arroyos y ríos que eventualmente desembocan en lagos u océanos. Una tercera parte queda retenida "
"temporalmente en forma de nieve o hielo en glaciares y casquetes polares, actuando como reservas de agua dulce "
"a largo plazo. Todo este ciclo se repite de forma indefinida, siendo fundamental para sostener la vida en el "
"planeta y regular el clima global."
),
"Historia de Internet": (
"Internet tiene su origen en ARPANET, una red desarrollada a finales de los años sesenta por el Departamento "
"de Defensa de los Estados Unidos a través de su agencia de proyectos avanzados, DARPA. El objetivo inicial no "
"era militar en sentido estricto, sino crear una red de comunicación descentralizada y resistente a fallos, "
"capaz de seguir funcionando aunque parte de sus nodos quedaran inoperativos. El primer mensaje se envió en "
"1969 entre las universidades de California en Los Ángeles y Stanford.\n\n"
"Durante los años setenta y ochenta se desarrollaron los protocolos que hoy siguen siendo la base de Internet, "
"especialmente TCP/IP, que permitía que redes distintas se comunicaran entre sí de forma estandarizada. Esto "
"posibilitó la interconexión de múltiples redes académicas y gubernamentales de todo el mundo, dando lugar "
"progresivamente a lo que empezamos a llamar 'Internet'. Sin embargo, durante estas décadas su uso quedó "
"restringido casi exclusivamente al ámbito científico y militar.\n\n"
"El gran salto hacia la popularización llegó en 1989, cuando Tim Berners-Lee, investigador del CERN, propuso "
"la World Wide Web: un sistema de documentos interconectados mediante hipervínculos, accesible a través de "
"navegadores. A diferencia de Internet (la infraestructura de redes), la Web es un servicio que se ejecuta "
"sobre esa infraestructura. Durante los años noventa, con la aparición de los primeros navegadores gráficos "
"como Mosaic y Netscape, Internet pasó de ser una herramienta académica a un fenómeno de masas que transformó "
"por completo la forma en que la sociedad se comunica, trabaja y accede a la información."
),
}
HTML_CONTENT = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proyecto HIPATIA | El Resumen de la Gran Biblioteca (IES Monterroso)</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>
:root {
--monterroso-blue: #004B87;
--monterroso-dark: #00335E;
--monterroso-light: #EBF3FA;
}
.bg-monterroso { background-color: var(--monterroso-blue); }
.bg-monterroso-dark { background-color: var(--monterroso-dark); }
.text-monterroso { color: var(--monterroso-blue); }
.border-monterroso { border-color: var(--monterroso-blue); }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #004B87; border-radius: 3px; }
</style>
</head>
<body class="bg-gray-50 text-gray-800 font-sans min-h-screen flex flex-col">
<header class="bg-monterroso text-white shadow-md border-b-4 border-amber-500">
<div class="max-w-7xl mx-auto px-4 py-3 flex flex-wrap justify-between items-center gap-4">
<div class="flex items-center space-x-3">
<div class="bg-white p-2 rounded-lg text-monterroso font-black text-xl flex items-center justify-center w-10 h-10 shadow">
HP
</div>
<div>
<div class="flex items-center gap-2">
<span class="text-xs tracking-wider uppercase opacity-90 font-semibold">IES Monterroso — Bachillerato</span>
<span class="px-2 py-0.5 bg-amber-900 text-[10px] font-mono rounded text-amber-200 border border-amber-700">Puerto :8064</span>
</div>
<h1 class="text-xl font-bold tracking-tight flex items-center gap-2">
<span>PROYECTO HIPATIA</span>
<span class="text-xs bg-amber-400 text-gray-900 px-2 py-0.5 rounded-full font-extrabold uppercase">La Gran Biblioteca</span>
</h1>
</div>
</div>
<div class="flex items-center space-x-2">
<span class="px-3 py-1 bg-amber-900 text-amber-100 rounded-full text-xs font-semibold flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-amber-400 animate-pulse"></span>
app_hipatia.py Activo (:8064)
</span>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-6 flex-1 w-full space-y-6">
<section class="bg-white p-5 rounded-xl shadow-sm border border-gray-200 space-y-3">
<div class="bg-gradient-to-r from-slate-900 via-monterroso to-amber-950 text-white p-3.5 rounded-lg flex items-center justify-between shadow-sm">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-full bg-amber-400/20 flex items-center justify-center text-amber-300 text-xl font-bold border border-amber-400/30">
<i class="fa-solid fa-scroll font-bold"></i>
</div>
<div>
<div class="text-[11px] uppercase tracking-wider text-amber-200 font-semibold">Taller de Síntesis de Textos (Bachillerato)</div>
<div class="text-sm font-bold">Descubre cómo una IA resume documentos que no caben de una sola vez en su memoria</div>
</div>
</div>
<span class="hidden md:inline-block text-xs text-amber-100 italic bg-white/10 px-3 py-1 rounded">
"Trocea, resume cada parte y funde el saber en un único resumen."
</span>
</div>
</section>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<section class="lg:col-span-5 space-y-4">
<div class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 space-y-3">
<h2 class="text-xs font-bold text-gray-700 uppercase tracking-wider flex items-center gap-2">
<i class="fa-solid fa-book-open text-monterroso"></i> 1. Cargar documento (Opcional)
</h2>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Elige un texto largo de ejemplo:</label>
<select id="topic-select" class="w-full p-2 border border-gray-300 rounded text-xs bg-gray-50 focus:bg-white mb-2">
<option value="Ruta de la Seda">Historia: La Ruta de la Seda</option>
<option value="Ciclo del Agua">Ciencias: El Ciclo del Agua</option>
<option value="Historia de Internet">Tecnología: Breve Historia de Internet</option>
</select>
<button onclick="loadSample()" id="btn-generate" class="w-full py-2 bg-monterroso hover:bg-monterroso-dark text-white font-bold text-xs rounded transition flex items-center justify-center gap-2 shadow">
<i class="fa-solid fa-file-import"></i> Cargar documento de ejemplo
</button>
</div>
</div>
<div class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 space-y-2">
<div class="flex justify-between items-center">
<h3 class="text-xs font-bold text-gray-700 uppercase flex items-center gap-1.5">
<i class="fa-solid fa-pen-to-square text-indigo-600"></i> 2. Documento (¡Puedes editarlo o pegar el tuyo!):
</h3>
<span id="badge-status" class="text-[10px] bg-gray-100 text-gray-600 px-2 py-0.5 rounded font-mono">Editable</span>
</div>
<p class="text-[11px] text-gray-500">
Carga un ejemplo arriba o <strong>pega tú mismo un texto largo</strong> (apuntes, un artículo, un capítulo) para ver cómo lo resume la IA.
</p>
<textarea id="full-text" rows="12" class="w-full p-3 bg-slate-50 border border-gray-300 rounded text-xs text-gray-800 leading-relaxed font-sans focus:bg-white focus:ring-2 focus:ring-monterroso focus:outline-none resize-y" placeholder="Pega aquí tu documento largo o haz clic en 'Cargar documento de ejemplo'..."></textarea>
<div class="flex items-center justify-between text-[10px] text-gray-500 font-mono">
<span id="word-count">0 palabras</span>
</div>
<div class="pt-2">
<label class="block text-[11px] font-bold text-gray-700 mb-1">✂️ Número de fragmentos (fase Map):</label>
<select id="chunk-count" class="w-full p-2 border border-gray-300 rounded text-xs bg-gray-50 focus:bg-white">
<option value="3">3 fragmentos</option>
<option value="4" selected>4 fragmentos</option>
<option value="5">5 fragmentos</option>
</select>
</div>
</div>
</section>
<section class="lg:col-span-7 bg-white p-4 rounded-xl shadow-sm border border-gray-200 flex flex-col">
<div class="flex border-b border-gray-200 text-xs font-bold mb-4">
<button onclick="switchTab('map')" id="tab-map" class="px-4 py-2 border-b-2 border-monterroso text-monterroso flex items-center gap-1.5">
<i class="fa-solid fa-diagram-project"></i> Fase Map
</button>
<button onclick="switchTab('reduce')" id="tab-reduce" class="px-4 py-2 text-gray-500 hover:text-gray-700 flex items-center gap-1.5">
<i class="fa-solid fa-compress"></i> Fase Reduce
</button>
<button onclick="switchTab('compare')" id="tab-compare" class="px-4 py-2 text-gray-500 hover:text-gray-700 flex items-center gap-1.5">
<i class="fa-solid fa-scale-balanced"></i> Comparativa
</button>
</div>
<div id="panel-map" class="space-y-3 flex-1 flex flex-col">
<p class="text-xs text-gray-600">
<strong>Fase Map:</strong> el documento se trocea en fragmentos y la IA resume cada uno por separado, sin ver el resto del texto.
</p>
<button onclick="runMap()" class="py-2 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs rounded transition flex items-center justify-center gap-2">
<i class="fa-solid fa-scissors text-cyan-200"></i> Trocear y Resumir por Partes
</button>
<div id="map-result" class="flex-1 space-y-2 overflow-y-auto max-h-[320px]">
<div class="p-3 bg-indigo-50/50 border border-indigo-100 rounded text-xs font-mono text-gray-500">
<em>Los mini-resúmenes de cada fragmento aparecerán aquí...</em>
</div>
</div>
</div>
<div id="panel-reduce" class="space-y-3 flex-1 hidden flex flex-col">
<p class="text-xs text-gray-600">
<strong>Fase Reduce:</strong> los mini-resúmenes de la fase Map se funden en un único resumen final coherente.
</p>
<button onclick="runReduce()" class="py-2 px-4 bg-teal-600 hover:bg-teal-700 text-white font-bold text-xs rounded transition flex items-center justify-center gap-2">
<i class="fa-solid fa-layer-group"></i> Fusionar en Resumen Final
</button>
<div id="reduce-result" class="flex-1 p-3 bg-teal-50/50 border border-teal-100 rounded text-xs font-mono text-gray-800 overflow-y-auto max-h-[280px]">
<em>Primero completa la Fase Map; después el resumen final fusionado aparecerá aquí...</em>
</div>
</div>
<div id="panel-compare" class="space-y-3 flex-1 hidden flex flex-col">
<p class="text-xs text-gray-600">
<strong>Comparativa crítica:</strong> pide a la IA un resumen directo (sin trocear) y compáralo con el resumen Map-Reduce. ¿Qué método conserva mejor los detalles del principio y del final?
</p>
<button onclick="runDirect()" class="py-2 px-4 bg-amber-600 hover:bg-amber-700 text-white font-bold text-xs rounded transition flex items-center justify-center gap-2">
<i class="fa-solid fa-bolt"></i> Pedir Resumen Directo
</button>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 flex-1">
<div>
<div class="text-[10px] font-bold text-amber-700 uppercase mb-1">Resumen Directo</div>
<div id="direct-result" class="p-3 bg-amber-50 border border-amber-200 rounded text-xs text-gray-800 overflow-y-auto max-h-[240px]">
<em>Aún no solicitado...</em>
</div>
</div>
<div>
<div class="text-[10px] font-bold text-teal-700 uppercase mb-1">Resumen Map-Reduce</div>
<div id="compare-reduce-result" class="p-3 bg-teal-50 border border-teal-200 rounded text-xs text-gray-800 overflow-y-auto max-h-[240px]">
<em>Completa antes la Fase Reduce...</em>
</div>
</div>
</div>
</div>
</section>
</div>
</main>
<footer class="bg-gray-800 text-gray-300 text-xs py-4 mt-auto border-t border-gray-700">
<div class="max-w-7xl mx-auto px-4 flex justify-between items-center">
<div><span class="font-semibold text-white">PROYECTO HIPATIA</span> — IES Monterroso (Bachillerato)</div>
<div class="font-mono">app_hipatia.py Activo en :8064</div>
</div>
</footer>
<script>
let currentPartialSummaries = [];
let currentFinalSummary = "";
function switchTab(tab) {
document.getElementById('panel-map').classList.add('hidden');
document.getElementById('panel-reduce').classList.add('hidden');
document.getElementById('panel-compare').classList.add('hidden');
document.getElementById('tab-map').className = "px-4 py-2 text-gray-500 hover:text-gray-700 flex items-center gap-1.5";
document.getElementById('tab-reduce').className = "px-4 py-2 text-gray-500 hover:text-gray-700 flex items-center gap-1.5";
document.getElementById('tab-compare').className = "px-4 py-2 text-gray-500 hover:text-gray-700 flex items-center gap-1.5";
if(tab === 'map') {
document.getElementById('panel-map').classList.remove('hidden');
document.getElementById('tab-map').className = "px-4 py-2 border-b-2 border-monterroso text-monterroso flex items-center gap-1.5 font-bold";
} else if(tab === 'reduce') {
document.getElementById('panel-reduce').classList.remove('hidden');
document.getElementById('tab-reduce').className = "px-4 py-2 border-b-2 border-teal-600 text-teal-700 flex items-center gap-1.5 font-bold";
} else {
document.getElementById('panel-compare').classList.remove('hidden');
document.getElementById('tab-compare').className = "px-4 py-2 border-b-2 border-amber-600 text-amber-700 flex items-center gap-1.5 font-bold";
document.getElementById('compare-reduce-result').innerText = currentFinalSummary || "Completa antes la Fase Reduce...";
}
}
function updateWordCount() {
const text = document.getElementById('full-text').value.trim();
const count = text ? text.split(/\\s+/).length : 0;
document.getElementById('word-count').innerText = count + " palabras";
}
document.getElementById('full-text').addEventListener('input', updateWordCount);
async function loadSample() {
const topic = document.getElementById('topic-select').value;
document.getElementById('badge-status').innerText = "Cargando...";
document.getElementById('badge-status').className = "text-[10px] bg-amber-100 text-amber-800 px-2 py-0.5 rounded font-mono animate-pulse";
try {
const res = await fetch('/api/load-sample', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ topic: topic })
});
const data = await res.json();
document.getElementById('full-text').value = data.full_text;
updateWordCount();
document.getElementById('badge-status').innerText = "Documento Cargado";
document.getElementById('badge-status').className = "text-[10px] bg-green-100 text-green-800 px-2 py-0.5 rounded font-mono font-bold";
} catch(e) {
alert("Error al conectar con app_hipatia.py");
}
}
async function runMap() {
const fullText = document.getElementById('full-text').value.trim();
if(!fullText) { alert("Carga o pega un documento primero."); return; }
const chunkCount = parseInt(document.getElementById('chunk-count').value);
document.getElementById('map-result').innerHTML = '<div class="p-3 text-xs text-gray-500 italic">Troceando el documento y resumiendo cada fragmento...</div>';
try {
const res = await fetch('/api/map-summarize', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ full_text: fullText, chunk_count: chunkCount })
});
const data = await res.json();
currentPartialSummaries = data.partial_summaries;
document.getElementById('map-result').innerHTML = data.fragments.map((frag, i) => `
<div class="p-3 bg-indigo-50/50 border border-indigo-100 rounded text-xs">
<div class="font-bold text-indigo-700 mb-1">Fragmento ${i + 1} / ${data.fragments.length}</div>
<div class="text-gray-400 italic mb-1 line-clamp-2">${frag.text.substring(0, 140)}...</div>
<div class="text-gray-800"><strong>Resumen:</strong> ${frag.summary}</div>
</div>
`).join('');
} catch(e) {
document.getElementById('map-result').innerHTML = '<div class="p-3 text-xs text-red-600">Error en la solicitud.</div>';
}
}
async function runReduce() {
if(!currentPartialSummaries || currentPartialSummaries.length === 0) {
alert("Completa primero la Fase Map.");
return;
}
document.getElementById('reduce-result').innerText = "Fusionando los mini-resúmenes en un resumen final...";
try {
const res = await fetch('/api/reduce-summarize', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ partial_summaries: currentPartialSummaries })
});
const data = await res.json();
currentFinalSummary = data.final_summary;
document.getElementById('reduce-result').innerText = currentFinalSummary;
} catch(e) {
document.getElementById('reduce-result').innerText = "Error en la solicitud.";
}
}
async function runDirect() {
const fullText = document.getElementById('full-text').value.trim();
if(!fullText) { alert("Carga o pega un documento primero."); return; }
document.getElementById('direct-result').innerText = "Pidiendo un resumen directo, sin trocear...";
try {
const res = await fetch('/api/direct-summarize', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ full_text: fullText })
});
const data = await res.json();
document.getElementById('direct-result').innerText = data.direct_summary;
} catch(e) {
document.getElementById('direct-result').innerText = "Error en la solicitud.";
}
document.getElementById('compare-reduce-result').innerText = currentFinalSummary || "Completa antes la Fase Reduce...";
}
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
def serve_web_interface():
return HTMLResponse(content=HTML_CONTENT)
@app.post("/api/load-sample")
def load_sample(req: SampleRequest):
full_text = SAMPLE_TEXTS.get(req.topic, SAMPLE_TEXTS["Ruta de la Seda"])
return {"full_text": full_text}
@app.post("/api/map-summarize")
def map_summarize(req: MapRequest):
chunks = split_into_chunks(req.full_text, req.chunk_count)
system_prompt = (
"Eres un ayudante de síntesis para Bachillerato. Resume el siguiente fragmento de un documento más largo "
"en un máximo de 2 frases claras y objetivas. No inventes información que no esté en el fragmento; "
"recuerda que no ves el resto del documento."
)
fragments = []
partial_summaries = []
for chunk in chunks:
summary = query_llm_provider([
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Fragmento a resumir:\n\"\"\"{chunk}\"\"\""}
], temperature=0.3, max_tokens=120)
if not summary:
words = chunk.split()
summary = " ".join(words[:22]) + ("..." if len(words) > 22 else "")
summary = f"[Resumen de reserva] {summary}"
fragments.append({"text": chunk, "summary": summary.strip()})
partial_summaries.append(summary.strip())
return {"fragments": fragments, "partial_summaries": partial_summaries}
@app.post("/api/reduce-summarize")
def reduce_summarize(req: ReduceRequest):
joined = "\n".join(f"- {s}" for s in req.partial_summaries)
system_prompt = (
"Eres un ayudante de síntesis para Bachillerato. A continuación tienes varios mini-resúmenes de fragmentos "
"consecutivos de un mismo documento. Combínalos en un único resumen final, coherente, bien redactado y sin "
"repeticiones, de un único párrafo de unas 5-6 frases."
)
final_summary = query_llm_provider([
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Mini-resúmenes a fusionar:\n{joined}"}
], temperature=0.4, max_tokens=280)
if not final_summary:
final_summary = (
"[Resumen de reserva - fase Reduce] " + " ".join(req.partial_summaries)
)
return {"final_summary": final_summary.strip()}
@app.post("/api/direct-summarize")
def direct_summarize(req: DirectRequest):
system_prompt = (
"Eres un ayudante de síntesis para Bachillerato. Resume el siguiente documento completo en un único "
"párrafo de unas 5-6 frases, de la forma más completa posible."
)
direct_summary = query_llm_provider([
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Documento completo:\n\"\"\"{req.full_text}\"\"\""}
], temperature=0.4, max_tokens=280)
if not direct_summary:
words = req.full_text.split()
direct_summary = (
"[Resumen de reserva - sin trocear] " + " ".join(words[:60]) +
("..." if len(words) > 60 else "")
)
return {"direct_summary": direct_summary.strip()}
if __name__ == "__main__":
uvicorn.run("app_hipatia:app", host="0.0.0.0", port=8064, reload=True)
Etiqueta:AIDARAC, resumen automático, map-reduce, ies monterroso, Innovación Educativa, Inteligencia Artificial en educación, montesteam, pensamiento crítico bachillerato, procesamiento del lenguaje natural, Proyecto Hipatia, ventana de contexto



