Technical Article

Označevanje besed (Word-by-Word Highlighting) pri pretvorbi besedila v govor (TTS) v Delphiju

Funkcija branja na glas ima poleg samega glasu še eno vidno nalogo: ko je posamezna beseda izgovorjena, jo mora na strani osvetliti in jo ohraniti v vidnem polju. Za to potrebujete očrtni okvir (bounding box) vsake besede, indeksiran na isti tok znakov, iz katerega bere govorni mehanizem. Če pridobite okvirje, a zgrešite indeksiranje, bo označba zaostajala besedo ali dve za zvokom; če pravilno indeksirate, a napačno upravljate stanje strani, pa bo označba pristala na povsem napačni strani. Sintetizator govora, ki predstavlja najzahtevnejši del sistema, se redko pokvari. SAPI poroča o mejah besed natančno do znaka. Tisto, kar se pokvari, je tanek preslikovalni sloj med odmikom znaka v govornem odložišču in pravokotnikom na izrisani strani.

PDFium Component ponuja to preslikavo za Delphi, C++Builder in Lazarus, pri čemer so okvirji besed na voljo od različice v1.53, sledilni kurzor pa od različice v1.56. Nabor funkcij je namenoma ozek: klic, ki vrne okvirje besed za stran, sledilnik, ki spremeni odmik znaka v izrisano označbo, ter nekaj lastnosti za barvo in samodejno pomikanje. Kljub enostavnosti pa vrstni red klicanja funkcij določa, ali bo funkcija sploh delovala. Večina spodnjih težav izvira iz klicanja pravih funkcij v napačnem zaporedju.

Znaki niso besede, govorni mehanizmi pa govorijo v znakih

Govorni mehanizem obdela raven niz znakov in poroča o napredku kot o znakovnih položajih znotraj tega niza. Stran PDF vsebuje glife (glyphs), nameščene v prostoru strani, kjer je "beseda" hevristična skupina zaporednih glifov. Ta dva koordinatna sistema nimata nič skupnega, razen če je besedilo, ki ga predate sintetizatorju, do zadnjega bajta enako besedilu, iz katerega so bili izračunani okvirji besed. To je prvo in neusmiljeno pravilo. Če pred izgovorjavo normalizirate presledke, odstranite delilne črtice ali kako drugače "očistite" ekstrahirano besedilo, bo vsak nadaljnji odmik napačen. Govorite natanko to, kar ste ekstrahirali, ali pa vzdržujte eksplicitno tabelo za preslikavo odmikov. Triaža pri dejanskih dokumentih ne obstaja.

Tabela za preslikavo ni le hipotetičen robni primer. V trenutku, ko vaš uporabniški vmesnik vgradi govorjeno obvestilo o strani ("stran pet") ali razširi kratico za sintetizator, se govorjeni niz začne razlikovati od ekstrahiranega. Zabeležite položaj in dolžino vsake vgradnje ter odštejte skupno prilagoditev pred vsakim klicem sledenja. To zahteva le približno dvajset vrstic kode, vendar pomeni razliko med označbo, ki bo preživela naslednjo nadgradnjo, in tisto, ki se bo pokvarila takoj, ko bo nekdo zahteval govorjene naslove.

Kaj vam ponuja okvir besede

Vsak zapis TPdfWordBox vsebuje besedilo besede, njen StartIndex in število znakov Count znotraj besedila strani, koordinatni pravokotnik Rect v prostoru strani ter 1-indeksirano številko strani Page. Polje StartIndex je most med obema koordinatnima sistemoma: gre za enak odmik, kot ga bo SAPI vrnil med branjem. Funkcija PageWordBoxes vrne celotno polje za aktivno stran:

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;

Opomba glede vrstnega reda je ključna. Metoda PageWordBoxes pregledovalnika tokenizira besedilni sloj strani, ki jo pogled trenutno prikazuje, zato najprej navigirajte pogled in šele nato ekstrahirajte. Izris (rendering) ni potreben, zadošča le odprt dokument. (Komponenta dokumenta TPdf ponuja lastno metodo PageWordBoxes, vezano na Pdf.PageNumber za headless uporabo. Ti dve številki strani sta neodvisni, kar je svoja past.) Prazen rezultat na strani, ki očitno vsebuje vsebino, pomeni, da gre za skenirano sliko. Usmerite jo v OCR ali pa vsaj predvajajte obvestilo ("stran 4 ne vsebuje berljivega besedila"), namesto da glas utihne brez pojasnila.

Povezovanje meja besed SAPI s sledilnikom

Metoda TrackReadingWordAt na pregledovalniku je osrednji del celotne funkcije. Predajte ji številko strani in indeks znaka; poiskala bo okvir besede, ki vsebuje ta znak, nanj izrisala bralni kurzor ter vrnila indeks besede oziroma -1, ko indeks pade med besede. SAPI-jevo obvestilo o meji besede ponuja natanko tisti položaj znaka, ki ga 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;

Dve podrobnosti tukaj opravljata varnostno nalogo. Prvič, TrackReadingWordAt vzdržuje lasten pomnilnik okvirjev besed za sledeno stran, ki se samodejno osveži ob spremembi strani, zato strošek na posamezno mejo ostane nizek ne glede na to, kako hitro meje prihajajo. Drugič, metoda ne izvaja ohlapnega preverjanja meja. Indeks na koncu ali čez število znakov na strani vrne -1 in se ne omeji samodejno na zadnjo besedo. Vrednost -1 obravnavajte kot navodilo "ohrani prejšnjo označbo" in nikoli kot napako, saj ločila in presledki med besedami povsem legitimno ustvarjajo meje, ki ne pripadajo nobeni besedi. Beleženje vsake vrednosti -1 bi ustvarilo preveč šuma v dnevnikih. Namesto tega jih štejte na stran in natančno preglejte vsako stran, kjer to razmerje izrazito poskoči, saj to običajno pomeni neskladje pri normalizaciji besedila iz prvega pravila.

Kurzor sam po sebi: barva, sledenje in čiščenje

Metoda SetReadingWord neposredno izriše označbo, ko sami držite okvir besede, ReadingWordColor določa njen slog, nastavitev ReadingWordFollow := True pa pomakne pogled ravno toliko, da govorjena beseda ostane vidna. Ta zadnja lastnost je izjemno pomembna. Ročno izdelano pomikanje tipa "središči trenutno besedo" povzroči, da stran ob vsakem prelomu vrstice sunkovito skoči, uporabniki, občutljivi na premikanje zaslona, pa bodo celotno funkcijo izklopili v minuti. Označba se izriše le na strani, ki jo trenutno prikazuje aktivni TPdfView, zato mora večstransko branje posodabljati PageNumber usklajeno z govorom, nato pa ponovno izvesti korak priprave za novo stran, preden prispe njen prvi dogodek meje besede. Če to izpustite, bo prvih nekaj označb na vsaki strani kazalo na zastarele 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 ob zaustavitvi je tisto, kar ohranja označevanje pravilno. Vsaka pot premora, zaustavitve ali obračanja strani se mora končati s klicem ClearReadingWord. Če to izpustite, bo oranžen pravokotnik ostal na zaustavljeni strani in deloval kot vizualna napaka, kar bo prijavil vsak preizkuševalec, čeprav tehnično ni nič pokvarjeno.

Hitrost govora obremeni ta cevovod bolj kot sama velikost dokumenta. Pri 300 besedah na minuto dogodki meja prihajajo vsakih 200 ms, pri najvišjih hitrostih SAPI pa prihajajo hitreje, kot jim oko lahko udobno sledi. Pravilen odziv je združevanje (coalesce) in ne ustvarjanje čakalne vrste. Če prispe nova meja, medtem ko posodobitev označbe še poteka, opustite zastarelo in izrišite najnovejšo. Kurzor, ki zaporedoma obišče vsako besedo, a zaostaja za pol sekunde, deluje pokvarjeno; tisti, ki občasno preskoči besedo, a ostane usklajen z glasom, pa ne.

Robni primeri, ki ločijo predstavitve od končnih izdelkov

Nekatere kategorije dokumentov razkrijejo šibke točke. Kombinirani znaki (combining characters) so najsubtilnejši: zaporedja Unicode, kot je osnovna črka plus kombinirani diakritični znak, lahko zasedejo več znakovnih indeksov, kot nakazuje vizualna beseda, zato vsaka aritmetika odmikov, ki predvideva en indeks na glif, počasi začne zaostajati. To je najmočnejši argument za to, da prepustite preslikavo metodi TrackReadingWordAt, namesto da ročno računate številke besed. Deljenje besed (hyphenation) je bolj vsakdanje, a pogostejše: beseda, prelomljena na koncu vrstice, postane dva okvirja. Če jo izgovorite kot en žeton, se dogodek meje za njeno drugo polovico razreši v prvi okvir. To je običajno sprejemljivo, vendar gre za odločitev, ki jo morate sprejeti načrtno. Označevanje (tagging) spremeni sam vrstni red branja. Če dokument vsebuje pravilne strukturne oznake (področje standarda ISO 14289, PDF/UA), zaporedje besed sledi logični strukturi; brez njih se vrne k hevristiki postavitve, pri čemer se dvostolpčna neoznačena stran lahko prebere preprosto vodoravno čez oba stolpca. Rotirane strani so zadnji pogost primer: Rect vsake besede jo še vedno pravilno omejuje v prostoru strani, vendar se politika sledenja vidnemu polju, prilagojena vodoravnemu toku, premika sunkovito, ko besedilo poteka navpično. Zato v svoj testni nabor vključite vsaj en rotiran dokument. Za upravljanje vrstnega reda branja, enote na ravni stavkov prek ReadingUnits in širšo podporno tehnologijo si oglejte članek o izdelavi dostopnega bralnika PDF v Delphiju.

Ena omejitev platforme vpliva na namestitev. SAPI je na voljo le v sistemu Windows. API za okvirje besed in sledenje je bajt za bajtom enak v okoljih Lazarus in FPC, vendar gradnje za Linux in macOS potrebujejo drug sintetizator, povezan z istimi dogodki meja; ta nastavitev je opisana v članku o delovanju pregledovalnika v okoljih Lazarus in FPC. Strošek označevanja vpliva tudi na vaš pomnilnik strani, ko hitrost govora naraste, proračunska aritmetika iz članka o pomnilniku izrisa in zmogljivosti približevanja pa se tukaj prenese brez sprememb.

Ko je označevanje posameznih besed napačna raven podrobnosti

Označevanje na ravni posameznih besed (karaoke slog) ni vedno tisto, kar poslušalec želi. Pri visokih hitrostih govora utripanje kurzorja besedo za besedo postane vizualni šum, nekateri poslušalci pa stavku lažje sledijo kot pa nenehnemu utripanju posameznih besed. Za ta primer komponenta ponuja večjo enoto. Lastnost ReadingUnits vrne enote na ravni stavkov in blokov, vsako z lastnimi pravokotniki označb, ki jih izrišete s pomočjo SetReadingHighlight namesto SetReadingWord. Povezava je enake oblike: odmik meje še vedno določa, katera enota zasveti, vendar enota, ki jo označite, obsega stavek ali vrstico namesto posameznega žetona. Počasnejši bralci in hitro predvajanje običajno preferirajo ta način, nič pa vam ne preprečuje, da bi ponudili oba načina prek nastavitve.

Pred razvojem je vredno določiti minimalne različice: okvirji besed zahtevajo PDFium Component v1.53 ali novejšo, sledilni kurzor pa v1.56. Celoten bralni API, enote na ravni stavkov in delujoča predstavitev branja na glas so na voljo na strani izdelka za PDFium Component.