Skip to content
Tutorials7 min read

ASR-Transkripte annotieren: ein durchgerechnetes Beispiel

Ein vollständiger Durchgang vom Ordner mit Whisper-Ausgaben bis zu gelabelten Sprecher-Turns: die Annotationseinheit wählen, die Diarisierung klären, die Konfiguration schreiben, die Aufgabe laufen lassen und mit intaktem Zeit-Alignment exportieren.

Potato Team

Hier läuft ein Projekt von Anfang bis Ende durch: 40 aufgezeichnete Forschungsinterviews, bereits mit Whisper transkribiert, die nach Thema und danach kodiert werden sollen, wer was gesagt hat. Das ist die konkrete Fassung der beiden Referenzanleitungen, mit jeder Entscheidung tatsächlich getroffen statt bloß beschrieben.

Wer stattdessen die Details Format für Format sucht, findet sie unter Transkriptformate.

Vom Ordner mit ASR-Ausgaben zu gelabelten Turns, in vier SchrittenPrüfen, was aufgehoben wurde, entscheiden, wer die Sprecher labelt, die Konfiguration bauen, mit Zeiten exportieren

Schritt 0: nachsehen, was tatsächlich da ist

Vor allem anderen herausfinden, was im Ordner liegt. Das dauert zehn Sekunden und spart einen Tag:

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).

Zwei Dinge lassen sich daraus lesen. Jede Datei ist nicht diarisiert, in diesem Korpus weiß also nichts, wer gerade spricht. Und interview_03.txt kam als ein einziger Turn mit Dauer null durch, weil ein .txt von Whisper Text enthält und sonst nichts. Darin gibt es keine Zeiten zurückzuholen.

Für diese dritte Datei muss das zugehörige .json auftauchen, oder das Audio muss erneut durch das Modell. Weiter unten in der Kette repariert das nichts mehr.

Schritt 1: die Annotationseinheit vor den Labels festlegen

Die Frage nach der Einheit entscheidet mehr über die Übereinstimmungswerte als das Labelset.

Die Segmente von Whisper haben ungefähr Äußerungslänge und brechen an Pausen, nicht an etwas Grammatischem. Für die Interviewkodierung ist das meist die richtige Einheit: Die Antwort eines Befragten kommt als mehrere Segmente an, und jedes einzeln zu kodieren ergibt eine feinere Aufzeichnung, als die ganze Antwort auf einmal zu kodieren.

Schiefgehen kann das bei automatischen Untertiteln von einer Videoplattform, wo die Cue-Grenzen dort liegen, wo das Untertitelfeld gerade voll war. Annotatoren über solchen Cues „jeden Satz" bewerten zu lassen, erzeugt Uneinigkeit darüber, wo die Sätze liegen, und nicht über das, was eigentlich gemessen werden sollte. Wer solche Eingaben hat, liest YouTube-Untertitel annotieren.

Hier sind die Segmente so brauchbar, wie sie sind, die Einheit ist also der Turn.

Schritt 2: entscheiden, wer die Sprecher zuweist

Whisper diarisiert nicht. Drei Möglichkeiten, und das ist eine echte Entscheidung und keine Formalie:

Mit WhisperX neu laufen lassen. Automatisch, schnell und bei überlappender Sprache oft genug falsch, dass es ohnehin jemand prüfen muss.

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

Eine Cloud-API mit eingeschalteter Diarisierung nutzen. Deepgram mit diarize=true, AssemblyAI mit speaker_labels, AWS Transcribe oder Rev.ai. Potato liest alle vier nativ.

Die Annotatoren die Sprecher beim Zuhören zuweisen lassen. Bei 40 Interviews mit je zwei Personen würden wir das wählen. Zwei Sprecher mit deutlich verschiedenen Rollen sind für einen Menschen der leichte Fall und für ein Modell nicht immer, und der Annotator hört das Audio ohnehin.

Wir nehmen die dritte Möglichkeit. Nicht diarisierte Turns erscheinen als Unassigned mit einer Auswahl, und die Zuweisung wird zusammen mit den Annotationen gespeichert.

Transkript-Turns mit grau schraffiertem Hintergrund, jeweils als Unassigned beschriftet, mit AuswahlmenüNicht diarisierte Turns kommen als Unassigned an, jeder mit einer eigenen Auswahl

Schritt 3: die Datendatei bauen

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

Transkripte werden über den Basisnamen ihrem Audio zugeordnet, interview_01.json findet also interview_01.mp3. Whispers doppelte Benennung interview_01.mp3.json wird berücksichtigt, und als Element-ID kommt interview_01 heraus.

Das Ergebnis:

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."}
    ]
  }
}

Dieser Schritt lässt sich ganz überspringen, wenn die Transkripte lieber Dateien bleiben sollen. Eine Datendatei kann direkt auf sie zeigen, und Potato liest und normalisiert beim Rendern:

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

Schritt 4: die Konfiguration schreiben

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"

Drei Dinge leisten hier die Arbeit.

Die Sprecherliste gibt den beiden Rollen feste Namen, Farben und Seiten, bevor irgendjemand einen einzigen Turn zugewiesen hat. Ohne sie bekommen die Sprecher zwar trotzdem Farben, aber deterministisch je Transkript und nicht einheitlich über das ganze Korpus.

turn_level: true zusammen mit turn_binding hängt die Themenfrage an jeden einzelnen Turn statt an das Interview als Ganzes. Das macht den Turn in der Praxis zur Annotationseinheit.

span_target: true und das span-Schema erlauben Hervorhebungen über Turn-Grenzen hinweg, was zählt, wenn die zitierfähige Stelle eine Frage und ihre Antwort umspannt. Die Offsets bleiben stabil, wenn ein Sprecher neu zugewiesen wird.

Starten:

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

Transkript-Turns als farbige Sprecherblasen mit einem Abspielknopf je Turn und einer eingebetteten BeschriftungsfrageJeder Turn bekommt einen Abspielknopf, der nur diesen Turn abspielt, dazu seine eigene Beschriftungsfrage

Jede Blase hat einen Abspielknopf, der genau diesen Turn abspielt und dann stoppt. In der Praxis ist das die Funktion, die den Annotatoren auffällt: Eine bestimmte Zeile gegen das Audio zu prüfen, ist keine Sucherei auf der Zeitleiste mehr.

Schritt 5: die Übereinstimmung an der richtigen Sache prüfen

Zwei Annotatoren pro Interview, und damit gibt es zwei Arten von Übereinstimmung anzusehen.

Die Themenlabels sind gewöhnliche kategoriale Übereinstimmung auf Turn-Ebene. Weil die Turn-IDs deterministisch sind, erzeugt dieselbe Datei immer dieselben IDs, und die Labels der beiden Annotatoren passen ohne einen Alignment-Schritt zusammen.

Die Sprecherzuweisung gehört gesondert geprüft. Sind die beiden Annotatoren bei 15 % der Turns uneinig darüber, wer spricht, ist das ein Signal über das Audio, und es heißt, dass eine automatische Diarisierung mindestens genauso oft danebengelegen hätte, ohne es zu sagen.

Die Maße stehen unter Inter-Annotator-Übereinstimmung, und für die Hervorhebungen gilt Übereinstimmung bei Spans, die eine eigene Behandlung brauchen, weil Annotatoren dort neben den Labels auch die Grenzen wählen.

Schritt 6: exportieren

Die Standardformate JSON, JSONL und CSV funktionieren wie gewohnt. Soll das Zeit-Alignment bis in ein Werkzeug zur Sprachanalyse überleben, exportiert man Ebenen-Annotationen nach ELAN oder Praat:

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

Beide laufen einen vollen Kreis, denn Potato liest EAF und TextGrid auch als Eingabe. Hier annotieren, in ELAN verfeinern, das Ergebnis wieder einlesen.

Die vier Dinge, die schiefgehen

Jemand hat das .txt behalten. Keine Zeiten, nicht zurückzuholen, ein erneuter Lauf ist nötig. --dry-run fängt das ab, bevor irgendetwas darauf aufgebaut ist.

Die Zeiten sind um Faktor 1000 falsch. Irgendwo vorgelagert wurden Sekunden und Millisekunden vermischt. Whisper und Deepgram geben Sekunden als Fließkommazahl aus; AssemblyAI, die Offsets von whisper.cpp und Whispers TSV geben ganzzahlige Millisekunden aus.

Schemata auf Satzebene über cue-basierten Eingaben. Oben behandelt, und der teuerste der vier Fälle, weil es erst bei der Berechnung der Übereinstimmung auffällt.

Einer ungeprüften Diarisierung vertrauen. Ein Diarisierungsfehler pflanzt sich in jedes Label fort, das an diesem Turn hängt, und bei der Ursachensuche sieht er aus wie Uneinigkeit der Annotatoren.

Weiterführende Informationen