Funkcia hlasitého čítania má okrem samotného hlasu jednu viditeľnú úlohu: pri vyslovení každého slova ho musí na stránke zvýrazniť a udržať ho v zornom poli. Na to potrebujete ohraničujúci rámec (bounding box) každého slova priradený k rovnakému prúdu znakov, z ktorého číta rečový syntetizátor. Ak získate rámce, ale netrafíte indexovanie, zvýraznenie sa oneskorí o jedno alebo dve slová za zvukom. Ak zase zvládnete indexovanie, ale nesprávne ošetríte stav stránky, zvýraznenie skončí na úplne nesprávnej stránke. Samotný syntetizátor, ktorý je z technologického hľadiska najzložitejšou časťou, sa kazí len zriedka. Rozhranie SAPI hlási hranice slov s presnosťou na znak. Čo však zlyháva, je tenká mapovacia vrstva medzi offsetom znaku v pamäti reči a obdĺžnikom na vykreslenej stránke.
Komponent PDFium Component poskytuje toto mapovanie pre Delphi, C++Builder a Lazarus, pričom ohraničenia slov (word boxes) sú dostupné od verzie 1.53 a sledovací kurzor od verzie 1.56. Rozhranie je zámerne minimalistické: volanie, ktoré vracia ohraničenia slov pre stránku, sledovač, ktorý premieňa offset znaku na nakreslené zvýraznenie, a niekoľko vlastností pre farbu a automatické posúvanie. Aj keď je rozhranie jednoduché, poradie, v akom funkcie voláte, rozhoduje o tom, či bude funkcia fungovať. Väčšina nižšie popísaných chýb vyplýva z volania správnych funkcií v nesprávnom poradí.
Znaky nie sú slová a TTS syntetizátory čítajú znaky
Rečový syntetizátor spracováva lineárny reťazec a hlási priebeh ako pozície znakov v rámci tohto reťazca. Stránka PDF obsahuje glyfy umiestnené v priestore stránky, kde „slovo“ predstavuje heuristický cluster sekvencií glyfov. Tieto dva súradnicové systémy nemajú nič spoločné, pokiaľ text, ktorý odovzdáte syntetizátoru, nezodpovedá bajt po bajte textu, z ktorého boli vypočítané ohraničenia slov. To je prvé a neúprosné pravidlo. Ak pred čítaním normalizujete biele znaky, odstránite mäkké pomlčky alebo inak „očistíte“ extrahovaný text, každý ďalší offset bude nesprávny. Čítajte presne to, čo ste extrahovali, alebo udržiavajte explicitnú remappingovú tabuľku offsetov. Neexistuje žiadna tretia možnosť, ktorá by v reálnych dokumentoch fungovala.
Remappingová tabuľka nie je len hypotetický okrajový prípad. Vo chvíli, keď vaše používateľské rozhranie vloží hovorené oznámenie o stránke („strana päť“) alebo rozbalí skratku pre syntetizátor, hovorený reťazec sa odchýli od toho extrahovaného. Zaznamenajte pozíciu a dĺžku každého vloženia a pred každým volaním sledovania odčítajte naakumulovanú úpravu. Je to asi dvadsať riadkov kódu, ktoré však rozhodujú o tom, či zvýraznenie prežije ďalšiu požiadavku na novú funkciu, alebo sa pokazí hneď, ako niekto požiada o čítanie nadpisov.
Čo vám poskytuje ohraničenie slova
Každý záznam typu TPdfWordBox obsahuje text slova, jeho StartIndex a počet znakov Count v rámci textu stránky, obdĺžnik v priestore stránky Rect a číslo stránky Page (indexované od 1). Pole StartIndex slúži ako most medzi dvoma súradnicovými systémami: ide o rovnaký offset, aký vráti SAPI počas čítania. Funkcia PageWordBoxes vracia celé pole pre aktívnu stránku:
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;
Komentár o poradí krokov je zásadný. Metóda PageWordBoxes prehliadača tokenizuje textovú vrstvu stránky, ktorú zobrazenie aktuálne zobrazuje, preto najprv prejdite na stránku v prehliadači a až potom extrahujte dáta. Nevyžaduje sa žiadne vykresľovanie, iba otvorený dokument. (Samotný dokumentový komponent, TPdf, vystavuje svoju vlastnú funkciu PageWordBoxes naviazanú na Pdf.PageNumber na použitie na pozadí bez vizuálneho prehliadača. Tieto dve čísla stránok sú nezávislé, čo predstavuje ďalšiu pascu.) Prázdny výsledok na stránke, ktorá viditeľne obsahuje text, znamená, že inde ide o čistý naskenovaný obrázok. Presmerujte ho na OCR alebo ho aspoň nahláste používateľovi („strana 4 neobsahuje čitateľný text“), namiesto toho, aby hlas bez vysvetlenia stíchol.
Prepojenie hraníc slov SAPI so sledovačom
Metóda TrackReadingWordAt prehliadača je jadrom celej tejto funkcie. Odovzdajte jej číslo stránky a index znaku. Metóda nájde ohraničenie slova obsahujúce tento znak, nakreslí naň čítací kurzor a vráti index slova alebo hodnotu -1, ak index spadne medzi slová. Upozornenie na hranicu slova zo SAPI poskytuje presne tú pozíciu znaku, ktorú potrebuje:
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;
Dva obranné detaily tu zohrávajú dôležitú rolu. Po prvé, TrackReadingWordAt si udržiava vlastnú vyrovnávaciu pamäť ohraničení slov pre sledovanú stránku, ktorá sa pri zmene stránky automaticky prebuduje, takže réžia na spracovanie hranice zostáva minimálna bez ohľadu na rýchlosť prichádzajúcich udalostí. Po druhé, metóda nekontroluje limity príliš benevolentne. Index na konci alebo za počtom znakov stránky vráti hodnotu -1 namiesto toho, aby sa prichytil k poslednému slovu. Považujte hodnotu -1 za požiadavku na zachovanie predchádzajúceho zvýraznenia, nikdy nie za chybu, pretože interpunkcia a medzery medzi slovami legitímne generujú hranice, ktoré nepatria žiadnemu slovu. Logovanie každej hodnoty -1 by vás zahltilo. Radšej ich počítajte na stránku a zamerajte sa na tie stránky, kde ich pomer prudko stúpne, pretože to zvyčajne znamená nesúlad v normalizácii textu podľa prvého pravidla.
Samotný kurzor: farba, sledovanie a čistenie
Metóda SetReadingWord nakreslí zvýraznenie priamo, ak sami držíte ohraničenie slova. Vlastnosť ReadingWordColor definuje jeho štýl a nastavenie ReadingWordFollow := True posunie zobrazenie presne tak, aby hovorené slovo zostalo viditeľné. Táto posledná vlastnosť má veľký význam. Vlastné riešenia na centrovanie aktuálneho slova často spôsobujú prudké trhnutie stránky pri každom prelomení riadku, čo citlivým čitateľom znepríjemní používanie celej funkcie už po minúte. Zvýraznenie sa vykresľuje iba na stránke, ktorá sa aktuálne zobrazuje v aktívnom TPdfView, takže čítanie viacerých stránok musí posúvať PageNumber synchrónne s rečou a následne znova spustiť prípravný krok pre novú stránku ešte pred tým, ako dorazí prvá udalosť hranice slova. Ak to vynecháte, prvých niekoľko zvýraznení na každej stránke bude ukazovať na neplatné súradnice.
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // halt SAPI playback first
PdfView.ClearReadingWord; // then remove the highlight; a stale cursor reads as a bug
end;
Symetria pri ukončení je to, čo udržiava zvýraznenie konzistentné. Každá cesta pozastavenia, zastavenia a otočenia stránky musí končiť volaním ClearReadingWord. Ak to vynecháte, na zastavenej stránke zostane žltý obdĺžnik, ktorý pôsobí ako chyba, a testeri to okamžite nahlásia ako nedostatok, aj keď nič nie je skutočne nefunkčné.
Rýchlosť reči zaťažuje tento systém viac ako veľkosť dokumentu. Pri rýchlosti 300 slov za minútu prichádzajú udalosti hraníc každých 200 ms a pri najvyšších rýchlostiach SAPI prichádzajú rýchlejšie, než ich oko dokáže pohodlne sledovať. Správnou reakciou je spájanie (coalescing) udalostí, nie ich zaraďovanie do frontu. Ak príde nová hranica, kým sa ešte čaká na aktualizáciu zvýraznenia, zahoďte starú a vykreslite najnovšiu. Kurzor, ktorý prechádza každé slovo v poradí, ale mešká pol sekundy, pôsobí nefunkčne. Kurzor, ktorý občas preskočí slovo, ale zostáva v plnom súlade s hlasom, funguje omnoho lepšie.
Okrajové prípady, ktoré odlišujú demoverzie od produktov
Niekoľko kategórií dokumentov odhaľuje slabé miesta implementácie. Kombinačné znaky sú najzákernejšie: sekvencie Unicode, ako je základné písmeno a kombinačné diakritické znamienko, môžu zaberať viac indexov znakov, než by zodpovedalo vizuálnemu slovu, takže akákoľvek aritmetika offsetov predpokladajúca jeden index na glyf začne postupne unikať. To je najsilnejší argument pre to, aby mapovanie riadila priamo metóda TrackReadingWordAt, namiesto manuálneho počítania čísel slov. Rozdeľovanie slov je o niečo bežnejšie: slovo rozdelené na konci riadku tvorí dva rámce, a ak ho vyslovíte ako jeden token, udalosť hranice pre jeho druhú polovicu sa priradí k prvému rámcu. To je zvyčajne v poriadku, ale mali by ste o tom rozhodnúť zámerne, a nie to nechať na náhodu. Tagovanie mení samotné poradie čítania. Ak dokument obsahuje správne štrukturálne značky (podľa normy ISO 14289, PDF/UA), poradie slov sa riadi logickou štruktúrou. Bez nich sa systém spolieha na heuristiku rozloženia a dvojstĺpcová neoznačená stránka sa môže čítať vodorovne cez oba stĺpce. Posledným častým prípadom sú otočené stránky: Rect každého slova ho stále správne ohraničuje v priestore stránky, ale sledovanie výrezu nastavené na horizontálny smer sa pri vertikálnom texte posúva trhane, preto do svojej testovacej sady pridajte aspoň jeden otočený dokument. Informácie o správe poradia čítania, vetných jednotkách cez ReadingUnits a širšom asistenčnom systéme nájdete v článku o vytvorení prístupnej čítačky PDF v Delphi.
Nasadenie ovplyvňuje jedno obmedzenie platformy. Rozhranie SAPI je dostupné iba pre Windows. API pre ohraničenie slov a sledovanie je pod Lazarusom a FPC identické, ale zostavy pre Linux a macOS potrebujú iný syntetizátor napojený na rovnaké udalosti hraníc. Toto nastavenie je popísané v článku o prevádzke prehliadača pod prostredím Lazarus a FPC. Náročnosť zvýrazňovania pri stúpajúcich rýchlostiach reči ovplyvňuje aj vyrovnávaciu pamäť stránok, pričom výpočty z článku o vyrovnávacej pamäti vykresľovania a výkone priblíženia platia bez zmeny aj tu.
Keď je zvýrazňovanie jednotlivých slov príliš detailné
Karaoke na úrovni jednotlivých slov nie je vždy to, čo čitateľ očakáva. Pri vysokých rýchlostiach reči sa blikanie kurzora slovo po slove stáva vizuálnym šumom a niektorí poslucháči sledujú vetu pohodlnejšie než striedanie jednotlivých slov. Pre tento prípad komponent ponúka hrubšiu jednotku. Vlastnosť ReadingUnits vracia vetné a blokové jednotky, z ktorých každá má svoje vlastné obdĺžniky zvýraznenia, a vy ich vykresľujete pomocou SetReadingHighlight namiesto SetReadingWord. Prepojenie má rovnakú štruktúru: offset hranice stále určuje, ktorá jednotka sa rozsvieti, ale zvýraznená jednotka pokrýva celú vetu alebo riadok namiesto jedného slova. Pomalší čitatelia a prehrávanie pri vysokej rýchlosti zvyčajne preferujú tento režim, a nič vám nebráni ponúknuť oba režimy v nastaveniach.
Pred začatím implementácie je dôležité overiť minimálne verzie: ohraničenia slov vyžadujú PDFium Component verzie 1.53 alebo novšej a sledovací kurzor verzie 1.56 alebo novšej. Kompletné čítacie API, vetné jednotky a funkčná ukážka hlasitého čítania sú k dispozícii na produktovej stránke pre PDFium Component.