Technical Article

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

Функцията за четене на глас има една видима задача освен самия глас: докато всяка дума се произнася, тя трябва да я осветява на страницата и да я държи в полезрението. За да постигнете това, се нуждаете от ограничаващата рамка (bounding box) на всяка дума, индексирана към същия поток от знаци, от който чете гласовият модул. Ако получите рамките, но пропуснете индексирането, маркирането ще изостане с дума-две след аудиото; ако направите индексирането, но не управлявате правилно състоянието на страницата, маркирането ще се озове на напълно грешна страница. Криптографски сложната част от това, самият синтезатор, се поврежда рядко. SAPI докладва границите на думите до конкретния знак. Това, което се чупи, е тънкият слой за съпоставяне между отместването на знака (character offset) в буфера за реч и правоъгълника върху рендираната страница.

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

Знаците не са думи, а TTS модулите говорят в знаци

Гласовият модул обработва плосък низ и отчита напредъка като позиции на знаци в рамките на този низ. PDF страницата съдържа глифове, разположени в пространството на страницата, където „думаâ€?е евристичен клъстер от поредици глифове. Двете координатни системи не споделят нищо общо, освен ако текстът, който подавате на синтезатора, не съвпада байт по байт с текста, въз основа на който са изчислени рамките на думите. Това е първото и абсолютно задължително правило. Ако нормализирате празните пространства, премахнете меките тирета (soft hyphens) или по друг начин „почиститеâ€?извлечения текст преди гласовото му възпроизвеждане, всяко следващо отместване ще бъде неточно. Подавайте точно това, което сте извлекли, или поддържайте ясна таблица за пренасочване на отместванията. Няма трети вариант, който да работи при реални документи.

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

Какво ви предоставя рамката за дума

Всеки запис TPdfWordBox съдържа текста на думата, нейния StartIndex и брой знаци (Count) в текста на страницата, нейното местоположение в пространството на страницата Rect и номера на страницата Page (базиран на 1). Полето 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 във визуализатора е ядрото на цялата функционалност. Подайте му номер на страница и индекс на знак; той намира рамката за дума, съдържаща този знак, изрисува курсора за четене върху нея и връща индекса на думата, или -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 поддържа свой собствен кеш на рамките за думи за проследяваната страница, който се обновява автоматично при смяна на страницата, така че цената за всяко известие остава постоянна, независимо колко бързо пристигат границите. Второ, методът не извършва либерална проверка на границите. Индекс на или надхвърлящ броя знаци на страницата връща -1, вместо да се ограничава до последната дума. Третирайте -1 като „задръжте предишното маркиранеâ€? а никога като грешка, тъй като препинателните знаци и празните пространства между думите логично генерират граници, които не принадлежат на никоя дума. Логването на всяко -1 ще претовари системата. Вместо това ги бройте на страница и анализирайте страниците, при които този процент скача, тъй като това обикновено показва несъответствие при нормализирането на текста съгласно първото правило.

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

Методът SetReadingWord изрисува маркирането директно, когдато вие сами държите рамката за дума, 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 ms, а при най-високите скорости на SAPI те идват по-бързо, отколкото окото може удобно да проследи. Правилното поведение е обединяване (coalesce), а не опашка от заявки. Ако пристигне ново събитие, докато актуализацията на маркирането все още се обработва, изхвърлете старото и изрисувайте най-новото. Курсор, който преминава през всяка дума подред, но закъснява с половин секунда, изглежда счупен; този, който понякога пропуска дума, но остава в синхрон с гласа, изглежда наред.

Крайни случаи, които отличават демо версиите от реалните продукти

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

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

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

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

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