Una función de lectura en voz alta (read-aloud) tiene un trabajo visible más allá de la voz: a medida que se pronuncia cada palabra, debe iluminar esa palabra en la página y mantenerla a la vista. Para hacer eso, necesita el cuadro delimitador (bounding box) de cada palabra, indexado al mismo flujo de caracteres desde el que está leyendo el motor de voz. Obtenga los cuadros pero omita la indexación y el resaltado se desplazará una o dos palabras detrás del audio; obtenga la indexación pero maneje mal el estado de la página y el resaltado aterrizará por completo en la página equivocada. La parte de la voz, el sintetizador en sí, es la parte que rara vez se rompe. SAPI reporta los límites de las palabras hasta el nivel del carácter. Lo que se rompe es la delgada capa de mapeo entre un desplazamiento (offset) de caracteres en el búfer de voz y un rectángulo en la página renderizada
PDFium Component incluye ese mapeo para Delphi, C++Builder y Lazarus, con los cuadros de palabras disponibles desde la versión 1.53 y el cursor de seguimiento desde la versión 1.56. La superficie es deliberadamente estrecha: una llamada que devuelve los cuadros de palabras de una página, un rastreador que convierte un desplazamiento de caracteres en un resaltado pintado, y un par de propiedades para el color y el desplazamiento automático (auto-scroll). Por muy estrecho que sea, el orden en el que llama a las cosas decide si la función trabaja, y la mayoría de las fallas a continuación provienen de llamar a las funciones correctas en la secuencia incorrecta
Los caracteres no son palabras, y los motores TTS hablan en caracteres
Un motor de voz consume una cadena plana y reporta el progreso como posiciones de caracteres dentro de esa cadena. Una página PDF tiene glifos colocados en el espacio de la página, donde una "palabra" es un grupo heurístico de series de glifos. Los dos sistemas de coordenadas no comparten nada, a menos que el texto que le entregue al sintetizador sea, byte por byte, el texto a partir del cual se calcularon los cuadros de palabras. Esa es la regla uno, y es implacable. Normalice los espacios en blanco, elimine los guiones blandos o, de alguna otra manera, "limpie" el texto extraído antes de pronunciarlo, y cada desplazamiento posterior será silenciosamente incorrecto. Pronuncie exactamente lo que extrajo, o mantenga una tabla de remapeo de desplazamientos explícita. No hay una tercera opción que sobreviva en documentos reales
La tabla de remapeo no es un caso extremo hipotético. En el momento en que su interfaz de usuario inserta el anuncio de una página hablada ("página cinco") o expande una abreviatura para el sintetizador, la cadena hablada difiere de la cadena extraída. Registre la posición y la longitud de cada inserción, luego reste el ajuste acumulado antes de cada llamada de seguimiento. Son tal vez veinte líneas de contabilidad (bookkeeping), y marcan la diferencia entre un resaltado que sobrevive a la próxima solicitud de función y uno que se rompe la primera vez que alguien solicita títulos hablados
Lo que le da un cuadro de palabra
Cada registro TPdfWordBox lleva el texto de la palabra, su StartIndex (índice de inicio) y Count (conteo) de caracteres dentro del texto de la página, un Rect en el espacio de la página y el número de Page (página) basado en 1. El campo StartIndex es el puente entre los dos sistemas de coordenadas: es el mismo desplazamiento que SAPI devolverá mientras lee. PageWordBoxes devuelve la matriz completa para la página activa:
procedure TReaderForm.PreparePage(PageNo: Integer);
begin
PdfView.PageNumber := PageNo; // los cuadros de palabras de la vista siguen su página mostrada
FWords := PdfView.PageWordBoxes;
FPageText := BuildSpeechText(FWords); // concatenar Word.Text en orden
if Length(FWords) = 0 then
HandleImageOnlyPage(PageNo); // un escaneo sin capa de texto
end;
El comentario sobre el ordenamiento es fundamental para soportar la carga. El PageWordBoxes del visor convierte en tokens (tokenizes) la capa de texto de la página que la vista muestra actualmente, así que navegue por la vista primero y extraiga después; no se requiere renderizado, solo un documento abierto. (El componente de documento, TPdf, expone su propio PageWordBoxes vinculado a Pdf.PageNumber para uso sin interfaz (headless). Los dos números de página son independientes, lo que es su propia trampa). Un resultado vacío en una página que visiblemente lleva contenido significa un escaneo solo de imagen. Envíelo a OCR, o al menos anúncielo ("la página 4 no contiene texto legible"), en lugar de dejar que la voz se quede en silencio sin ninguna explicación
Conectar los límites de palabras de SAPI al rastreador
TrackReadingWordAt, en el visor, es la bisagra de toda la función. Déle un número de página y un índice de carácter; encuentra el cuadro de la palabra que contiene ese carácter, pinta el cursor de lectura en él y devuelve el índice de la palabra, o −1 cuando el índice cae entre palabras. La notificación de límite de palabra de SAPI proporciona exactamente la posición del carácter que desea:
procedure TReaderForm.OnSpeechWordBoundary(StreamPos: Integer);
var
WordIdx: Integer;
begin
// Asigna el desplazamiento a un cuadro de palabra y mueve el resaltado en una sola llamada
WordIdx := PdfView.TrackReadingWordAt(FPageNo, StreamPos);
if WordIdx < 0 then
Exit; // el límite cayó fuera de cualquier palabra: mantenga el último resaltado
end;
Dos detalles defensivos valen la pena aquí. En primer lugar, TrackReadingWordAt mantiene su propia memoria caché del cuadro de palabras para la página rastreada, reconstruida automáticamente cuando cambia la página, de modo que el costo por límite se mantiene plano sin importar qué tan rápido lleguen los límites. En segundo lugar, no verifica los límites con generosidad. Un índice igual o mayor al recuento de caracteres de la página devuelve −1 en lugar de limitarse (clamping) a la última palabra. Trate el −1 como "mantener el resaltado anterior", nunca como un error, porque las secuencias de puntuación y los espacios en blanco entre palabras producen legítimamente límites que no pertenecen a ninguna palabra. Registrar cada −1 lo sepultará. Cuéntelos por página en su lugar, y mire detenidamente cualquier página donde la proporción se dispare, ya que eso generalmente significa una falta de coincidencia en la normalización del texto en la regla uno
El cursor en sí: color, seguimiento y limpieza
SetReadingWord pinta el resaltado directamente cuando usted mismo sostiene el cuadro de la palabra, ReadingWordColor le da estilo y ReadingWordFollow := True desplaza la vista lo suficiente como para mantener visible la palabra hablada. Esa última propiedad se gana su lugar. Un desplazamiento "centrar la palabra actual" hecho a mano hace que la página se sacuda en cada salto de línea, y los lectores sensibles al movimiento apagarán toda la función en un minuto. El resaltado se renderiza solo en la página que se muestra actualmente en el TPdfView activo, por lo que la lectura de varias páginas tiene que avanzar PageNumber al ritmo de la voz, y luego volver a ejecutar el paso de preparación para la nueva página antes de que llegue su primer evento de límite. Omita eso y los primeros resaltados en cada página apuntarán a coordenadas obsoletas
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // detenga primero la reproducción de SAPI
PdfView.ClearReadingWord; // luego quite el resaltado; un cursor obsoleto se percibe como un error
end;
La simetría al apagar es lo que mantiene honesto al resaltado. Toda ruta de pausa, parada y pase de página debe terminar en ClearReadingWord. Omítalo y un rectángulo ámbar se sentará en una página detenida luciendo exactamente como un defecto, que es el tipo de cosa que todos los probadores (testers) reportarán aunque en realidad no haya nada roto
La velocidad de la voz ejerce más presión sobre esta secuencia que el tamaño del documento. A 300 palabras por minuto, los eventos de los límites llegan cada 200 ms, y en las velocidades de SAPI más rápidas, llegan más rápido de lo que el ojo sigue cómodamente. La respuesta correcta es fusionar (coalesce), no poner en cola (queue). Si llega un nuevo límite mientras una actualización de resaltado aún está pendiente, descarte la anterior y pinte la más reciente. Un cursor que visita cada palabra en orden pero que se retrasa medio segundo se siente roto; uno que ocasionalmente omite una palabra mientras se mantiene sincronizado con la voz no lo hace
Casos extremos que separan a las demostraciones de los productos
Algunas categorías de documentos exponen las costuras. Los caracteres de combinación (combining characters) son los más sutiles: las secuencias de Unicode, como una letra base más un diacrítico de combinación, pueden ocupar más índices de caracteres de lo que sugiere la palabra visual, de modo que cualquier aritmética de desplazamiento que asuma un índice por glifo se desvía (drifts) lentamente. Ese es el argumento más sólido para permitir que TrackReadingWordAt posea el mapeo en lugar de calcular los números de palabras a mano. La separación de sílabas (hyphenation) es más mundana pero más común: una palabra dividida en un salto de línea se convierte en dos cuadros, y si la pronuncia como un solo token, el evento de límite de su segunda mitad se resuelve en el primer cuadro. Eso generalmente está bien, pero es una decisión, así que tómela a propósito en lugar de descubrirla. El etiquetado (tagging) cambia el orden de lectura en sí. Cuando un documento lleva etiquetas de estructura adecuadas (el territorio de ISO 14289, PDF/UA), la secuencia de palabras sigue la estructura lógica; sin ellas, vuelve a las heurísticas de diseño, y una página sin etiquetar de dos columnas puede leerse directamente a través de ambas columnas. Las páginas rotadas son el último caso común: el Rect de cada palabra todavía la limita correctamente en el espacio de la página, pero una política de seguimiento de la ventana gráfica (viewport) ajustada para el flujo horizontal se desplaza de manera discordante cuando el texto se ejecuta verticalmente, así que mantenga al menos un documento rotado en el conjunto de regresión. Para el manejo del orden de lectura, las unidades a nivel de oración mediante ReadingUnits y la pila de asistencia más amplia, consulte la construcción de un lector de PDF accesible en Delphi
Una restricción de plataforma da forma a la implementación. SAPI es solo para Windows. El cuadro de palabras y la API de seguimiento son, byte por byte, idénticos en Lazarus y FPC, pero las compilaciones de Linux y macOS necesitan un sintetizador diferente conectado detrás de los mismos eventos de límites; esa configuración se trata en la ejecución del visor en Lazarus y FPC. El costo del resaltado también interactúa con el caché de su página una vez que suben las velocidades de voz, y la aritmética del presupuesto en render caching and zoom performance se traslada aquí sin cambios
Cuando el resaltado de una sola palabra es la granularidad incorrecta
El karaoke a nivel de palabra no siempre es lo que un lector quiere. A altas velocidades de voz, el cursor parpadeando palabra por palabra se convierte en su propio ruido visual, y algunos oyentes siguen una oración de manera más cómoda que una luz estroboscópica de palabras sueltas. Para ese caso, el componente expone una unidad más gruesa. ReadingUnits devuelve unidades a nivel de oración y bloque, cada una con sus propios rectángulos de resaltado, y usted los pinta con SetReadingHighlight en lugar de SetReadingWord. El cableado tiene la misma forma: un desplazamiento de límite aún determina qué unidad se ilumina, pero la unidad que resalta abarca una cláusula o una línea en lugar de un solo token. Tanto los lectores más lentos como la reproducción de alta velocidad tienden a preferirlo, y nada le impide ofrecer ambos modos detrás de una configuración
Vale la pena definir las versiones mínimas antes de compilar esto: los cuadros de palabras necesitan PDFium Component v1.53 o posterior, y el cursor de seguimiento necesita v1.56. La API de lectura completa, las unidades a nivel de oración y una demostración de lectura en voz alta que funciona se encuentran en la página del producto para PDFium Component