Egy felolvasó (read-aloud) funkciónak a hangon túl egy látható feladata is van: amint minden szót kiejtenek, ki kell világítania azt a szót az oldalon, és szem előtt kell tartania. Ehhez szüksége van minden szó befoglaló keretére (bounding box), indexelve ugyanahhoz a karakterfolyamhoz, amelyből a beszédfelismerő (speech engine) olvas. Ha megkapja a kereteket, de eltéveszti az indexelést, a kiemelés (highlight) egy-két szóval elcsúszik a hang mögött; ha megvan az indexelés, de rosszul kezeli az oldalállapotot (page state), a kiemelés teljesen rossz oldalon landol. Ennek a beszéd része – maga a szintetizátor – az, ami ritkán törik el. A SAPI karakterszinten jelenti a szóhatárokat (word boundaries). Ami eltörik, az a vékony leképezési réteg (mapping layer) a beszédpufferben lévő karaktereltolás (offset) és a renderelt oldalon lévő téglalap között
A PDFium Component szállítja ezt a leképezést a Delphi, a C++Builder és a Lazarus számára, a szókeretek az 1.53-as, a követőkurzor (tracking cursor) pedig az 1.56-os verzió óta elérhető. A felület (surface) szándékosan szűk: egy hívás, amely visszaadja az oldal szókereteit, egy követő (tracker), amely egy karaktereltolást festett kiemeléssé alakít, valamint néhány tulajdonság a színhez és az automatikus görgetéshez (auto-scroll). Bármilyen szűk is, az a sorrend, ahogyan a dolgokat meghívja, dönti el, hogy a funkció működik-e, és az alábbi hibák többsége a megfelelő függvények rossz sorrendben történő meghívásából ered
A karakterek nem szavak, és a TTS motorok karakterekben beszélnek
A beszédmotor (speech engine) egy lapos (flat) karakterláncot fogyaszt, és az előrehaladást ezen karakterláncon belüli karakterpozíciókként jelenti. Egy PDF oldal glifákat (glyphs) tartalmaz az oldaltérben elhelyezve, ahol a "szó" a glifafutások (glyph runs) heurisztikus halmaza. A két koordinátarendszernek semmi közös vonása nincs, hacsak az a szöveg, amelyet a szintetizátornak átad, nem bájtról bájtra az a szöveg, amelyből a szókereteket kiszámították. Ez az első szabály, és ez könyörtelen. Ha normalizálja a szóközöket, eltávolítja a lágy kötőjeleket, vagy más módon "megtisztítja" a kinyert szöveget a felolvasás előtt, akkor minden lejjebb (downstream) lévő eltolás (offset) csendben rossz lesz. Mondja fel pontosan azt, amit kinyert, vagy tartson fenn egy explicit eltolás-újraleképezési táblázatot (offset remapping table). Nincs harmadik lehetőség, amely túlélné a valódi dokumentumokat
Az újraleképezési táblázat nem egy hipotetikus peremeset (edge case). Abban a pillanatban, amikor a felhasználói felülete beilleszt egy kimondott oldalbejelentést ("ötödik oldal"), vagy kibont egy rövidítést a szintetizátor számára, a kimondott karakterlánc eltér a kinyerttől. Rögzítse minden egyes beillesztés pozícióját és hosszát, majd vonja le a felhalmozott korrekciót minden követési hívás (tracking call) előtt. Ez talán húsz sornyi könyvelés, és ez a különbség egy olyan kiemelés között, amely túléli a következő funkciókérést, és egy olyan között, amelyik eltörik, amikor valaki először kér felolvasott címsorokat
Amit egy szókeret ad önnek
Minden TPdfWordBox rekord hordozza a szó szövegét, annak StartIndex és karakter Count (darab) értékét az oldalszövegben, egy oldaltérbeli Rect-et, valamint az 1-alapú Page oldalszámot. A StartIndex mező a híd a két koordinátarendszer között: ez ugyanaz az eltolás, amelyet a SAPI olvasás közben visszaad. A PageWordBoxes visszaadja a teljes tömböt az aktív oldalhoz:
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;
A sorrendi megjegyzés teherviselő (load-bearing). A nézegető PageWordBoxes tulajdonsága tokenizálja a nézet által aktuálisan megjelenített oldal szövegrétegét, ezért először navigálja a nézetet, és csak azután végezze el a kinyerést; ehhez nincs szükség renderelésre, csak egy nyitott dokumentumra. (A dokumentum komponens, a TPdf, felfedi a saját PageWordBoxes tulajdonságát a Pdf.PageNumber-hez kötve fej nélküli (headless) használatra. A két oldalszám független, ami önmagában is egy csapda.) Egy üres eredmény egy olyan oldalon, amely láthatóan tartalmaz tartalmat, egy csak képet tartalmazó beolvasást (scan) jelent. Irányítsa tovább az OCR-hez, vagy legalább jelentse be ("a 4. oldal nem tartalmaz olvasható szöveget"), ahelyett, hogy hagyná elhallgatni a hangot mindenféle magyarázat nélkül
A SAPI szóhatárok bekötése a követőbe (tracker)
A nézegetőn a TrackReadingWordAt az egész funkció zsanérja (hinge). Adjon neki egy oldalszámot és egy karakterindexet; megtalálja az azt a karaktert tartalmazó szókeretet, ráfesti az olvasókurzort (reading cursor), és visszaadja a szóindexet, vagy −1-et, ha az index szavak közé esik. A SAPI szóhatár (word-boundary) értesítése pontosan azt a karakterpozíciót szolgáltatja, amit kér:
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;
Két defenzív részlet szolgálja meg az árát itt. Először is, a TrackReadingWordAt saját szókeret-gyorsítótárat (cache) tart fenn a követett oldalhoz, amely az oldalváltáskor automatikusan újraépül, így a határonkénti költség lapos marad, függetlenül attól, hogy milyen gyorsan érkeznek a határok. Másodszor, nem ellenőrzi a határokat (bounds-check) nagyvonalúan. Egy index, amely eléri vagy meghaladja az oldal karakterszámát, −1-et ad vissza ahelyett, hogy a végső szóhoz igazítaná magát (clamping). Kezelje a −1-et úgy, mint "tartsa meg az előző kiemelést", soha ne hibaként, mivel az írásjelek sorozatai és a szavak közötti szóközök jogosan hoznak létre olyan határokat, amelyek egyik szóhoz sem tartoznak. Ha minden −1-et naplóz, az eltemeti önt. Ehelyett számolja meg őket oldalanként, és alaposan vizsgáljon meg minden olyan oldalt, ahol az arány megugrik, mivel ez általában az egyes szabályra visszavezethető szövegnormalizálási eltérést jelent
A kurzor maga: szín, követés és takarítás
A SetReadingWord közvetlenül festi meg a kiemelést, amikor ön maga fogja a szókeretet, a ReadingWordColor stílusozza azt, a ReadingWordFollow := True pedig éppen annyira görgeti a nézetet, hogy a kimondott szó látható maradjon. Ez az utolsó tulajdonság kiérdemli a helyét. Egy kézzel írt (hand-rolled) "középre a jelenlegi szót" görgetés minden sortörésnél meglódítja az oldalt, és a mozgásérzékeny olvasók egy percen belül kikapcsolják az egész funkciót. A kiemelés csak az aktív TPdfView-ban jelenleg mutatott oldalon renderelődik, így a többoldalas olvasásnak (multi-page reading) a beszéddel lépésben kell léptetnie a PageNumber értékét, majd újra le kell futtatnia az előkészítő lépést az új oldalhoz, mielőtt az első határesemény megérkezne. Hagyja ki ezt, és minden oldal első néhány kiemelése elavult (stale) koordinátákra mutat majd
procedure TReaderForm.StopReading;
begin
FVoice.Stop; // halt SAPI playback first
PdfView.ClearReadingWord; // then remove the highlight; a stale cursor reads as a bug
end;
A leállítás (shutdown) szimmetriája az, ami őszintén tartja a kiemelést. Minden szüneteltetési, leállítási és lapozási útvonalnak a ClearReadingWord paranccsal kell végződnie. Ha kihagyja, egy borostyánsárga (amber) téglalap ül egy leállított oldalon, ami pontosan úgy néz ki, mint egy hiba (defect), és ez az a fajta dolog, amit minden tesztelő jelenteni fog, holott valójában semmi sincs eltörve
A beszédsebesség jobban megterheli ezt a csővezetéket (pipeline), mint a dokumentum mérete. Percenként 300 szónál a határesemények (boundary events) 200 milliszekundumonként érkeznek, és a leggyorsabb SAPI sebességnél gyorsabban jönnek, mint ahogy a szem kényelmesen követni tudná. A helyes válasz az összevonás (coalesce), nem pedig a sorba állítás (queue). Ha új határ érkezik, miközben egy kiemelésfrissítés (highlight update) még függőben van, dobja el az elavultat, és fesse meg a legújabbat. Egy kurzor, amelyik minden szót sorban meglátogat, de fél másodpercet késik, töröttnek érződik; az, amelyik alkalmanként kihagy egy szót, miközben szinkronban marad a hanggal, nem
Peremesetek (Edge cases), amelyek elválasztják a demókat a termékektől
A dokumentumok néhány kategóriája felfedi a varratokat (seams). A kombináló karakterek a legkifinomultabbak: a Unicode szekvenciák – például egy alapbetű és egy kombináló diakritikus jel – több karakterindexet is elfoglalhatnak, mint amit a vizuális szó sugall, így bármilyen eltolási aritmetika (offset arithmetic), amely glifánként egy indexet feltételez, lassan elcsúszik (drifts). Ez a legerősebb érv amellett, hogy hagyjuk a TrackReadingWordAt-et birtokolni a leképezést, ahelyett, hogy kézzel számolnánk a szószámokat. Az elválasztás (hyphenation) földhözragadtabb, de gyakoribb: egy sortörésen átívelően megtört szó két keretté válik, és ha egyetlen tokenként mondja ki, akkor a második felének határeseménye az első keretre oldódik fel (resolves). Ez általában rendben van, de ez egy döntés, ezért hozza meg szándékosan, ahelyett, hogy csak úgy felfedezné. A címkézés (tagging) magát az olvasási sorrendet (reading order) változtatja meg. Amikor egy dokumentum megfelelő struktúracímkéket hordoz (az ISO 14289, PDF/UA területe), a szósorrend (word sequencing) a logikai struktúrát követi; nélkülük visszaesik az elrendezési heurisztikákra (layout heuristics), és egy kétoszlopos, címkézetlen oldal egyenesen mindkét oszlopon keresztül olvashat. Az elforgatott oldalak az utolsó gyakori esetek: minden egyes szó Rect-je továbbra is helyesen határolja azt az oldaltérben, de a vízszintes áramlásra hangolt nézetablak-követő (viewport-follow) házirend zavaróan görget, amikor a szöveg függőlegesen fut, ezért tartson legalább egy elforgatott dokumentumot a regressziós halmazban. Az olvasási sorrend kezelésével, a ReadingUnits-en keresztüli mondatszintű (sentence-level) egységekkel és a szélesebb körű kisegítő (assistive) vermmel kapcsolatban lásd az akadálymentes PDF olvasó építése Delphiben című részt
Egy platformkorlátozás formálja a telepítést (deployment). A SAPI csak Windowson érhető el. A szókeret (word-box) és a követési API bájtról bájtra azonos a Lazarus és az FPC alatt, de a Linux és macOS fordításokhoz (builds) egy másik szintetizátorra van szükség, amelyet ugyanazok a határesemények mögé kötnek be; erről a beállításról a nézegető futtatása Lazarus és FPC alatt című cikk foglalkozik. A kiemelési költség (highlight cost) szintén kölcsönhatásba lép az oldal gyorsítótárával, amint a beszédsebesség növekszik, és a renderelési gyorsítótárazás és nagyítási (zoom) teljesítmény költségvetési aritmetikája (budget arithmetic) változtatás nélkül átkerül ide is
Amikor az egyszavas kiemelés a rossz granularitás (granularity)
A szószintű karaoke nem mindig az, amit az olvasó akar. Nagy beszédsebességnél a szavanként villogó kurzor önálló vizuális zajá (visual noise) válik, és egyes hallgatók egy mondatot kényelmesebben követnek, mint az egyes szavak stroboszkópját. Erre az esetre a komponens egy durvább egységet (coarser unit) tár fel. A ReadingUnits mondat- és blokkszintű egységeket ad vissza, mindegyik saját kiemelési téglalappal rendelkezik, és ön a SetReadingHighlight-tal festi meg őket a SetReadingWord helyett. A bekötés ugyanolyan formájú: továbbra is egy határeltolás (boundary offset) vezérli, hogy melyik egység világítson, de a kiemelt egység egy tagmondatot vagy egy sort ível át (spans), nem pedig egyetlen tokent. A lassabb olvasók és a nagy sebességű lejátszás is egyaránt előnyben részesíti ezt, és semmi sem akadályozza meg abban, hogy mindkét módot felkínálja egy beállítás mögött
A verzióküszöböket (version floors) érdemes leszögezni, mielőtt ez ellen építkezne: a szókeretekhez a PDFium Component 1.53-as vagy újabb verziója, a követőkurzorhoz pedig az 1.56-os verzió szükséges. A teljes olvasási API, a mondatszintű egységek és egy működő read-aloud demó a PDFium Component termékoldalán található