Um recurso de leitura em voz alta tem um trabalho visível além da voz: conforme cada palavra é falada, ele deve iluminar essa palavra na página e mantê-la à vista. Para fazer isso você precisa da caixa delimitadora de cada palavra, indexada ao mesmo fluxo de caracteres de onde o motor de fala está lendo. Pegue as caixas mas erre a indexação e o destaque deriva uma palavra ou duas atrás do áudio; pegue a indexação mas manuseie mal o estado da página e o destaque pousa na página errada inteiramente. A parte da fala disso, o próprio sintetizador, é a parte que raramente quebra. SAPI relata limites de palavra ao caractere. O que quebra é a fina camada de mapeamento entre um deslocamento de caractere no buffer de fala e um retângulo na página renderizada
PDFium Component envia esse mapeamento para Delphi, C++Builder e Lazarus, com caixas de palavra disponíveis desde v1.53 e o cursor de rastreamento desde v1.56. A superfície é deliberadamente estreita: uma chamada que retorna as caixas de palavra para uma página, um rastreador que transforma um deslocamento de caractere em um destaque pintado, e um par de propriedades para cor e rolagem automática. Estreita que seja, a ordem em que você chama as coisas decide se o recurso funciona, e a maioria das falhas abaixo vem de chamar as funções certas na sequência errada
Caracteres não são palavras, e motores TTS falam em caracteres
Um motor de fala consome uma string plana e relata progresso como posições de caractere dentro dessa string. Uma página PDF tem glifos colocados no espaço da página, onde uma "palavra" é um cluster heurístico de runs de glifo. Os dois sistemas de coordenada não compartilham nada a menos que o texto que você entrega ao sintetizador seja byte-a-byte o texto de onde as caixas de palavra foram computadas. Essa é a regra um, e ela é implacável. Normalize espaços em branco, remova hífens suaves, ou de outra forma "limpe" o texto extraído antes de falá-lo, e todo deslocamento a jusante está silenciosamente errado. Fale exatamente o que você extraiu, ou mantenha uma tabela explícita de remapeamento de deslocamento. Não há terceira opção que sobreviva a documentos reais
A tabela de remapeamento não é um caso de borda hipotético. O momento em que sua UI insere um anúncio de página falada ("página cinco") ou expande uma abreviação para o sintetizador, a string falada diverge da extraída. Registre a posição e comprimento de cada inserção, então subtraia o ajuste acumulado antes de toda chamada de rastreamento. É talvez vinte linhas de contabilidade, e é a diferença entre um destaque que sobrevive à próxima solicitação de recurso e um que quebra na primeira vez que alguém pede cabeçalhos falados
O que uma caixa de palavra lhe dá
Cada registro TPdfWordBox carrega o texto da palavra, seu StartIndex e Count de caracteres dentro do texto da página, um Rect no espaço da página, e o número de Page base-1. O campo StartIndex é a ponte entre os dois sistemas de coordenada: é o mesmo deslocamento que SAPI devolverá conforme lê. PageWordBoxes retorna o array completo para a página ativa:
procedure TReaderForm.PreparePage(PageNo: Integer);
begin
PdfView.PageNumber := PageNo; // as caixas de palavra da view seguem a página que ela exibe
FWords := PdfView.PageWordBoxes;
FPageText := BuildSpeechText(FWords); // concatena Word.Text em ordem
if Length(FWords) = 0 then
HandleImageOnlyPage(PageNo); // um scan sem camada de texto
end;
O comentário de ordenação é portador de carga. O PageWordBoxes do visualizador tokeniza a camada de texto da página que o visualizador exibe atualmente, então navegue o visualizador primeiro e extraia segundo; nenhuma renderização é requerida, apenas um documento aberto. (O componente de documento, TPdf, expõe seu próprio PageWordBoxes indexado por Pdf.PageNumber para uso headless. Os dois números de página são independentes, o que é sua própria armadilha.) Um resultado vazio em uma página que visivelmente carrega conteúdo significa um scan apenas-imagem. Roteie-o para OCR, ou pelo menos anuncie-o ("a página 4 não contém texto legível"), em vez de deixar a voz cair em silêncio sem explicação
Conectando limites de palavra SAPI ao rastreador
TrackReadingWordAt, no visualizador, é a dobradiça de todo o recurso. Dê a ele um número de página e um índice de caractere; ele encontra a caixa de palavra que contém aquele caractere, pinta o cursor de leitura nela, e retorna o índice da palavra, ou −1 quando o índice cai entre palavras. A notificação de limite de palavra do SAPI fornece exatamente a posição de caractere que ele quer:
procedure TReaderForm.OnSpeechWordBoundary(StreamPos: Integer);
var
WordIdx: Integer;
begin
// Mapeia o deslocamento para uma caixa de palavra e move o destaque em uma chamada
WordIdx := PdfView.TrackReadingWordAt(FPageNo, StreamPos);
if WordIdx < 0 then
Exit; // o limite caiu fora de qualquer palavra: mantenha o último destaque
end;
Dois detalhes defensivos ganham seu lugar aqui. Primeiro, TrackReadingWordAt mantém seu próprio cache de caixa-de-palavra para a página rastreada, reconstruído automaticamente quando a página muda, então o custo por limite permanece plano não importa quão rápido os limites cheguem. Segundo, ele não checa limites generosamente. Um índice no ou além da contagem de caracteres da página retorna −1 em vez de fixar na palavra final. Trate −1 como "mantenha o destaque anterior", nunca como um erro, porque runs de pontuação e espaço em branco entre palavras legitimamente produzem limites que pertencem a nenhuma palavra. Registrar todo −1 o enterrará. Conte-os por página em vez disso, e olhe duro para qualquer página onde a razão pica, já que isso geralmente significa uma incompatibilidade de normalização de texto de volta na regra um
O cursor em si: cor, seguir e limpeza
SetReadingWord pinta o destaque diretamente quando você mesmo segura a caixa de palavra, ReadingWordColor o estiliza, e ReadingWordFollow := True rola o visualizador apenas o suficiente para manter a palavra falada visível. Essa última propriedade ganha seu lugar. Uma rolagem "centralizar a palavra atual" feita à mão faz a página dar um soltão em cada quebra de linha, e leitores sensíveis a movimento desligarão todo o recurso dentro de um minuto. O destaque renderiza apenas na página atualmente mostrada no TPdfView ativo, então leitura multi-página tem que avançar PageNumber em passo com a fala, então re-executar o passo de preparação para a nova página antes que seu primeiro evento de limite pouse. Pule isso e os primeiros destaques em cada página apontam para coordenadas obsoletas
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // interrompa a reprodução SAPI primeiro
PdfView.ClearReadingWord; // depois remova o destaque; um cursor obsoleto parece um bug
end;
Simetria no desligamento é o que mantém o destaque honesto. Todo caminho de pausa, parada e virada-de-página tem que terminar em ClearReadingWord. Deixe-o de fora e um retângulo âmbar senta em uma página parada parecendo exatamente um defeito, que é o tipo de coisa que todo testador reportará mesmo nada estando realmente quebrado
A taxa de fala estressa esse pipeline mais que o tamanho do documento. A 300 palavras por minuto os eventos de limite chegam a cada 200 ms, e nas taxas mais rápidas do SAPI eles vêm mais rápido do que o olho confortavelmente rastreia. A resposta certa é coalescer, não enfileirar. Se um novo limite chega enquanto uma atualização de destaque ainda está pendente, descarte a obsoleta e pinte a mais recente. Um cursor que visita cada palavra em ordem mas fica atrás meio segundo parece quebrado; um que ocasionalmente pula uma palavra enquanto permanece em sincronia com a voz não
Casos de borda que separam demos de produtos
Algumas categorias de documento expõem as costuras. Caracteres combinantes são os mais sutis: sequências Unicode como uma letra base mais um diacrítico combinante podem ocupar mais índices de caractere do que a palavra visual sugere, então qualquer aritmética de deslocamento que assume um índice por glifo lentamente deriva. Esse é o argumento mais forte para deixar TrackReadingWordAt ser dono do mapeamento em vez de computar números de palavra à mão. Hifenização é mais mundana mas mais comum: uma palavra quebrada através de uma quebra de linha se torna duas caixas, e se você a fala como um único token, o evento de limite para sua segunda metade resolve para a primeira caixa. Isso geralmente é bom, mas é uma decisão, então tome-a de propósito em vez de descobri-la. Marcação muda a própria ordem de leitura. Quando um documento carrega tags de estrutura próprias (o território de ISO 14289, PDF/UA), o sequenciamento de palavras segue a estrutura lógica; sem elas ele cai para heurísticas de layout, e uma página de duas colunas sem tags pode ler direto através de ambas as colunas. Páginas rotacionadas são a última comum: o Rect de cada palavra ainda a limita corretamente no espaço da página, mas uma política de seguir-viewport ajustada para fluxo horizontal rola de forma chocante quando o texto corre verticalmente, então mantenha pelo menos um documento rotacionado no conjunto de regressão. Para manuseio de ordem de leitura, unidades de nível de sentença via ReadingUnits, e a pilha assistiva mais ampla, veja construindo um leitor de PDF acessível em Delphi
Uma restrição de plataforma molda a implantação. SAPI é apenas-Windows. O API de caixa-de-palavra e rastreamento é byte-a-byte idêntico sob Lazarus e FPC, mas builds Linux e macOS precisam de um sintetizador diferente conectado por trás dos mesmos eventos de limite; essa configuração é coberta em executando o visualizador sob Lazarus e FPC. Custo de destaque também interage com seu cache de página uma vez que taxas de fala sobem, e a aritmética de orçamento em cache de render e desempenho de zoom carrega para cá sem mudança
Quando destacar palavra-única é a granularidade errada
Karaoke de nível de palavra nem sempre é o que um leitor quer. Em taxas de fala altas o cursor piscando palavra por palavra se torna seu próprio ruído visual, e alguns ouvintes seguem uma sentença mais confortavelmente do que um estrobe de palavras únicas. Para esse caso o componente expõe uma unidade mais grossa. ReadingUnits retorna unidades de nível sentença e bloco, cada uma com seus próprios retângulos de destaque, e você as pinta com SetReadingHighlight em vez de SetReadingWord. A conexão é da mesma forma: um deslocamento de limite ainda dirige qual unidade acende, mas a unidade que você destaca abrange uma cláusula ou uma linha em vez de um único token. Leitores mais lentos e playback de alta taxa ambos tendem a preferi-lo, e nada impede você de oferecer ambos os modos por trás de uma configuração
Os pisos de versão valem a pena fixar antes de você construir contra isso: caixas de palavra precisam de PDFium Component v1.53 ou posterior, e o cursor de rastreamento precisa de v1.56. O API de leitura completo, as unidades de nível sentença, e uma demo de leitura-em-voz-alta funcional estão na página do produto para PDFium Component