La première démonstration de notre fonction de lecture à voix haute pour une application d’alphabétisation s’est bien passée pendant deux paragraphes. Puis la page a rencontré une lettrine, la voix a dit « Chapter » pendant que la surbrillance restait sur la ligne précédente, et en bas de page le curseur suivait l’audio avec trois mots de retard. La voix n’était jamais le problème : SAPI signalait les limites de mots avec précision. Le problème était la couche de correspondance entre les décalages de caractères dans le tampon vocal et les rectangles sur une page PDF rendue, et c’est cette couche qui décide de la survie de toute surbrillance façon karaoké. PDFium Component (boîtes de mots depuis la v1.53, tracker et curseur de surbrillance depuis la v1.56) fournit cette correspondance pour Delphi, C++Builder et Lazarus sous forme d’une petite API volontairement ciblée : boîtes de mots, tracker offset-vers-mot et curseur de surbrillance avec défilement automatique. Utilisée dans le bon ordre, elle est robuste ; utilisée dans le mauvais ordre, elle produit exactement la dérive que nous avons démontrée
Les caractères ne sont pas des mots, et les moteurs TTS parlent en caractères
Un moteur de synthèse vocale consomme une chaîne plate et signale sa progression sous forme de positions de caractères dans cette chaîne. Une page PDF, elle, contient des glyphes placés dans l’espace de page, où un « mot » est un regroupement heuristique de séries de glyphes. Les deux systèmes de coordonnées n’ont rien en commun, sauf si le texte que vous donnez au synthétiseur est, octet pour octet, le texte à partir duquel les boîtes de mots ont été calculées. C’est la première règle, et elle ne pardonne pas : normalisez les espaces, supprimez les traits d’union conditionnels ou « nettoyez » autrement le texte extrait avant de le lire, et tous les offsets suivants deviennent silencieusement faux. Lisez exactement ce que vous avez extrait, ou maintenez une table explicite de remappage des offsets ; il n’existe pas de troisième option qui survive aux documents réels
L’option de remappage n’est pas théorique. Si votre interface insère des annonces de page parlées (« page cinq ») ou développe des abréviations pour le synthétiseur, enregistrez la position et la longueur de chaque insertion, puis soustrayez l’ajustement cumulé avant chaque appel de suivi. C’est vingt lignes de comptabilité, et c’est la différence entre une surbrillance qui survit à l’évolution de la fonction et une autre qui casse la première fois que le produit demande des titres prononcés
Ce que donne une boîte de mot
Chaque enregistrement TPdfWordBox porte le texte du mot, son StartIndex et son Count de caractères dans le texte de page, un Rect en coordonnées de page, et le numéro Page basé sur 1. PageWordBoxes renvoie le tableau complet pour la page active :
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;
Le commentaire sur l’ordre est porteur : le PageWordBoxes de la visionneuse tokenise la couche texte de la page actuellement affichée par la vue ; naviguez donc d’abord dans la vue, puis extrayez, sans rendu nécessaire, seulement avec un document ouvert. (Le composant document propose son propre PageWordBoxes indexé par Pdf.PageNumber pour un usage sans interface.) Un résultat vide sur une page qui contient visiblement du contenu indique une numérisation image seule ; dirigez-la vers l’OCR ou ignorez-la à l’oral (« la page 4 ne contient aucun texte lisible ») plutôt que de laisser la voix se taire sans explication
Relier les limites de mots SAPI au tracker
TrackReadingWordAt, sur la visionneuse, est la charnière de toute la fonction : donnez-lui un numéro de page et un index de caractère, et elle trouve la boîte de mot qui contient ce caractère, peint le curseur de lecture dessus et renvoie l’index du mot, ou −1. La notification de limite de mot de SAPI fournit exactement la position de caractère nécessaire :
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;
Deux détails défensifs. TrackReadingWordAt conserve son propre cache de boîtes de mots pour la page suivie (reconstruit automatiquement quand la page change), donc le coût par limite reste stable ; et il ne borne pas généreusement : un index égal ou supérieur au nombre de caractères de la page renvoie −1 au lieu de se rabattre sur le dernier mot. Traitez −1 comme « conserver la surbrillance précédente », jamais comme une erreur, car les suites de ponctuation et les espaces inter-mots produisent légitimement des limites qui n’appartiennent à aucun mot. Si vous journalisez chaque −1, vous serez submergé ; comptez-les par page et examinez les pages où le ratio grimpe, ce qui signale généralement une divergence de normalisation du texte par rapport à la première règle
Le curseur lui-même : couleur, suivi et nettoyage
SetReadingWord peint directement la surbrillance lorsque vous détenez vous-même la boîte de mot, ReadingWordColor la stylise, et ReadingWordFollow := True fait défiler la vue juste assez pour garder le mot prononcé visible. Cette dernière propriété compte plus qu’il n’y paraît : un défilement fait main qui « centre le mot courant » fait sursauter la page à chaque retour à la ligne, et les lecteurs sensibles au mouvement désactiveront la fonction en moins d’une minute. La surbrillance ne se rend que sur la page actuellement affichée dans le TPdfView actif ; une lecture multipage doit donc avancer PageNumber en phase avec la parole, puis relancer la préparation pour la nouvelle page avant l’arrivée de son premier événement de limite, afin que le texte parlé et les offsets correspondent à la page fraîche
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 symétrie compte à l’arrêt : chaque chemin de pause, d’arrêt et de changement de page doit finir par ClearReadingWord. Le « bogue » le plus signalé dans notre bêta était un rectangle ambré resté sur une page mise en pause ; inoffensif, mais chaque testeur l’a remonté
Le débit de parole stresse davantage ce pipeline que la taille du document. À 300 mots par minute, les événements de limite arrivent toutes les 200 ms ; aux débits SAPI les plus rapides, plus vite que l’œil ne suit confortablement. Fusionnez plutôt que d’empiler : si une nouvelle limite arrive alors qu’une mise à jour de surbrillance est encore en attente, abandonnez l’ancienne. Un curseur qui visite chaque mot dans l’ordre avec une demi-seconde de retard paraît cassé ; un curseur qui saute parfois un mot mais reste synchronisé ne le paraît pas
Les cas limites qui séparent les démonstrations des produits
Trois catégories reviennent. Les caractères combinants : des séquences Unicode comme des lettres de base avec diacritiques combinants peuvent occuper plus d’indices de caractères que le mot visuel ne le suggère, donc une arithmétique d’offset qui suppose un indice par glyphe visible dérive ; c’est une raison de plus de laisser TrackReadingWordAt faire la correspondance au lieu de calculer vous-même des numéros de mots. La césure : un mot coupé sur un saut de ligne devient deux boîtes ; si vous le prononcez comme un seul jeton, l’événement de limite de sa seconde moitié se résout vers la première boîte, ce qui peut être acceptable, mais décidez-le volontairement. Et les documents balisés face aux non balisés : le séquencement des mots suit la structure logique du document quand un balisage correct existe (le domaine d’ISO 14289, PDF/UA) et retombe sinon sur des heuristiques de mise en page, si bien qu’une page non balisée à deux colonnes peut se lire tout droit à travers les deux colonnes. Les pages pivotées ajoutent un quatrième cas : le Rect de chaque mot l’encadre toujours correctement dans l’espace de page, mais une politique de suivi de viewport réglée pour un flux horizontal défile brutalement lorsque le texte est vertical ; gardez donc au moins un document pivoté dans le jeu de régression. Pour la gestion de l’ordre de lecture, les unités au niveau phrase via ReadingUnits et la pile d’assistance plus large, consultez la construction d’un lecteur PDF accessible en Delphi
Note de plateforme : SAPI est propre à Windows. L’API de boîtes de mots et de suivi est identique sous Lazarus/FPC, mais les builds Linux et macOS ont besoin d’un autre synthétiseur derrière les mêmes événements de limite ; les différences de mise en place sont couvertes dans l’exécution de la visionneuse sous Lazarus et FPC. Le coût de rendu de la surbrillance interagit aussi avec votre cache de pages aux débits de parole élevés ; l’arithmétique de budget de la mise en cache du rendu et des performances de zoom s’applique ici sans changement
Questions fréquentes
Pourquoi TrackReadingWordAt renvoie-t-il toujours −1 ?
Généralement pour l’une de trois raisons : le numéro de page passé est hors limites ou le document n’est pas actif, le texte fourni au moteur TTS diffère du texte de page extrait et les offsets ne s’alignent donc pas, ou l’index de caractère appartient à un espace entre deux mots. Vérifiez-les dans cet ordre
Pourquoi la surbrillance cesse-t-elle de se mettre à jour après un changement de page ?
Le curseur de lecture ne se dessine que sur la page courante de la vue active. Avancez PageNumber et récupérez de nouveau PageWordBoxes pour le texte parlé avant de reprendre, afin que les offsets de limite désignent la page désormais affichée
Puis-je surligner des phrases entières au lieu de mots isolés ?
Oui, ReadingUnits renvoie des unités au niveau phrase et bloc avec leurs propres rectangles de surbrillance (peignez-les avec SetReadingHighlight), ce qui convient aux auditeurs plus lents et réduit l’agitation visuelle aux débits de parole élevés
Les exigences de version (v1.53 ou ultérieure pour les boîtes de mots, v1.56 pour le curseur de suivi), l’API complète de lecture et une démo fonctionnelle de lecture à voix haute sont sur la page produit : PDFium Component