Una función de lectura en voz alta tiene una tarea visible más allá de la propia voz: a medida que se pronuncia cada palabra, debe iluminar esa palabra en la página y mantenerla a la vista. Para conseguirlo, es necesario contar con el cuadro delimitador de cada palabra, indexado al mismo flujo de caracteres que está leyendo el motor de voz. Si obtiene los cuadros pero se equivoca en la indexación, el resaltado se atrasará una o dos palabras respecto al audio; si logra la indexación pero gestiona mal el estado de la página, el resaltado terminará por completo en una página equivocada. La parte del habla, el sintetizador en sí mismo, es la pieza que raramente se descompone. SAPI notifica los límites de las palabras al nivel del carácter. Lo que verdaderamente falla es esa fina capa de mapeo entre un desplazamiento de carácter en el búfer de voz y un rectángulo en la página renderizada
PDFium Component incluye este mapeo para Delphi, C++Builder y Lazarus, disponiendo de cuadros de palabras (word boxes) desde la versión 1.53 y del cursor de seguimiento (tracking cursor) desde la versión 1.56. Su superficie es deliberadamente acotada: una llamada que devuelve los cuadros de palabras de una página, un rastreador que convierte un desplazamiento de carácter en un resaltado pintado, y un par de propiedades referentes a color y desplazamiento automático. Pese a lo estrecha que es, el orden en el que se efectúan las llamadas determina si la función operará, y la gran mayoría de los fallos detallados a continuación provienen de invocar las funciones correctas en una secuencia equivocada
Los caracteres no son palabras, y los motores TTS hablan en caracteres
Un motor de voz consume una cadena plana e informa sobre su progreso como posiciones de caracteres en dicha cadena. Una página PDF contiene glifos ubicados en el espacio de la página, donde una "palabra" es un agrupamiento heurístico de tramos de glifos. Estos dos sistemas de coordenadas no comparten nada en común a menos que el texto que se le facilite al sintetizador equivalga byte a byte al texto del cual se calcularon los cuadros de palabras. Esa es la primera regla, y no perdona. Si normaliza los espacios en blanco, elimina los guiones opcionales o de algún otro modo "limpia" el texto extraído antes de dictarlo, cada desplazamiento resultante estará silenciosamente equivocado. Usted debe dictar exactamente lo que extrajo o, en su defecto, llevar una tabla de remapeo de desplazamientos explícita. No existe una tercera alternativa que sobreviva a documentos reales
La tabla de remapeo no constituye un caso extremo e hipotético. En el instante en que su interfaz de usuario inserte un anuncio oral de página ("página cinco") o despliegue una abreviatura para el sintetizador, la cadena oral divergirá de la extraída. Registre la posición y longitud de cada inserción, y luego reste el ajuste acumulado antes de cualquier llamada de seguimiento. Podrán ser quizás unas veinte líneas de contabilidad, pero significan la diferencia entre un resaltado que sobreviva a la siguiente solicitud de características y uno que colapse la primera vez que alguien pida encabezados leídos en voz alta
Lo que le ofrece un cuadro de palabras
Cada registro TPdfWordBox contiene el texto de la palabra, su StartIndex (índice de inicio) y el 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 página (Page) empezando desde 1. El campo StartIndex sirve de puente entre los dos sistemas de coordenadas: es el mismo desplazamiento que SAPI devolverá al ir leyendo. PageWordBoxes arroja el arreglo completo de la página activa:
procedure TReaderForm.PreparePage(PageNo: Integer);
begin
PdfView.PageNumber := PageNo; // the view's word boxes track its displayed page
FWords := PdfView.PageWordBoxes;
FPageText := BuildSpeechText(FWords); // concatenate Word.Text in order
if Length(FWords) = 0 then
HandleImageOnlyPage(PageNo); // a scan with no text layer
end;
El apunte sobre el orden posee importancia vital. PageWordBoxes del visor convierte en tokens (tokenizes) la capa de texto de la página que se halla mostrando el visor actualmente, de manera que navegue por la vista primero y extraiga después; no es menester ninguna renderización, solo un documento abierto. (El componente de documento, TPdf, expone su propio PageWordBoxes vinculado a Pdf.PageNumber para un uso en segundo plano, sin interfaz gráfica. Los dos números de página resultan independientes, lo cual constituye una trampa en sí mismo). Un resultado vacío sobre una página que alberga contenido a simple vista presupone un escaneo puramente de imagen. Rediríjalo a un OCR, o cuando menos anúncielo ("la página 4 no contiene texto legible"), preferible a permitir que la voz enmudezca sin mediar explicación
Conexión de los límites de palabras de SAPI al rastreador
TrackReadingWordAt, en el visor, es la bisagra de toda la función. Facilítele un número de página y un índice de caracteres; encontrará el cuadro de palabras que contiene a ese carácter, delineará allí el cursor de lectura y retornará el índice de la palabra, o −1 cuando el índice recaiga entre palabras. La notificación de límites de palabras emanada por SAPI subministra exactamente la posición del carácter que precisa:
procedure TReaderForm.OnSpeechWordBoundary(StreamPos: Integer);
var
WordIdx: Integer;
begin
// Maps the offset to a word box and moves the highlight in one call
WordIdx := PdfView.TrackReadingWordAt(FPageNo, StreamPos);
if WordIdx < 0 then
Exit; // boundary fell outside any word: keep last highlight
end;
Son dos los detalles defensivos que justifican su cometido aquí. Primero, TrackReadingWordAt guarda su propia memoria caché del cuadro de palabras referida a la página rastreada, reconstruida de forma automática cada que la página muda, por ende el coste por límite se sostiene plano sin importar la rapidez con que arriben los topes. Segundo, no se prodiga en comprobar los límites holgadamente. Un índice radicado en o más allá del recuento de caracteres de la página arrojará −1 antes que estancarse en la palabra conclusiva. Acepte a −1 bajo el concepto "mantener el resaltado anterior", en ningún momento como un error, atento a que secuencias de puntuación y espaciados de transición entre términos generan lícitamente lindes excluidos de pertenecer a palabras en sí. Registrar y grabar en bitácora (logging) cada caso −1 le soterraría. Computarícelos preferiblemente al margen de la página, e inspeccione detenidamente cada hoja donde la proporción repunte, al estar presuponiendo generalmente eso un desajuste de normalización del texto remontable a la regla número uno
El cursor mismo: color, seguimiento y limpieza
SetReadingWord pinta el resaltado de un modo directo al ser usted mismo el tenedor del cuadro de palabras, ReadingWordColor lo estiliza, al par que ReadingWordFollow := True mueve el desplazamiento apenas el trecho justo a preservar la palabra pronunciada avistada. Esa rematada propiedad adquiere con honores su puesto. Un movimiento por scroll artesanal tipo "centrar la actual palabra" obligará a la plana a convulsionarse sobre el lomo de cada quiebro de línea, en tanto que un auditorio predispuesto al mareo desactivaría en el lapso de un minuto la función integra. La pintura resaltada relucirá nada más sobre la página visualizada a la sazón bajo el TPdfView reinante, concluyendo que la declamación esparcida a lo largo de hojas múltiples deberá franquear PageNumber a pulso con el habla, e iterar la operación anticipatoria consagrada a la página naciente antes que aterrice su primer acaecimiento demarcatorio. Desatienda este imperativo, y se hallará con que los resaltados iniciales sobre toda nueva plana recalarán bajo coordenadas apócrifas y marchitas
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // halt SAPI playback first
PdfView.ClearReadingWord; // then remove the highlight; a stale cursor reads as a bug
end;
La simetría al apagar resulta garante del resaltado honesto. La suma de transcursos, ya fuere de pausas, detenciones y trayectos de páginas debe abocar irremediablemente en ClearReadingWord. Obvíelo, y un recuadro ámbar pervivirá insepulto sobre una hoja yerta aparentando, a simple luz, ser una pifia sin atenuantes, percance arquetípico que el contingente de probadores (testers) sin dudar anotaría si bien, en suma, el aparto no acusa estrago ninguno
El ritmo oral castiga a esta canalización con una crudeza mayor a la propiciada por la corpulencia de los documentos. Bajo tasas equivalentes a las 300 dicciones por intervalo de minuto, surgen advenimientos limítrofes a cada 200 ms, a tal punto que mediante ritmos SAPI expeditivos sobrevienen superando con creces la barrera óptica permisible a rastreos plácidos y cómodos. La idónea resolución radicaría en aglutinar, antes que almacenar a expensas de colar por orden. Al llegar un margen limítrofe nuevo no finiquitando de actualizar su iluminador predecesor, claudique frente al vetusto esparciendo a su reemplazante. Aquel rastro de avance deteniéndose bajo todo vocablo reglamentariamente, si padece un letargo cercano a los quinientos milisegundos parecerá arruinado; al contrario de su análogo predispuesto a saltear una de tantas dicciones preservando el ritmo dictado de su portavoz afín a las contingencias del caso, conservando vigencia e indemnidad
Casos extremos que separan a las demostraciones de los productos
Algunas categorías de documentos dejan al descubierto los puntos débiles. Los caracteres de combinación (combining characters) son los más sutiles: secuencias Unicode como una letra base más un diacrítico combinatorio 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 desfasará poco a poco. Ese es el argumento más sólido para permitir que TrackReadingWordAt se encargue del mapeo, en lugar de calcular los números de palabras a mano. La división con guiones (hyphenation) es más mundana pero más habitual: 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 apuntará al primer cuadro. Esto suele estar bien, pero es una decisión, así que tómela a propósito en lugar de descubrirla sobre la marcha. El etiquetado (tagging) modifica el propio orden de lectura. Cuando un documento tiene etiquetas de estructura adecuadas (el territorio de ISO 14289, PDF/UA), la secuencia de palabras sigue la estructura lógica; sin ellas, recae en heurísticas de diseño, y una página de dos columnas sin etiquetas puede llegar a leerse de corrido atravesando ambas columnas. Las páginas rotadas son el último caso frecuente: el Rect de cada palabra aún la delimita correctamente en el espacio de la página, pero una política de seguimiento del visor ajustada para un flujo horizontal resulta discordante cuando el texto discurre de manera vertical, de modo que conserve al menos un documento rotado en su conjunto de regresión. Para el manejo del orden de lectura, las unidades a nivel de oración mediante ReadingUnits y todo el ecosistema de asistencia más amplio, consulte crear un lector de PDF accesible en Delphi
Hay una limitación de la plataforma que condiciona la implementación. SAPI es exclusivo de Windows. La API de seguimiento y cuadros de palabras resulta byte a byte idéntica en Lazarus y FPC, mas las compilaciones orientadas a Linux y macOS demandarán que un sintetizador distinto se conecte detrás de esos mismos eventos demarcatorios; dicha configuración se aborda en ejecutar el visor en Lazarus y FPC. Los costos por procesamiento de los resaltados también colisionan con el caché de su página toda vez que se disparan las velocidades vocales, al tanto que los márgenes en cifras plasmados en rendimiento de zoom y almacenamiento en caché de renderizado resultan aquí igualmente extrapolables y sin mudanza
Cuando el resaltado de una sola palabra es la granularidad equivocada
El formato de karaoke palabra por palabra no siempre es lo que un lector desea. A velocidades de voz elevadas, el parpadeo del cursor palabra a palabra se convierte en ruido visual en sí mismo, y algunos oyentes siguen una oración con más comodidad que un parpadeo de palabras individuales aisladas. Para ese supuesto, el componente expone una unidad más general. ReadingUnits devuelve unidades a nivel de oraciones y bloques, cada una con sus propios rectángulos de resaltado, y usted las pinta con SetReadingHighlight en lugar de SetReadingWord. Las conexiones presentan la misma forma: un desplazamiento de límite sigue dictando qué unidad se ilumina, pero la unidad que se resalta abarca una cláusula o una línea en vez de un solo token. Tanto los lectores pausados como las reproducciones a alta velocidad suelen inclinarse por él, y nada le impide ofrecer ambos modos dentro de un ajuste
Conviene fijar las versiones base antes de compilar con esto: los cuadros de palabras precisan PDFium Component v1.53 o superior, y el cursor de seguimiento requiere la v1.56. Toda la API de lectura exhaustiva, las unidades a nivel de oración y una demostración operativa de lectura en voz alta obran a su alcance dentro de la página del producto para PDFium Component