
Proyecto PINSAPIA: El Traductor de Ida y Vuelta (Línea Vaughan)
📌 Ficha Técnica y Metadatos
- Slug:
pinsapia-linea-vaughan-traductor-ida-y-vuelta-montesteam - Extract: Experimento de evaluación de robustez semántica y pérdida de significado mediante traducción encadenada (ida y vuelta) con LLM local (Qwen en LM Studio). Enmarcado en la iniciativa MonteSTEAM y el modelo AIDARAC del IES Monterroso, el alumnado somete oraciones literales, refranes y frases con doble sentido a viajes de traducción directa y en cascada (Español → Inglés → Francés → Español) para auditar dónde y por qué se roment las metáforas o el lenguaje figurado.
- Palabras clave:
aidarac,montesteam,ies monterroso,proyecto pinsapia,linea vaughan,dorothy vaughan,traduccion ida y vuelta,back-translation,robustez semantica,fastapi,puerto 8086,masterman,ia en el aula,pensamiento critico
1. Dorothy Vaughan y la auditabilidad del código y del lenguaje
Dorothy Vaughan (1910–2008) fue una matemática y programadora estadounidense, célebre por liderar el grupo de calculistas West Area Computers en la NACA/NASA y por ser una de las verdaderas visionarias de la era de la computación. Anticipándose a la automatización de los cálculos mecánicos, Vaughan aprendió de forma autodidacta el lenguaje de programación FORTRAN y formó a sus compañeras para garantizar la transición fluida del cálculo analógico al digital. Su trabajo garantizó que la traducción de algoritmos matemáticos complejos en instrucciones máquina fuera transparente, verificable y exacta.
Su papel en el “Traductor de Ida y Vuelta”
Traer la figura de Dorothy Vaughan a esta experiencia de traducción encadenada responde a un principio esencial de la computación: la verificación de fidelidad. Un programa o un algoritmo no es fiable solo porque compile a la primera; se demuestra sólido cuando, al revertir las operaciones o recompilar las estructuras, el significado o el dato original permanece intacto.
En el aula, los sistemas de traducción automática suelen percibirse como cajas negras de un solo sentido: se introduce una frase en español y sale un resultado en otro idioma. Siguiendo la disciplina de prueba de Vaughan, este proyecto introduce el viaje de vuelta (back-translation). Al someter un texto a la traducción inversa (o en cascada a través de varios idiomas), el alumnado realiza un test de estrés al modelo de lenguaje para descubrir si la semántica profunda sobrevive o si el lenguaje figurado se degrada por el camino.
2. Guía de Instalación y Código Fuente (app_vaughan.py)
Requisitos previos e instalación
Abre la terminal de tu sistema e instala las librerías necesarias:
Bash
pip install fastapi "uvicorn[standard]" httpx pydantic
Código Completo del Servidor (app_vaughan.py)
Guarda el siguiente código en un archivo llamado app_vaughan.py:
Python
import difflib
import random
import time
from typing import List, Dict, Any, Optional
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import httpx
app = FastAPI(
title="Proyecto PINSAPIA - Línea Vaughan: Traductor de Ida y Vuelta",
description="Auditoría de robustez semántica y preservación de significado en traducciones encadenadas con LLM local."
)
LM_STUDIO_API_URL = "http://localhost:1234/v1/chat/completions"
MODEL_NAME = "local-model"
# -----------------------------------------------------------------------------
# EJEMPLOS PREDETERMINADOS PARA EL AULA
# -----------------------------------------------------------------------------
PRESET_EXAMPLES = [
{
"type": "literal",
"category": "Frase Literal / Directa",
"text": "El gato negro duerme plácidamente en el sofá de la entrada."
},
{
"type": "idiom",
"category": "Refrán / Modismo Popular",
"text": "Más vale pájaro en mano que ciento volando."
},
{
"type": "figurative",
"category": "Doble Sentido / Expresión Coloquial",
"text": "A Juan se le fue la olla por completo durante la broma del recreo."
},
{
"type": "figurative2",
"category": "Ironía / Lenguaje Figurado",
"text": "No hay mal que por bien no venga, aunque ahora parezca el fin del mundo."
}
]
# -----------------------------------------------------------------------------
# DICCIONARIO DE RESPALDO (FALLBACK EN CASO DE SIN CONEXIÓN LLM)
# -----------------------------------------------------------------------------
FALLBACK_TRANSLATIONS = {
"El gato negro duerme plácidamente en el sofá de la entrada.": {
"en": "The black cat sleeps peacefully on the entrance sofa.",
"fr": "Le chat noir dort paisiblement sur le canapé de l'entrée.",
"back_es": "El gato negro duerme plácidamente en el sofá de la entrada."
},
"Más vale pájaro en mano que ciento volando.": {
"en": "A bird in the hand is worth two in the bush.",
"fr": "Un tien vaut mieux que deux tu l'auras.",
"back_es": "Es mejor un pájaro en la mano que dos en el matorral."
},
"A Juan se le fue la olla por completo durante la broma del recreo.": {
"en": "Juan completely lost his pot during the recess joke.",
"fr": "Juan a complètement perdu sa casserole pendant la blague de la récréation.",
"back_es": "Juan perdió completamente su olla/cacerola durante el chiste del descanso."
}
}
# -----------------------------------------------------------------------------
# MODELOS DE DATOS PREDICATIVOS
# -----------------------------------------------------------------------------
class SingleTranslationRequest(BaseModel):
text: str
target_lang: str
class RoundtripRequest(BaseModel):
original_text: str
mode: str # "direct" (ES -> Lang -> ES) o "cascade" (ES -> Lang1 -> Lang2 -> ES)
primary_lang: str
secondary_lang: Optional[str] = "fr"
# -----------------------------------------------------------------------------
# FUNCIÓN AUXILIAR DE TRADUCCIÓN VÍA LLM LOCAL
# -----------------------------------------------------------------------------
async def translate_text(text: str, target_lang: str) -> str:
lang_names = {
"en": "English",
"fr": "French",
"de": "German",
"it": "Italian",
"es": "Spanish"
}
lang_label = lang_names.get(target_lang, target_lang)
system_prompt = (
f"You are a professional, accurate translator. Translate the text provided by the user directly into {lang_label}. "
"CRITICAL RULES: Output ONLY the translated text. Do NOT add preamble, quotes, explanations, or additional comments."
)
try:
async with httpx.AsyncClient() as client:
payload = {
"model": MODEL_NAME,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
],
"temperature": 0.2,
"max_tokens": 150
}
res = await client.post(LM_STUDIO_API_URL, json=payload, timeout=6.0)
if res.status_code == 200:
translated = res.json()["choices"][0]["message"]["content"].strip()
# Limpieza de comillas dobles externas si el modelo las añade
if translated.startswith('"') and translated.endswith('"'):
translated = translated[1:-1]
return translated
except Exception:
pass
# Modo Contingencia/Fallback si el LLM local no responde
fb = FALLBACK_TRANSLATIONS.get(text, {})
if target_lang in fb:
return fb[target_lang]
if target_lang == "es" and "back_es" in fb:
return fb["back_es"]
return f"[{target_lang.upper()}] {text}"
# -----------------------------------------------------------------------------
# COMPARADOR DE TEXTOS (DIFF EN PYTHON SIN LIBRERÍAS EXTERNAS)
# -----------------------------------------------------------------------------
def compute_word_diff(original: str, final: str):
orig_words = original.split()
final_words = final.split()
matcher = difflib.SequenceMatcher(None, orig_words, final_words)
orig_html = []
final_html = []
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'equal':
orig_html.append(" ".join(orig_words[i1:i2]))
final_html.append(" ".join(final_words[j1:j2]))
elif tag == 'replace':
orig_html.append(f'<span class="bg-rose-100 text-rose-800 line-through px-1 rounded font-semibold">{ " ".join(orig_words[i1:i2]) }</span>')
final_html.append(f'<span class="bg-emerald-100 text-emerald-800 font-bold px-1 rounded">{ " ".join(final_words[j1:j2]) }</span>')
elif tag == 'delete':
orig_html.append(f'<span class="bg-rose-100 text-rose-800 line-through px-1 rounded font-semibold">{ " ".join(orig_words[i1:i2]) }</span>')
elif tag == 'insert':
final_html.append(f'<span class="bg-emerald-100 text-emerald-800 font-bold px-1 rounded">{ " ".join(final_words[j1:j2]) }</span>')
return " ".join(orig_html), " ".join(final_html)
# -----------------------------------------------------------------------------
# ENDPOINTS DE LA API
# -----------------------------------------------------------------------------
@app.get("/api/presets")
async def get_presets():
return PRESET_EXAMPLES
@app.post("/api/run_roundtrip")
async def run_roundtrip(req: RoundtripRequest):
steps = []
current_text = req.original_text.strip()
if not current_text:
raise HTTPException(status_code=400, detail="El texto original no puede estar vacío.")
# Paso 1: Traducir a idioma primario (Ida)
translated_1 = await translate_text(current_text, req.primary_lang)
steps.append({
"step_number": 1,
"from_lang": "Español",
"to_lang": req.primary_lang.upper(),
"input": current_text,
"output": translated_1
})
if req.mode == "cascade" and req.secondary_lang:
# Paso 2 (Cascada): Traducir de idioma primario a secundario
translated_2 = await translate_text(translated_1, req.secondary_lang)
steps.append({
"step_number": 2,
"from_lang": req.primary_lang.upper(),
"to_lang": req.secondary_lang.upper(),
"input": translated_1,
"output": translated_2
})
# Paso 3: Volver a Español
final_es = await translate_text(translated_2, "es")
steps.append({
"step_number": 3,
"from_lang": req.secondary_lang.upper(),
"to_lang": "Español",
"input": translated_2,
"output": final_es
})
else:
# Paso 2: Volver directamente a Español (Vuelta)
final_es = await translate_text(translated_1, "es")
steps.append({
"step_number": 2,
"from_lang": req.primary_lang.upper(),
"to_lang": "Español",
"input": translated_1,
"output": final_es
})
diff_orig, diff_final = compute_word_diff(req.original_text, final_es)
return {
"original_text": req.original_text,
"final_text": final_es,
"mode": req.mode,
"steps": steps,
"diff_original_html": diff_orig,
"diff_final_html": diff_final
}
# -----------------------------------------------------------------------------
# INTERFAZ WEB HTML
# -----------------------------------------------------------------------------
@app.get("/", response_class=HTMLResponse)
async def home():
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 PINSAPIA - Línea Vaughan: El Traductor de Ida y Vuelta</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: #f8fafc; }}
.pinsapia-topbar {{ background-color: #0f172a; }}
</style>
</head>
<body class="min-h-screen text-slate-800 flex flex-col">
<header class="pinsapia-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-amber-500 text-black px-2 py-0.5 rounded font-extrabold text-sm uppercase tracking-wider">PINSAPIA</span>
<span class="text-white text-sm font-semibold tracking-wider">PROYECTO VAUGHAN</span>
<span class="bg-indigo-600 text-white text-[10px] px-1.5 py-0.5 rounded font-mono font-bold ml-1">PUERTO :8086</span>
</div>
<div class="flex items-center space-x-2 mt-1 sm:mt-0">
<span class="bg-slate-800 text-amber-400 border border-amber-500/30 text-[11px] px-2.5 py-0.5 rounded-full font-medium">
IES MONTERROSO — LÍNEA VAUGHAN
</span>
</div>
</header>
<main class="max-w-4xl w-full mx-auto p-4 sm:p-6 space-y-6 flex-grow">
<div class="bg-gradient-to-r from-slate-900 to-indigo-950 rounded-xl p-5 text-white shadow-lg border border-slate-800 flex justify-between items-center">
<div>
<p class="text-amber-400 font-bold text-xs uppercase tracking-widest mb-1">
Auditoría de Robustez Semántica
</p>
<h1 class="text-xl sm:text-2xl font-extrabold tracking-tight">
🔄 El Traductor de Ida y Vuelta
</h1>
</div>
<div class="text-3xl sm:text-4xl text-amber-400 pl-4">
<i class="fa-solid fa-route"></i>
</div>
</div>
<!-- PANEL DE ENTRADA Y CONFIGURACIÓN -->
<section class="bg-white rounded-xl shadow-sm border border-slate-200 p-5 space-y-5">
<div class="flex justify-between items-center border-b border-slate-100 pb-2">
<h2 class="font-bold text-slate-800 text-sm flex items-center gap-2">
<i class="fa-solid fa-pen-to-square text-indigo-600"></i>
<span>1. SELECCIONA O ESCRIBE UNA FRASE EN ESPAÑOL</span>
</h2>
</div>
<!-- PRESETS -->
<div class="space-y-2">
<label class="block text-xs font-bold text-slate-500 uppercase">Ejemplos preconfigurados del aula:</label>
<div id="presetContainer" class="grid grid-cols-1 sm:grid-cols-3 gap-2"></div>
</div>
<div class="space-y-1">
<label class="block text-xs font-bold text-slate-700 uppercase">Texto a evaluar:</label>
<textarea id="inputOriginal" rows="2" class="w-full p-3 border border-slate-300 rounded-lg text-sm font-medium outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Escribe aquí tu frase..."></textarea>
</div>
<!-- SELECTORES DE MODO Y LENGUAJES -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 pt-2 border-t border-slate-100">
<div>
<label class="block text-xs font-bold text-slate-700 uppercase mb-1">Modalidad de Viaje:</label>
<select id="selMode" onchange="toggleCascadeUI()" class="w-full p-2 border border-slate-300 rounded-lg text-xs font-semibold bg-white outline-none">
<option value="direct">Ida y Vuelta Directa (ES ➔ IDIOMA ➔ ES)</option>
<option value="cascade">Cascada Multi-Idioma (ES ➔ IDIOMA1 ➔ IDIOMA2 ➔ ES)</option>
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 uppercase mb-1">Idioma Principal (Ida):</label>
<select id="selPrimary" class="w-full p-2 border border-slate-300 rounded-lg text-xs font-semibold bg-white outline-none">
<option value="en">Inglés (EN)</option>
<option value="fr">Francés (FR)</option>
<option value="de">Alemán (DE)</option>
<option value="it">Italiano (IT)</option>
</select>
</div>
<div id="boxSecondary" class="hidden">
<label class="block text-xs font-bold text-slate-700 uppercase mb-1">Idioma Secundario (Cascada):</label>
<select id="selSecondary" class="w-full p-2 border border-slate-300 rounded-lg text-xs font-semibold bg-white outline-none">
<option value="fr">Francés (FR)</option>
<option value="de">Alemán (DE)</option>
<option value="en">Inglés (EN)</option>
<option value="it">Italiano (IT)</option>
</select>
</div>
</div>
<button onclick="ejecutarExperimento()" id="btnRun" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 rounded-lg shadow transition flex items-center justify-center space-x-2 text-sm">
<i class="fa-solid fa-arrows-spin"></i>
<span>INICIAR VIAJE DE TRADUCCIÓN Y ANALIZAR</span>
</button>
</section>
<!-- CARGANDO -->
<div id="loader" class="hidden bg-white rounded-xl p-8 border border-slate-200 text-center space-y-3">
<i class="fa-solid fa-gear fa-spin text-3xl text-indigo-600"></i>
<p class="text-xs font-bold text-slate-600 uppercase tracking-wider">Procesando traducción encadenada con el LLM local...</p>
</div>
<!-- RESULTADOS DEL VIAJE -->
<section id="panelResults" class="space-y-6 hidden">
<!-- PASOS INTERMEDIOS DEL VIAJE -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5 space-y-4">
<h3 class="font-bold text-slate-800 text-xs uppercase tracking-wider border-b border-slate-100 pb-2 flex items-center gap-2">
<i class="fa-solid fa-map-location-dot text-indigo-600"></i>
<span>2. Trazabilidad Completa del Viaje Paso a Paso</span>
</h3>
<div id="containerSteps" class="space-y-3"></div>
</div>
<!-- COMPARACIÓN VISUAL SIDE-BY-SIDE CON DIFF -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5 space-y-4">
<h3 class="font-bold text-slate-800 text-xs uppercase tracking-wider border-b border-slate-100 pb-2 flex items-center gap-2">
<i class="fa-solid fa-code-compare text-indigo-600"></i>
<span>3. Comparativa Side-by-Side de Cambios en Español</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="p-4 bg-slate-50 border border-slate-200 rounded-lg space-y-2">
<span class="text-[10px] font-extrabold uppercase text-slate-400">Punto de Partida (Original):</span>
<div id="diffOriginal" class="text-sm font-medium leading-relaxed text-slate-800"></div>
</div>
<div class="p-4 bg-slate-50 border border-slate-200 rounded-lg space-y-2">
<span class="text-[10px] font-extrabold uppercase text-slate-400">Punto de Llegada (Tras el viaje):</span>
<div id="diffFinal" class="text-sm font-medium leading-relaxed text-slate-800"></div>
</div>
</div>
</div>
<!-- EVALUACIÓN SUBJETIVA DEL ALUMNADO -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5 space-y-4">
<h3 class="font-bold text-slate-800 text-xs uppercase tracking-wider border-b border-slate-100 pb-2 flex items-center gap-2">
<i class="fa-solid fa-star-half-stroke text-amber-500"></i>
<span>4. Evaluación de Conservación del Significado (0 al 10)</span>
</h3>
<p class="text-xs text-slate-600">
Independientemente de las palabras exactas, ¿cuánto del <strong>significado original</strong> ha sobrevivido al viaje?
</p>
<div class="flex items-center space-x-4 bg-slate-50 p-4 rounded-xl border border-slate-200">
<input type="range" id="scoreRange" min="0" max="10" value="8" class="w-full accent-indigo-600 cursor-pointer" oninput="updateScoreVal(this.value)">
<span id="scoreVal" class="text-2xl font-extrabold font-mono text-indigo-600 w-12 text-center">8</span>
</div>
<div id="scoreFeedback" class="text-xs font-semibold p-3 rounded-lg"></div>
</div>
</section>
<!-- PREGUNTA DE CIERRE -->
<section class="bg-amber-50 border border-amber-200 rounded-xl p-5 space-y-2">
<div class="flex items-center space-x-2 text-amber-900 font-bold text-xs uppercase">
<i class="fa-solid fa-triangle-exclamation"></i>
<span>Pregunta de Cierre para el Debate en Grupo</span>
</div>
<blockquote class="text-xs text-amber-950 font-medium italic leading-relaxed">
"Si tradujerais un contrato legal o una receta médica con este mismo sistema y le dierais una vuelta de más, ¿qué tipo de frases o palabras os preocuparía más que se rompieran durante el viaje?"
</blockquote>
</section>
</main>
<script>
let presetsData = [];
async function loadPresets() {{
try {{
const res = await fetch('/api/presets');
presetsData = await res.json();
const container = document.getElementById('presetContainer');
container.innerHTML = presetsData.map((p, idx) => `
<button onclick="applyPreset(${{idx}})" class="text-left p-2.5 bg-slate-50 hover:bg-indigo-50 border border-slate-200 hover:border-indigo-300 rounded-lg transition space-y-1">
<span class="text-[10px] font-bold text-indigo-700 uppercase block">${{p.category}}</span>
<p class="text-xs font-medium text-slate-700 truncate">"${{p.text}}"</p>
</button>
`).join('');
if (presetsData.length > 0) applyPreset(0);
}} catch (e) {{
console.error("Error cargando presets:", e);
}}
}}
function applyPreset(idx) {{
if (presetsData[idx]) {{
document.getElementById('inputOriginal').value = presetsData[idx].text;
}}
}}
function toggleCascadeUI() {{
const mode = document.getElementById('selMode').value;
const boxSec = document.getElementById('boxSecondary');
if (mode === 'cascade') {{
boxSec.classList.remove('hidden');
}} else {{
boxSec.classList.add('hidden');
}}
}}
async function ejecutarExperimento() {{
const orig = document.getElementById('inputOriginal').value.trim();
if (!orig) {{
alert("Por favor introduce una frase.");
return;
}}
document.getElementById('panelResults').classList.add('hidden');
document.getElementById('loader').classList.remove('hidden');
const payload = {{
original_text: orig,
mode: document.getElementById('selMode').value,
primary_lang: document.getElementById('selPrimary').value,
secondary_lang: document.getElementById('selSecondary').value
}};
try {{
const res = await fetch('/api/run_roundtrip', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify(payload)
}});
const data = await res.json();
document.getElementById('loader').classList.add('hidden');
document.getElementById('panelResults').classList.remove('hidden');
// Renderizar Pasos de Trazabilidad
const stepsCont = document.getElementById('containerSteps');
stepsCont.innerHTML = data.steps.map(s => `
<div class="p-3 bg-slate-50 border border-slate-200 rounded-lg flex flex-col sm:flex-row justify-between sm:items-center text-xs gap-2">
<div class="flex items-center space-x-2">
<span class="bg-indigo-600 text-white font-mono font-bold px-2 py-0.5 rounded text-[10px]">Paso ${{s.step_number}}</span>
<span class="font-bold text-slate-700">${{s.from_lang}} ➔ ${{s.to_lang}}</span>
</div>
<div class="font-mono text-slate-800 bg-white px-3 py-1 border border-slate-200 rounded text-xs italic">
"${{s.output}}"
</div>
</div>
`).join('');
// Renderizar Diffs
document.getElementById('diffOriginal').innerHTML = data.diff_original_html;
document.getElementById('diffFinal').innerHTML = data.diff_final_html;
updateScoreVal(document.getElementById('scoreRange').value);
}} catch (e) {{
alert("Error en el proceso de traducción: " + e);
document.getElementById('loader').classList.add('hidden');
}}
}}
function updateScoreVal(val) {{
document.getElementById('scoreVal').innerText = val;
const fb = document.getElementById('scoreFeedback');
if (val >= 8) {{
fb.className = "text-xs font-semibold p-3 rounded-lg bg-emerald-50 text-emerald-900 border border-emerald-200";
fb.innerText = "Alta robustez semántica: El significado ha sobrevivido al viaje con variaciones menores o sin alteración relevante.";
}} else if (val >= 5) {{
fb.className = "text-xs font-semibold p-3 rounded-lg bg-amber-50 text-amber-900 border border-amber-200";
fb.innerText = "Pérdida parcial: El sentido principal se conserva, pero se han deslizado matices imprecisos o traducciones literales descontextualizadas.";
}} else {{
fb.className = "text-xs font-semibold p-3 rounded-lg bg-rose-50 text-rose-900 border border-rose-200";
fb.innerText = "Ruptura semántica grave: El significado original se ha roto por completo durante el viaje. Típico en metáforas, modismos o giros de humor.";
}}
}}
window.onload = loadPresets;
</script>
</body>
</html>
"""
# -----------------------------------------------------------------------------
# EJECUCIÓN DEL SERVIDOR UVICORN (PUERTO 8086)
# -----------------------------------------------------------------------------
if __name__ == "__main__":
import uvicorn
uvicorn.run("app_vaughan:app", host="0.0.0.0", port=8086, reload=True)
Cómo ejecutar la aplicación en el puerto 8086
Abre la terminal en el directorio del proyecto y arranca el servidor:
Bash
uvicorn app_vaughan:app --host 0.0.0.0 --port 8086 --reload
O directamente mediante Python:
Bash
python app_vaughan:app
Accede desde cualquier navegador web en http://localhost:8086.
3. Detalle Técnico e Ingeniería de la Comparación
1. Peticiones Encadenadas sin Nuevos Componentes
La aplicación no añade librerías externas de PLN ni capas complejas. Reutiliza el motor del modelo de lenguaje local (Qwen en LM Studio a través del puerto 1234) ejecutando solicitudes consecutivas donde la salida del primer paso sirve como entrada del siguiente:
[Español Original] ──(Paso 1)──> [Traducción Inglés] ──(Paso 2)──> [Traducción Español Final]
2. Trazabilidad Completa del Viaje
Para evitar que el fallo se convierta en un misterio indecidible, la aplicación almacena los resultados de cada salto del viaje. Si una metáfora llega destruida a la meta en español, la tabla de trazabilidad permite inspeccionar exactamente en qué idioma o traducción intermedia se rompió la equivalencia semántica.
3. Comparador de Diferencias (Word Diff)
El código implementa una comparación palabra a palabra utilizando el módulo nativo difflib.SequenceMatcher de Python. Genera fragmentos HTML resaltados con estilos de Tailwind CSS:
- Palabras eliminadas o alteradas en la frase original: Trazadas en rojo con tachado (
line-through). - Palabras introducidas o modificadas en la frase final: Destacadas en verde negrita.
4. Guía Didáctica para el Aula: Flujo del Experimento
[1. Selección de Frase] ──> [2. Viaje Encadenado] ──> [3. Trazabilidad y Diff] ──> [4. Evaluación Semántica]
Literal, Refrán o Ida y vuelta o Inspección paso a paso y Puntuación 0-10 y
Doble sentido Cascada Multi-Idioma cómputo de diferencias tabla de resultados
Dinámica paso a paso
- Selección del tipo de frase: El alumnado elige o redacta oraciones pertenecientes a tres categorías claramente diferenciadas:
- Frase literal:“El gato negro duerme plácidamente en el sofá.”
- Refrán o modismo:“Más vale pájaro en mano que ciento volando.”
- Doble sentido o ironía:“A Juan se le fue la olla por completo durante la broma.”
- Ejecución del viaje de traducción:
- Modo Directo: Español ➔ Inglés ➔ Español.
- Modo Cascada: Español ➔ Inglés ➔ Francés ➔ Español.
- Inspección de la trazabilidad: La clase analiza el cuadro de trazabilidad para ver la frase intermedia generada por el LLM.
- Cómputo visual del Diff: La aplicación muestra ambas frases en español frente a frente, resaltando los cambios léxicos.
- Evaluación de conservación semántica (0 a 10): Cada estudiante valora del 0 al 10 cuánto se parece el significado (no las palabras exactas) de la frase resultante respecto al punto de partida, registrando los resultados en la pizarra para ver qué categorías sufren mayor degradación.


5. Debate en Grupo y Pregunta de Cierre
Tras completar varias pruebas con oraciones literales y figuradas, orienta la discusión del grupo hacia la interpretación del lenguaje y los límites de la traducción automática.
- Interpretación vs. Diccionario: ¿Por qué un modelo no se limita a sustituir palabras de un diccionario? ¿Qué ocurre cuando intenta interpretar un refrán de forma literal en otro idioma?
- Acumulación de ruido en cascada: ¿Qué ocurre con la calidad del significado cuando pasamos por dos o tres idiomas antes de regresar al español?
- El matiz de ingeniería: ¿Por qué la traducción de lenguaje humano sigue requiriendo contexto cultural y no solo precisión sintáctica?
❓ Pregunta de Cierre para el Debate en Grupo
“Si tradujerais un contrato legal o una receta médica con este mismo sistema y le dierais una vuelta de más, ¿qué tipo de frases o palabras os preocuparía más que se rompieran durante el viaje?”



