التعليق على نصوص ASR التفريغية: مثال مُنفَّذ بالكامل
شرح كامل من مجلد مخرجات Whisper إلى أدوار متحدثين مصنَّفة: اختيار وحدة التعليق، والتعامل مع فصل المتحدثين، وكتابة التهيئة، وتشغيل المهمة، والتصدير مع بقاء المحاذاة الزمنية سليمة.
يمشي هذا المقال بمشروع واحد من أوله إلى آخره: 40 مقابلة بحثية مسجّلة، فُرّغت سلفاً بـ Whisper، وتحتاج إلى ترميز للموضوع ولمن قال ماذا. وهو النسخة الملموسة من الدليلين المرجعيين، مع حسم كل قرار بدل وصفه.
وإن أردت التفصيل صيغةً صيغة بدل ذلك، فمكانه صيغ النصوص التفريغية.
افحص ما احتفظت به، واحسم من يصنّف المتحدثين، وابنِ التهيئة، وصدّر مع التوقيتات
الخطوة 0: انظر فيما لديك فعلاً
قبل أي شيء آخر، اعرف ما في المجلد. يستغرق هذا عشر ثوانٍ ويوفّر عليك يوماً:
potato transcripts ./whisper_out --dry-runScanned 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. تلقائي وسريع، وخاطئ في الكلام المتداخل بما يكفي ليضطر أحدهم إلى مراجعته على أي حال.
whisperx interview_01.mp3 --model medium --diarize --output_format jsonاستخدم واجهة سحابية مع تفعيل فصل المتحدثين. Deepgram مع diarize=true، أو AssemblyAI مع speaker_labels، أو AWS Transcribe، أو Rev.ai. ويقرأ Potato الأربعة أصلياً.
دع المعلّقين يُسنِدون المتحدثين أثناء استماعهم. ولأجل 40 مقابلة ثنائية، هذا هو الخيار الذي سنختاره. فمتحدثان بدورين مختلفين بوضوح حالة سهلة على الإنسان وليست دائماً سهلة على النموذج، والمعلّق يستمع إلى الصوت في كل الأحوال.
ونحن نمضي مع الثالث. فالأدوار غير المفصولة تُعرض بوصفها Unassigned مع أداة اختيار، ويُحفظ الإسناد مع التعليقات.
تصل الأدوار غير المفصولة بوصفها Unassigned، مع أداة اختيار على كل واحد
الخطوة 3: ابنِ ملف البيانات
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.jsonتُزاوَج النصوص التفريغية مع صوتها بالاسم الأساسي، فيجد interview_01.json ملف interview_01.mp3. والتسمية المضاعفة interview_01.mp3.json التي ينتجها Whisper مُعالَجة، ويخرج معرّف العنصر بوصفه interview_01.
والنتيجة:
{
"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 يقرأ ويوحّد عند العرض:
{"id": "interview_01", "conversation": {"audio": "audio/interview_01.mp3",
"transcript": "whisper_out/interview_01.json"}}الخطوة 4: اكتب التهيئة
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 أن يمتد التظليل عبر حدود الأدوار، وهو ما يهم حين يمتد المقطع القابل للاقتباس على سؤال وإجابته. وتبقى الإزاحات مستقرة حين يُعاد إسناد متحدث.
شغّلها:
python potato/flask_server.py start config.yaml -p 8000
كل دور يحصل على زر تشغيل يشغّل ذلك الدور وحده، وعلى سؤال تسمية خاص به
ولكل فقاعة زر تشغيل يشغّل ذلك الدور فقط ثم يتوقف. وعملياً هذه هي الميزة التي يعلّق عليها المعلّقون: فمقابلة سطر بعينه بالصوت تكفّ عن كونها تمريناً في السحب على شريط التقدّم.
الخطوة 5: افحص الاتفاق على الشيء الصحيح
معلّقان لكل مقابلة، وصار لديك الآن نوعان من الاتفاق تنظر فيهما.
تصنيفات الموضوع اتفاق فئوي عادي على مستوى الدور. ولأن معرّفات الأدوار حتمية، ينتج الملف نفسه المعرّفات نفسها دائماً، فتتحاذى تصنيفات المعلّقَين دون أي خطوة محاذاة.
أما إسناد المتحدثين فيستحق فحصاً منفصلاً. فإن اختلف معلّقاك حول من يتكلم في 15% من الأدوار، فتلك إشارة عن الصوت، وتعني أن الفصل التلقائي كان سيخطئ بذلك القدر على الأقل دون أن يخبرك.
انظر شرح اتفاق المُعلّقين للمقاييس، والاتفاق على النطاقات والمخرجات المهيكلة للتظليلات، فهي تحتاج معالجة مختلفة لأن المعلّقين يختارون الحدود إضافةً إلى التصنيفات.
الخطوة 6: صدّر
تعمل صيغ JSON وJSONL وCSV القياسية كالمعتاد. وحين تريد للمحاذاة الزمنية أن تنجو إلى داخل أداة تحليل كلام، صدّر التعليقات الطبقية إلى ELAN أو Praat:
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 فتخرج ميلي ثوانٍ صحيحة.
مخططات على مستوى الجملة فوق مدخلات قائمة على مقاطع الترجمة. غُطّي هذا أعلاه، وهو أغلى الأربعة لأنك لا تكتشفه إلا حين تحسب الاتفاق.
الوثوق بفصل متحدثين لم يراجعه أحد. خطأ في الفصل ينتشر إلى كل تصنيف مرتبط بذلك الدور، ويبدو وكأنه اختلاف بين المعلّقين حين تذهب تبحث عن السبب.
قراءات إضافية
- كيفية شرح نصوص Whisper التفريغية
- كيفية شرح ترجمات YouTube
- صيغ النصوص التفريغية
- Transcript Format Ingestion، تصميم قابل للتشغيل بست صيغ جنباً إلى جنب
- Potato 2.7.1: النص التفريغي موجود سلفاً