Teknisk artikel

Ord-for-ord TTS-fremhævelse i Delphi PDFium-fremvisere

En højtlæsningsfunktion har én synlig opgave ud over stemmen: Efterhånden som hvert ord tales, skal den lyse det pågældende ord op på siden og holde det i syne. For at gøre det har du brug for afgrænsningsboksen (bounding box) for hvert ord, indekseret til den samme tegnstrøm, som talemotoren læser fra. Få boksene, men miss indekseringen, og fremhævelsen driver et ord eller to bagud for lyden; få indekseringen, men håndtér sidetilstanden forkert, og fremhævelsen lander på den helt forkerte side. Taledelen af dette, selve synthesizeren, er den del, der sjældent går i stykker. SAPI rapporterer ordgrænser til tegnet. Det, der går i stykker, er det tynde kortlægningslag mellem en tegnoffset i talebufferen og et rektangel på den gengivne side

PDFium Component leverer den kortlægning til Delphi, C++Builder og Lazarus, med ordbokse tilgængelige siden v1.53 og sporingsmarkøren (tracking cursor) siden v1.56. Overfladen er bevidst smal: et kald der returnerer ordboksene for en side, en tracker der gør en tegnoffset til en malet fremhævelse, og et par egenskaber til farve og auto-scroll. Selvom det er smalt, afgør den rækkefølge, du kalder tingene i, om funktionen virker, og de fleste af fejlene nedenfor kommer fra at kalde de rigtige funktioner i den forkerte rækkefølge

Tegn er ikke ord, og TTS-motorer taler i tegn

En talemotor forbruger en flad streng og rapporterer fremskridt som tegnpositioner inden for den streng. En PDF-side har glyffer placeret i sideområdet, hvor et "ord" er en heuristisk klynge af glyf-kørsler. De to koordinatsystemer deler intet, medmindre den tekst, du giver synthesizeren, er byte-for-byte den tekst, ordboksene blev beregnet ud fra. Det er regel et, og den er ubarmhjertig. Normaliser mellemrum, fjern bløde bindestreger, eller "ryd op" på anden vis i den udpakkede tekst før du taler den, og hver downstream-offset er stille og roligt forkert. Tal præcis, hvad du trak ud, eller behold en eksplicit offset-omlægningstabel. Der er ingen tredje mulighed, der overlever rigtige dokumenter

Omlægningstabellen er ikke et hypotetisk kanttilfælde. Det øjeblik din brugergrænseflade indsætter en talt sidebesked ("side fem") eller udvider en forkortelse for synthesizeren, afviger den talte streng fra den udtrukne. Registrer hver indsættelses position og længde, og træk derefter den akkumulerede justering fra før hvert sporingskald. Det er måske tyve linjers bogholderi, og det er forskellen mellem en fremhævelse, der overlever den næste funktionsanmodning, og en, der går i stykker, første gang nogen beder om talte overskrifter

Hvad en ord-boks giver dig

Hver TPdfWordBox post bærer ordets tekst, dets StartIndex og tegn-Count inden for sideteksten, en side-område Rect, og det 1-baserede Page nummer. Feltet StartIndex er broen mellem de to koordinatsystemer: det er den samme offset, SAPI vil aflevere tilbage, når den læser. PageWordBoxes returnerer det fulde array for den aktive side:

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;

Bemærkningen om rækkefølge er bærende. Fremviserens PageWordBoxes tokeniserer tekstlaget på den side, fremvisningen aktuelt viser, så naviger i visningen først og træk ud derefter; ingen gengivelse er påkrævet, kun et åbent dokument. (Dokumentkomponenten, TPdf, eksponerer sin egen PageWordBoxes, der er knyttet til Pdf.PageNumber til headless brug. De to sidenumre er uafhængige, hvilket er sin egen fælde.) Et tomt resultat på en side, der synligt bærer indhold, betyder en scanning, der kun består af billeder. Diriger det til OCR, eller annoncer det i det mindste ("side 4 indeholder ingen læsbar tekst"), i stedet for at lade stemmen forstumme uden forklaring

Forbindelse af SAPI-ordgrænser til trackeren

TrackReadingWordAt på fremviseren er hængslet for hele funktionen. Giv den et sidenummer og et tegnindeks; den finder den ord-boks, der indeholder det tegn, maler læsemarkøren på den og returnerer ordindekset, eller -1 når indekset falder mellem ord. SAPIs meddelelse om ordgrænse leverer præcis den tegnposition, den ønsker:

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;

To defensive detaljer tjener deres berettigelse her. For det første holder TrackReadingWordAt sin egen ord-boks-cache for den sporede side, som automatisk genopbygges, når siden ændres, så omkostningerne pr. grænse forbliver flade, uanset hvor hurtigt grænserne ankommer. For det andet foretager den ikke grænsekontrol (bounds-check) generøst. Et indeks på eller ud over sidens tegntælling returnerer -1 i stedet for at klamre sig til det sidste ord. Behandl -1 som "behold den forrige fremhævelse", aldrig som en fejl, fordi tegnsætningskørsler og mellemrum mellem ord legitimt producerer grænser, der ikke tilhører noget ord. At logge hver -1 vil begrave dig. Tæl dem pr. side i stedet, og kig nøje på enhver side, hvor forholdet stiger, da det normalt betyder en tekstnormaliserings-uoverensstemmelse tilbage ved regel et

Selve markøren: farve, følg, og oprydning

SetReadingWord maler fremhævelsen direkte, når du selv holder ord-boksen, ReadingWordColor styler den, og ReadingWordFollow := True ruller visningen lige nok til at holde det talte ord synligt. Den sidste egenskab tjener sin plads. Et håndrullet "centrer det aktuelle ord"-rul får siden til at rykke ved hvert linjeskift, og bevægelsesfølsomme læsere vil slå hele funktionen fra inden for et minut. Fremhævelsen gengives kun på den side, der aktuelt vises i den aktive TPdfView, så læsning af flere sider skal rykke PageNumber frem i takt med talen, og derefter køre forberedelsestrinnet for den nye side, før dens første grænsehændelse (boundary event) lander. Springer du det over, peger de første par fremhævelser på hver side på forældede koordinater

procedure TReaderForm.StopReading;
begin
  FVoice.Stop;                // halt SAPI playback first
  PdfView.ClearReadingWord;   // then remove the highlight; a stale cursor reads as a bug
end;

Symmetri ved nedlukning er det, der holder fremhævelsen ærlig. Enhver pause, stop og sideskift-sti skal ende i ClearReadingWord. Udelad det, og et gult rektangel sidder på en stoppet side og ligner nøjagtig en defekt, hvilket er den slags ting, enhver tester vil rapportere, selvom intet faktisk er i stykker

Talehastighed stresser denne pipeline hårdere end dokumentstørrelse gør. Ved 300 ord i minuttet ankommer grænsehændelserne hver 200 ms, og ved de hurtigste SAPI-hastigheder kommer de hurtigere, end øjet komfortabelt kan spore. Det rigtige svar er at sammensmelte, ikke lægge i kø. Hvis en ny grænse ankommer, mens en fremhævelsesopdatering stadig afventer, skal du droppe den forældede og male den seneste. En markør, der besøger hvert ord i rækkefølge, men halter et halvt sekund bagefter, føles i stykker; en, der lejlighedsvis springer et ord over, mens den forbliver synkroniseret med stemmen, gør ikke

Kanttilfælde, der adskiller demoer fra produkter

Et par kategorier af dokumenter afslører sømmene. Kombinerende tegn er de mest subtile: Unicode-sekvenser såsom et basisbogstav plus et kombinerende diakritisk tegn kan optage flere tegnindekser end det visuelle ord antyder, så enhver offset-aritmetik, der forudsætter et indeks pr. glyf, driver langsomt. Det er det stærkeste argument for at lade TrackReadingWordAt eje kortlægningen i stedet for at beregne ordnumre i hånden. Orddeling er mere verdslig, men mere almindelig: et ord brudt over et linjeskift bliver til to bokse, og hvis du taler det som et enkelt token, løses grænsehændelsen for dens anden halvdel til den første boks. Det er normalt fint, men det er en beslutning, så tag den med vilje i stedet for at opdage den. Tagging ændrer selve læserækkefølgen. Når et dokument bærer korrekte strukturmærker (området for ISO 14289, PDF/UA), følger ordsekventering den logiske struktur; uden dem falder det tilbage til layoutheuristik, og en to-søjlet utagget side kan læses lige over begge søjler. Roterende sider er den sidste almindelige: hvert ords Rect afgrænser det stadig korrekt i sideområdet, men en viewport-følg politik, der er afstemt til vandret flow, ruller skurrende, når teksten løber lodret, så behold mindst et roteret dokument i regressionssættet. For håndtering af læserækkefølge, sætningsniveau-enheder via ReadingUnits, og den bredere assistive stak, se opbygning af en tilgængelig PDF-læser i Delphi

En platformbegrænsning former udrulningen. SAPI er kun til Windows. Word-box- og sporings-API'et er byte-for-byte identisk under Lazarus og FPC, men Linux- og macOS-builds har brug for en anden synthesizer, der er forbundet bag de samme grænsehændelser; den opsætning er dækket i kørsel af fremviseren under Lazarus og FPC. Fremhævelsesomkostninger interagerer også med din side-cache, når talehastighederne stiger, og budgetaritmetikken i gengivelses-cache og zoomydelse overføres hertil uden ændringer

Når fremhævelse af enkeltord er den forkerte granularitet

Ord-niveau karaoke er ikke altid det, en læser ønsker. Ved høje talehastigheder bliver markørens flimren ord for ord til sin egen visuelle støj, og nogle lyttere følger en sætning mere komfortabelt end en stroboskop (strobe) af enkelte ord. For det tilfælde eksponerer komponenten en grovere enhed. ReadingUnits returnerer sætnings- og blokniveau-enheder, hver med sine egne fremhævelsesrektangler, og du maler dem med SetReadingHighlight i stedet for SetReadingWord. Forbindelsen har samme form: en grænse-offset styrer stadig, hvilken enhed der lyser op, men den enhed, du fremhæver, spænder over en sætning eller en linje snarere end et enkelt token. Både langsommere læsere og højhastighedsafspilning har en tendens til at foretrække det, og intet forhindrer dig i at tilbyde begge tilstande bag en indstilling

Versionsgulvene er værd at fastslå, før du bygger imod dette: ord-bokse kræver PDFium Component v1.53 eller nyere, og sporingsmarkøren kræver v1.56. Det fulde læse-API, sætningsniveau-enhederne og en fungerende højtlæsnings-demo findes på produktsiden for PDFium Component