Техническа статия

TTS маркиране дума по дума в Delphi PDFium прегледатели

Функцията за четене на глас има една видима задача отвъд гласа: докато всяка дума бива произнасяна, тя трябва да осветли тази дума на страницата и да я държи видима. За да направите това, имате нужда от ограничаващата кутия на всяка дума, индексирана към същия символен поток, от който чете речевата машина. Получите кутиите, но пропуснете индексирането и маркирането изостава с дума или две след аудиото; получите индексирането, но обработите грешно страничното състояние и маркирането попада на погрешна страница изцяло. Речевата част от това, самият синтезатор, е частта, която рядко се разваля. SAPI докладва границите на думите до символа. Онова, което се разваля, е тънкият слой на съпоставяне между отместването на символ в речевия буфер и правоъгълник на рендерираната страница

PDFium Component доставя това съпоставяне за Delphi, C++Builder и Lazarus, с word boxes налични от v1.53 и курсорът за проследяване от v1.56. Повърхността е умишлено тясна: извикване, връщащо word boxes за страница, проследявач, превръщащ отместване на символ в нарисуван маркер, и няколко свойства за цвят и автоматично превъртане. Колкото и тясно да е, редът на извикване решава дали функцията работи, и повечето от провалите по-долу идват от извикване на правилните функции в грешна последователност

Символите не са думи, а TTS машините говорят в символи

Речевата машина консумира плосък низ и докладва напредъка като позиции на символи в него. PDF страница има глифове, поставени в страничното пространство, където „дума" е хевристичен клъстер от изпълнения на глифове. Двете координатни системи не споделят нищо, освен ако текстът, предаден на синтезатора, не е байт по байт текстът, от който са изчислени word boxes. Това е правило едно и то е безмилостно. Нормализирайте интервалите, изтрийте меките тирета или по друг начин „изчистете" извлечения текст преди да го произнесете, и всяко последващо отместване е тихо грешно. Произнасяйте точно онова, което сте извлекли, или поддържайте явна таблица за преравняване на отместванията. Няма трета опция, оцеляваща с реални документи

Таблицата за преравняване не е хипотетичен граничен случай. В момента, в който вашият потребителски интерфейс вмъква произнесено съобщение за страница ("страница пет") или разширява съкращение за синтезатора, произнесеният низ се отклонява от извлечения. Записвайте позицията и дължината на всяко вмъкване, след което изваждайте натрупаната корекция преди всяко извикване за проследяване. Това е може би двадесет реда счетоводство, и е разликата между маркер, оцеляващ при следващата заявка за функция, и такъв, счупващ се при първото, когато някой поиска произнесени заглавия

Какво ви дава word box

Всеки запис TPdfWordBox носи текста на думата, нейния StartIndex и символен Count в текста на страницата, Rect в страничното пространство, и номера Page с единица за основа. Полето StartIndex е мостът между двете координатни системи: то е същото отместване, което SAPI ще върне при четене. PageWordBoxes връща пълния масив за активната страница:

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;

Коментарът за наредбата е важен. PageWordBoxes на прегледателя токенизира текстовия слой на страницата, текущо показвана от изгледа, затова навигирайте изгледа първо и извличайте второ; рендериране не се изисква, само отворен документ. (Компонентът на документа, TPdf, излага свои собствени PageWordBoxes, ключирани към Pdf.PageNumber, за употреба без интерфейс. Двата номера на страниците са независими, което е собствен капан.) Празен резултат на страница, визуално носеща съдържание, означава само-изображение сканиране. Насочете го към OCR, или поне го обявете ("страница 4 не съдържа четлив текст"), вместо да оставяте гласа да замлъкне без обяснение

Свързване на SAPI границите на думи с проследявача

TrackReadingWordAt, на прегледателя, е шарнирът на цялата функция. Дайте му номер на страница и индекс на символ; той намира word box, съдържащ този символ, рисува върху него курсора за четене, и връща индекса на думата, или −1 когато индексът попада между думи. SAPI-то уведомление за граница на дума доставя точно позицията на символ, която изисква:

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;

Два защитни детайла тук заслужават място. Първо, TrackReadingWordAt поддържа собствен кеш на word boxes за проследяваната страница, автоматично обновяван при промяна на страницата, така че разходът за всяка граница остава постоянен независимо от скоростта на пристигане. Второ, той не проверява границите щедро. Индекс на или след броя символи на страницата връща −1 вместо да се захваща към последната дума. Третирайте −1 като „задръжте предишния маркер", никога като грешка, защото пунктуационните изпълнения и интервалите между думите легитимно произвеждат граници, не принадлежащи на никоя дума. Записването на всяко −1 ще ви затрупа. Вместо това ги броете на страница и внимателно гледайте всяка страница, при която съотношението рязко нараства, тъй като обикновено това означава несъответствие в нормализацията на текста още при правило едно

Самият курсор: цвят, следване и почистване

SetReadingWord рисува маркера директно, когато държите самия word box, ReadingWordColor го стилизира, а ReadingWordFollow := True превърта изгледа точно толкова, за да поддържа видима произнасяната дума. Това последно свойство заслужава место. Ръчно написан „центрирай текущата дума" скрол разтърсва страницата при всяко прекъсване на ред, и читатели с чувствителност към движение ще изключат цялата функция за минута. Маркерът се рендерира само на страницата, текущо показвана в активния TPdfView, така че четенето на множество страници трябва да придвижва PageNumber в стъпка с речта, след което да повтаря стъпката за подготовка за новата страница преди да пристигне първото й гранично събитие. Пропуснете това и първите няколко маркера на всяка страница сочат към остарели координати

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

Симетрията при изключване е онова, което поддържа маркера честен. Всеки път за пауза, спиране и обръщане на страница трябва да завършва с ClearReadingWord. Оставете го и кехлибарен правоъгълник стои на спряна страница, изглеждайки точно като дефект, което е нещото, което всеки тестер ще докладва, дори когато нищо реално не е счупено

Скоростта на речта натоварва тази конвейерна линия по-силно от размера на документа. При 300 думи в минута граничните събития пристигат на всеки 200 мс, а при най-бързите SAPI скорости идват по-бързо, отколкото окото удобно проследява. Правилният отговор е да се обединяват, а не да се наредят в опашка. Ако нова граница пристига докато актуализацията на маркера все още е в изчакване, изхвърлете остарялата и рисувайте най-новата. Курсор, посещаващ всяка дума по ред, но изоставащ с половин секунда, изглежда счупен; такъв, случайно прескачащ дума докато остава синхронизиран с гласа, не изглежда

Гранични случаи, разграничаващи демотата от продуктите

Няколко категории документи излагат шевовете. Комбиниращите символи са най-фините: Unicode последователности като основна буква плюс комбиниращ диакритичен знак могат да заемат повече символни индекси, отколкото визуалната дума предполага, така че всяка аритметика на отместванията, приемаща един индекс за глиф, бавно се отклонява. Това е най-силният аргумент да оставите TrackReadingWordAt да притежава съпоставянето вместо да изчислявате номерата на думите на ръка. Прекъсването е по-тривиално, но по-честo: дума, прекъсната на прекъсване на ред, се превръща в две кутии, и ако я произнасяте като единичен токен, граничното събитие за втората й половина се разрешава до първата кутия. Обикновено е добре, но е решение, затова го вземете нарочно вместо да го откривате. Тагирането променя самия ред на четене. Когато документ носи правилни структурни тагове (територията на ISO 14289, PDF/UA), наредбата на думите следва логическата структура; без тях се връща към евристика за оформление, и двуколонна нетагирана страница може да чете директно и в двете колони. Завъртените страници са последния общ случай: Rect на всяка дума все още я ограничава правилно в страничното пространство, но политика за следване на изгледа, настроена за хоризонтален поток, превърта неловко когато текстът тече вертикално, затова дръжте поне един завъртян документ в регресионния набор. За обработка на реда на четене, единици на ниво изречение чрез ReadingUnits и по-широкия стек за помощни технологии, вижте изграждане на достъпен PDF четец в Delphi

Едно платформено ограничение оформя разполагането. SAPI е само за Windows. API за word boxes и проследяване е идентичен байт за байт под Lazarus и FPC, но сборките за Linux и macOS се нуждаят от различен синтезатор, включен зад същите гранични събития; тази настройка е разгледана в стартиране на прегледателя под Lazarus и FPC. Разходът за маркиране също взаимодейства с кеша за страниците, когато скоростите на речта се покачат, и бюджетната аритметика в кеширане на рендериране и производителност при мащабиране се пренася тук без промяна

Когато маркирането на единична дума е погрешна гранулярност

Karaoke на ниво дума не винаги е това, което читателят иска. При висока скорост на речта курсорът, трептящ дума по дума, сам по себе си се превръща в визуален шум, и някои слушатели следват изречение по-удобно от строб с единични думи. За този случай компонентът излага по-груба единица. ReadingUnits връща единици на ниво изречение и блок, всяка с нейни собствени правоъгълници за маркиране, и ги рисувате с SetReadingHighlight вместо с SetReadingWord. Схемата е същата форма: отместване на граница все още управлява коя единица светва, но единицата, която маркирате, обхваща клауза или ред вместо единичен токен. По-бавните читатели и бързото възпроизвеждане и двете са склонни да го предпочитат, и нищо не ви пречи да предлагате и двата режима зад настройка

Долните граници на версиите са важни за фиксиране преди да строите срещу тях: word boxes изискват PDFium Component v1.53 или по-нова версия, а курсорът за проследяване изисква v1.56. Пълният API за четене, единиците на ниво изречение и работно демо за четене на глас са на продуктовата страница на PDFium Component