مقال تقني

تمييز القراءة النصية (TTS) كلمة بكلمة في عارضات PDFium في دلفي

تتمتع ميزة القراءة بصوت عالٍ بمهمة مرئية واحدة تتجاوز الصوت: مع نطق كل كلمة، يجب أن تُضيء تلك الكلمة على الصفحة وتُبقيها في العرض. للقيام بذلك، تحتاج إلى المربع المحيط بكل كلمة، مفهرسًا لنفس تدفق الأحرف الذي يقرأ منه محرك التحدث. إذا حصلت على المربعات ولكنك أخطأت في الفهرسة، فإن التمييز سينحرف بكلمة أو اثنتين خلف الصوت؛ وإذا حصلت على الفهرسة ولكنك أسأت التعامل مع حالة الصفحة، فإن التمييز سيستقر على الصفحة الخطأ تمامًا. إن جزء التحدث من هذا، أي المُركِّب (synthesizer) نفسه، هو الجزء الذي نادرًا ما يتعطل. تُبلغ SAPI عن حدود الكلمات بدقة الحرف. ما يتعطل هو طبقة التعيين (mapping layer) الرقيقة بين إزاحة الحرف في المخزن المؤقت للتحدث والمستطيل على الصفحة المعروضة

يشحن مكون PDFium هذا التعيين لكل من دلفي، و C++Builder، و Lazarus، مع توفر مربعات الكلمات منذ الإصدار v1.53 ومؤشر التتبع منذ الإصدار v1.56. السطح ضيق بشكل متعمد: استدعاء يُرجع مربعات الكلمات للصفحة، ومتتبع يُحول إزاحة الحرف إلى تمييز مرسوم، وزوج من الخصائص للون والتمرير التلقائي. على الرغم من ضيقه، فإن الترتيب الذي تستدعي به الأشياء يقرر ما إذا كانت الميزة ستعمل، ومعظم حالات الفشل الموضحة أدناه تنبع من استدعاء الدوال الصحيحة بتسلسل خاطئ

الأحرف ليست كلمات، ومحركات تحويل النص إلى كلام (TTS) تتحدث بالأحرف

يستهلك محرك التحدث سلسلة نصية مسطحة ويُبلغ عن التقدم كمواضع أحرف داخل تلك السلسلة. تحتوي صفحة PDF على أشكال حروف (glyphs) موضوعة في مساحة الصفحة، حيث تكون "الكلمة" عبارة عن مجموعة استدلالية من تسلسلات أشكال الحروف. لا يتشارك نظاما الإحداثيات في شيء ما لم يكن النص الذي تسلمه للمُركِّب هو نفسه النص الذي تم حساب مربعات الكلمات منه بالبايت. هذه هي القاعدة الأولى، وهي لا ترحم. إذا قمت بتسوية المسافات البيضاء، أو إزالة الواصلات اللينة، أو "تنظيف" النص المستخرج بأي طريقة أخرى قبل التحدث به، فإن كل إزاحة تالية ستكون خاطئة بصمت. انطق بالضبط ما استخرجته، أو احتفظ بجدول صريح لإعادة تعيين الإزاحة. لا يوجد خيار ثالث ينجو مع المستندات الحقيقية

جدول إعادة التعيين ليس حالة طرفية افتراضية. في اللحظة التي تُدرج فيها واجهة المستخدم الخاصة بك إعلانًا منطوقًا للصفحة ("الصفحة الخامسة") أو توسع اختصارًا للمُركِّب، فإن السلسلة المنطوقة تنحرف عن السلسلة المستخرجة. سجّل موضع وطول كل إدراج، ثم اطرح التعديل المتراكم قبل كل استدعاء للتتبع. قد يتطلب الأمر حوالي عشرين سطرًا من مسك الدفاتر، وهو الفرق بين تمييز ينجو من طلب الميزة التالي وبين آخر ينكسر في أول مرة يطلب فيها شخص ما عناوين منطوقة

ما يقدمه لك مربع الكلمة

يحمل كل سجل 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 الخاص بالعارض بتقسيم طبقة النص للصفحة التي يعرضها العرض حاليًا، لذلك انتقل في العرض أولاً ثم استخرج ثانياً؛ لا حاجة للتقديم (rendering)، فقط مستند مفتوح. (يعرض مكون المستند، 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. إذا تركته، فسيستقر مستطيل كهرماني على صفحة متوقفة ليبدو تمامًا كأنه خلل، وهو نوع من الأشياء التي سيقوم كل مُختبِر بتسجيلها رغم أنه لا يوجد شيء مكسور في الواقع

يضغط معدل التحدث على هذا المسار (pipeline) بقوة أكبر مما يفعله حجم المستند. عند 300 كلمة في الدقيقة، تصل أحداث الحدود كل 200 مللي ثانية، وفي أسرع معدلات SAPI، تأتي بشكل أسرع مما يمكن للعين تتبعه براحة. الاستجابة الصحيحة هي الدمج، وليس وضعها في قائمة انتظار (queue). إذا وصل حد جديد بينما لا يزال تحديث التمييز معلقًا، فأسقط القديم وارسم الأحدث. المؤشر الذي يزور كل كلمة بالترتيب ولكنه يتأخر نصف ثانية يبدو مكسورًا؛ بينما المؤشر الذي يتخطى كلمة من حين لآخر مع البقاء متزامنًا مع الصوت لا يبدو كذلك

الحالات الطرفية التي تفصل العروض التوضيحية عن المنتجات

تكشف بعض فئات المستندات عن العيوب. تُعد الأحرف المجمعة (Combining characters) هي الأكثر دقة: حيث يمكن أن تشغل تسلسلات Unicode مثل الحرف الأساسي بالإضافة إلى علامة التشكيل المجمعة فهارس أحرف أكثر مما توحي به الكلمة المرئية، لذا فإن أي حساب للإزاحة يفترض فهرسًا واحدًا لكل شكل حرف (glyph) ينحرف ببطء. هذه هي أقوى حجة لترك TrackReadingWordAt يمتلك التعيين بدلاً من حساب أرقام الكلمات يدويًا. تُعد الواصلة (Hyphenation) أكثر اعتيادية ولكنها أكثر شيوعًا: تصبح الكلمة المكسورة عبر فاصل سطر مربعين، وإذا نطقتها كرمز مميز (token) واحد، فإن الحدث الحدودي للنصف الثاني سيتم حله إلى المربع الأول. يكون ذلك جيدًا في العادة، ولكنه قرار، لذا اتخذه عن قصد بدلاً من اكتشافه. يُغير وضع العلامات (Tagging) ترتيب القراءة نفسه. عندما يحمل المستند علامات البنية المناسبة (منطقة ISO 14289، PDF/UA)، فإن تسلسل الكلمات يتبع البنية المنطقية؛ وبدونها يتراجع إلى استدلالات التخطيط، ويمكن لصفحة غير مميزة بعلامات مكونة من عمودين أن تُقرأ بشكل مستقيم عبر كلا العمودين. الصفحات المستديرة هي الحالة الشائعة الأخيرة: لا يزال Rect لكل كلمة يحدها بشكل صحيح في مساحة الصفحة، ولكن سياسة اتباع منفذ العرض المضبوطة للتدفق الأفقي تمرر بشكل مزعج عندما يمتد النص عموديًا، لذا احتفظ بمستند مستدير واحد على الأقل في مجموعة الانحدار (regression set). للتعامل مع ترتيب القراءة، ووحدات مستوى الجملة عبر ReadingUnits، والمكدس المساعد الأوسع، راجع بناء قارئ PDF يمكن الوصول إليه في دلفي

يُشكل قيد واحد للمنصة عملية النشر. ميزة SAPI مخصصة لنظام التشغيل Windows فقط. واجهة برمجة تطبيقات (API) مربع الكلمات والتتبع متطابقة بايت مقابل بايت تحت Lazarus و FPC، ولكن عمليات بناء Linux و macOS تحتاج إلى مُركِّب مختلف متصل خلف نفس أحداث الحدود؛ تتم تغطية هذا الإعداد في تشغيل العارض تحت Lazarus و FPC. تتفاعل تكلفة التمييز أيضًا مع ذاكرة التخزين المؤقت لصفحتك بمجرد ارتفاع معدلات التحدث، كما أن حساب الميزانية الموضح في التخزين المؤقت للعرض وأداء التكبير ينتقل إلى هنا دون تغيير

متى يكون تمييز الكلمة المفردة هو مستوى الدقة الخاطئ

إن تتبع الكلمات (karaoke) على مستوى الكلمة ليس دائمًا ما يريده القارئ. في معدلات التحدث العالية، يصبح وميض المؤشر كلمة بكلمة ضوضاء بصرية بحد ذاته، ويتابع بعض المستمعين الجملة بشكل أكثر راحة من وميض الكلمات المفردة. لهذه الحالة، يعرض المكون وحدة أكثر خشونة. يُرجع ReadingUnits وحدات على مستوى الجملة والكتلة، لكل منها مستطيلات تمييز خاصة بها، وتقوم برسمها باستخدام SetReadingHighlight بدلاً من SetReadingWord. الارتباطات هي بنفس الشكل: لا تزال إزاحة الحدود تُحدد أي وحدة ستُضيء، ولكن الوحدة التي تميزها تمتد عبر عبارة أو سطر بدلاً من رمز مميز (token) واحد. يميل كل من القراء البطيئين والتشغيل بمعدل عالٍ إلى تفضيل ذلك، ولا شيء يمنعك من تقديم كلا الوضعين خلف إعداد

الحدود الدنيا للإصدارات تستحق التحديد قبل البناء عليها: تحتاج مربعات الكلمات إلى مكون PDFium الإصدار v1.53 أو أحدث، ويحتاج مؤشر التتبع إلى الإصدار v1.56. تتوفر واجهة برمجة تطبيقات (API) القراءة الكاملة، والوحدات على مستوى الجملة، وعرض توضيحي عملي للقراءة بصوت عالٍ على صفحة منتج مكون PDFium