Technical Article

Isticanje riječi po riječ pri TTS-u u Delphi PDFium preglednicima

Značajka čitanja naglas ima jedan vidljiv zadatak osim samog glasa: kako se koja riječ izgovara, mora osvijetliti tu riječ na stranici i zadržati je u vidnom polju. Da biste to postigli, potreban vam je granični okvir (bounding box) svake riječi, indeksiran prema istom toku znakova koji mehanizam za govor čita. Ako dobijete okvire, ali promašite indeksiranje, isticanje će kasniti riječ ili dvije za zvukom; ako pogodite indeksiranje, ali loše upravljate stanjem stranice, isticanje će završiti na potpuno pogrešnoj stranici. Dio ovog sustava koji je na razini kriptografije, sam sintisajzer, rijetko se kvari. SAPI prijavljuje granice riječi točno u znak. Ono što se kvari je tanki sloj mapiranja između pomaka znaka (character offset) u spremniku govora i pravokutnika na iscrtanoj stranici.

PDFium Component donosi to mapiranje za Delphi, C++Builder i Lazarus, pri čemu su okviri riječi dostupni od verzije 1.53, a kursor za praćenje od verzije 1.56. Sučelje je namjerno usko: poziv koji vraća okvire riječi za stranicu, sustav za praćenje koji pretvara pomak znaka u nacrtano isticanje te nekoliko svojstava za boju i automatsko pomicanje. Koliko god bilo usko, redoslijed kojim pozivate funkcije određuje radi li značajka uopće, a većina dolje navedenih pogrešaka proizlazi iz pozivanja ispravnih funkcija pogrešnim redoslijedom.

Znakovi nisu riječi, a TTS mehanizmi govore u znakovima

Mehanizam za govor obrađuje ravni niz znakova (string) i javlja napredak kao pozicije znakova unutar tog niza. PDF stranica sadrži glifove smještene u prostoru stranice, gdje je "riječ" heuristički skup nizova glifova. Ova dva koordinatna sustava ne dijele ništa zajedničko osim ako tekst koji prosljeđujete sintisajzeru nije bajt po bajt identičan tekstu iz kojeg su izračunati okviri riječi. To je prvo pravilo i ono je neumoljivo. Ako normalizirate razmake, uklonite meke crtice ili na drugi način "očistite" ekstrahirani tekst prije nego što ga predate govoru, svaki sljedeći pomak bit će neprimjetno pogrešan. Govorite točno ono što ste ekstrahirali ili vodite eksplicitnu tablicu za remapiranje pomaka. Ne postoji treća opcija koja može preživjeti rad sa stvarnim dokumentima.

Tablica za remapiranje nije hipotetski rubni slučaj. Trenutak kada vaše korisničko sučelje umetne izgovorenu najavu stranice ("stranica pet") ili proširi kraticu za sintisajzer, izgovoreni niz se razilazi od ekstrahiranog. Zabilježite poziciju i duljinu svakog umetanja, a zatim oduzmite akumuliranu prilagodbu prije svakog poziva za praćenje. To je možda dvadesetak linija koda za evidenciju, ali čini razliku između isticanja koje će preživjeti sljedeći zahtjev za novom značajkom i onoga koje će se slomiti čim netko zatraži izgovaranje naslova.

Što vam pruža okvir riječi

Svaki zapis TPdfWordBox sadrži tekst riječi, njezin StartIndex i broj znakova Count unutar teksta stranice, Rect u prostoru stranice te broj stranice Page počevši od 1. Polje StartIndex je most između dvaju koordinatnih sustava: to je isti pomak koji će SAPI vratiti tijekom čitanja. Funkcija PageWordBoxes vraća cijeli niz 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 iznimno važan. PageWordBoxes preglednika tokenizira tekstualni sloj stranice koju preglednik trenutno prikazuje, pa najprije navigirajte kroz preglednik, a tek onda ekstrahirajte; iscrtavanje nije potrebno, već samo otvoren dokument. (Komponenta dokumenta, TPdf, izlaže vlastiti PageWordBoxes povezan s Pdf.PageNumber za korištenje bez sučelja. Ta dva broja stranica su neovisna, što je zamka sama po sebi.) Prazan rezultat na stranici koja očito sadrži sadržaj označava skenirani dokument koji sadrži samo sliku. Usmjerite ga na OCR ili barem najavite ("stranica 4 ne sadrži tekst pogodan za čitanje"), umjesto da pustite da glas utihne bez ikakvog objašnjenja.

Povezivanje SAPI granica riječi s praćenjem

Metoda TrackReadingWordAt na pregledniku je ključ cijele ove značajke. Proslijedite joj broj stranice i indeks znaka; ona pronalazi okvir riječi koji sadrži taj znak, iscrtava kursor za čitanje na njemu i vraća indeks riječi, ili -1 kada indeks padne između riječi. SAPI-jeva obavijest o granici riječi pruža 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 svoju primjenu. Prvo, TrackReadingWordAt održava vlastitu predmemoriju okvira riječi za praćenu stranicu, koja se automatski ponovno gradi kada se stranica promijeni, pa trošak po pojedinoj granici ostaje konstantan bez obzira na to koliko brzo granice pristižu. Drugo, metoda ne provjerava granice previše velikodušno. Indeks na ili izvan broja znakova stranice vraća -1 umjesto da se ograniči na posljednju riječ. Tretirajte -1 kao "zadrži prethodno isticanje", a nikada kao pogrešku, karena interpunkcijski nizovi i razmaci između riječi opravdano stvaraju granice koje ne pripadaju nijednoj riječi. Bilježenje svakog rezultata -1 će vas preplaviti. Umjesto toga, brojite ih po stranici i pažljivo proučite svaku stranicu na kojoj taj omjer naglo skoči, jer to obično znači nepodudaranje u normalizaciji teksta iz prvog pravila.

Sam kursor: boja, praćenje i čišćenje

Svojstvo SetReadingWord iscrtava isticanje izravno kada sami držite okvir riječi, ReadingWordColor definira njegov stil, a ReadingWordFollow := True pomiče prikaz točno onoliko koliko je potrebno da izgovorena riječ ostane vidljiva. Ovo posljednje svojstvo u potpunosti opravdava svoje postojanje. Ručno izrađeno pomicanje za "centriranje trenutne riječi" uzrokuje nagle pomake stranice pri svakom prijelazu u novi red, pa će čitatelji osjetljivi na pokrete isključiti cijelu značajku u roku od jedne minute. Isticanje se iscrtava samo na stranici koja se trenutno prikazuje u aktivnom TPdfView, tako da čitanje više stranica mora pomicati PageNumber u korak s govorom, a zatim ponovno pokrenuti korak pripreme za novu stranicu prije nego što stigne njezin prvi događaj granice. Ako to preskočite, prvih nekoliko isticanja na svakoj stranici pokazivat će na zastarjele 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 isključivanju je ono što održava isticanje ispravnim. Svaki put za pauziranje, zaustavljanje i okretanje stranice mora završiti s ClearReadingWord. Ako to izostavite, jantarni pravokutnik ostaje na zaustavljenoj stranici i izgleda točno kao pogreška u programu, što je stvar koju će svaki tester prijaviti iako zapravo ništa nije pokvareno.

Brzina govora opterećuje ovaj cjevovod znatno više od veličine dokumenta. Pri 300 riječi u minuti, događaji granica dolaze svakih 200 ms, a pri najbržim SAPI stopama stižu brže nego što ih oko može ugodno pratiti. Ispravan odgovor je spajanje događaja, a ne njihovo stavljanje u red čekanja. Ako stigne nova granica dok je ažuriranje isticanja još na čekanju, odbacite zastarjelo i iscrtajte najnovije. Kursor koji posjećuje svaku riječ po redu, ali kasni pola sekunde, djeluje pokvareno; onaj koji povremeno preskoči riječ, ali ostaje usklađen s glasom, ne djeluje tako.

Rubni slučajevi koji razlikuju demonstracije od gotovih proizvoda

Nekoliko kategorija dokumenata otkriva slabosti. Kombinirajući znakovi su najsuptilniji: Unicode sekvence poput osnovnog slova i kombiniranog dijakritičkog znaka mogu zauzimati više indeksa znakova nego što vizualna riječ sugerira, pa svaka aritmetika pomaka koja pretpostavlja jedan indeks po glifu polako počinje odstupati. To je najjači argument da prepustite metodi TrackReadingWordAt upravljanje mapiranjem umjesto ručnog izračunavanja brojeva riječi. Rastavljanje riječi na kraju retka je uobičajenije: riječ podijeljena prijelazom retka postaje dva okvira, a ako je izgovorite kao jedan token, događaj granice za njezinu drugu polovicu razriješit će se na prvi okvir. To je obično u redu, ali to je odluka koju trebate donijeti svjesno, a ne je tek naknadno otkriti. Označavanje (tagging) mijenja sam redoslijed čitanja. Kada dokument sadrži ispravne strukturne oznake (područje norme ISO 14289, PDF/UA), redoslijed riječi slijedi logičku strukturu; bez njih se oslanja na heuristiku rasporeda, pa se neoznačena stranica s dva stupca može čitati vodoravno preko oba stupca. Rotirane stranice su posljednji česti slučaj: Rect svake riječi i dalje je ispravno ograničava u prostoru stranice, ali pravilo praćenja vidnog polja prilagođeno vodoravnom toku uzrokuje neugodno pomicanje kada tekst ide okomito, pa u skup za regresijsko testiranje uvrstite barem jedan rotirani dokument. Za upravljanje redoslijedom čitanja, jedinice na razini rečenice putem ReadingUnits i širi prateći sustav za pristupačnost, pogledajte članak o izgradnji pristupačnog PDF čitača u Delphi okruženju.

Jedno ograničenje platforme oblikuje implementaciju. SAPI je dostupan isključivo na sustavu Windows. API za okvire riječi i praćenje je bajt po bajt identičan pod Lazarusom i FPC-om, ali verzije za Linux i macOS zahtijevaju drugačiji sintisajzer povezan iza istih događaja granica; ta je konfiguracija opisana u članku o pokretanju preglednika pod Lazarusom i FPC-om. Trošak isticanja također utječe na predmemoriju stranica kada brzina govora poraste, a proračun iz članka o predmemoriranju iscrtavanja i performansama zumiranja ovdje se prenosi bez promjena.

Kada je isticanje pojedinačnih riječi pogrešna razina granulacije

Isticanje na razini pojedine riječi nije uvijek ono što čitatelj želi. Pri visokim brzinama govora, treperenje kursora s riječi na riječ postaje vizualni šum, a neki slušatelji lakše prate rečenicu nego bljeskanje pojedinačnih riječi. Za taj slučaj komponenta izlaže grublju jedinicu. Svojstvo ReadingUnits vraća jedinice na razini rečenice i bloka, od kojih svaka ima vlastite pravokutnike isticanja, a iscrtavate ih pomoću SetReadingHighlight umjesto SetReadingWord. Povezivanje je istog oblika: pomak granice i dalje određuje koja jedinica svijetli, ali jedinica koju ističete obuhvaća rečenicu ili redak umjesto jednog tokena. Sporiji čitatelji i reprodukcija velikom brzinom obično preferiraju ovaj način, a ništa vas ne sprječava da ponudite oba načina rada unutar postavki.

Minimalne verzije vrijedi utvrditi prije nego što započnete s razvojem: okviri riječi zahtijevaju PDFium Component v1.53 ili noviju verziju, a kursor za praćenje zahtijeva v1.56 ili noviju verziju. Cijeli API za čitanje, jedinice na razini rečenice i funkcionalna demonstracija čitanja naglas nalaze se na stranici proizvoda za PDFium Component.