Tekninen artikkeli

Sanakohtainen (Word-by-Word) TTS-korostus (Highlighting) Delphi PDFium -katseluohjelmissa

Ääneenlukutoiminnolla on äänen lisäksi yksi näkyvä tehtävä: jokaisen puhutun sanan kohdalla kyseinen sana on korostettava sivulla ja pidettävä näkyvissä Tähän tarvitaan jokaisen sanan rajaava suorakulmio indeksoituna samaan merkkivirtaan, josta puhemoottori lukee Jos rajaavat suorakulmiot saadaan mutta indeksointi epäonnistuu, korostus jää sana tai kaksi jälkeen äänestä; jos indeksointi onnistuu mutta sivutilan käsittely mokataan, korostus osuu kokonaan väärälle sivulle Tämän puhepuoli, itse syntetisaattori, on osa, joka harvoin hajoaa SAPI raportoi sanarajat merkin tarkkuudella Se, mikä hajoaa, on ohut kartoituskerros puhepuskurin merkkisiirtymän ja renderöidyllä sivulla olevan suorakulmion välillä

PDFium Component toimittaa tämän kartoituksen Delphille, C++Builderille ja Lazarukselle; sanalaatikot ovat olleet saatavilla versiosta v1.53 lähtien ja seurantakursori versiosta v1.56 Pinta-ala on tarkoituksella kapea: kutsu, joka palauttaa sivun sanalaatikot, seuraaja, joka muuttaa merkkisiirtymän maalatuksi korostukseksi, sekä pari ominaisuutta väriä ja automaattista vieritystä varten Kapeudestaan huolimatta järjestys, jossa kutsut tehdään, ratkaisee toimiiko ominaisuus, ja useimmat alla kuvatuista virheistä johtuvat oikeiden funktioiden kutsumisesta väärässä järjestyksessä

Merkit eivät ole sanoja, ja puhesynteesimoottorit puhuvat merkkeinä

Puhemoottori kuluttaa litteän merkkijonon ja raportoi edistymisen merkkiasemina kyseisessä merkkijonossa PDF-sivulla on glyyfit sijoitettuna sivutilaan, jossa "sana" on heuristinen glyyfijonojen klusteri Nämä kaksi koordinaatistoa eivät jaa mitään, ellei syntetisaattorille annettu teksti ole tavusta tavuun sama teksti, josta sanalaatikot on laskettu Tämä on sääntö numero yksi, eikä se anna anteeksi Jos tyhjätila normalisoidaan, pehmeät tavuviivat poistetaan tai poimittua tekstiä muutoin "siistitään" ennen sen puhumista, jokainen myöhempi siirtymä menee hiljaisesti väärin Puhu tarkalleen se, mitä poimit, tai pidä yllä eksplisiittistä siirtymien uudelleenkartoitustaulukkoa Kolmatta vaihtoehtoa, joka selviytyisi todellisista asiakirjoista, ei ole

Uudelleenkartoitustaulukko ei ole hypoteettinen reunatapaus Heti kun käyttöliittymäsi lisää puhutun sivuilmoituksen ("sivu viisi") tai laajentaa lyhenteen syntetisaattoria varten, puhuttu merkkijono eroaa poimitusta Tallenna jokaisen lisäyksen sijainti ja pituus, ja vähennä sitten kertynyt korjaus ennen jokaista seurantakutsua Kirjanpitoa on ehkä kaksikymmentä riviä, ja se on ero korostuksen välillä, joka selviää seuraavasta ominaisuuspyynnöstä, ja sellaisen välillä, joka hajoaa heti kun joku pyytää puhuttuja otsikoita

Mitä sanalaatikko antaa

Jokainen TPdfWordBox-tietue kantaa sanan tekstin, sen StartIndex-arvon ja merkkimäärän Count sivutekstin sisällä, sivutilassa olevan Rect-suorakulmion sekä ykkösestä alkavan Page-numeron StartIndex-kenttä on silta kahden koordinaatiston välillä: se on sama siirtymä, jonka SAPI antaa takaisin lukiessaan PageWordBoxes palauttaa koko taulukon aktiiviselle sivulle

procedure TReaderForm.PreparePage(PageNo: Integer);
begin
  PdfView.PageNumber := PageNo;   // näkymän sanalaatikot seuraavat sen näyttämää sivua

  FWords := PdfView.PageWordBoxes;
  FPageText := BuildSpeechText(FWords);   // yhdistä Word.Text-arvot järjestyksessä

  if Length(FWords) = 0 then
    HandleImageOnlyPage(PageNo);          // skannaus ilman tekstikerrosta
end;

Järjestystä koskeva kommentti on ratkaisevan tärkeä Katseluohjelman PageWordBoxes tokenisoi sen sivun tekstikerroksen, jota näkymä parhaillaan näyttää, joten navigoi näkymä ensin ja poimi vasta sitten; renderöintiä ei tarvita, vain avattu asiakirja (Asiakirjakomponentti, TPdf, tarjoaa oman PageWordBoxes-toiminnon, joka on sidottu Pdf.PageNumber-arvoon päätöntä käyttöä varten Nämä kaksi sivunumeroa ovat toisistaan riippumattomia, mikä on oma ansansa) Tyhjä tulos sivulla, joka näyttää sisältävän sisältöä, tarkoittaa vain kuvana olevaa skannausta Ohjaa se OCR:ään tai ilmoita siitä ainakin ääneen ("sivu 4 ei sisällä luettavaa tekstiä") sen sijaan, että ääni vaikenisi ilman selitystä

Kaavio Delphi teksti puheeksi -putkesta, jossa SAPI-merkin siirtymät virtaavat TPdfWordBox StartIndex -arvojen kautta PDFium-sanasuorakulmioiksi, jotka TrackReadingWordAt maalaa
SAPI raportoi merkkioffsetin, ja jokaisen TPdfWordBox:n StartIndex muuntaa kyseisen offsetin sanasuorakulmioksi renderöidylle sivulle

SAPI-sanarajojen kytkeminen seuraajaan

TrackReadingWordAt, näkymässä, on koko ominaisuuden sarana Anna sille sivunumero ja merkki-indeksi; se löytää kyseisen merkin sisältävän sanalaatikon, maalaa lukukursorin sen päälle ja palauttaa sanaindeksin, tai −1, kun indeksi osuu sanojen väliin SAPI:n sanarajailmoitus toimittaa juuri sen merkkiaseman, jota se tarvitsee

procedure TReaderForm.OnSpeechWordBoundary(StreamPos: Integer);
var
  WordIdx: Integer;
begin
  // Kartoittaa siirtymän sanalaatikkoon ja siirtää korostuksen yhdellä kutsulla
  WordIdx := PdfView.TrackReadingWordAt(FPageNo, StreamPos);
  if WordIdx < 0 then
    Exit;                     // raja osui minkään sanan ulkopuolelle: pidä edellinen korostus
end;

Kaksi varovaisuutta osoittavaa yksityiskohtaa ansaitsee huomion tässä Ensinnäkin TrackReadingWordAt pitää omaa sanalaatikkovälimuistiaan seuratulle sivulle, joka rakennetaan uudelleen automaattisesti sivun vaihtuessa, joten kustannus per raja pysyy tasaisena riippumatta siitä, kuinka nopeasti rajat saapuvat Toiseksi se ei tarkista rajoja anteliaasti Indeksi, joka on sivun merkkimäärän tasalla tai sen yli, palauttaa −1 sen sijaan, että se rajattaisiin viimeiseen sanaan Kohtele −1-arvoa "pidä edellinen korostus" -tilana, ei koskaan virheenä, koska välimerkkijaksot ja sanojen välinen tyhjätila tuottavat oikeutetusti rajoja, jotka eivät kuulu millekään sanalle Jokaisen −1:n lokittaminen hautaa sinut Laske ne sivukohtaisesti sen sijaan, ja tarkastele tarkkaan jokaista sivua, jolla suhde piikittyy, sillä se yleensä tarkoittaa tekstin normalisoinnin ristiriitaa takaisin sääntöön numero yksi

Itse kursori: väri, seuranta ja siivous

SetReadingWord maalaa korostuksen suoraan, kun pidät sanalaatikkoa itse hallussasi, ReadingWordColor tyylittelee sen, ja ReadingWordFollow := True vierittää näkymää juuri sen verran, että puhuttu sana pysyy näkyvissä Tuo viimeinen ominaisuus ansaitsee paikkansa Käsin rakennettu "keskitä nykyinen sana" -vieritys saa sivun nykimään jokaisella rivinvaihdolla, ja liikeherkät lukijat sammuttavat koko ominaisuuden minuutin sisällä Korostus renderöityy vain sivulle, joka näytetään aktiivisessa TPdfView-komponentissa parhaillaan, joten monisivuisen lukemisen on edettävä PageNumber-arvon kanssa puheen tahdissa ja sitten ajettava valmisteluvaihe uudelleen uudelle sivulle ennen kuin sen ensimmäinen rajatapahtuma saapuu Jätä se tekemättä, ja jokaisen sivun ensimmäiset korostukset osoittavat vanhentuneisiin koordinaatteihin

Kaavio puhevirrasta, joka ylittää PDF-sivun käännöksen Delphissä, edeten PageNumberissa ja valmistellen sanalaatikot uudelleen ennen seuraavaa rajatapahtumaa
Kun rajatapahtuma ylittää seuraavalle sivulle, etene PageNumber-osoittimessa ja aja valmisteluvaihe uudelleen ennen kuin seuraava korostus laskeutuu
procedure TReaderForm.StopReading;
begin
  FVoice.Stop;                // pysäytä SAPI-toisto ensin
  PdfView.ClearReadingWord;   // poista sitten korostus; vanhentunut kursori näyttää bugilta
end;

Symmetria sammutuksessa on se, mikä pitää korostuksen rehellisenä Jokaisen tauon, pysäytyksen ja sivunvaihdon polun on päädyttävä ClearReadingWord-kutsuun Jätä se pois, ja keltainen suorakulmio jää lojumaan pysäytetylle sivulle näyttäen täsmälleen vialta, minkä jokainen testaaja raportoi, vaikka mikään ei oikeasti ole rikki

Puhenopeus kuormittaa tätä putkea enemmän kuin asiakirjan koko 300 sanan minuuttinopeudella rajatapahtumat saapuvat 200 ms:n välein, ja nopeimmilla SAPI-nopeuksilla ne tulevat nopeammin kuin silmä mukavasti seuraa Oikea vastaus on yhdistäminen, ei jonottaminen Jos uusi raja saapuu, kun korostuksen päivitys on vielä kesken, hylkää vanhentunut ja maalaa uusin Kursori, joka käy läpi jokaisen sanan järjestyksessä mutta laahaa puoli sekuntia jäljessä, tuntuu rikkinäiseltä; sellainen, joka silloin tällöin hyppää sanan yli pysyen synkronissa äänen kanssa, ei tunnu

Reunatapaukset, jotka erottavat demot tuotteista

Muutamat asiakirjaluokat paljastavat saumat Yhdistelmämerkit ovat hienovaraisimpia: Unicode-sekvenssit, kuten peruskirjain plus yhdistelmädiakriitti, voivat viedä enemmän merkki-indeksejä kuin visuaalinen sana antaa ymmärtää, joten mikä tahansa siirtymäaritmetiikka, joka olettaa yhden indeksin per glyyfi, ajautuu hitaasti vinoon Tämä on vahvin argumentti sen puolesta, että TrackReadingWordAt saa omistaa kartoituksen sen sijaan, että sananumerot laskettaisiin käsin Tavutus on arkisempaa mutta yleisempää: rivinvaihdon yli katkennut sana muuttuu kahdeksi laatikoksi, ja jos puhut sen yhtenä yksikkönä, sen jälkiosan rajatapahtuma ratkeaa ensimmäiseen laatikkoon Tämä on yleensä ihan ok, mutta se on päätös, joten tee se tarkoituksella sen sijaan, että löytäisit sen sattumalta Tagitus muuttaa itse lukujärjestystä Kun asiakirjalla on kunnolliset rakennetagit (ISO 14289:n, PDF/UA:n, alue), sanajärjestys seuraa loogista rakennetta; ilman niitä se turvautuu asettelun heuristiikkaan, ja kaksipalstainen tagittamaton sivu voi lukeutua suoraan molempien palstojen yli Kierretyt sivut ovat viimeinen yleinen tapaus: jokaisen sanan Rect rajaa sen edelleen oikein sivutilassa, mutta vaakavirtaukselle viritetty näkymän seurantakäytäntö vierittää nykivästi, kun teksti kulkee pystysuunnassa, joten pidä ainakin yksi kierretty asiakirja regressiojoukossa Lukujärjestyksen käsittelystä, lausetason yksiköistä ReadingUnits-toiminnon kautta sekä laajemmasta avustavasta pinosta, katso esteettömän PDF-lukijan rakentaminen Delphissä

Yksi alustarajoite muovaa käyttöönottoa SAPI toimii vain Windowsissa Sanalaatikko- ja seurantarajapinta on tavusta tavuun identtinen Lazaruksen ja FPC:n alla, mutta Linux- ja macOS-käännökset tarvitsevat eri syntetisaattorin kytkettynä samojen rajatapahtumien taakse; tuo asennus käydään läpi artikkelissa katseluohjelman ajaminen Lazaruksella ja FPC:llä Korostuksen kustannus on myös vuorovaikutuksessa sivuvälimuistisi kanssa puhenopeuksien kasvaessa, ja budjettiaritmetiikka artikkelissa renderöintivälimuisti ja zoomauksen suorituskyky pätee tässäkin muuttumattomana

Kun yksittäisen sanan korostus on väärä tarkkuustaso

Sanatason karaoke ei aina ole se, mitä lukija haluaa Korkeilla puhenopeuksilla sana sanalta vilkkuva kursori muuttuu omaksi visuaaliseksi häiriökseen, ja osa kuuntelijoista seuraa lausetta mukavammin kuin yksittäisten sanojen strobovaloa Tätä tapausta varten komponentti tarjoaa karkeamman yksikön ReadingUnits palauttaa lause- ja lohkotason yksiköitä, joilla kullakin on omat korostussuorakulmionsa, ja ne maalataan SetReadingHighlight-funktiolla SetReadingWord-funktion sijaan Kytkentä on samanmuotoinen: rajasiirtymä ohjaa edelleen, mikä yksikkö syttyy, mutta korostettava yksikkö kattaa lauseenosan tai rivin yksittäisen sanan sijaan Sekä hitaammat lukijat että nopea toisto suosivat yleensä tätä, eikä mikään estä tarjoamasta molempia tiloja asetuksen takana

Versiorajat kannattaa selvittää ennen tämän varaan rakentamista: sanalaatikot vaativat PDFium Component -version v1.53 tai uudemman, ja seurantakursori vaatii version v1.56 Koko lukurajapinta, lausetason yksiköt ja toimiva ääneenlukudemo löytyvät PDFium Component -tuotesivulta