Technischer Artikel

Wort-für-Wort TTS-Hervorhebung in Delphi PDFium-Viewern

Eine Vorlesefunktion hat neben der Stimme eine weitere sichtbare Aufgabe: Während jedes Wort gesprochen wird, muss dieses Wort auf der Seite aufleuchten und im Blickfeld bleiben. Dafür benötigen Sie die Bounding Box jedes Wortes, indiziert auf den gleichen Zeichenstrom (Character Stream), aus dem die Sprach-Engine vorliest. Haben Sie die Boxen, verfehlen aber die Indizierung, driftet die Hervorhebung ein oder zwei Wörter hinter dem Ton her; haben Sie die Indizierung, handhaben aber den Seitenstatus falsch, landet die Hervorhebung auf der völlig falschen Seite. Der Sprachteil davon, der Synthesizer selbst, geht selten kaputt. SAPI meldet Wortgrenzen auf das Zeichen genau. Was kaputtgeht, ist die dünne Zuordnungsschicht zwischen einem Zeichen-Offset im Sprachpuffer und einem Rechteck auf der gerenderten Seite

Die PDFium Component liefert diese Zuordnung für Delphi, C++Builder und Lazarus mit, wobei Wortboxen seit Version 1.53 und der Tracking-Cursor seit v1.56 verfügbar sind. Die API-Oberfläche ist absichtlich schmal gehalten: ein Aufruf, der die Wortboxen für eine Seite zurückgibt, ein Tracker, der einen Zeichen-Offset in eine gezeichnete Hervorhebung verwandelt, und ein paar Eigenschaften für Farbe und Auto-Scroll. So schmal sie auch ist, die Reihenfolge, in der Sie Dinge aufrufen, entscheidet darüber, ob die Funktion funktioniert, und die meisten der folgenden Fehler resultieren daraus, dass die richtigen Funktionen in der falschen Reihenfolge aufgerufen wurden

Zeichen sind keine Wörter, und TTS-Engines sprechen in Zeichen

Eine Sprach-Engine verarbeitet einen flachen String und meldet den Fortschritt als Zeichenpositionen innerhalb dieses Strings. Eine PDF-Seite verfügt über Glyphen, die im Seitenraum platziert sind, wo ein „Wort“ eine heuristische Gruppierung von Glyphenläufen ist. Die beiden Koordinatensysteme haben nichts gemeinsam, es sei denn, der Text, den Sie dem Synthesizer übergeben, ist Byte für Byte der Text, aus dem die Wortboxen berechnet wurden. Das ist Regel Nummer eins, und sie ist unnachgiebig. Wenn Sie vor dem Vorlesen Leerzeichen normalisieren, weiche Trennzeichen entfernen oder den extrahierten Text auf andere Weise „aufräumen“, ist jeder nachgelagerte Offset unbemerkt falsch. Lassen Sie genau das vorlesen, was Sie extrahiert haben, oder pflegen Sie eine explizite Offset-Zuordnungstabelle. Es gibt keine dritte Option, die bei echten Dokumenten funktioniert

Die Neuzuordnungstabelle ist kein hypothetischer Randfall. In dem Moment, in dem Ihre Benutzeroberfläche eine gesprochene Seitenankündigung („Seite fünf“) einfügt oder eine Abkürzung für den Synthesizer ausschreibt, weicht der gesprochene String vom extrahierten ab. Protokollieren Sie die Position und Länge jeder Einfügung und ziehen Sie dann die kumulierte Anpassung vor jedem Tracking-Aufruf ab. Es sind vielleicht zwanzig Zeilen Buchführung, und es ist der Unterschied zwischen einer Hervorhebung, die die nächste Funktionsanfrage überlebt, und einer, die beim ersten Mal kaputtgeht, wenn jemand nach vorgelesenen Überschriften fragt

Was eine Wortbox Ihnen liefert

Jeder -Datensatz enthält den Text des Wortes, seinen und die Zeichenanzahl () innerhalb des Seitentextes, ein Seitenraum-Rechteck () und die 1-basierte Seitennummer (). Das Feld ist die Brücke zwischen den beiden Koordinatensystemen: Es ist derselbe Offset, den SAPI beim Vorlesen zurückgibt. gibt das vollständige Array für die aktive Seite zurück:

procedure TReaderForm.PreparePage(PageNo: Integer);
begin
  PdfView.PageNumber := PageNo;   // 视图的词框追踪其显示的页面

  FWords := PdfView.PageWordBoxes;
  FPageText := BuildSpeechText(FWords);   // 按顺序连接 Word.Text

  if Length(FWords) = 0 then
    HandleImageOnlyPage(PageNo);          // 没有文本层的扫描件
end;

Der Kommentar zur Reihenfolge ist von tragender Bedeutung. Das des Viewers zerlegt die Textebene der aktuell vom View angezeigten Seite in Token; navigieren Sie also zuerst im View und extrahieren Sie danach; es ist kein Rendern erforderlich, nur ein geöffnetes Dokument. (Die Dokumentkomponente stellt für den Headless-Betrieb ein eigenes zur Verfügung, das an gebunden ist. Die beiden Seitennummern sind unabhängig voneinander, was eine eigene Falle darstellt.) Ein leeres Ergebnis auf einer Seite, die sichtbar Inhalt trägt, bedeutet, dass es sich um einen reinen Bildscan handelt. Leiten Sie dies an eine OCR weiter oder kündigen Sie es zumindest an („Seite 4 enthält keinen lesbaren Text“), anstatt die Stimme ohne Erklärung verstummen zu lassen

SAPI-Wortgrenzen mit dem Tracker verdrahten

TrackReadingWordAt

procedure TReaderForm.OnSpeechWordBoundary(StreamPos: Integer);
var
  WordIdx: Integer;
begin
  // 一次调用完成偏移量到词框的映射并移动高亮
  WordIdx := PdfView.TrackReadingWordAt(FPageNo, StreamPos);
  if WordIdx < 0 then
    Exit;                     // 边界落在任何词语之外:保留最后的高亮
end;

Zwei defensive Details machen sich hier bezahlt. Erstens hält einen eigenen Wortbox-Cache für die verfolgte Seite bereit, der bei Seitenwechseln automatisch neu erstellt wird, sodass die Kosten pro Grenze flach bleiben, egal wie schnell die Grenzen eintreffen. Zweitens prüft es die Grenzen nicht großzügig. Ein Index bei oder über der Zeichenanzahl der Seite hinaus gibt −1 zurück, anstatt auf das letzte Wort zu klammern. Behandeln Sie −1 als „vorherige Hervorhebung beibehalten“, niemals als Fehler, denn Satzzeichen und Leerzeichen zwischen Wörtern erzeugen legitimerweise Grenzen, die zu keinem Wort gehören. Jedes −1 zu protokollieren, wird Sie überfluten. Zählen Sie sie stattdessen pro Seite und schauen Sie sich jede Seite genau an, auf der die Quote sprunghaft ansteigt, da dies normalerweise auf eine Diskrepanz bei der Textnormalisierung (zurück bei Regel 1) hinweist

Der Cursor selbst: Farbe, Folgen und Aufräumen

SetReadingWordReadingWordColorReadingWordFollow := TrueTPdfViewPageNumber

procedure TReaderForm.StopReading;
begin
  FVoice.Stop;                // 首先停止 SAPI 播放
  PdfView.ClearReadingWord;   // 然后移除高亮;过时的光标会被视为 Bug
end;

Symmetrie beim Herunterfahren hält die Hervorhebung sauber. Jeder Pfad für Pause, Stopp und Seitenumbruch muss in enden. Lassen Sie es weg, und ein bernsteinfarbenes Rechteck bleibt auf einer gestoppten Seite liegen und sieht exakt wie ein Fehler aus – genau das, was jeder Tester als Bug melden wird, obwohl eigentlich nichts kaputt ist

Die Sprechgeschwindigkeit beansprucht diese Pipeline stärker als die Dokumentgröße. Bei 300 Wörtern pro Minute treffen die Grenzereignisse alle 200 ms ein, und bei den schnellsten SAPI-Raten kommen sie schneller, als das Auge bequem verfolgen kann. Die richtige Antwort darauf ist das Zusammenfassen (Coalesce), nicht das Einreihen (Queue). Wenn eine neue Grenze eintrifft, während ein Update der Hervorhebung noch aussteht, verwerfen Sie das alte und zeichnen Sie das neueste. Ein Cursor, der der Reihe nach jedes Wort besucht, aber eine halbe Sekunde hinterherhinkt, fühlt sich fehlerhaft an; einer, der gelegentlich ein Wort überspringt, aber synchron mit der Stimme bleibt, hingegen nicht

Grenzfälle, die Demos von echten Produkten unterscheiden

Einige Dokumentkategorien legen die Schwachstellen offen. Kombinierende Zeichen (Combining Characters) sind die subtilsten: Unicode-Sequenzen wie ein Basisbuchstabe plus ein kombinierendes diakritisches Zeichen können mehr Zeichenindizes belegen, als das visuelle Wort vermuten lässt, sodass jede Offset-Arithmetik, die von einem Index pro Glyphe ausgeht, langsam abdriftet. Das ist das stärkste Argument dafür, das Mapping von übernehmen zu lassen, anstatt Wortnummern von Hand zu berechnen. Silbentrennung ist banaler, aber weitaus häufiger: Ein Wort, das über einen Zeilenumbruch getrennt wird, wird zu zwei Boxen, und wenn Sie es als einzelnes Token aussprechen, wird das Grenzereignis für seine zweite Hälfte auf die erste Box aufgelöst. Das ist normalerweise in Ordnung, aber es ist eine Entscheidung, also treffen Sie sie absichtlich, anstatt sie zufällig zu entdecken. Tagging verändert die Lesereihenfolge selbst. Wenn ein Dokument ordnungsgemäße Struktur-Tags enthält (das Gebiet von ISO 14289, PDF/UA), folgt die Wortsequenzierung der logischen Struktur; ohne diese fällt sie auf Layout-Heuristiken zurück, und eine ungetaggte zweispaltige Seite kann geradewegs über beide Spalten hinweg vorgelesen werden. Rotierte Seiten sind der letzte häufige Fall: Das jedes Wortes umrahmt es im Seitenraum immer noch korrekt, aber eine auf horizontalen Fluss abgestimmte Viewport-Follow-Richtlinie scrollt beim vertikal verlaufenden Text störend; behalten Sie also mindestens ein rotiertes Dokument im Regressionsset. Zur Handhabung der Lesereihenfolge, Einheiten auf Satzebene über und dem breiteren assistiven Stack siehe das Erstellen eines barrierefreien PDF-Readers in Delphi

Eine Plattformbeschränkung bestimmt die Bereitstellung. SAPI ist Windows-exklusiv. Unter Lazarus und FPC sind die Wortrahmen- und Tracking-APIs identisch, aber Linux- und macOS-Builds müssen unterschiedliche Synthesizer an dieselben Grenzereignisse anhängen; diese Konfiguration ist im Betrieb des Viewers unter Lazarus und FPC enthalten. Sobald sich die Sprachgeschwindigkeit erhöht, interagiert der Overhead der Hervorhebung auch mit Ihrem Seitencache, und das Budget-Arithmetik-Regelwerk in Rendering-Cache und Zoom-Leistung gilt auch hier

Wenn die Worthervorhebung die falsche Granularität ist

Copyright 2026 losLab Software

Home · Search · losLab.com