Značajka čitanja naglas ima jedan vidljivi posao izvan glasa: kako se svaka riječ izgovara, mora osvijetliti tu riječ na stranici i zadržati je u prikazu. Da biste to učinili, potreban vam je granični okvir (bounding box) svake riječi, indeksiran na isti tok znakova s kojeg čita govorni mehanizam (speech engine). Dohvatite okvire, ali promašite indeksiranje i isticanje će zaostajati riječ ili dvije iza zvuka; pogodite indeksiranje, ali pogrešno rukujte stanjem stranice, i isticanje će u potpunosti sletjeti na pogrešnu stranicu. Govorni dio ovoga, sam sintesajzer, dio je koji se rijetko kvari. SAPI izvještava o granicama riječi do znaka. Ono što se kvari je tanki sloj mapiranja između pomaka (offset) znaka u međuspremniku govora (speech buffer) i pravokutnika na iscrtanoj (rendered) stranici
PDFium Component isporučuje to mapiranje za Delphi, C++Builder i Lazarus, s okvirima riječi dostupnima od verzije v1.53 i pokazivačem za praćenje (tracking cursor) od v1.56. Površina je namjerno uska: poziv koji vraća okvire riječi za stranicu, program za praćenje koji pretvara pomak znaka u naslikano isticanje i nekoliko svojstava za boju i automatsko pomicanje. Koliko god usko bilo, redoslijed kojim pozivate stvari odlučuje hoće li značajka raditi, a većina grešaka u nastavku dolazi od pozivanja pravih funkcija u pogrešnom nizu
Znakovi nisu riječi, a TTS mehanizmi govore u znakovima
Govorni mehanizam troši ravni niz (flat string) i izvještava o napretku kao o položajima znakova unutar tog niza. PDF stranica ima glifove (glyphs) smještene u prostor stranice, gdje je "riječ" heuristički klaster niza glifova. Ta dva koordinatna sustava ne dijele ništa osim ako tekst koji predate sintesajzeru nije bajt-po-bajt isti tekst iz kojeg su izračunati okviri riječi. To je pravilo broj jedan, i neumoljivo je. Normalizirajte razmake, uklonite meke crtice ili na drugi način "očistite" izdvojeni tekst prije nego što ga izgovorite, i svaki pomak nizvodno tiho će biti pogrešan. Izgovorite točno ono što ste izdvojili ili čuvajte eksplicitnu tablicu remapiranja pomaka. Ne postoji treća opcija koja preživljava stvarne dokumente
Tablica remapiranja nije hipotetski granični slučaj. Onog trenutka kada vaše korisničko sučelje (UI) umetne izgovorenu najavu stranice ("stranica pet") ili proširi kraticu za sintesajzer, izgovoreni niz (string) odvaja se od izdvojenog. Zabilježite položaj i duljinu svakog umetanja, a zatim oduzmite akumuliranu prilagodbu prije svakog poziva za praćenje. To je možda dvadesetak redaka vođenja evidencije, i to je razlika između isticanja koje preživljava sljedeći zahtjev za novom značajkom i onoga koje puca prvi put kada netko zatraži izgovorene naslove
Što vam daje okvir riječi
Svaki zapis TPdfWordBox nosi tekst riječi, njegov StartIndex i Count znakova unutar teksta stranice, Rect u prostoru stranice i broj Page temeljen na 1 (1-based). Polje StartIndex most je između dvaju koordinatnih sustava: to je isti pomak (offset) koji će SAPI vratiti dok čita. PageWordBoxes vraća cijeli niz (array) za aktivnu stranicu:
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;
Komentar o redoslijedu je nosiv. Komponenta gledatelja (viewer) PageWordBoxes tokenizira tekstualni sloj stranice koju prikaz trenutno prikazuje, stoga prvo navigirajte prikaz, a zatim izdvojite; nije potrebno iscrtavanje (rendering), samo otvoren dokument. (Komponenta dokumenta, TPdf, izlaže vlastiti PageWordBoxes vezan za Pdf.PageNumber za bezglavu (headless) upotrebu. Ta dva broja stranica su neovisna, što je samo po sebi zamka.) Prazan rezultat na stranici koja vidljivo nosi sadržaj znači sken koji je samo slika. Usmjerite to u OCR ili barem najavite ("stranica 4 ne sadrži čitljiv tekst"), umjesto da pustite da glas utihne bez objašnjenja
Ožičenje (wiring) SAPI granica riječi na tragač (tracker)
TrackReadingWordAt, na gledatelju (viewer), stožer je cijele značajke. Dajte joj broj stranice i indeks znaka; on pronalazi okvir riječi (word box) koji sadrži taj znak, slika na njemu kursor za čitanje i vraća indeks riječi ili −1 kada indeks padne između riječi. SAPI-jeva obavijest o granici riječi isporučuje točno onu poziciju znaka koju želi:
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 obrambena detalja ovdje opravdavaju svoje postojanje. Prvo, TrackReadingWordAt drži vlastitu predmemoriju (cache) okvira riječi za praćenu stranicu, koja se automatski obnavlja kada se stranica promijeni, tako da trošak po granici ostaje ravan bez obzira koliko brzo granice pristižu. Drugo, ne provjerava izdašno granice niza (bounds-check). Indeks na ili iznad broja znakova stranice vraća −1 umjesto da se veže na posljednju riječ. Tretirajte −1 kao "zadrži prethodno isticanje", nikada kao pogrešku, jer nizovi interpunkcije i bjelina između riječi legitimno proizvode granice koje ne pripadaju nijednoj riječi. Zapisivanje svakog −1 će vas zatrpati. Umjesto toga, brojite ih po stranici i dobro pogledajte bilo koju stranicu na kojoj omjer skoči, jer to obično znači nepodudaranje normalizacije teksta natrag na pravilu jedan
Sam kursor: boja, praćenje i čišćenje
SetReadingWord izravno slika isticanje kada sami držite okvir riječi, ReadingWordColor ga stilizira, a ReadingWordFollow := True pomiče prikaz točno toliko da izgovorena riječ ostane vidljiva. To zadnje svojstvo zaslužuje svoje mjesto. Ručno izrađeno pomicanje "centriraj trenutnu riječ" (center the current word) čini da se stranica trza (lurch) na svakom prijelomu retka, i čitatelji osjetljivi na pokret isključit će cijelu značajku u roku od jedne minute. Isticanje se iscrtava samo na stranici koja je trenutno prikazana u aktivnom TPdfView, tako da čitanje više stranica mora unaprijediti PageNumber u koraku s govorom, a zatim ponovno pokrenuti korak pripreme za novu stranicu prije nego što sleti prvi granični događaj (boundary event). Preskočite to i prvih nekoliko isticanja na svakoj stranici pokazivat će na ustajale (stale) koordinate
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // halt SAPI playback first
PdfView.ClearReadingWord; // then remove the highlight; a stale cursor reads as a bug
end;
Simetrija pri gašenju (shutdown) ono je što drži isticanje iskrenim. Svaka pauza, zaustavljanje (stop) i putanja okretanja stranice moraju završiti u ClearReadingWord. Izostavite to i žuti pravokutnik sjedit će na zaustavljenoj stranici izgledajući točno kao kvar, što je vrsta stvari koju će svaki tester prijaviti iako zapravo ništa nije pokvareno
Brzina govora opterećuje ovaj cjevovod više od veličine dokumenta. Pri 300 riječi u minuti, granični događaji stižu svakih 200 ms, a pri najbržim SAPI brzinama dolaze brže nego što ih oko ugodno prati. Pravi odgovor je spajanje (coalesce), a ne stavljanje u red (queue). Ako nova granica stigne dok ažuriranje isticanja još uvijek čeka, odbacite staro (stale) i oslikajte najnovije. Kursor koji posjećuje svaku riječ po redu, ali kasni pola sekunde, osjeća se slomljeno; onaj koji povremeno preskoči riječ dok ostaje sinkroniziran s glasom, ne osjeća se
Granični slučajevi koji odvajaju demoe od proizvoda
Nekoliko kategorija dokumenata otkriva šavove (seams). Kombinirani znakovi (Combining characters) su najsuptilniji: Unicode sekvence kao što su osnovno slovo plus kombinirani dijakritički znak mogu zauzeti više indeksa znakova nego što vizualna riječ sugerira, tako da bilo kakva aritmetika pomaka koja pretpostavlja jedan indeks po glifu polako skreće (drifts). To je najjači argument za to da se dopusti TrackReadingWordAt posjedovanje mapiranja, umjesto da se ručno izračunavaju brojevi riječi. Rastavljanje riječi na slogove (Hyphenation) je prozaičnije, ali češće: riječ prekinuta prijelomom retka postaje dva okvira, a ako je izgovorite kao jedan token (token), granični događaj za njezinu drugu polovicu rješava se na prvi okvir. To je obično u redu, ali to je odluka, pa je donesite namjerno umjesto da je otkrijete. Označavanje (Tagging) mijenja sam redoslijed čitanja. Kada dokument nosi ispravne oznake strukture (područje ISO 14289, PDF/UA), slijed riječi prati logičku strukturu; bez njih, vraća se na heuristiku rasporeda (layout heuristics), a neoznačena stranica u dva stupca može se čitati ravno preko oba stupca. Rotirane stranice posljednja su uobičajena pojava: Rect svake riječi i dalje je točno ograničava u prostoru stranice, ali politika praćenja prikaza (viewport-follow) podešena za vodoravni tok pomiče se na trzav način kada tekst ide okomito, stoga zadržite barem jedan rotirani dokument u regresijskom skupu. Za rukovanje redoslijedom čitanja, jedinice na razini rečenice (sentence-level) preko ReadingUnits i širi stog za pomoćne tehnologije (assistive stack), pogledajte izgradnju pristupačnog PDF čitača u Delphiju
Jedno ograničenje platforme oblikuje implementaciju (deployment). SAPI je samo za Windows. API za okvir riječi i praćenje (word-box and tracking API) bajt-po-bajt je identičan pod Lazarusom i FPC-om, ali buildovima za Linux i macOS treba drugačiji sintesajzer ožičen iza istih graničnih događaja; ta je postavka pokrivena u pokretanju preglednika pod Lazarusom i FPC-om. Trošak isticanja (Highlight cost) također je u interakciji s predmemorijom stranice (page cache) kada se brzina govora poveća, a aritmetika proračuna u predmemoriranju iscrtavanja (render caching) i performansama zumiranja ovdje se prenosi bez promjena
Kada je isticanje jedne riječi kriva granularnost
Karaoke na razini riječi nije uvijek ono što čitatelj želi. Pri visokim brzinama govora titranje pokazivača riječ po riječ postaje vlastiti vizualni šum (visual noise), i neki slušatelji prate rečenicu ugodnije od stroboskopa pojedinačnih riječi. Za taj slučaj, komponenta izlaže grublju jedinicu. ReadingUnits vraća jedinice na razini rečenice i bloka, svaka sa svojim vlastitim pravokutnicima za isticanje, i vi ih oslikavate pomoću SetReadingHighlight umjesto SetReadingWord. Ožičenje (wiring) je istog oblika: pomak granice (boundary offset) još uvijek određuje koja će jedinica zasvijetliti, ali jedinica koju istaknete proteže se na klauzulu ili redak umjesto na jedan token. Sporiji čitatelji i visokobrzinska reprodukcija (high-rate playback) oboje ga radije biraju, i ništa vas ne sprječava da ponudite oba načina iza postavke (setting)
Pragovi verzija (version floors) vrijedni su pribadanja prije nego što počnete graditi prema ovome: okviri riječi trebaju PDFium Component v1.53 ili noviji, a kursor za praćenje treba v1.56. Puni API za čitanje, jedinice na razini rečenice i radni demo za čitanje naglas nalaze se na stranici proizvoda za komponentu PDFium Component