# Auditoría del repo contra los escaneos

Qué se ha hecho para comprobar, palabra a palabra, que el texto que sirve
vedabase.cc coincide con las primeras ediciones impresas, y cómo repetirlo.

## Las tres piezas

| Pieza | Dónde | Qué es |
|---|---|---|
| Escaneos | `~/git_projects/scan_vedabase` | 87 PDF de las ediciones originales. **La fuente de verdad.** |
| Repo | `~/git_projects/vedabase-original` | El texto en Markdown. De aquí salen D1, los ebooks y los packs. |
| Auditoría | `astro_vedabase/scripts/scan_audit` | Lo que compara los dos anteriores. |

La regla que gobierna todo: **manda el impreso, incluso cuando trae una errata
evidente**. Las erratas que se conservan a propósito están listadas en
`PRINT_ERRATA_PRESERVED.md` y `ERRATAS_DEL_IMPRESO.md`. La única excepción es
cuando el sospechoso es el OCR y no el papel; esos casos se miran en la imagen
antes de tocar nada.

---

## 1. Leer el papel

### Primera pasada: doble Tesseract

`ocr_pages.py` renderiza cada página a 300 dpi y la lee **dos veces** con
configuraciones distintas:

- `psm 6` — bloque uniforme de texto
- `psm 4` — columna de texto de tamaños variables

las dos con `--oem 1` (LSTM). Donde las dos lecturas coinciden hay consenso
fiable. Donde discrepan, la página queda marcada como dudosa.

El problema de Tesseract es que se equivoca **siempre igual**: `h` por `il`,
`e` por `c`. Un error sistemático no se cancela leyendo dos veces con el mismo
motor, así que el consenso por sí solo no basta para adjudicar.

### Segunda pasada: Surya

`surya_pages.py` y `surya_full.py` pasan un motor neuronal distinto, Surya, que
no comparte esos errores. Eso es lo que lo hace útil: sirve de árbitro.

- `surya_pages.py` lee **solo** las páginas con candidatos abiertos. Renderiza
  a PNG, llama a `surya_ocr` una vez por carpeta (cargar el modelo es lo caro) y
  escribe `surya/<libro>/<pdf>/pNNNN.surya.txt`.
- `surya_full.py` acabó leyendo **todas** las páginas de todos los PDF:
  **35.509 páginas** en 21 libros. Es reanudable: salta las que ya tienen
  `.surya.txt`.

Surya trajo un beneficio que no se buscaba y resultó decisivo: **conserva los
guiones largos, los puntos y coma y los diacríticos**. Con Tesseract, las
glosas llegaban como un chorro de palabras sin puntuación. Con Surya llegan tal
como están impresas:

```
śrī sūtaḥ uvāca—Śrī Sūta Gosvāmī said; viduraḥ—Vidura; tīrtha-yātrāyām—…
```

Eso permitió leer las parejas `clave—glosa` directamente del papel, sin adivinar
dónde acaba cada glosa. Volveremos a ello en el punto 4.

## 2. El diff

`diff_align.py` (Tesseract) y `diff_align_surya.py` (Surya) normalizan los dos
lados —el Markdown del repo y el OCR del libro— y los pasan por `difflib`. Cada
operación `replace`, `insert` o `delete` se convierte en un candidato:

| Tipo | Significa |
|---|---|
| `REPO_TYPO` | el repo tiene una errata clara |
| `REVISAR` | difieren de verdad, hay que decidir |
| `AMBIGUO` | no se puede decidir sin la imagen |
| `OCR_NOISE` | ruido del escaneo |
| `REPO_OMISSION` | el escaneo tiene texto que falta en el repo |
| `REPO_EXTRA` | el repo tiene texto que no está en el escaneo |

Salieron **8.724 candidatos** en total.

## 3. El registro

`build_ledger.py` resuelve un problema práctico que costó tiempo antes de
existir: **el número de candidatos no baja cuando corriges**. El diff se vuelve
a calcular desde cero y vuelve a proponer lo mismo, así que cada sesión corría
el riesgo de readjudicar lo ya decidido, a veces al revés.

El ledger cruza los candidatos con todo lo decidido y deja un estado por
candidato en `ledger.jsonl`:

| Estado | Significa | Hoy |
|---|---|---|
| `RUIDO` | cae en una clase de ruido documentada | 3.496 |
| `DESCARTADO` | ya se rechazó, con motivo | 2.501 |
| `APLICADO` | el texto que reportaba ya no existe en el repo | 1.343 |
| `VERIFICADO` | comprobado contra la imagen: **no volver a proponer** | 203 |
| `AUDITADO` | revisado en una pasada posterior | 23 |
| `ABIERTO` | lo único que queda por mirar | **145** |

Los 145 abiertos están caracterizados como ruido de OCR. No queda ninguna
imagen de página pendiente de mirar.

## 4. Las glosas, aparte

Los significados (`clave—glosa; clave—glosa; …`) necesitaron su propia pasada,
por una razón concreta: **el alineador de `difflib` compara dos chorros de
palabras**, así que una glosa larga se parte por la mitad y aparece como tres
candidatos sueltos que por separado no dicen nada. En sb/1/18/42 salieron
`by the prowess`/`deserve` y `of whom`/`by whose` como si fueran cosas
distintas. Puestas en su pareja se ve de golpe qué pasa:

```
impreso   arhasi—deserve          yat—by whose
repo      arhasi—by the prowess   yat—of whom
```

El repo le había puesto a `arhasi` la glosa de `tejasā`, que viene dos parejas
más allá. Es un salto de copia, y solo se ve emparejado.

`glosas_impreso.py` lee las parejas del impreso **tal cual vienen** —lo que
habilitó Surya— y compara la glosa de la misma clave en los dos sitios. Tres
detalles que costaron errores y ahora están blindados:

- **El ancla es la transliteración, y tiene que casar UNA sola vez.** Anclar en
  la primera clave del repo no vale: `tasmāt—` encabeza glosas en decenas de
  versos y el bloque salía de otro sitio. Se prueban ventanas de más a menos
  palabras y se acepta la primera que sea inequívoca.
- **Detrás del ancla tiene que empezar el bloque de glosas.** En sb/7/13/7 el
  ancla caía dentro de un comentario que cita la traducción del verso, y el
  «bloque» era prosa de otro sitio. De ahí salió un cambio equivocado que hubo
  que revertir.
- **Una clave puede repetirse en el mismo verso con glosas distintas.** En
  sb/1/1/19 `svādu` sale dos veces, «relishing» y «palatable», y así lo trae
  también el impreso. Guardando solo la primera se inventaban divergencias, y
  además salían invertidas. Hay que emparejar la primera con la primera.

`verificar_glosa_suelta.py` cubre el caso en que el OCR estropea la
transliteración y no hay ancla. Localiza la glosa por **dos claves del mismo
verso a la vez**: `te—` encabeza glosas en doscientos sitios, pero `te—` a menos
de 400 caracteres de una clave rara del mismo verso solo puede ser esa.

De 234 diferencias de glosa, 149 eran ruido de OCR y 85 texto distinto de
verdad. `aplicar_glosas.py` aplicó las que el impreso sostiene sin dudas y
apartó dos clases: las glosas que el escaneo cortó por el final (lo impreso es
principio exacto de lo del repo) y las lecturas absurdas
(`sarasvatī—the godless of learning` es «goddess» con la `dd` leída `dl`).

## 5. Repo contra D1

`arbitrar_deriva.py` usa el escaneo para desempatar cuando el repo y D1 dicen
cosas distintas, sin necesidad de abrir una imagen:

```
el escaneo dice lo del repo  ->  el equivocado es D1
el escaneo dice lo de D1     ->  el equivocado es el repo (raro; mirar el caso)
el escaneo no dice ninguno   ->  hace falta la imagen
```

Con la palabra sola no basta —«one» sale en todas las páginas—, así que se toma
el contexto del repo alrededor de la divergencia y se busca ese contexto en el
OCR del libro.

## 6. Las traducciones

Cuando el inglés cambia, español, portugués, hindi y ruso tienen que seguirle.
Eso vive en `trad/`.

**De dónde salen los pares.** No de `corrections.jsonl`: ese fichero guarda
fragmentos, y al reconstruir los pares desde ahí 119 de 149 salían incoherentes.
`pares_desde_git.py` los saca del **historial de git** —`git diff --unified=0`
sobre la línea de significados—, que es la única fuente que sabe qué decía la
glosa antes y qué dice ahora. Solo se acepta el par cuando el número de glosas
coincide a los dos lados; si el verso se reestructuró, se aparta.

**Cómo se emparejan las lenguas.** Por **posición**, no por texto de la clave: el
ruso escribe las claves en cirílico y buscarlas por texto latino localiza una de
cada ochenta. Las listas son paralelas en el 98% de los versos. Donde no lo son
—el portugués de cc/adi/5/113 lleva una glosa de menos— `aplicar.py` busca la
clave por nombre, y eso solo vale en español y portugués, que la conservan en
latín.

**Los filtros.** Dos pasadas mecánicas quitaron 132 falsos candidatos:

- `filtrar_articulos.py` — cuando el inglés solo pierde o gana un artículo
  (`the mouth` → `mouth`), ninguna de las cuatro lenguas cambia.
- `filtrar_fragmentos.py` — cuando `viejo` no es una glosa sino un resto del
  alineador de git (`one`, `into`, `are`, `results`).

**La comprobación.** Después de cada lote, `sincronizar.py` manda a D1 la línea
entera del `.md` y **la relee para compararla**. Un `UPDATE` que no casa ninguna
fila no da error: simplemente no cambia nada. Sin esa relectura, un lote podía
darse por bueno estando a medias.

Herramientas:

| Script | Para qué |
|---|---|
| `pares_desde_git.py` | saca los pares viejo→nuevo del historial |
| `construir_lote.py` | arma el bloque de un libro y empareja por posición |
| `ver_lote.py` | muestra cada candidato con las glosas inglesas vecinas y las cuatro traducciones |
| `filtrar_articulos.py`, `filtrar_fragmentos.py` | apartan lo que no es trabajo real |
| `aplicar.py` | escribe en las cuatro traducciones |
| `sincronizar.py` | sube a D1 y verifica `repo == D1` |

---

## Cómo repetirlo

```bash
cd ~/git_projects/astro_vedabase/scripts/scan_audit

# 1. OCR (solo si hay escaneos nuevos). Necesita el venv de Surya.
python3 ocr_pages.py                       # doble tesseract
surya-venv/bin/python surya_full.py        # surya, reanudable

# 2. Diff y registro
python3 diff_align_surya.py
python3 build_ledger.py                    # -> ledger.jsonl

# 3. Qué queda abierto
python3 clasificar.py

# 4. Glosas
python3 glosas_impreso.py --lista          # -> difs_impreso.json
python3 aplicar_glosas.py                  # ensayo
python3 aplicar_glosas.py --aplicar

# 5. Traducciones, libro a libro
cd trad
python3 pares_desde_git.py
python3 construir_lote.py sb/1             # -> lote_sb-1.json
python3 filtrar_articulos.py sb1_resto.json
python3 filtrar_fragmentos.py sb1_resto.json
python3 ver_lote.py sb1_resto.json 30      # decidir a ojo, en contexto
# ... aplicar con aplicar.aplicar(...) ...
python3 sincronizar.py /tmp/lote.json      # sube a D1 y verifica
```

## Lo que salió mal, y por qué está escrito aquí

Cada uno de estos costó una reversión. Están en el código como comentario, en el
sitio donde se toma la decisión que los evita.

- **Seis cambios de glosa aplicados al revés.** El ancla había caído en otro
  sitio y el impreso decía justo lo que el repo ya tenía. Se cazaron
  reextrayendo los pares del `git diff` y volviendo a verificar con el ancla
  corregida. De ahí sale la exigencia de que el ancla case una sola vez.
- **cc/madhya/8/219.** El mismo verso se cita tres veces en el Caitanya-caritāmṛta
  con glosas impresas distintas. Se habían aplicado las del ādi-līlā al
  madhya-līlā: doce glosas revertidas.
- **Plurales tomados por erratas.** Un `rstrip('s')` en el detector clasificaba
  «vacas»→«vaca» como errata tipográfica. En sb/1/14/19 dejó «el ternero» junto
  a «las vacas». Lo cazó la instrucción de comprobar que el texto tiene sentido
  en su contexto, no solo que la posición cuadre.
- **Texto ruso escrito en el nodo inglés del pack.** `refresh_versos_sueltos.py`
  deducía el idioma del nombre del fichero, que es idéntico en las cinco lenguas.
  Ahora lo deduce de la **ruta**. Se revirtió antes de subir nada.
- **`SQLITE_TOOBIG`.** Los `UPDATE` de más de 100 KB fallan en D1. Se arregla
  troceando con `SET campo = campo || '…'`.
- **`wrangler d1 execute --command` parte por `;`.** Un patrón `LIKE '%…;%'`
  rompe la sentencia. Hay que usar `--file`.

## Ficheros de referencia

| Fichero | Contenido |
|---|---|
| `NO_REPETIR.md` | decisiones cerradas: qué no volver a proponer y por qué |
| `PRINT_ERRATA_PRESERVED.md` | erratas del impreso que se conservan a propósito |
| `ERRATAS_DEL_IMPRESO.md` | catálogo de erratas encontradas |
| `SYNONYMS_AUDIT_HANDOFF.md` | estado de la auditoría de significados |
| `CC_OCR_ESTADO.md` | estado del OCR del Caitanya-caritāmṛta |
| `ledger.jsonl` | un estado por candidato |
