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.
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.
Prü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:
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).
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.
whisperx interview_01.mp3 --model medium --diarize --output_format jsonEine 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.
Nicht diarisierte Turns kommen als Unassigned an, jeder mit einer eigenen Auswahl
Schritt 3: die Datendatei bauen
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.jsonTranskripte 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:
{
"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:
{"id": "interview_01", "conversation": {"audio": "audio/interview_01.mp3",
"transcript": "whisper_out/interview_01.json"}}Schritt 4: die Konfiguration schreiben
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:
python potato/flask_server.py start config.yaml -p 8000
Jeder 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:
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
- Whisper-Transkripte annotieren
- YouTube-Untertitel annotieren
- Transkriptformate
- Transcript Format Ingestion, ein lauffähiges Design mit sechs Formaten nebeneinander
- Potato 2.7.1: Das Transkript gibt es schon