Um recurso de leitura em voz alta tem um trabalho visível além da voz: à medida que cada palavra é falada, ele deve iluminar aquela palavra na página e mantê-la em vista. Para fazer isso você precisa da caixa delimitadora de cada palavra, indexada ao mesmo fluxo de caracteres do qual o mecanismo de fala está lendo. Obtenha as caixas mas erre a indexação e o destaque deriva/drifts uma ou duas palavras atrás do áudio; obtenha a indexação mas lide mal com o estado da página e o destaque pousa/lands inteiramente na página errada. A parte de fala disso, o sintetizador em si, é a parte que raramente quebra. O SAPI relata os limites da palavra até o caractere. O que quebra é a fina camada de mapeamento entre um deslocamento/offset de caractere no buffer de fala e um retângulo na página renderizada
O Componente PDFium distribui/ships esse mapeamento para Delphi, C++Builder e Lazarus, com as caixas de palavras 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 palavras para uma página, um rastreador que transforma um deslocamento/offset de caractere em um destaque pintado, e um par/couple de propriedades para cor e auto-scroll. Estreita como ela é, a ordem em que você chama as coisas decide se o recurso funciona, e a maioria das falhas abaixo vêm de chamar as funções certas na sequência errada
Caracteres não são palavras, e mecanismos de TTS falam em caracteres
Um mecanismo de fala consome uma string plana/flat e relata o progresso como posições de caracteres dentro daquela string. Uma página PDF tem glifos colocados no espaço da página, onde uma "palavra" é um cluster heurístico de corridas/runs de glifos. Os dois sistemas de coordenadas não compartilham nada a menos que o texto que você entregue/hand ao sintetizador seja byte-por-byte o texto a partir do qual as caixas de palavras foram computadas. Essa é a regra um, e ela é implacável/unforgiving. Normalize o espaço em branco, remova/strip hifens suaves/soft, ou de outra forma "limpe" o texto extraído antes de falá-lo, e todo o deslocamento/offset downstream está silenciosamente errado. Fale exatamente o que você extraiu, ou mantenha/keep uma tabela de remapeamento de deslocamento explícita. Não há terceira opção que sobreviva a documentos reais
A tabela de remapeamento não é um caso limite/edge hipotético. O momento em que a sua IU/UI insere um anúncio de página falado ("página cinco") ou expande uma abreviação para o sintetizador, a string falada diverge da extraída. Registre a posição e o comprimento de cada inserção, e então subtraia o ajuste acumulado antes de cada chamada de rastreamento. São talvez vinte linhas de contabilidade/bookkeeping, e essa é 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 por cabeçalhos falados
O que uma caixa de palavras lhe dá
Cada registro TPdfWordBox carrega o texto da palavra, o seu StartIndex e a contagem/Count de caracteres dentro do texto da página, um Rect no espaço da página e o número da página baseado em 1. O campo StartIndex é a ponte entre os dois sistemas de coordenadas: ele é o mesmo deslocamento/offset que o SAPI entregará de volta à medida que lê. PageWordBoxes retorna o array completo para a página ativa:
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;
O comentário de ordenação é de sustentação/load-bearing. O PageWordBoxes do visualizador tokeniza/tokenizes a camada de texto da página que a visualização exibe atualmente, então navegue a visualização primeiro e extraia em segundo lugar; nenhuma renderização é necessária, apenas um documento aberto. (O componente de documento, TPdf, expõe o seu próprio PageWordBoxes chaveado ao Pdf.PageNumber para uso sem tela/headless. Os dois números de página são independentes, o que é a sua própria armadilha/trap.) Um resultado vazio em uma página que visivelmente carrega conteúdo significa uma digitalização apenas de imagem. Roteie-a para o OCR, ou pelo menos anuncie isso ("a página 4 não contém nenhum texto legível"), em vez de deixar a voz ficar/fall silenciosa sem nenhuma explicação
Ligando limites de palavras do SAPI ao rastreador
TrackReadingWordAt, no visualizador, é a dobradiça/hinge de todo o recurso. Dê a ele um número de página e um índice de caractere; ele encontra a caixa de palavra contendo aquele caractere, pinta o cursor de leitura nele, 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 do caractere que ele quer:
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;
Dois detalhes defensivos ganham o seu sustento/earn their keep aqui. Primeiro, TrackReadingWordAt mantém o seu próprio cache de caixas-de-palavras para a página rastreada, reconstruído automaticamente quando a página muda, então o custo por-limite permanece plano/flat não importa quão rápido os limites cheguem. Segundo, ele não faz a verificação de limites/bounds-check generosamente. Um índice no ou além do a contagem/Count de caracteres da página retorna −1 em vez de limitar/clamping à palavra final. Trate o −1 como "mantenha o destaque anterior," nunca como um erro, porque corridas/runs de pontuação e o espaço em branco entre-palavras legitimamente produzem limites que não pertencem a nenhuma palavra. Fazer o log de cada −1 o enterrará. Conte-os por página em vez disso, e olhe atentamente/hard para qualquer página onde a proporção/ratio dê um pico/spikes, uma vez que isso geralmente significa uma incompatibilidade/mismatch de normalização de texto de volta à regra um
O próprio cursor: cor, acompanhar (follow) e limpeza
SetReadingWord pinta o destaque diretamente quando você segura/hold a caixa de palavra você mesmo/yourself, o ReadingWordColor o estiliza, e o ReadingWordFollow := True rola/scrolls a visualização apenas o suficiente para manter a palavra falada visível. Aquela/That última propriedade ganha o seu lugar. Uma rolagem/scroll feita à mão/hand-rolled "centralize a palavra atual" faz a página dar trancos/lurch em cada quebra de linha, e leitores sensíveis ao movimento desligarão todo o recurso dentro de um minuto. O destaque renderiza apenas na página atualmente mostrada no TPdfView ativo, então a leitura multi-página tem que avançar o PageNumber em passo com a fala, e então reexecutar/re-run o passo de preparação para a nova página antes que o seu primeiro evento de limite pouse/lands. Pule isso e os primeiros poucos destaques em cada página apontam para coordenadas obsoletas/stale
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // halt SAPI playback first
PdfView.ClearReadingWord; // then remove the highlight; a stale cursor reads as a bug
end;
A 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 como um defeito, o que é o tipo de coisa que cada testador irá arquivar/file mesmo que nada esteja realmente quebrado
A taxa de fala estressa esse pipeline mais fortemente/harder do 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 rastreia confortavelmente. A resposta certa é coalescer, não enfileirar. Se um novo limite chega enquanto uma atualização de destaque ainda está pendente, descarte/drop o obsoleto/stale e pinte o mais recente. Um cursor que visita cada palavra em ordem mas atrasa/lags meio segundo parece quebrado; um que ocasionalmente pula/skips uma palavra enquanto fica/staying em sincronia com a voz não parece
Casos limite (edge cases) que separam demonstrações de produtos
Algumas poucas categorias de documento expõem as costuras/seams. Os caracteres de combinação são os mais sutis: sequências Unicode como uma letra base mais um diacrítico de combinação podem ocupar mais índices de caractere do que a palavra visual sugere, então qualquer aritmética de deslocamento/offset que assuma um índice por glifo deriva/drifts lentamente. Esse é o argumento mais forte para deixar o TrackReadingWordAt possuir/own o mapeamento em vez de computar/computing números de palavras à mão. A 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 token único, o evento de limite para a sua segunda metade se resolve para a primeira caixa. Isso geralmente está bom/fine, mas é uma decisão, então tome-a/make it de propósito em vez de descobri-la. A marcação/Tagging muda a própria ordem de leitura. Quando um documento carrega tags de estrutura adequadas (o território da ISO 14289, PDF/UA), o sequenciamento de palavras segue a estrutura lógica; sem elas ele recai/falls back para heurísticas de layout, e uma página sem tags/untagged de duas colunas 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 acompanhar/follow o viewport ajustada para o fluxo horizontal rola de forma chocante/jarringly quando o texto corre verticalmente, então mantenha pelo menos um documento rotacionado no conjunto de regressão. Para o tratamento da 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 no Delphi
Uma restrição de plataforma molda a implantação. O SAPI é apenas-Windows. A caixa-de-palavras e a API de rastreamento são idênticas byte-por-byte no Lazarus e no FPC, mas builds para Linux e macOS precisam de um sintetizador diferente ligado atrás dos mesmos eventos de limite; essa configuração é coberta em executando o visualizador sob Lazarus e FPC. O custo do destaque também interage com o seu cache de página uma vez que as taxas de fala sobem, e a aritmética de orçamento em cache de renderização e desempenho de zoom é transferida para cá sem alterações
Quando o destaque de palavra-única (single-word) é a granularidade errada
O karaokê no nível de palavra não é sempre o que um leitor quer. Em altas taxas de fala, o cursor piscando/flickering palavra por palavra se torna o seu próprio ruído visual, e alguns ouvintes acompanham uma sentença mais confortavelmente do que um estroboscópio de palavras únicas. Para aquele caso o componente expõe uma unidade mais grossa/coarser. O ReadingUnits retorna unidades no nível de sentença e bloco, cada uma com os seus próprios retângulos de destaque, e você os pinta com o SetReadingHighlight em vez de SetReadingWord. A forma é a mesma: um deslocamento/offset de limite ainda direciona qual unidade se ilumina, mas a unidade que você destaca abrange/spans uma cláusula ou uma linha em vez de um token único. Leitores mais lentos e a reprodução de alta taxa ambos tendem a preferi-la, e nada o impede de oferecer ambos os modos atrás de uma configuração
Vale a pena fixar os pisos/floors de versão antes que você construa contra isso: as caixas de palavras precisam do Componente PDFium v1.53 ou posterior, e o cursor de rastreamento precisa do v1.56. A API de leitura completa, as unidades no nível de sentença, e uma demonstração de leitura em voz alta funcionando estão na página do produto para o Componente PDFium