Funkce čtení nahlas má kromě samotného hlasu i jeden vizuální úkol: jak je vyslovováno každé slovo, musí se toto slovo na stránce rozsvítit a zůstat v dohledu. Abyste toho dosáhli, potřebujete ohraničující obdélník (bounding box) každého slova indexovaný ke stejnému proudu znaků, ze kterého čte hlasový engine. Pokud získáte boxy, ale vynecháte indexování, zvýraznění se bude zpožďovat o slovo nebo dvě za zvukem; pokud získáte indexování, ale špatně zpracujete stav stránky, zvýraznění se ocitne na úplně jiné stránce. Hlasová část tohoto celku, samotný syntetizér, je tou částí, která se rozbije jen zřídka. SAPI hlásí hranice slov s přesností na znak. Co se však rozbíjí, je tenká mapovací vrstva mezi posunem znaků ve vyrovnávací paměti řeči a obdélníkem na vykreslené stránce
PDFium Component dodává toto mapování pro Delphi, C++Builder a Lazarus, přičemž boxy slov jsou k dispozici od verze 1.53 a sledovací kurzor od verze 1.56. Rozhraní (API) je záměrně úzké: volání, které vrátí boxy slov pro danou stránku, tracker (sledovač), který přemění posun (offset) znaku na vykreslené zvýraznění, a pár vlastností pro barvu a automatické posouvání. Jakkoli je úzké, pořadí, ve kterém věci voláte, rozhoduje o tom, zda funkce funguje, a většina níže uvedených chyb pramení z volání správných funkcí v nesprávném pořadí
Znaky nejsou slova a TTS stroje mluví ve znacích
Hlasový engine zpracovává plochý řetězec a hlásí průběh jako pozice znaků v rámci tohoto řetězce. Stránka PDF má glyfy umístěné v prostoru stránky, kde „slovo“ je heuristický shluk sekvencí glyfů. Tyto dva souřadnicové systémy nesdílejí vůbec nic, pokud text, který předáte syntetizéru, není přesně stejný bajt po bajtu s textem, ze kterého byly vypočítány boxy slov. To je pravidlo číslo jedna a je nekompromisní. Normalizujte mezery, odstraňte měkké rozdělovníky (soft hyphens) nebo jinak „vyčistěte“ extrahovaný text předtím, než ho necháte mluvit, a každý další navazující posun bude tiše špatný. Nechte syntetizér přečíst přesně to, co jste extrahovali, nebo si udržujte tabulku explicitního přemapování posunů. Neexistuje žádná třetí možnost, která by přežila v reálných dokumentech
Tabulka pro přemapování není jen nějakým hypotetickým okrajovým případem. Ve chvíli, kdy vaše uživatelské rozhraní vloží hlasové oznámení stránky („strana pět“) nebo rozvine zkratku pro syntetizér, se mluvený řetězec začne lišit od extrahovaného. Zaznamenejte pozici a délku každého vložení a poté odečtěte nashromážděnou úpravu před každým voláním sledování. Je to možná dvacet řádků účetnictví a přitom to znamená rozdíl mezi zvýrazněním, které přežije další žádost o novou funkci, a zvýrazněním, které se rozbije, jakmile někdo poprvé požádá o čtení nadpisů
Co vám poskytne box slova
Každý záznam TPdfWordBox v sobě nese text slova, jeho index StartIndex, celkový počet znaků (Count) v rámci textu stránky, obdélník z prostoru stránky Rect a číslo stránky (Page) číslované od jedné. Pole StartIndex je mostem mezi oběma souřadnicovými systémy: je to ten stejný offset, který vám SAPI vrátí při čtení. Metoda PageWordBoxes vrátí úplné pole pro aktivní 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ář týkající se pořadí je zde nosný (load-bearing). Metoda PageWordBoxes u prohlížeče tokenizuje textovou vrstvu stránky, kterou aktuálně zobrazuje daný pohled (view), takže nejdříve proveďte navigaci v pohledu a teprve potom text extrahujte; žádné vykreslování (rendering) není nutné, vyžaduje se pouze otevřený dokument. (Komponenta dokumentu TPdf vystavuje svou vlastní metodu PageWordBoxes klíčovanou podle hodnoty Pdf.PageNumber pro použití v režimu bez hlavičky (headless). Obě čísla stránek jsou na sobě nezávislá, což představuje další samostatnou past.) Prázdný výsledek na stránce, která zjevně nese nějaký obsah, znamená sken sestávající pouze z obrázků. Nasměrujte jej na OCR, nebo to alespoň ohlaste („strana 4 neobsahuje čitelný text“), než abyste nechali hlas utichnout bez jakéhokoli vysvětlení
Propojení hranic slov SAPI se sledovačem (tracker)
Metoda TrackReadingWordAt v prohlížeči je stěžejním bodem celé této funkce. Dejte jí číslo stránky a index znaku; ona najde box slova obsahující onen znak, vykreslí na něj čtecí kurzor a vrátí index slova, případně hodnotu -1, pokud index spadá do prostoru mezi slovy. Upozornění SAPI o dosažení hranice slova poskytuje přesně tu pozici znaku, kterou k tomu vyžaduje:
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 defenzivní detaily si zde vydělají na své živobytí. Za prvé, TrackReadingWordAt udržuje svou vlastní mezipaměť pro boxy slov aktuálně sledované stránky, jež je při změně stránky automaticky přestavěna, takže výkonnostní zátěž vázaná na jednotlivé hranice slov zůstává neměnná bez ohledu na to, jak rychle tyto hranice přicházejí. Za druhé, metoda nikterak velkoryse nekontroluje hranice. Jakýkoli index odpovídající celkovému počtu znaků dané stránky, či dokonce sahající až za něj, prostě vrátí -1 a nepřipne k poslednímu slovu. Berte vrácenou hodnotu -1 ve smyslu „udržet původní zvýraznění“ a nikdy na ni nepohlížejte jako na chybu, protože uzel složený pouze z interpunkce i bílé znaky oddělující slova zcela oprávněně produkují hranice nepatřící žádnému slovu. Zaznamenávání každé jedné -1 by vás velice brzy pohřbilo. Raději je zaznamenávejte v podobě celkového součtu za stránku a důkladněji prozkoumejte každou stránku, kde poměr těchto událostí prudce naroste, neboť to obvykle značí chybu při normalizaci textu, jež odkazuje zpátky k pravidlu číslo jedna
Samotný kurzor: barva, sledování a úklid
Zatímco SetReadingWord přímo vykresluje zvýraznění ve chvílích, kdy si vy sami uchováváte box slova, ReadingWordColor takovému zvýraznění nastavuje styl a vlastnost ReadingWordFollow := True roluje pohledem přesně natolik, aby vždy bylo čtené slovo zachováno jako viditelné. Tato posledně jmenovaná vlastnost si skutečně vydělává na své místo. Ručně psané rolování pro „vycentrování aktuálního slova“ má totiž za následek to, že celá stránka sebou trhne při jakémkoli zalomení řádku a čtenáři s citlivostí na tento typ pohybů celou funkci během minuty vypnou. Zvýraznění se vykresluje pouze na stránce, jež je aktuálně zobrazená v onom aktivním objektu TPdfView, a tak vícestránkové čtení musí při shodném posunu s řečí vždy navyšovat aktuální PageNumber a následně ještě jednou znovu spustit celou úvodní přípravu pro novou stránku ještě před tím, než vůbec dorazí první událost oznamující hranici slova. Přeskočíte-li toto, prvních několik zvýraznění u každé jedné stránky bude odkazovat na zcela staré souřadnice
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // halt SAPI playback first
PdfView.ClearReadingWord; // then remove the highlight; a stale cursor reads as a bug
end;
Symetrie při vypínání je tím, co uchovává zvýraznění poctivým. Každé pozastavení, ukončení i trasa obratu na další stránku zkrátka a dobře musí vždy končit ve volání metody ClearReadingWord. Vynecháte-li tuto záležitost, jantarový obdélník vám následně zůstane sedět i na zcela zastavené stránce a bude tak vypadat přesně jako defekt, čili se stane tím druhem hlášení, který s radostí zapíše do systému každý tester, třebaže de facto není vůbec nic v nepořádku
Míra rychlosti řeči zatěžuje tuto pipeline nepoměrně silněji, než jak to kdy dokáže celková velikost daného dokumentu. Na 300 slovech za minutu přijde událost oznamující dosaženou hranici slova v průměru každých 200 ms, a u těch vůbec nejvyšších rychlostí SAPI přicházejí takovéto hranice tak rychle, že je běžné lidské oko už ani nedokáže plynule zaznamenávat. Rozhodně nejsprávnější reakcí na takové situace je slučovat příchozí události dohromady namísto toho je pouze strkat do stávající fronty. Bude-li stále nedokončená ta úplně předešlá aktualizace zvýraznění právě ve chvíli, kdy v ten moment dorazí další a zcela nová hranice, musíte tuto aktuálně nedokončenou zahodit a místo ní zkrátka začít okamžitě vykreslovat tu skutečně nejnovější, čili nejdosaženější. Ten samotný kurzor, jež projde doslova úplně každé jedno stávající slovo jedno po druhém, nicméně s jistým půlsekundovým zpožděním, bude ve výsledku působit poněkud narušeně. A naopak ten samý kurzor, který při plné a kontinuální shodě s řečí dokáže klidně jedno takové slovíčko bez lítosti zahodit a přeskočit, rozhodně nebude budit naprosto žádný špatný dojem
Okrajové případy, které oddělují dema od produktů
Několik podkategorií v rámci dokumentů dokáže zcela odhalit různé nekompletní hrany (seams). Tím vůbec nejméně zřetelným jsou spojovací (combining) znaky: sekvence v systému Unicode, jež se skládají třeba ze základního písmena zkombinovaného s patřičnou diakritikou, totiž mohou ve svém důsledku obsadit daleko víc odpovídajících indexů znaků, než jak by toto dané vizuální slovo mohlo na svém povrchu naznačovat. Celková aritmetika posunů (offsetů), jež ze své podstaty vždy počítá s jedním indexem znaku na jeden jediný patřičný glyf, pak tedy docela spolehlivě odnese svou stávající chybovost mimo přesnost. Právě to se vlastně stává vůbec tím nejsilnějším pádným argumentem ve prospěch řešení, že je lepší přenechat tuto konkrétní zodpovědnost za dané mapování rovnou na metodu TrackReadingWordAt a určitě by bylo chybou spoléhat v tomto směru na to, že čísla jednotlivých slov počítáme takříkajíc ručně. Dělení slov (hyphenation) spadá do daleko prozaičtějších potíží, objevuje se ale mnohem častěji: ze slova přelomeného mezerou při zalamování textu se rázem stávají dva různé boxy, a budete-li jej nechat přečíst jakožto pouhý jediný nedílný token, celková stávající událost na hranici slova v jeho druhé, pozdější polovině, bude tím pádem zcela logicky patřit do první předešlé sekce (prvního boxu). Obvykle se tímto sice nic nekazí, ale v každém případě je to vaše osobní rozhodnutí a proto jej udělejte se záměrem spíše než s překvapením. Při tzv. tagování se mění zcela samotné stávající celkové pořadí čtení. Jestliže s sebou dotyčný dokument již nese řádně a správně utvořené strukturální tagy (oblast s normou ISO 14289 neboli tzv. PDF/UA), logické sekvenování stávajících slov začne striktně sledovat zcela stejnou strukturu. Pakliže však žádnými nedisponuje, rázem spadnete plně zpátky pod jurisdikci standardních a prostých heuristik u stávajícího layoutu (rozvržení stránky) a rázem začne docházet zkrátka a prostě k tomu, že i ta běžná dvousloupcová a zároveň nijak nepotagovaná (untagged) stránka se rázem bude zcela jistě a neomylně číst přímo zkrz na zkrz u obou sloupečků, čili zleva doprava přes jejich veškeré případné hranice. Zcela posledním běžným, čili často se zjevujícím kazem, by se jistě mohly stát tzv. zrotované neboli pootočené stránky: obdélník Rect každého jednotlivého slova dané slovo sice i nadále zcela správně ohraničuje napříč všemi body ve stávajícím prostoru dané konkrétní dokumentové stránky, avšak pokud uživatelská strategie rolování plně naladěná i zaměřená výhradně na běžné sledování obsahu se vší parádou odpovídá výhradně plynulému a čistě horizontálnímu pohybu okna vpřed, jakmile celkový směr textu začne padat čistě vertikálně dolů, ihned se bude vyznačovat výraznými trhanci, takže raději opravdu mějte v dané testovací či regresní sadě plně nachystaný na pozoru i nějaký ten zrotovaný neboli jinak natočený PDF dokument. Podrobnosti ohledně přesného a správného zacházení se stávajícím aktuálním pořadím čtení v dokumentech i další informace k jednotkám, jež mají rozměry neboli hranice vymezené pouze jako běžné celé normální a standardní věty skrze pole ReadingUnits, společně s daleko širším asistenčním i zpřístupňujícím (assistive) programátorským zásobníkem (stackem) můžete velmi snadno nalézt pod odkazy v článku s titulem vytvoření přístupné čtečky PDF v Delphi
Kdy je zvýrazňování jednotlivých slov příliš jemné (nesprávná granularita)
Uživatel a případný budoucí čtenář ale také ne úplně vždy vyloženě a striktně vyžaduje ono proklamované „karaoke“ na pouhé přesné urovni jednotlivých drobných slůvek. Zvláště u velmi vysokých a nadstandardních rychlostí čteného mluveného slova, jak se kurzor přesouvá rychlostí blesku po slovech blikajících jedno přes druhé, se velice brzy promění na zcela nepřehledný samostatný rušivý element plný jakéhosi čistě vlastního zbytečného šumu přímo ve vašich očích, načež nejeden obvyklý konzument onoho vizuálního blikotajícího světelného toku ocení namísto obří plejády blesků u samotných malých slůvek o poznání klidnější a mírumilovnější postup na přesné bází jedné ucelené a plnohodnotné věty. Právě a jen z tohoto specifického a prozaičtějšího důvodu vystavuje dotyčná programátorská komponenta další možnou měřítkovou hrubší a robustnější vrstvu či úroveň ve formě zcela samostatné čtecí jednotky. Jde tedy o to, že vlastnost ReadingUnits nekompromisně dokáže velmi ochotně vrátit plně vybavené plnohodnotné kompletní jednotky a čtecí uzly na zcela plné úrovni celého souvislého, případně jen částečně souvislého shluku slov neboli pouhých plnohodnotných vět a bloků, kde si naprosto bezchybně celá každá jedna taková stávající ucelená jednotka disponuje rovnou celým vlastním plnohodnotným stávajícím, ohraničujícím obdélníkem, a posléze máte za hlavní a základní úkol vykreslit ji takovou zcela ucelenou se zařízením, či pomocí metody s označením SetReadingHighlight s tím, že plně a spolehlivě nahradíte tu předešlou obvyklou alternativu a možnost s metodou SetReadingWord. Všechna zbylá propojení se v podstatě vůbec nijak nezmění: zjištěný posun přes hranici slova sice stále řídí a definuje naprosto bezezbytku veškerou vizualizaci k tomu, čili svede ovládat přesné zapínání (vysvícení) jakékoli aktivní jednotky, ale ta celá výsledná osvětlená aktivní jednotka nakonec přesáhne klidně rovnou i celou jedinou plnohodnotnou větu s jejími souvislými čtecími řádky či úseky spíše nežli ten pouhý samotný osamocený jediný token. Je pak jisté, že jak velmi pomalí a nezkušení čtenáři bez patřičných dlouhodobých a trvalých návyků, tak případně na druhou stranu i jakékoliv jiné přehrávající systémy hrající vysokou měrou nad obvyklým uživatelským i konvenčním průměrem si tento daleko jemnější postup začnou preferovat a zcela nic vám přitom přece nebude jakkoli bránit v tom nabídnout jim v nastavení tyto postupy oba najednou
Rozhodně pak naprosto neváhejte si plně zakonzervovat, poznačit a ukotvit si tu přesnou počáteční informaci spojenou se zcela spodní hranicí požadovaných stávajících verzí: slovní obdélníky v daném ohledu totiž skutečně a nezbytně začnou vyžadovat nejméně programátorskou verzi v1.53 od vaší komponenty a to ať už tuhle anebo všechny pozdější, no a kurzor, který dokáže stopovat a ohlídat plně kompletní slovo, nepojede na ničem starším než na verzi s hodnotou v1.56. Ten veškerý celistvý i robustní podrobný balíček programátorského (API) rozhraní, celky na bázi ucelených stávajících souvislých vět, i plně a funkčně zařízené spouštěcí, výukové i interaktivní čtecí mluvící demo k celému tomuto podniku můžete naprosto snadno objevit u té samotné odpovídající úvodní a nejdůležitější produktové propagační stránce zaměřené výhradně a speciálně jen pro tu stávající komponentu PDFium Component