Skip to content
Tutorials6 min read

التعليق على نصوص ASR التفريغية: مثال مُنفَّذ بالكامل

شرح كامل من مجلد مخرجات Whisper إلى أدوار متحدثين مصنَّفة: اختيار وحدة التعليق، والتعامل مع فصل المتحدثين، وكتابة التهيئة، وتشغيل المهمة، والتصدير مع بقاء المحاذاة الزمنية سليمة.

Potato Team

يمشي هذا المقال بمشروع واحد من أوله إلى آخره: 40 مقابلة بحثية مسجّلة، فُرّغت سلفاً بـ Whisper، وتحتاج إلى ترميز للموضوع ولمن قال ماذا. وهو النسخة الملموسة من الدليلين المرجعيين، مع حسم كل قرار بدل وصفه.

وإن أردت التفصيل صيغةً صيغة بدل ذلك، فمكانه صيغ النصوص التفريغية.

من مجلد مخرجات التعرّف على الكلام إلى أدوار مصنَّفة، في أربع خطواتافحص ما احتفظت به، واحسم من يصنّف المتحدثين، وابنِ التهيئة، وصدّر مع التوقيتات

الخطوة 0: انظر فيما لديك فعلاً

قبل أي شيء آخر، اعرف ما في المجلد. يستغرق هذا عشر ثوانٍ ويوفّر عليك يوماً:

bash
potato transcripts ./whisper_out --dry-run
text
Scanned 40 file(s):
  interview_01.json      Whisper JSON      42 turns    891.4s  undiarized
  interview_02.json      Whisper JSON      51 turns   1120.8s  undiarized
  interview_03.txt       plain text         1 turns      0.0s  undiarized
  ...

40 item(s), 1683 turn(s).

أمران يُقرآن من ذلك. كل ملف غير مفصول المتحدثين، فلا شيء في هذا المتن يعرف من المتحدث. وقد جاء interview_03.txt بوصفه دوراً واحداً بمدة صفر، لأن ملف .txt من Whisper يحتوي نصاً ولا شيء غيره. ولا توجد فيه توقيتات تُسترجع.

وذلك الملف الثالث يحتاج إلى العثور على ملف .json الخاص به، أو إعادة تشغيل الصوت. ولن يصلحه أي شيء لاحق.

الخطوة 1: اختر وحدة التعليق قبل أن تختار التصنيفات

سؤال الوحدة يحسم في أرقام اتفاقك أكثر مما تحسم مجموعة التصنيفات.

ومقاطع Whisper بحجم الملفوظ تقريباً، وهي تنكسر عند الوقفات لا عند أي شيء نحوي. ولترميز المقابلات تكون تلك عادةً الوحدة الصحيحة: فإجابة المشارك تصل في عدة مقاطع، وترميز كل واحد على حدة يعطيك سجلاً أدق حبيبةً من ترميز الإجابة كاملة دفعة واحدة.

وموضع الخلل هنا هو الترجمات التلقائية من منصات الفيديو، حيث تقع حدود المقاطع حيثما امتلأ صندوق الترجمة. فأن تطلب من المعلّقين تقييم "كل جملة" فوق مقاطع كهذه ينتج اختلافاً حول مواضع الجمل، لا حول الشيء الذي أردت قياسه. وإن كان ذلك هو مدخلك، فانظر كيفية شرح ترجمات YouTube.

وهنا المقاطع صالحة كما هي، فالوحدة هي الدور.

الخطوة 2: احسم من يُسنِد المتحدثين

Whisper لا يفصل المتحدثين. وأمامك ثلاثة خيارات، وهذا قرار حقيقي لا إجراء شكلي:

أعد التشغيل بـ WhisperX. تلقائي وسريع، وخاطئ في الكلام المتداخل بما يكفي ليضطر أحدهم إلى مراجعته على أي حال.

bash
whisperx interview_01.mp3 --model medium --diarize --output_format json

استخدم واجهة سحابية مع تفعيل فصل المتحدثين. Deepgram مع diarize=true، أو AssemblyAI مع speaker_labels، أو AWS Transcribe، أو Rev.ai. ويقرأ Potato الأربعة أصلياً.

دع المعلّقين يُسنِدون المتحدثين أثناء استماعهم. ولأجل 40 مقابلة ثنائية، هذا هو الخيار الذي سنختاره. فمتحدثان بدورين مختلفين بوضوح حالة سهلة على الإنسان وليست دائماً سهلة على النموذج، والمعلّق يستمع إلى الصوت في كل الأحوال.

ونحن نمضي مع الثالث. فالأدوار غير المفصولة تُعرض بوصفها Unassigned مع أداة اختيار، ويُحفظ الإسناد مع التعليقات.

أدوار نص تفريغي بخلفيات رمادية مخطّطة، كل منها موسوم بـ Unassigned مع قائمة منسدلةتصل الأدوار غير المفصولة بوصفها Unassigned، مع أداة اختيار على كل واحد

الخطوة 3: ابنِ ملف البيانات

bash
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.json

تُزاوَج النصوص التفريغية مع صوتها بالاسم الأساسي، فيجد interview_01.json ملف interview_01.mp3. والتسمية المضاعفة interview_01.mp3.json التي ينتجها Whisper مُعالَجة، ويخرج معرّف العنصر بوصفه interview_01.

والنتيجة:

json
{
  "id": "interview_01",
  "conversation": {
    "audio": "audio/interview_01.mp3",
    "turns": [
      {"turn_id": "t0", "speaker": null, "start": 0.0, "end": 6.5,
       "text": "So I want to start with how the team was structured."}
    ]
  }
}

ويمكنك تخطي هذه الخطوة كلياً إن كنت تفضّل إبقاء النصوص التفريغية ملفات. فملف البيانات يستطيع أن يشير إليها مباشرة، وPotato يقرأ ويوحّد عند العرض:

json
{"id": "interview_01", "conversation": {"audio": "audio/interview_01.mp3",
                                        "transcript": "whisper_out/interview_01.json"}}

الخطوة 4: اكتب التهيئة

yaml
annotation_task_name: "Interview Coding"
task_dir: .
data_files:
  - data/interviews.json
 
item_properties:
  id_key: id
  text_key: conversation
 
instance_display:
  fields:
    - key: conversation
      type: audio_dialogue
      label: "Interview"
      span_target: true
      display_options:
        show_timestamps: true
        scroll_height: 520px
        allow_speaker_assignment: auto
        speakers:
          - id: interviewer
            name: "Interviewer"
            color: "#7c3aed"
            side: left
          - id: participant
            name: "Participant"
            color: "#059669"
            side: right
 
annotation_schemes:
  - annotation_type: radio
    name: turn_topic
    description: "What is this turn about?"
    labels: [structure, workload, tooling, morale, other]
 
  - annotation_type: span
    name: quotes
    description: "Highlight anything quotable in the writeup"
    labels:
      - name: quotable
        key_value: "q"

ثلاثة أشياء تؤدي العمل هنا.

تعطي قائمة المتحدثين الدورين أسماء وألواناً وجهات ثابتة قبل أن يُسنِد أحد دوراً واحداً. وبدونها يظل المتحدثون يحصلون على ألوان، لكنها تُسنَد بشكل حتمي داخل كل نص تفريغي لا على نحو متسق عبر المتن.

ويربط turn_level: true مع turn_binding سؤال الموضوع بكل دور بدل ربطه بالمقابلة كاملة. وذلك ما يجعل الدور وحدة التعليق عملياً.

ويتيح span_target: true مع مخطط span أن يمتد التظليل عبر حدود الأدوار، وهو ما يهم حين يمتد المقطع القابل للاقتباس على سؤال وإجابته. وتبقى الإزاحات مستقرة حين يُعاد إسناد متحدث.

شغّلها:

bash
python potato/flask_server.py start config.yaml -p 8000

أدوار نص تفريغي معروضة كفقاعات متحدثين ملوّنة، لكل منها زر تشغيل وسؤال تسمية مضمّنكل دور يحصل على زر تشغيل يشغّل ذلك الدور وحده، وعلى سؤال تسمية خاص به

ولكل فقاعة زر تشغيل يشغّل ذلك الدور فقط ثم يتوقف. وعملياً هذه هي الميزة التي يعلّق عليها المعلّقون: فمقابلة سطر بعينه بالصوت تكفّ عن كونها تمريناً في السحب على شريط التقدّم.

الخطوة 5: افحص الاتفاق على الشيء الصحيح

معلّقان لكل مقابلة، وصار لديك الآن نوعان من الاتفاق تنظر فيهما.

تصنيفات الموضوع اتفاق فئوي عادي على مستوى الدور. ولأن معرّفات الأدوار حتمية، ينتج الملف نفسه المعرّفات نفسها دائماً، فتتحاذى تصنيفات المعلّقَين دون أي خطوة محاذاة.

أما إسناد المتحدثين فيستحق فحصاً منفصلاً. فإن اختلف معلّقاك حول من يتكلم في 15% من الأدوار، فتلك إشارة عن الصوت، وتعني أن الفصل التلقائي كان سيخطئ بذلك القدر على الأقل دون أن يخبرك.

انظر شرح اتفاق المُعلّقين للمقاييس، والاتفاق على النطاقات والمخرجات المهيكلة للتظليلات، فهي تحتاج معالجة مختلفة لأن المعلّقين يختارون الحدود إضافةً إلى التصنيفات.

الخطوة 6: صدّر

تعمل صيغ JSON وJSONL وCSV القياسية كالمعتاد. وحين تريد للمحاذاة الزمنية أن تنجو إلى داخل أداة تحليل كلام، صدّر التعليقات الطبقية إلى ELAN أو Praat:

bash
python -m potato.export --config config.yaml --format eaf --output ./out/
python -m potato.export --config config.yaml --format textgrid --output ./out/

وكلاهما يكمل دورة كاملة، لأن Potato يقرأ EAF وTextGrid مدخلاً أيضاً. علّق هنا، ونقّح في ELAN، ثم أعد قراءة النتيجة.

الأشياء الأربعة التي تنحرف

أحدهم احتفظ بملف .txt. بلا توقيتات، وغير قابل للاسترجاع، وإعادة التشغيل مطلوبة. و--dry-run يلتقطه قبل أن تكون قد بنيت شيئاً فوقه.

التوقيتات خاطئة بمقدار ألف ضعف. شيء في مرحلة سابقة خلط الثواني بالميلي ثواني. فـ Whisper وDeepgram يخرجان ثوانيَ عشرية؛ أما AssemblyAI وإزاحات whisper.cpp وملف TSV من Whisper فتخرج ميلي ثوانٍ صحيحة.

مخططات على مستوى الجملة فوق مدخلات قائمة على مقاطع الترجمة. غُطّي هذا أعلاه، وهو أغلى الأربعة لأنك لا تكتشفه إلا حين تحسب الاتفاق.

الوثوق بفصل متحدثين لم يراجعه أحد. خطأ في الفصل ينتشر إلى كل تصنيف مرتبط بذلك الدور، ويبدو وكأنه اختلاف بين المعلّقين حين تذهب تبحث عن السبب.

قراءات إضافية