Skip to content
Tutorials8 min read

ASR ट्रांसक्रिप्ट एनोटेट करना: एक पूरा उदाहरण

Whisper आउटपुट के एक फ़ोल्डर से लेबल किए हुए वक्ता टर्न तक का पूरा सफ़र: एनोटेशन इकाई चुनना, डायराइज़ेशन सँभालना, कॉन्फ़िग लिखना, टास्क चलाना, और समय-संरेखण बचाए रखते हुए निर्यात करना।

Potato Team

यह एक परियोजना को शुरू से आख़िर तक ले जाता है: 40 रिकॉर्ड किए गए शोध साक्षात्कार, जिन्हें Whisper से पहले ही ट्रांसक्राइब किया जा चुका है और जिन्हें विषय के हिसाब से और यह तय करने के लिए कोड करना है कि किसने क्या कहा। यह उन दो संदर्भ मार्गदर्शिकाओं का ठोस रूप है, जिसमें हर निर्णय बताया नहीं, लिया गया है।

अगर आपको इसके बजाय प्रारूप-दर-प्रारूप विवरण चाहिए, तो वह ट्रांसक्रिप्ट प्रारूप पर है।

ASR आउटपुट के एक फ़ोल्डर से लेबल किए हुए टर्न तक, चार क़दमों मेंदेखिए आपने रखा क्या है, तय कीजिए वक्ता कौन लेबल करेगा, कॉन्फ़िग बनाइए, समय के साथ निर्यात कीजिए

क़दम 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 शून्य अवधि वाले एक ही टर्न के रूप में आया, क्योंकि Whisper की .txt में सिर्फ़ पाठ होता है और कुछ नहीं। उसमें वापस पाने लायक कोई समय है ही नहीं।

उस तीसरी फ़ाइल के लिए उसकी .json ढूँढ़नी होगी, या ऑडियो दोबारा चलाना होगा। आगे की कोई भी प्रक्रिया इसे ठीक नहीं करेगी।

क़दम 1: लेबल चुनने से पहले एनोटेशन इकाई चुनिए

इकाई का सवाल आपकी सहमति की संख्याओं के बारे में लेबल-समुच्चय से ज़्यादा तय करता है।

Whisper के खंड मोटे तौर पर एक उच्चारण जितने होते हैं, और वे व्याकरण के बजाय ठहरावों पर टूटते हैं। साक्षात्कार कोड करने के लिए आम तौर पर यही सही इकाई है: उत्तरदाता का जवाब कई खंडों में आता है, और हर खंड को अलग-अलग कोड करने से पूरे जवाब को एक साथ कोड करने के मुक़ाबले बारीक रिकॉर्ड मिलता है।

यह वहाँ बिगड़ता है जहाँ किसी वीडियो प्लेटफ़ॉर्म के स्वतः कैप्शन हों, जिनमें क्यू की सीमाएँ वहीं पड़ती हैं जहाँ कैप्शन बॉक्स भर गया था। ऐसे क्यू पर एनोटेटरों से “हर वाक्य” को आँकने को कहने से असहमति इस बात पर पैदा होती है कि वाक्य कहाँ हैं, न कि उस चीज़ पर जिसे आप नापना चाहते थे। अगर आपका इनपुट यही है, तो देखिए YouTube सबटाइटल कैसे एनोटेट करें

यहाँ खंड जैसे-के-तैसे इस्तेमाल लायक हैं, इसलिए इकाई टर्न है।

क़दम 2: तय कीजिए कि वक्ता कौन असाइन करेगा

Whisper डायराइज़ नहीं करता। तीन विकल्प हैं, और यह औपचारिकता नहीं, असली निर्णय है:

WhisperX के साथ दोबारा चलाइए। यह स्वचालित है, तेज़ है, और आपस में उलझी हुई बोली पर इतनी बार ग़लत होता है कि किसी को उसे जाँचना ही पड़ेगा।

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

डायराइज़ेशन चालू करके कोई क्लाउड API इस्तेमाल कीजिए। diarize=true के साथ Deepgram, speaker_labels के साथ AssemblyAI, 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 मिल जाती है। Whisper के दोहरे नाम interview_01.mp3.json को सँभाल लिया जाता है, और आइटम आईडी 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_binding के साथ turn_level: true विषय वाले सवाल को पूरे साक्षात्कार के बजाय हर टर्न से जोड़ देता है। व्यवहार में टर्न को एनोटेशन इकाई यही बनाता है।

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 इसे तब पकड़ लेता है जब आपने उस पर कुछ खड़ा नहीं किया होता।

समय 1000× ग़लत हैं। आगे कहीं सेकंड और मिलीसेकंड मिल गए। Whisper और Deepgram दशमलव सेकंड देते हैं; AssemblyAI, whisper.cpp के ऑफ़सेट और Whisper का TSV पूर्णांक मिलीसेकंड देते हैं।

क्यू-आधारित इनपुट पर वाक्य-स्तरीय योजनाएँ। ऊपर बताया जा चुका है, और चारों में सबसे महँगी यही है क्योंकि आपको पता तब चलता है जब आप सहमति गिनते हैं।

बिना जाँचे डायराइज़ेशन पर भरोसा करना। डायराइज़ेशन की एक ग़लती उस टर्न से जुड़े हर लेबल में फैल जाती है, और जब आप वजह ढूँढ़ने निकलते हैं तो वह एनोटेटरों की असहमति जैसी दिखती है।

आगे पढ़ें