Spaces:
Sleeping
Sleeping
Alexander Sanchez commited on
Commit ·
0c0e3c8
1
Parent(s): 962adf4
Evaluator udpdated
Browse files
app.py
CHANGED
|
@@ -13,6 +13,7 @@ Requiere:
|
|
| 13 |
|
| 14 |
import os
|
| 15 |
import json
|
|
|
|
| 16 |
import gradio as gr
|
| 17 |
from dotenv import load_dotenv
|
| 18 |
|
|
@@ -28,16 +29,13 @@ load_dotenv()
|
|
| 28 |
|
| 29 |
print(" Inicializando Scriptorium RAG...")
|
| 30 |
|
| 31 |
-
# Cargar corpus desde disco (si existe) + pares de ejemplo embebidos
|
| 32 |
loader = CorpusLoader(os.getenv("CORPUS_PATH", "./corpus"))
|
| 33 |
disk_pairs = loader.load()
|
| 34 |
all_pairs = SAMPLE_PAIRS + disk_pairs
|
| 35 |
|
| 36 |
-
# Variable global para el vector store activo
|
| 37 |
current_embed_model = "openai"
|
| 38 |
vs = VectorStore(embedding_model="openai")
|
| 39 |
|
| 40 |
-
# Auto-indexar solo si el índice está vacío (evita re-indexar en cada arranque)
|
| 41 |
if vs.count() == 0:
|
| 42 |
print(f"⏳ Índice vacío — indexando {len(all_pairs)} pares, espera...")
|
| 43 |
vs.index(all_pairs)
|
|
@@ -61,52 +59,51 @@ DEMO_EXAMPLES = [
|
|
| 61 |
"fizo pareçer ante si a los testigos q̃ dixeron ser mayores de veynte annos",
|
| 62 |
]
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
def cambiar_embedding(embed_model: str):
|
| 65 |
global vs, corrector, current_embed_model
|
| 66 |
-
|
| 67 |
if embed_model == current_embed_model:
|
| 68 |
return f"ℹ Ya estás usando **{embed_model}**"
|
| 69 |
-
|
| 70 |
try:
|
| 71 |
current_embed_model = embed_model
|
| 72 |
vs = VectorStore(embedding_model=embed_model)
|
| 73 |
-
|
| 74 |
-
# Indexar si la colección está vacía
|
| 75 |
if vs.count() == 0:
|
| 76 |
vs.index(all_pairs)
|
| 77 |
msg = f" Re-indexado con **{embed_model}** · {vs.count()} docs"
|
| 78 |
else:
|
| 79 |
msg = f" Cargado índice existente **{embed_model}** · {vs.count()} docs"
|
| 80 |
-
|
| 81 |
-
# Recrear el corrector con el nuevo vector store
|
| 82 |
corrector = RAGCorrector(vs)
|
| 83 |
return msg
|
| 84 |
-
|
| 85 |
except Exception as e:
|
| 86 |
return f" Error cambiando embedding: {e}"
|
| 87 |
|
| 88 |
|
| 89 |
-
|
| 90 |
-
# ── Función principal ─────────────────────────────────────────────────────────
|
| 91 |
-
|
| 92 |
def corregir(htr_text: str, top_k: int, mostrar_prompt: bool, model: str):
|
| 93 |
if not htr_text.strip():
|
| 94 |
-
return "", "", "", "", " Introduce un texto HTR para corregir."
|
| 95 |
-
|
| 96 |
if not os.getenv("OPENAI_API_KEY"):
|
| 97 |
-
return "", "", "", "", " Falta OPENAI_API_KEY en el fichero .env"
|
| 98 |
-
|
| 99 |
try:
|
| 100 |
-
result = corrector.correct(htr_text, top_k=int(top_k), model=
|
| 101 |
except Exception as e:
|
| 102 |
-
return "", "", "", "", f" Error al llamar a la API: {e}"
|
| 103 |
|
| 104 |
corrected = result["corrected"]
|
| 105 |
retrieved = result["retrieved"]
|
| 106 |
htr_errors = result["htr_errors"]
|
| 107 |
grafia_w = result["grafia_warns"]
|
| 108 |
|
| 109 |
-
# ── Panel de documentos recuperados ──────────────────────────────────────
|
| 110 |
docs_md = f"### Top-{len(retrieved)} documentos recuperados\n\n"
|
| 111 |
for i, doc in enumerate(retrieved, 1):
|
| 112 |
docs_md += (
|
|
@@ -119,25 +116,25 @@ def corregir(htr_text: str, top_k: int, mostrar_prompt: bool, model: str):
|
|
| 119 |
docs_md += f"- **Correcciones:** {', '.join(doc['corrections'])}\n"
|
| 120 |
docs_md += "\n---\n"
|
| 121 |
|
| 122 |
-
# ── Panel de análisis ─────────────────────────────────────────────────────
|
| 123 |
analysis_md = "### Análisis del texto\n\n"
|
| 124 |
-
|
| 125 |
if htr_errors:
|
| 126 |
analysis_md += "**⚠ Posibles errores HTR detectados:**\n"
|
| 127 |
for e in htr_errors:
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
analysis_md += "\n"
|
| 130 |
-
|
| 131 |
if grafia_w:
|
| 132 |
analysis_md += "**✦ Alertas de grafía (NO modernizar):**\n"
|
| 133 |
for g in grafia_w:
|
| 134 |
analysis_md += f"- `{g['modern']}` → mantener `{g['ancient']}`: {g['rule']}\n"
|
| 135 |
analysis_md += "\n"
|
| 136 |
-
|
| 137 |
if not htr_errors and not grafia_w:
|
| 138 |
analysis_md += "*No se detectaron patrones conocidos de error en el texto.*\n"
|
| 139 |
|
| 140 |
-
# Diff visual (diferencias)
|
| 141 |
diff_md = "### Diferencias HTR → Corregido\n\n"
|
| 142 |
orig_words = htr_text.split()
|
| 143 |
corr_words = corrected.split()
|
|
@@ -155,11 +152,6 @@ def corregir(htr_text: str, top_k: int, mostrar_prompt: bool, model: str):
|
|
| 155 |
diff_md += " ".join(diff_parts)
|
| 156 |
diff_md += f"\n\n*{changed} palabra(s) modificada(s) de {len(orig_words)} totales.*"
|
| 157 |
|
| 158 |
-
# ── Prompt (opcional) ─────────────────────────────────────────────────────
|
| 159 |
-
#prompt_md = ""
|
| 160 |
-
#if mostrar_prompt:
|
| 161 |
-
# prompt_md = f"```\nSYSTEM:\n{result.get('_system', '(ver rag_corrector.py)')}\n\nUSER:\n{result['prompt']}\n```"
|
| 162 |
-
|
| 163 |
status = f" Corrección completada con **{result['model']}** · {vs.count()} docs en índice"
|
| 164 |
|
| 165 |
prompt_visible = ""
|
|
@@ -173,49 +165,174 @@ def corregir(htr_text: str, top_k: int, mostrar_prompt: bool, model: str):
|
|
| 173 |
|
| 174 |
return corrected, docs_md, analysis_md, diff_md, status, prompt_visible
|
| 175 |
|
|
|
|
| 176 |
def evaluar_par(htr_text: str, gt_text: str):
|
| 177 |
if not htr_text.strip() or not gt_text.strip():
|
| 178 |
return "⚠ Introduce tanto el texto HTR como el groundtruth."
|
| 179 |
try:
|
| 180 |
-
result
|
| 181 |
metrics = evaluator.evaluate_pair(htr_text, result["corrected"], gt_text)
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
report = (
|
| 185 |
-
f"### Métricas de evaluación\n\n"
|
| 186 |
-
f"| Métrica | Antes (HTR) | Después (RAG) | Mejora |\n"
|
| 187 |
-
f"|---------|------------|---------------|--------|\n"
|
| 188 |
-
f"| **CER** | {m['cer_before']:.2%} | {m['cer_after']:.2%} | {m['cer_improvement']:+.2%} |\n"
|
| 189 |
-
f"| **WER** | {m['wer_before']:.2%} | {m['wer_after']:.2%} | {m['wer_improvement']:+.2%} |\n\n"
|
| 190 |
-
f"**Detector de modernismos:** score={mod['score']:.2f} "
|
| 191 |
-
f"({mod['count']} problema(s) detectado(s))\n"
|
| 192 |
-
)
|
| 193 |
-
if mod["issues"]:
|
| 194 |
-
report += "\nFormas modernas introducidas incorrectamente:\n"
|
| 195 |
-
for iss in mod["issues"]:
|
| 196 |
-
report += f"- `{iss['modern']}` (debería ser `{iss['ancient']}`): {iss['rule']}\n"
|
| 197 |
-
|
| 198 |
-
report += f"\n**Texto corregido por RAG:**\n> {result['corrected']}"
|
| 199 |
return report
|
| 200 |
except Exception as e:
|
| 201 |
return f" Error: {e}"
|
| 202 |
|
| 203 |
|
| 204 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
if not htr_text.strip() or not gt_text.strip():
|
| 206 |
return "⚠ HTR y GT son obligatorios."
|
| 207 |
try:
|
| 208 |
-
pair_id
|
| 209 |
new_pair = {
|
| 210 |
-
"id":
|
| 211 |
-
"htr":
|
| 212 |
-
"gt":
|
| 213 |
-
"type":
|
| 214 |
-
"region":
|
| 215 |
-
"date":
|
| 216 |
-
"caligrafia":
|
| 217 |
"corrections": [],
|
| 218 |
-
"source":
|
| 219 |
}
|
| 220 |
added = vs.index([new_pair])
|
| 221 |
if added:
|
|
@@ -244,7 +361,6 @@ with gr.Blocks(
|
|
| 244 |
""",
|
| 245 |
) as demo:
|
| 246 |
|
| 247 |
-
# ── Header ────────────────────────────────────────────────────────────────
|
| 248 |
gr.HTML("""
|
| 249 |
<div class="header">
|
| 250 |
<h1>RAG CODEX for Historical Spanish</h1>
|
|
@@ -270,46 +386,29 @@ with gr.Blocks(
|
|
| 270 |
)
|
| 271 |
model_selector = gr.Dropdown(
|
| 272 |
label="Modelo LLM",
|
| 273 |
-
choices=[
|
| 274 |
-
"llama-3.3-70b-versatile",
|
| 275 |
-
"openai/gpt-oss-120b",
|
| 276 |
-
],
|
| 277 |
value="llama-3.3-70b-versatile",
|
| 278 |
)
|
| 279 |
embedding_selector = gr.Dropdown(
|
| 280 |
label="Modelo de Embedding",
|
| 281 |
-
choices=[
|
| 282 |
-
"openai", # text-embedding-3-small
|
| 283 |
-
"mpnet", # paraphrase-multilingual-mpnet-base-v2
|
| 284 |
-
"mt5",
|
| 285 |
-
],
|
| 286 |
value="openai",
|
| 287 |
)
|
| 288 |
-
|
| 289 |
show_prompt = gr.Checkbox(label="Show RAG prompt", value=False)
|
| 290 |
btn_corregir = gr.Button("✦ Correct with RAG", variant="primary")
|
| 291 |
-
|
| 292 |
-
gr.Examples(
|
| 293 |
-
examples=DEMO_EXAMPLES,
|
| 294 |
-
inputs=htr_input,
|
| 295 |
-
label="Demonstration examples",
|
| 296 |
-
)
|
| 297 |
|
| 298 |
with gr.Column(scale=2):
|
| 299 |
-
corrected_out = gr.Textbox(
|
| 300 |
-
|
| 301 |
-
lines=6,
|
| 302 |
-
interactive=False,
|
| 303 |
-
)
|
| 304 |
-
status_out = gr.Markdown(elem_classes=["status-bar"])
|
| 305 |
|
| 306 |
with gr.Row():
|
| 307 |
with gr.Column():
|
| 308 |
-
docs_out
|
| 309 |
with gr.Column():
|
| 310 |
analysis_out = gr.Markdown(label="Pattern analysis")
|
| 311 |
|
| 312 |
-
diff_out
|
| 313 |
prompt_out = gr.Markdown(label="Prompt sent to the LLM", visible=True)
|
| 314 |
|
| 315 |
btn_corregir.click(
|
|
@@ -317,17 +416,15 @@ with gr.Blocks(
|
|
| 317 |
inputs=[htr_input, top_k_slider, show_prompt, model_selector],
|
| 318 |
outputs=[corrected_out, docs_out, analysis_out, diff_out, status_out, prompt_out],
|
| 319 |
)
|
| 320 |
-
|
| 321 |
embed_status = gr.Markdown()
|
| 322 |
-
embedding_selector.change(
|
| 323 |
-
fn=cambiar_embedding,
|
| 324 |
-
inputs=[embedding_selector],
|
| 325 |
-
outputs=[embed_status],
|
| 326 |
-
)
|
| 327 |
|
| 328 |
-
# ── Pestaña 2: Evaluación ─────────────────────────────────
|
| 329 |
with gr.TabItem(" Evaluation with GT"):
|
| 330 |
-
gr.Markdown(
|
|
|
|
|
|
|
|
|
|
| 331 |
with gr.Row():
|
| 332 |
eval_htr = gr.Textbox(label="HTR text", lines=4)
|
| 333 |
eval_gt = gr.Textbox(label="Groundtruth (reference)", lines=4)
|
|
@@ -335,30 +432,68 @@ with gr.Blocks(
|
|
| 335 |
eval_out = gr.Markdown()
|
| 336 |
btn_eval.click(fn=evaluar_par, inputs=[eval_htr, eval_gt], outputs=eval_out)
|
| 337 |
|
| 338 |
-
# ── Pestaña 3:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
with gr.TabItem("➕ Add to corpus"):
|
| 340 |
gr.Markdown("Add new pairs to the vector store to improve the RAG continuously.")
|
| 341 |
with gr.Row():
|
| 342 |
-
add_htr
|
| 343 |
-
add_gt
|
| 344 |
with gr.Row():
|
| 345 |
-
add_type
|
| 346 |
-
add_region
|
| 347 |
-
add_date
|
| 348 |
add_caligrafia = gr.Dropdown(
|
| 349 |
label="Caligrafía",
|
| 350 |
-
choices=["desconocida", "
|
| 351 |
value="desconocida",
|
| 352 |
-
)
|
| 353 |
-
btn_add
|
| 354 |
-
add_out
|
| 355 |
btn_add.click(
|
| 356 |
fn=add_to_corpus,
|
| 357 |
inputs=[add_htr, add_gt, add_type, add_region, add_date, add_caligrafia],
|
| 358 |
outputs=add_out,
|
| 359 |
)
|
| 360 |
|
| 361 |
-
# ── Pestaña
|
| 362 |
with gr.TabItem("ℹ System"):
|
| 363 |
gr.Markdown(f"""
|
| 364 |
## System status
|
|
@@ -381,28 +516,22 @@ Texto HTR
|
|
| 381 |
│ │
|
| 382 |
│ └─► Búsqueda top-k en ChromaDB ──► Few-shot dinámico
|
| 383 |
│
|
| 384 |
-
└─► Prompt constructor ──►
|
| 385 |
```
|
| 386 |
|
| 387 |
-
##
|
| 388 |
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
"type": "notarial", "region": "Castilla", "date": "1542"}},
|
| 394 |
-
...
|
| 395 |
-
]
|
| 396 |
-
```
|
| 397 |
-
O CSV con columnas: `id, htr, gt, type, region, date`
|
| 398 |
""")
|
| 399 |
|
| 400 |
if __name__ == "__main__":
|
| 401 |
demo.launch(
|
| 402 |
server_name="0.0.0.0",
|
| 403 |
server_port=7860,
|
| 404 |
-
auth=("admin", "admin"),
|
| 405 |
-
|
| 406 |
share=False,
|
| 407 |
show_error=True,
|
| 408 |
)
|
|
|
|
| 13 |
|
| 14 |
import os
|
| 15 |
import json
|
| 16 |
+
import random
|
| 17 |
import gradio as gr
|
| 18 |
from dotenv import load_dotenv
|
| 19 |
|
|
|
|
| 29 |
|
| 30 |
print(" Inicializando Scriptorium RAG...")
|
| 31 |
|
|
|
|
| 32 |
loader = CorpusLoader(os.getenv("CORPUS_PATH", "./corpus"))
|
| 33 |
disk_pairs = loader.load()
|
| 34 |
all_pairs = SAMPLE_PAIRS + disk_pairs
|
| 35 |
|
|
|
|
| 36 |
current_embed_model = "openai"
|
| 37 |
vs = VectorStore(embedding_model="openai")
|
| 38 |
|
|
|
|
| 39 |
if vs.count() == 0:
|
| 40 |
print(f"⏳ Índice vacío — indexando {len(all_pairs)} pares, espera...")
|
| 41 |
vs.index(all_pairs)
|
|
|
|
| 59 |
"fizo pareçer ante si a los testigos q̃ dixeron ser mayores de veynte annos",
|
| 60 |
]
|
| 61 |
|
| 62 |
+
# ── Caligrafías disponibles en el corpus ──────────────────────────────────────
|
| 63 |
+
|
| 64 |
+
def get_caligrafias():
|
| 65 |
+
cals = set()
|
| 66 |
+
for p in all_pairs:
|
| 67 |
+
c = p.get("caligrafia", "desconocida")
|
| 68 |
+
if c: cals.add(c)
|
| 69 |
+
return ["Todas"] + sorted(cals)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ── Funciones ─────────────────────────────────────────────────────────────────
|
| 73 |
+
|
| 74 |
def cambiar_embedding(embed_model: str):
|
| 75 |
global vs, corrector, current_embed_model
|
|
|
|
| 76 |
if embed_model == current_embed_model:
|
| 77 |
return f"ℹ Ya estás usando **{embed_model}**"
|
|
|
|
| 78 |
try:
|
| 79 |
current_embed_model = embed_model
|
| 80 |
vs = VectorStore(embedding_model=embed_model)
|
|
|
|
|
|
|
| 81 |
if vs.count() == 0:
|
| 82 |
vs.index(all_pairs)
|
| 83 |
msg = f" Re-indexado con **{embed_model}** · {vs.count()} docs"
|
| 84 |
else:
|
| 85 |
msg = f" Cargado índice existente **{embed_model}** · {vs.count()} docs"
|
|
|
|
|
|
|
| 86 |
corrector = RAGCorrector(vs)
|
| 87 |
return msg
|
|
|
|
| 88 |
except Exception as e:
|
| 89 |
return f" Error cambiando embedding: {e}"
|
| 90 |
|
| 91 |
|
|
|
|
|
|
|
|
|
|
| 92 |
def corregir(htr_text: str, top_k: int, mostrar_prompt: bool, model: str):
|
| 93 |
if not htr_text.strip():
|
| 94 |
+
return "", "", "", "", " Introduce un texto HTR para corregir.", ""
|
|
|
|
| 95 |
if not os.getenv("OPENAI_API_KEY"):
|
| 96 |
+
return "", "", "", "", " Falta OPENAI_API_KEY en el fichero .env", ""
|
|
|
|
| 97 |
try:
|
| 98 |
+
result = corrector.correct(htr_text, top_k=int(top_k), model=model)
|
| 99 |
except Exception as e:
|
| 100 |
+
return "", "", "", "", f" Error al llamar a la API: {e}", ""
|
| 101 |
|
| 102 |
corrected = result["corrected"]
|
| 103 |
retrieved = result["retrieved"]
|
| 104 |
htr_errors = result["htr_errors"]
|
| 105 |
grafia_w = result["grafia_warns"]
|
| 106 |
|
|
|
|
| 107 |
docs_md = f"### Top-{len(retrieved)} documentos recuperados\n\n"
|
| 108 |
for i, doc in enumerate(retrieved, 1):
|
| 109 |
docs_md += (
|
|
|
|
| 116 |
docs_md += f"- **Correcciones:** {', '.join(doc['corrections'])}\n"
|
| 117 |
docs_md += "\n---\n"
|
| 118 |
|
|
|
|
| 119 |
analysis_md = "### Análisis del texto\n\n"
|
|
|
|
| 120 |
if htr_errors:
|
| 121 |
analysis_md += "**⚠ Posibles errores HTR detectados:**\n"
|
| 122 |
for e in htr_errors:
|
| 123 |
+
examples = e.get("examples", e.get("example", ""))
|
| 124 |
+
if isinstance(examples, list):
|
| 125 |
+
examples = "; ".join(examples[:2])
|
| 126 |
+
sev = e.get("severity", "")
|
| 127 |
+
sev_label = f" `[{sev.upper()}]`" if sev else ""
|
| 128 |
+
analysis_md += f"- `{e['htr']}` → `{e['gt']}`{sev_label}: {e['context']} \n *Ej: {examples}*\n"
|
| 129 |
analysis_md += "\n"
|
|
|
|
| 130 |
if grafia_w:
|
| 131 |
analysis_md += "**✦ Alertas de grafía (NO modernizar):**\n"
|
| 132 |
for g in grafia_w:
|
| 133 |
analysis_md += f"- `{g['modern']}` → mantener `{g['ancient']}`: {g['rule']}\n"
|
| 134 |
analysis_md += "\n"
|
|
|
|
| 135 |
if not htr_errors and not grafia_w:
|
| 136 |
analysis_md += "*No se detectaron patrones conocidos de error en el texto.*\n"
|
| 137 |
|
|
|
|
| 138 |
diff_md = "### Diferencias HTR → Corregido\n\n"
|
| 139 |
orig_words = htr_text.split()
|
| 140 |
corr_words = corrected.split()
|
|
|
|
| 152 |
diff_md += " ".join(diff_parts)
|
| 153 |
diff_md += f"\n\n*{changed} palabra(s) modificada(s) de {len(orig_words)} totales.*"
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
status = f" Corrección completada con **{result['model']}** · {vs.count()} docs en índice"
|
| 156 |
|
| 157 |
prompt_visible = ""
|
|
|
|
| 165 |
|
| 166 |
return corrected, docs_md, analysis_md, diff_md, status, prompt_visible
|
| 167 |
|
| 168 |
+
|
| 169 |
def evaluar_par(htr_text: str, gt_text: str):
|
| 170 |
if not htr_text.strip() or not gt_text.strip():
|
| 171 |
return "⚠ Introduce tanto el texto HTR como el groundtruth."
|
| 172 |
try:
|
| 173 |
+
result = corrector.correct(htr_text)
|
| 174 |
metrics = evaluator.evaluate_pair(htr_text, result["corrected"], gt_text)
|
| 175 |
+
report = evaluator.format_pair_report(metrics)
|
| 176 |
+
report += f"\n\n**Texto corregido por RAG:**\n> {result['corrected']}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
return report
|
| 178 |
except Exception as e:
|
| 179 |
return f" Error: {e}"
|
| 180 |
|
| 181 |
|
| 182 |
+
def evaluar_batch(n_samples: int, caligrafia_filtro: str, model: str):
|
| 183 |
+
"""
|
| 184 |
+
Evalúa el sistema sobre N pares aleatorios del corpus que tengan GT.
|
| 185 |
+
Muestra las tres comparaciones: GT vs HTR, GT vs Corregido, HTR vs Corregido.
|
| 186 |
+
"""
|
| 187 |
+
# Filtrar pares con HTR y GT no vacíos
|
| 188 |
+
pares_validos = [
|
| 189 |
+
p for p in all_pairs
|
| 190 |
+
if p.get("htr", "").strip() and p.get("gt", "").strip()
|
| 191 |
+
]
|
| 192 |
+
|
| 193 |
+
# Filtrar por caligrafía si se especifica
|
| 194 |
+
if caligrafia_filtro and caligrafia_filtro != "Todas":
|
| 195 |
+
pares_validos = [
|
| 196 |
+
p for p in pares_validos
|
| 197 |
+
if p.get("caligrafia", "") == caligrafia_filtro
|
| 198 |
+
]
|
| 199 |
+
|
| 200 |
+
if not pares_validos:
|
| 201 |
+
return "⚠ No hay pares con GT disponibles para evaluar con ese filtro."
|
| 202 |
+
|
| 203 |
+
# Muestra aleatoria
|
| 204 |
+
n = min(int(n_samples), len(pares_validos))
|
| 205 |
+
muestra = random.sample(pares_validos, n)
|
| 206 |
+
|
| 207 |
+
yield f"⏳ Evaluando {n} pares con modelo **{model}**...\n\n"
|
| 208 |
+
|
| 209 |
+
results = []
|
| 210 |
+
errores = []
|
| 211 |
+
for i, pair in enumerate(muestra, 1):
|
| 212 |
+
try:
|
| 213 |
+
out = corrector.correct(pair["htr"], model=model)
|
| 214 |
+
metrics = evaluator.evaluate_pair(
|
| 215 |
+
htr=pair["htr"],
|
| 216 |
+
corrected=out["corrected"],
|
| 217 |
+
gt=pair["gt"],
|
| 218 |
+
)
|
| 219 |
+
metrics["id"] = pair.get("id", f"par_{i}")
|
| 220 |
+
metrics["htr"] = pair["htr"]
|
| 221 |
+
metrics["corrected"] = out["corrected"]
|
| 222 |
+
metrics["gt"] = pair["gt"]
|
| 223 |
+
metrics["caligrafia"]= pair.get("caligrafia", "desconocida")
|
| 224 |
+
results.append(metrics)
|
| 225 |
+
except Exception as e:
|
| 226 |
+
errores.append(f" - {pair.get('id','?')}: {e}")
|
| 227 |
+
|
| 228 |
+
# Progreso intermedio cada 5 pares
|
| 229 |
+
if i % 5 == 0:
|
| 230 |
+
yield f"⏳ Procesados {i}/{n}...\n\n"
|
| 231 |
+
|
| 232 |
+
if not results:
|
| 233 |
+
yield "❌ No se obtuvieron resultados.\n" + "\n".join(errores)
|
| 234 |
+
return
|
| 235 |
+
|
| 236 |
+
# ── Resumen global ────────────────────────────────────────────────────────
|
| 237 |
+
def avg(key):
|
| 238 |
+
return sum(r[key] for r in results) / len(results)
|
| 239 |
+
|
| 240 |
+
n_res = len(results)
|
| 241 |
+
mejorados = sum(1 for r in results if r["cer_improvement"] > 0.02)
|
| 242 |
+
empeorados = sum(1 for r in results if r["cer_improvement"] < -0.02)
|
| 243 |
+
sin_cambio = n_res - mejorados - empeorados
|
| 244 |
+
|
| 245 |
+
md = f"## 📊 Evaluación por lotes — {n_res} pares\n\n"
|
| 246 |
+
if caligrafia_filtro != "Todas":
|
| 247 |
+
md += f"**Filtro caligrafía:** {caligrafia_filtro} | "
|
| 248 |
+
md += f"**Modelo:** {model}\n\n"
|
| 249 |
+
md += "---\n\n"
|
| 250 |
+
|
| 251 |
+
# Comparación 1 — GT vs HTR
|
| 252 |
+
md += "### ① Error de partida (GT vs HTR original)\n\n"
|
| 253 |
+
md += f"| CER medio | WER medio |\n|---|---|\n"
|
| 254 |
+
md += f"| {avg('cer_before'):.2%} | {avg('wer_before'):.2%} |\n\n"
|
| 255 |
+
|
| 256 |
+
# Comparación 2 — GT vs Corregido
|
| 257 |
+
md += "### ② Error final (GT vs Texto corregido)\n\n"
|
| 258 |
+
md += f"| CER medio | WER medio | Mejora CER | Mejora WER |\n|---|---|---|---|\n"
|
| 259 |
+
md += (
|
| 260 |
+
f"| {avg('cer_after'):.2%} "
|
| 261 |
+
f"| {avg('wer_after'):.2%} "
|
| 262 |
+
f"| {avg('cer_improvement'):+.2%} "
|
| 263 |
+
f"| {avg('wer_improvement'):+.2%} |\n\n"
|
| 264 |
+
)
|
| 265 |
+
md += (
|
| 266 |
+
f"| ✓ Mejorados | ✗ Empeorados | ~ Sin cambio |\n|---|---|---|\n"
|
| 267 |
+
f"| {mejorados} ({mejorados/n_res:.0%}) "
|
| 268 |
+
f"| {empeorados} ({empeorados/n_res:.0%}) "
|
| 269 |
+
f"| {sin_cambio} ({sin_cambio/n_res:.0%}) |\n\n"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# Comparación 3 — HTR vs Corregido (modernismos)
|
| 273 |
+
md += "### ③ Modernismos introducidos (HTR vs Corregido)\n\n"
|
| 274 |
+
md += f"Score promedio: **{avg('modernism_score'):.2%}** (1.0 = sin modernismos)\n\n"
|
| 275 |
+
modernismos_total = sum(r["modernism"]["count"] for r in results)
|
| 276 |
+
if modernismos_total == 0:
|
| 277 |
+
md += "✓ El LLM no introdujo modernismos en ningún par.\n\n"
|
| 278 |
+
else:
|
| 279 |
+
md += f"✗ {modernismos_total} modernismo(s) introducidos en total.\n\n"
|
| 280 |
+
|
| 281 |
+
# ── Desglose por caligrafía ───────────────────────────────────────────────
|
| 282 |
+
from collections import defaultdict
|
| 283 |
+
by_cal = defaultdict(list)
|
| 284 |
+
for r in results:
|
| 285 |
+
by_cal[r["caligrafia"]].append(r)
|
| 286 |
+
|
| 287 |
+
if len(by_cal) > 1:
|
| 288 |
+
md += "### Desglose por caligrafía\n\n"
|
| 289 |
+
md += "| Caligrafía | N | CER antes | CER después | Mejora CER | Modernismos |\n"
|
| 290 |
+
md += "|---|---|---|---|---|---|\n"
|
| 291 |
+
for cal, rs in sorted(by_cal.items()):
|
| 292 |
+
a_cer = sum(r["cer_before"] for r in rs) / len(rs)
|
| 293 |
+
d_cer = sum(r["cer_after"] for r in rs) / len(rs)
|
| 294 |
+
imp = a_cer - d_cer
|
| 295 |
+
mods = sum(r["modernism"]["count"] for r in rs)
|
| 296 |
+
md += f"| {cal} | {len(rs)} | {a_cer:.2%} | {d_cer:.2%} | {imp:+.2%} | {mods} |\n"
|
| 297 |
+
md += "\n"
|
| 298 |
+
|
| 299 |
+
# ── Detalle por par ───────────────────────────────────────────────────────
|
| 300 |
+
md += "### Detalle por par\n\n"
|
| 301 |
+
md += "| ID | Cal | CER antes | CER después | Mejora | Veredicto | Modernismos |\n"
|
| 302 |
+
md += "|---|---|---|---|---|---|---|\n"
|
| 303 |
+
for r in results:
|
| 304 |
+
md += (
|
| 305 |
+
f"| `{r['id'][:25]}` "
|
| 306 |
+
f"| {r['caligrafia'][:12]} "
|
| 307 |
+
f"| {r['cer_before']:.2%} "
|
| 308 |
+
f"| {r['cer_after']:.2%} "
|
| 309 |
+
f"| {r['cer_improvement']:+.2%} "
|
| 310 |
+
f"| {r['verdict']} "
|
| 311 |
+
f"| {r['modernism']['count']} |\n"
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
# Errores
|
| 315 |
+
if errores:
|
| 316 |
+
md += f"\n\n⚠ {len(errores)} pares fallaron:\n" + "\n".join(errores)
|
| 317 |
+
|
| 318 |
+
yield md
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def add_to_corpus(htr_text, gt_text, doc_type, region, date, caligrafia):
|
| 322 |
if not htr_text.strip() or not gt_text.strip():
|
| 323 |
return "⚠ HTR y GT son obligatorios."
|
| 324 |
try:
|
| 325 |
+
pair_id = f"user_{abs(hash(htr_text)) % 100000:05d}"
|
| 326 |
new_pair = {
|
| 327 |
+
"id": pair_id,
|
| 328 |
+
"htr": htr_text.strip(),
|
| 329 |
+
"gt": gt_text.strip(),
|
| 330 |
+
"type": doc_type or "desconocido",
|
| 331 |
+
"region": region or "desconocida",
|
| 332 |
+
"date": date or "",
|
| 333 |
+
"caligrafia": caligrafia or "desconocida",
|
| 334 |
"corrections": [],
|
| 335 |
+
"source": "user_added",
|
| 336 |
}
|
| 337 |
added = vs.index([new_pair])
|
| 338 |
if added:
|
|
|
|
| 361 |
""",
|
| 362 |
) as demo:
|
| 363 |
|
|
|
|
| 364 |
gr.HTML("""
|
| 365 |
<div class="header">
|
| 366 |
<h1>RAG CODEX for Historical Spanish</h1>
|
|
|
|
| 386 |
)
|
| 387 |
model_selector = gr.Dropdown(
|
| 388 |
label="Modelo LLM",
|
| 389 |
+
choices=["llama-3.3-70b-versatile", "openai/gpt-oss-120b"],
|
|
|
|
|
|
|
|
|
|
| 390 |
value="llama-3.3-70b-versatile",
|
| 391 |
)
|
| 392 |
embedding_selector = gr.Dropdown(
|
| 393 |
label="Modelo de Embedding",
|
| 394 |
+
choices=["openai", "mpnet", "mt5"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
value="openai",
|
| 396 |
)
|
|
|
|
| 397 |
show_prompt = gr.Checkbox(label="Show RAG prompt", value=False)
|
| 398 |
btn_corregir = gr.Button("✦ Correct with RAG", variant="primary")
|
| 399 |
+
gr.Examples(examples=DEMO_EXAMPLES, inputs=htr_input, label="Demonstration examples")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
|
| 401 |
with gr.Column(scale=2):
|
| 402 |
+
corrected_out = gr.Textbox(label="Corrected text (RAG output)", lines=6, interactive=False)
|
| 403 |
+
status_out = gr.Markdown(elem_classes=["status-bar"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
with gr.Row():
|
| 406 |
with gr.Column():
|
| 407 |
+
docs_out = gr.Markdown(label="Documents recovered from the corpus")
|
| 408 |
with gr.Column():
|
| 409 |
analysis_out = gr.Markdown(label="Pattern analysis")
|
| 410 |
|
| 411 |
+
diff_out = gr.Markdown(label="Word-by-word differences")
|
| 412 |
prompt_out = gr.Markdown(label="Prompt sent to the LLM", visible=True)
|
| 413 |
|
| 414 |
btn_corregir.click(
|
|
|
|
| 416 |
inputs=[htr_input, top_k_slider, show_prompt, model_selector],
|
| 417 |
outputs=[corrected_out, docs_out, analysis_out, diff_out, status_out, prompt_out],
|
| 418 |
)
|
|
|
|
| 419 |
embed_status = gr.Markdown()
|
| 420 |
+
embedding_selector.change(fn=cambiar_embedding, inputs=[embedding_selector], outputs=[embed_status])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
+
# ── Pestaña 2: Evaluación individual ─────────────────────────────────
|
| 423 |
with gr.TabItem(" Evaluation with GT"):
|
| 424 |
+
gr.Markdown(
|
| 425 |
+
"Compara la corrección del RAG con el groundtruth real para medir "
|
| 426 |
+
"CER/WER y detectar modernismos introducidos por el LLM."
|
| 427 |
+
)
|
| 428 |
with gr.Row():
|
| 429 |
eval_htr = gr.Textbox(label="HTR text", lines=4)
|
| 430 |
eval_gt = gr.Textbox(label="Groundtruth (reference)", lines=4)
|
|
|
|
| 432 |
eval_out = gr.Markdown()
|
| 433 |
btn_eval.click(fn=evaluar_par, inputs=[eval_htr, eval_gt], outputs=eval_out)
|
| 434 |
|
| 435 |
+
# ── Pestaña 3: Evaluación por lotes ──────────────────────────────────
|
| 436 |
+
with gr.TabItem("📊 Batch Evaluation"):
|
| 437 |
+
gr.Markdown(
|
| 438 |
+
"Evalúa el sistema sobre una muestra aleatoria del corpus. "
|
| 439 |
+
"Muestra las tres comparaciones: **GT vs HTR** (error de partida), "
|
| 440 |
+
"**GT vs Corregido** (error final) y **HTR vs Corregido** (modernismos)."
|
| 441 |
+
)
|
| 442 |
+
with gr.Row():
|
| 443 |
+
batch_n = gr.Slider(
|
| 444 |
+
minimum=5, maximum=100, value=20, step=5,
|
| 445 |
+
label="Número de pares a evaluar",
|
| 446 |
+
)
|
| 447 |
+
batch_cal = gr.Dropdown(
|
| 448 |
+
label="Filtrar por caligrafía",
|
| 449 |
+
choices=get_caligrafias(),
|
| 450 |
+
value="Todas",
|
| 451 |
+
)
|
| 452 |
+
batch_model = gr.Dropdown(
|
| 453 |
+
label="Modelo LLM",
|
| 454 |
+
choices=["llama-3.3-70b-versatile", "openai/gpt-oss-120b"],
|
| 455 |
+
value="llama-3.3-70b-versatile",
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
with gr.Row():
|
| 459 |
+
gr.Markdown(
|
| 460 |
+
f"ℹ Corpus disponible: **{len([p for p in all_pairs if p.get('gt','').strip()])} "
|
| 461 |
+
f"pares con GT** de {len(all_pairs)} totales."
|
| 462 |
+
)
|
| 463 |
+
|
| 464 |
+
btn_batch = gr.Button("▶ Ejecutar evaluación por lotes", variant="primary")
|
| 465 |
+
batch_out = gr.Markdown()
|
| 466 |
+
|
| 467 |
+
btn_batch.click(
|
| 468 |
+
fn=evaluar_batch,
|
| 469 |
+
inputs=[batch_n, batch_cal, batch_model],
|
| 470 |
+
outputs=batch_out,
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
# ── Pestaña 4: Añadir al corpus ───────────────────────────────────────
|
| 474 |
with gr.TabItem("➕ Add to corpus"):
|
| 475 |
gr.Markdown("Add new pairs to the vector store to improve the RAG continuously.")
|
| 476 |
with gr.Row():
|
| 477 |
+
add_htr = gr.Textbox(label="Texto HTR", lines=4)
|
| 478 |
+
add_gt = gr.Textbox(label="Groundtruth corregido", lines=4)
|
| 479 |
with gr.Row():
|
| 480 |
+
add_type = gr.Textbox(label="Document type", placeholder="notarial / judicial / eclesiastico")
|
| 481 |
+
add_region = gr.Textbox(label="Region", placeholder="Castilla, Andalucía…")
|
| 482 |
+
add_date = gr.Textbox(label="Date", placeholder="1542")
|
| 483 |
add_caligrafia = gr.Dropdown(
|
| 484 |
label="Caligrafía",
|
| 485 |
+
choices=["desconocida", "Procesal", "Encadenada", "Italica_cursiva", "Redonda"],
|
| 486 |
value="desconocida",
|
| 487 |
+
)
|
| 488 |
+
btn_add = gr.Button("Add to corpus", variant="primary")
|
| 489 |
+
add_out = gr.Markdown()
|
| 490 |
btn_add.click(
|
| 491 |
fn=add_to_corpus,
|
| 492 |
inputs=[add_htr, add_gt, add_type, add_region, add_date, add_caligrafia],
|
| 493 |
outputs=add_out,
|
| 494 |
)
|
| 495 |
|
| 496 |
+
# ── Pestaña 5: Info del sistema ───────────────────────────────────────
|
| 497 |
with gr.TabItem("ℹ System"):
|
| 498 |
gr.Markdown(f"""
|
| 499 |
## System status
|
|
|
|
| 516 |
│ │
|
| 517 |
│ └─► Búsqueda top-k en ChromaDB ──► Few-shot dinámico
|
| 518 |
│
|
| 519 |
+
└─► Prompt constructor ──► LLM ──► Texto corregido
|
| 520 |
```
|
| 521 |
|
| 522 |
+
## Evaluación por lotes
|
| 523 |
|
| 524 |
+
Las tres comparaciones:
|
| 525 |
+
1. **GT vs HTR** — error de partida (cuánto se equivocó el HTR)
|
| 526 |
+
2. **GT vs Corregido** — error final (cuánto mejoró el RAG)
|
| 527 |
+
3. **HTR vs Corregido** — modernismos (qué cambió el LLM que no debía)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
""")
|
| 529 |
|
| 530 |
if __name__ == "__main__":
|
| 531 |
demo.launch(
|
| 532 |
server_name="0.0.0.0",
|
| 533 |
server_port=7860,
|
| 534 |
+
auth=("admin", "admin"),
|
|
|
|
| 535 |
share=False,
|
| 536 |
show_error=True,
|
| 537 |
)
|