Techninis straipsnis

Žodis po žodžio TTS (teksto į kalbą) paryškinimas Delphi PDFium peržiūros programose

Skaitymo garsu funkcija, be balso generavimo, turi vieną svarbų vizualų tikslą: kiekvieną ištartą žodį reikia apšviesti puslapyje ir išlaikyti jį matomą ekrane. Tam reikia gauti kiekvieno žodžio apribojantį langelį (bounding box), susietą su tuo pačiu simbolių srautu, kurį naudoja kalbos sintezatorius. Jei turėsite langelius, bet pamiršite indeksavimą, paryškinimas atsiliks nuo garso vienu ar dviem žodžiais; jei indeksuosite tinkamai, bet nesusitvarkysite su puslapio būsena, paryškinimas išvis atsidurs kitame puslapyje. Pati kalbos funkcija, pats sintezatorius, genda retai. SAPI atpažįsta žodžių ribas vieno simbolio tikslumu. Problema dažniausiai atsiranda ploname sinchronizavimo sluoksnyje, esančiame tarp kalbos buferio simbolio postūmio (offset) ir atvaizduoto puslapio stačiakampio

Komponentas „PDFium Component“ turi šią susiejimo funkciją programoms „Delphi“, „C++Builder“ ir „Lazarus“, su žodžių apribojimo langeliais nuo v1.53 versijos ir sekančiu žymekliu (tracking cursor) nuo v1.56 versijos. Sistemos galimybės pateikiamos sąmoningai siaurai: vienas metodas, grąžinantis puslapio žodžių langelius, sekiklis (tracker), kuris simbolio postūmį paverčia nuspalvintu paryškinimu, ir kelios savybės spalvai bei automatiniam slinkimui nustatyti. Kad ir koks paprastas būtų šis sprendimas, vykdymo seka nulemia ar funkcija veiks sėkmingai, ir dauguma žemiau išvardintų klaidų kyla būtent dėl neteisingos komandų eilės

Simboliai nėra žodžiai, ir TTS varikliai kalba simboliais

Kalbos variklis naudoja „plokščią“ teksto eilutę (flat string) ir seka procesą remdamasis simbolių pozicijomis joje. PDF puslapyje glifai dedami į erdvę, kur „žodis“ laikomas euristiniu (heuristic) glifų fragmentų junginiu. Skirtingos koordinačių sistemos neturi bendrų taškų, išskyrus tuos atvejus, kai tekstas, paduodamas sintezatoriui, baitu į baitą sutampa su tekstu, naudotu identifikuoti (compute) tuos pačius žodžių rėmelius. Būtent tai yra pirmoji taisyklė ir jai nedaroma jokių išimčių. Jei tekstą „išvalysite“: normalizuosite tarpus (whitespace), pašalinsite minkštuosius brūkšnelius, kiekvienas vėlesnis rodiklis taps nematomas ir neteisingas. Teksto sintezei paduokite (speak) tik tai, ką ištraukėte iš pradžių (extracted), arba veskite atskirą žodžių perskaičiavimo (offset) lentelę. Trečios opcijos, veikiančios su tikrais dokumentais – paprasčiausiai nėra

Atkūrimo ir modifikacijų identifikavimo lentelė – toli gražu ne teorinė išlyga (hypothetical edge case). Vos tik jūs į vartotojo sistemą pridedate automatinį puslapio pranešimą (pvz. „puslapis numeris penki“) arba sistemos sintezatoriui praplečiate santrumpas, iš karto sakomas tekstas pradeda skirtis nuo tolesnių skaitmeninių duomenų ištrauktų anksčiau. Įrašykite kiekvieno naujo (insertion) įrašymo vietą, po to kiekvieną kartą dar prieš aktyvuojant skambučių atsekimą atimkite susikaupusį nuokrypį nuo originalo. Sistemoje užtenka visai minimaliai įrašų – apie dvidešimt, tam kad išvengtume defektų kai bus pateiktas pirmasis prašymas identifikuoti (iššaukti) „kalbančių“ (spoken) pavadinimų funkcijas

Ką jums duoda žodžių dėžutė (word box)

Kiekvienas TPdfWordBox įrašas turi pačio žodžio tekstą, jo StartIndex (pradinį indeksą) ir raidžių Count (kiekį) tame faile esančiame puslapio tekste, bei matmenų stačiakampį Rect puslapio erdvėje (page-space) kartu su 1-grįstu Page (puslapiu) formatu. Komanda StartIndex šiame procese (bridge) prilygsta vieninteliam svertui atliekančiam dviejų koordinačių sujungimą į vientisą sistemą: jį nuskaitęs SAPI patikimai sugrąžins iš karto po įvykio. Komanda PageWordBoxes nedelsiant pateikia atgal (returns) nustatytą masyvą jūsų aktyviam dokumento langui (active page):

procedure TReaderForm.PreparePage(PageNo: Integer);
begin
  PdfView.PageNumber := PageNo;   // skaityklės įrankiai fiksuoja tuometinį savo displėjaus ekrano langą

  FWords := PdfView.PageWordBoxes;
  FPageText := BuildSpeechText(FWords);   // nuoseklus visų Word.Text komandų sutalpinimas paeiliui

  if Length(FWords) = 0 then
    HandleImageOnlyPage(PageNo);          // skenavimo metu patekusi versija neturinti teksto pagrindo
end;

Sistemos įkėlimo komentarai tiesiog privalomi jūsų įkrovai (load-bearing). Komponentas PageWordBoxes padalina puslapio teksto sluoksnį (layer) į žetonus (tokenizes). Puslapio žetonai parodomi komponente TPdfView (jį ir naudosite skaityklėje), todėl su komanda nustatykite matymo puslapį, o tik po to – tekstų ekstrakciją. Pats procesas nevyksta be pačio failo, atvaizdavimui (rendering) failą būtina turėti atidarytą sistemoje. (Dokumentinis komponentas TPdf, atskleidžia (exposes) asmenines reikšmes per PageWordBoxes (kurios tiesiogiai užkoduojamos (keyed) komandoje Pdf.PageNumber nereikalaujant galvosūkio). Reikšmės, esančios dviejų dokumentų puslapių numeriuose absoliučiai nieko viena kitai nereiškianti sistema, ir visame kode ji yra ne kas kito, kaip užslėpta bomba (trap). Jeigu komanda iššaukė klaidas toje pačioje sistemoje, pabaigus ieškojimus ji praneš nulinę teksto reikšmę, nepaisant ekrane vizualiai atsirandančios, tačiau tik tekstinės informacijos prilygstančios atvaizdui. Nukreipkite į OCR (skaitmeninimo funkciją) (arba garsiai perskaitykite nurodymą „Šiame, numeris ketvirtas, puslapyje neturime normalaus įskaitomo teksto“), ir niekuomet neleiskite „garso (balsui) užtilti nepaliekant klausytojams pasiteisinimo“

SAPI žodžio ribų prijungimas prie (tracker) sekimo

Funkcijos TrackReadingWordAt vieta peržiūros lange veikia lyg uždaras pagrindinis durų vyris (hinge). Pateikite iššaukiamam komponentui pačio puslapio skaičių numerį, kaip ir atskirą asmeninį komandos indikatorių (index); ši procedūra nedelsiant ieško tekstinio puslapio dėžės „box“, apimančio visą jūsų (trackerio indikatoriaus paduotą nurodymą), nupiešia (dažo) aktyvų, nuskaitomą vaizdo rodiklį (reading cursor) – grąžina visų su indeksais surištų (associated with) išraiškų rodiklį arba, klaidų procesuose pateikia reikšmę (grąžina atgal atvaizdavimui išraišką −1, nurodydama trūkstamą duomenų indikatorių, pamestą patikros (išgavimo) ribose tarp nustatytų skaičiaus verčių „words“). O svarbiausia: SAPI tekstas tiesiogiai „informuoja“ paduodant reikalaujamą (demanded) indeksą be nukrypimo išraiškų:

procedure TReaderForm.OnSpeechWordBoundary(StreamPos: Integer);
var
  WordIdx: Integer;
begin
  // Suderinamai su žodžių riboms „priskiriama“ (offset) pačiam naršyklės laukui po procedūros pasukimo vienu iškvietimu (call)
  WordIdx := PdfView.TrackReadingWordAt(FPageNo, StreamPos);
  if WordIdx < 0 then
    Exit;                     // žymeklio komandos vieta, liko neliesta - pasiliekamas įprastinis (naudotas pradinio formato) spalvų formatas
end;

Pats kodas (the code) palieka dvi esmines procedūras – užkertančias problemas (defensive details). Pati funkcija TrackReadingWordAt, išlaiko savyje žodžių blokų atmintinę atskirtam sistemos naudojimui, persikeliant per apžiūrėtas (tracked) puslapio lango reikšmes, pasiekiamas ir be-apkraunamai nustatomus automatiškai, nepaisant naujų nuskaitomų apribojimų pritekėjimo tempo. Taip įgaunant spartesnį (flat) apdirbimą ir nereikalaujant pakartotinų tikrinimų, net ir pačiu didžiausiu nuskaitomu leistinu įkrovimo laiku. Antra, patikrinimas indeksų srityse visiškai apribojamas (bounds-check generously). Kuomet ribos skaičius nustatomas ties galimybių užbaigimu arba jį viršija, vietoje automatinio sustojimo fiksuojamas −1 rezultatas. −1 laikykite nuostata („išsaugant paskutinį sistemos įjungimą (highlight)“) ir jokiu būdu nelaikykite to „būgštavimo“ (error) pagrindu. Pagrindiniai skyrybos įrankiai dažnai nepriklauso jokiai tekstinės dalies sistemai – juos sugrąžinama iššaukus (reikšmių nesuteikiant). Jums bus per daug informacijos loguojant atskirus kiekvieno puslapio rezultatus, todėl geriausia juos pačius atsekti sistemos puslapių rėmuose, taip skaičiuojant atsirandančius (spikes) suaktyvėjimus – tikras tekstinės padalos (normalizavimo sistemoje) trūkumas nurodo, kad pačio teksto atskaitymo „rėmuose“ atsirado nukrypimas pagal pirmąją išvadų formuluotės „Rule One“ (Pirma Taisyklė) tvarką

Pats žymeklis: spalva, stebėjimas ir sistemos pašalinimas

Sistema komandoje SetReadingWord nudažo apšviečiamą vietą tuo pačiu momentu kai „pasibaigia“ (hold) apribojantį žodžio bloką savarankiškai (yourself). Šablono (Color) indikatorius (styles it) nustato parametrą pagal ReadingWordColor, po jo seka nuoseklus aktyvavimas, kuomet komandai ReadingWordFollow := True suaktyvinamas paprastas puslapio „slidimo“ atvaizdavimo „išlaikant esamo nuskaitymo“ tekstą. Suaktyvintas režimas palengvina (užsidirba savo vietą). Jei patys savarankiškai „subalansuojate žodžius ekrano centre“, sistema nepriekaištingai persikels pačiam teksto formatui (lurch) (on every line break). Naudotojai (readers), nepaisant vizualinės reakcijos, gali akimirksniu pašalinti sisteminę atitiktį jei per daug suksis (perkels) „kalbant“ tekstui. Paryškintos apimties tekstas rodomas tik tuo metu rodomame lange programos sistemoje (aktyviame) TPdfView (peržiūroje). Todėl įvairiapusės apimties lapų rodiklių (multi-page) padidinimai per PageNumber formatą pasiekiami sklandžiai (at in step) per (speech), prieš tai pasikliaunant (re-run) atsinaujinusia versija su pakeistu per-nustatyto (prepare step) pasirinkimu. Atsisakykite apribojimų (skip that) atsisakydami apibendrinimų, palikdami kelis klaidinančius kodo (highlights) pakeitimus, visiems laikams nusistovinti sistemoje be peržiūrėjimo ribos

procedure TReaderForm.StopReading;
begin
  FVoice.Stop;                // sustabdyti SAPI atkūrimą pirmiausia
  PdfView.ClearReadingWord;   // paskui pašalinti paryškinimą; pasenęs žymeklis traktuojamas kaip klaida (bug)
end;

Kritinis dalykas sprendimo įvykio stotele (shutdown) vadinamas tiesa palaikančio (highlight honest) sisteminio perėjimo pagrindu. Bet kurio puslapio keitimo posūkis turi prasidėti per ClearReadingWord atlikto išsamesnio sustojimo. Būtina vengti neaiškių geltonų kvadrato stačiakampių stotelėje – dėl palikto vaizdinio programinės įrangos kodo vizualo visi pasidarys sprendimus testuoti gedimus (defect), kaip testų išvadose nors faktiškai sistemoje nesklandumų nebūta

Sintetinamo garso nuskaitomi reikalavimai (stress) atsiperka labiau – negu dokumento talpinimo formatas. Su sklandžiais 300 ištariamų žodžių kiekvieną minutę – perėjimo komandos signalai priteka greičiau, nei suspėja žmogaus apžvalginis (tracking) supratimas per kiekvieną 200 ms dalies trukmės normą, o pati SAPI atsispindi daug greičiau, nei pavyksta normaliam akiračiui susikaupti atsekti. Geriausia šio etapo reakcija, reaguoti priskiriant sklandžioms apjungimams – o ne palikti pranašumu eile. Paleidžiant priskirtas naujas sistemos kodo gaires tuo atveju jei vizualinė teksto versija bus (pending) apdorojimo fazėje (nepriimama iš esmės) pašalinkite nereikšmingus duomenis ir tęskite nupiešdami tik vėliausius sprendimus. Jei perbėgęs visus įrašus – markas palieka vėlavimą, pusę sekundės, tai suvokiama kaip techninė brokuotė – ir priešingu atveju atskirai retkarčiais paslepiamas „žodis“ tiesiog idealiai sutampa atliekant pranešėjui nuskaitant – viskas atrodo puikiai (does not)

Ribiniai atvejai skiriantys pavyzdžius nuo produktų

Keletas dokumentų tipų išties išryškina problemines vietas (seams). Daugiausiai kėblumų sukuria besijungiantys simboliai (combining characters): tokios „Unicode“ sekos, kaip pagrindinė raidė su jungiamuoju diakritiniu ženklu, gali užimti daugiau simbolių indeksų, nei sufleruoja vizualus žodis. Dėl to bet kokia postūmio (offset) aritmetika, preziumuojanti, jog vienam glifui tenka vienas indeksas (vienai raidei), po truputį ima nukrypti. Tai pats stipriausias argumentas leisti pačiam TrackReadingWordAt atsakyti už pozicijų atvaizdavimą, užuot bandžius žodžių numeraciją skaičiuoti rankiniu būdu. Žodžių kėlimas (hyphenation) per eilutes yra paprastesnis, bet pasitaiko gerokai dažniau: padalintas žodis per eilutės lūžį pavirsta į du skirtingus langelius (two boxes). Jeigu toks žodis nuskaitomas lyg vienas atskiras vienetas (token), jo antrajai pusei priklausantis ribos įvykis (boundary event) sugeneruoja pirmojo langelio atitikmenį. Tai dažniausiai priimtina (fine), bet tai yra jūsų dizaino sprendimas, todėl priimkite jį sąmoningai vietoje to, kad susidurtumėte atsitiktinai. Žymėjimas (Tagging) jau pats iš savęs modifikuoja skaitymo tvarką (reading order). Kai dokumentas (document) aprūpinamas tikslingomis struktūrinėmis (proper) žymomis (ISO 14289, PDF/UA sritis), žodžių seka paklūsta loginei struktūrai; be jų, remiamasi išdėstymo (layout) spėjimais, o dviejų stulpelių bežymis (untagged) puslapis gali būti skaitomas išilgai abiejų stulpelių. Galiausiai pasukami puslapiai (rotated pages) sukuria paskutinį iššūkį: kiekvieno žodžio Rect vis dar teisingai aprėpia jį puslapio erdvėje, tačiau horizontalaus (horizontal) slinkimo strategija, suderinta peržiūrai judant kartu (viewport-follow policy), elgsis labai erzinančiai pritaikius ją vertikaliai slenkančiam tekstui (runs vertically), taigi turėkite po ranka bent vieną pritaikomą besisukantį dokumentą sistemos testavimų palyginimams

Vienas platformos apribojimas smarkiai įtakoja sistemos diegimą. SAPI skirtas tik (Windows) aplinkoms. Pats langelių ir atsekamųjų komandų (word-box and tracking) kodų API funkcionalumas yra baitu į baitą (byte-for-byte) identiškas laukiant Lazarus bei FPC diegimuose, o Linux ir macOS platformos pareikalaus kitokių papildymų suderintam sinchronizuojančiam (synthesizer) komponentui veikiančiam per vienodus įvykių iššaukimus. Ta specifika detaliai aprėpiama skyriuje naudojant peržiūros programą su Lazarus ir FPC. Taip pat apšvietimo (highlight) funkcija reikalauja resursų, tad pradėjus kilti generuojamo garso dažniui – puslapio kodo spartinimui (cache) paliktas krūvis bus panašus, kokį turėjome su biudžeto išlaidų santykių aritmetika (budget arithmetic) skiltyje „render caching and zoom performance“ ir šie apribojimai čia išlieka neliesti

Kada pavienių žodžių paryškinimas yra per smulkus

Pats vadinamasis „karaokė žodis-po-žodžio“ (karaoke) sprendimas – toli gražu ne tas sprendimas, kurio pageidauja pats klausytojas. Padidinus garso perdavimo greitį (speech rates) labai staigus rodiklio peršokimas (flickering) pats savaime virsta optiniu (visual) diskomforto faktoriumi – daliai naudotojų yra patogiau vienu metu susifokusuoti stebint rodomus apšvietimus ant esamo (sentence) sakinio – ne per kiekvieną žodį stebint asmeninių parametrų pakitimus prilygstančius blykstėms (strobe). Tam tikslui komponente sukurtas apibendrintas apšvietimo pasirinkimas (coarser unit). Komponento parametras ReadingUnits automatiškai pasiūlys teksto atkarpas bloko- ar sakinių rėmuose, parūpinant kiekvienam priderinto atskiro stačiakampio bloko sritį, išpiešiant jį (paint) per metodą SetReadingHighlight , o ne prieš tai buvusiu SetReadingWord formatu. Vidinis programavimo apjungimas nesiskiria – atskaitos taškas vis tiek diktuoja nustatytą vienetą (lighting up), bet nurodyta stebima atkarpa (unit) priskirta didesniam segmentui (span), pavyzdžiui visai eilutei iš karto (clause), o ne trumpai išrašytai „pavienio žodžio atspindžio reikšmei“ (token). Taip lėtesni bei didesniu garso takčiu klausantys naudotojai abu gali komfortabiliai (both tend to prefer it) prisiderinti prie individualių (reading) sprendimų, kadangi jums tiesiog nereikia nuspręsti kuriuos pasirinkimus blokuoti – juos įjungiant ir išjungiant paprasčiausiai pakeičiant per meniu langą (behind a setting)

Reikėtų patvirtinti versijų priklausomybes prieš statant funkcionalumą per-darant į šį metodą: žodžio aprėpties elementai (word boxes) pareikalaus (need) PDFium Component nuo versijos v1.53 ar (or) vėlesnės, sekėjui (tracking cursor) prireiks – nuo versijos v1.56. Visa aktuali atskiro priėjimo dokumentavimo sritis (reading API), kaip ir pateikiami struktūruoti nustatymai sakinių pagrindu paremti su integruota bandomąja audio garsinio įrašymo (read-aloud demo) sistema dokumentuota (are on the) specialiajame skaitmeninio puslapio priede – PDFium Component produkto pagrindiniame informaciniame puslapyje