Alexander Sanchez commited on
Commit
0c0e3c8
·
1 Parent(s): 962adf4

Evaluator udpdated

Browse files
Files changed (1) hide show
  1. app.py +245 -116
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= 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
- analysis_md += f"- `{e['htr']}` → `{e['gt']}`: {e['context']} \n *Ej: {e['example']}*\n"
 
 
 
 
 
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 = corrector.correct(htr_text)
181
  metrics = evaluator.evaluate_pair(htr_text, result["corrected"], gt_text)
182
- m = metrics
183
- mod = m["modernism"]
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 add_to_corpus(htr_text: str, gt_text: str, doc_type: str, region: str, date: str, caligrafia: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  if not htr_text.strip() or not gt_text.strip():
206
  return "⚠ HTR y GT son obligatorios."
207
  try:
208
- pair_id = f"user_{abs(hash(htr_text)) % 100000:05d}"
209
  new_pair = {
210
- "id": pair_id,
211
- "htr": htr_text.strip(),
212
- "gt": gt_text.strip(),
213
- "type": doc_type or "desconocido",
214
- "region": region or "desconocida",
215
- "date": date or "",
216
- "caligrafia": caligrafia or "desconocida",
217
  "corrections": [],
218
- "source": "user_added",
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
- label="Corrected text (RAG output)",
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 = gr.Markdown(label="Documents recovered from the corpus")
309
  with gr.Column():
310
  analysis_out = gr.Markdown(label="Pattern analysis")
311
 
312
- diff_out = gr.Markdown(label="Word-by-word differences")
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("Compare the RAG correction against the actual groundtruth to measure CER/WER and detect modernisms.")
 
 
 
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: Añadir al corpus ───────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = gr.Textbox(label="Texto HTR", lines=4)
343
- add_gt = gr.Textbox(label="Groundtruth corregido", lines=4)
344
  with gr.Row():
345
- add_type = gr.Textbox(label="Document type", placeholder="notarial / judicial / eclesiastico")
346
- add_region = gr.Textbox(label="Region", placeholder="Castilla, Andalucía…")
347
- add_date = gr.Textbox(label="Date", placeholder="1542")
348
  add_caligrafia = gr.Dropdown(
349
  label="Caligrafía",
350
- choices=["desconocida", "procesal", "encadenada", "italica"],
351
  value="desconocida",
352
- )
353
- btn_add = gr.Button("Add to corpus", variant="primary")
354
- add_out = gr.Markdown()
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 4: Info del sistema ───────────────────────────────────────
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 ──► GPT-4o ──► Texto corregido
385
  ```
386
 
387
- ## Formato del corpus
388
 
389
- Para añadir tu corpus, crea `./corpus/` con ficheros JSON:
390
- ```json
391
- [
392
- {{"id": "doc001", "htr": "texto htr...", "gt": "groundtruth...",
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"), # ← autenticación básica (opcional)
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
  )