Hoy NavTorá lee cada columna con una red propia, en el teléfono (está contado en Cómo NavTorá sabe en qué columna está abierto el sefer). La primera versión, en septiembre de 2026, no tenía esa red. La app sacaba la foto, la achicaba a 1.800 píxeles en el lado más largo y la mandaba al servidor, que se la pasaba a Claude Opus 5 por la API de Anthropic. Claude transcribía los renglones, y desde ahí todo seguía como ahora, con la misma búsqueda entre las palabras de la Torá.
Qué le pedíamos a Claude
Desde el principio, la idea fue que el modelo solo lea. La posición y la confianza las decide la búsqueda, que se puede probar y medir sin gastar nada. Por eso, lo que le pedíamos a Claude era bastante acotado:
- Transcribir los primeros 14 renglones, solo con letras, sin nikud, teamim ni puntuación.
- Respetar las letras finales y los espacios entre palabras.
- Escribir
?en cada letra que no se puede leer. - Contar todos los renglones que se ven, aunque no los transcriba.
- Leer la tinta y no completar de memoria. Si el pergamino dice algo distinto de lo esperado, escribir lo que dice el pergamino.
La respuesta venía con un formato fijo (salida estructurada, con un esquema JSON). Traía los renglones, cuáles eran dudosos, dónde había espacios en blanco y cuántos renglones se veían. Esta es la llamada, simplificada.
const response = await client.messages.parse({
model: "claude-opus-5",
max_tokens: 8000,
system: systemPrompt(14),
output_config: { effort: "low", format: zodOutputFormat(OcrResultSchema) },
messages: [
{
role: "user",
content: [
{ type: "image", source: { type: "base64", media_type: "image/jpeg", data } },
{ type: "text", text: "Transcribe the first 14 lines of this Sefer Torah column and count the visible lines." },
],
},
],
});
if (response.stop_reason === "refusal") throw new OcrError("refusal", "The model refused to transcribe the image.");
if (response.stop_reason === "max_tokens") throw new OcrError("truncated", "The transcription was cut off by the token limit.");
const parsed = response.parsed_output; // { lines, uncertainLines, gapBeforeLines, lineCountVisible }
if (!parsed) throw new OcrError("bad-format", "The model did not return a transcription in the expected format.");El código completo sigue en tools/eval, donde lo usamos para comparar las dos formas de leer sobre las mismas fotos.
¿Lee o recita?
Claude conoce la Torá, así que había que chequear que leyera lo escrito y no lo que recuerda. Para probarlo, armamos una columna con seis palabras cambiadas por otras palabras hebreas reales. Claude transcribió las seis tal como estaban escritas, y ninguna volvió al texto original. Cuando la imagen cortaba el principio de los renglones, transcribió solo lo que se veía.
Cómo le fue
Leía muy bien. Probamos cuánto podía pensar el modelo (el parámetro effort) y cuántos renglones transcribía, sobre columnas escaneadas del sefer de Elihu Shannon, que sigue el formato estándar y está publicado en Wikimedia Commons. Todas quedaron bien ubicadas.
| Configuración | Tiempo por foto | Error por letra |
|---|---|---|
| Opus 5, columna completa, esfuerzo medio | 52 s | 0,19 % |
| Opus 5, 14 renglones, esfuerzo bajo | 15 s | 0,38 % |
| Sonnet 5, 14 renglones, esfuerzo bajo | 12 s | 0,60 % |
Con 14 renglones alcanzaba para ubicar la columna, y transcribir menos bajaba mucho el tiempo y el costo. Con Opus 5, esfuerzo bajo y 14 renglones, cada foto costaba unos 3 centavos de dólar. En 78 imágenes escaneadas de 4 sifrei nunca dio una posición equivocada, y en las pruebas con sifrei de verdad cada escaneo tardó 16 o 17 segundos.
¿Por qué lo cambiamos?
El MVP funcionaba, pero tenía tres problemas para usarlo en una kehilá:
- El tiempo: 16 segundos con el teléfono apuntando al sefer es mucho, sobre todo si hay que escanear más de una vez.
- El costo: cada foto costaba plata, así que no se podía abrir la app a cualquiera.
- La foto: tenía que salir del teléfono y pasar por un servidor.
Con la red propia, en las pruebas con un sefer cada escaneo tardó 1 o 2 segundos. No cuesta nada por escaneo y la foto no sale del teléfono. A cambio, lee bastante peor, con muchas más letras equivocadas o con ?. Como la búsqueda está pensada para lecturas con errores, igual llega a la columna correcta. Cómo entrenamos esa red está en otro artículo.