Annoter des transcriptions de reconnaissance vocale : un exemple complet
Un pas-à-pas complet, d'un dossier de sorties Whisper à des tours de parole étiquetés : choisir l'unité d'annotation, gérer la diarisation, écrire la configuration, lancer la tâche et exporter sans perdre l'alignement temporel.
Ce billet suit un projet de bout en bout : 40 entretiens de recherche enregistrés, déjà transcrits avec Whisper, qu'il faut coder par thème et par locuteur. C'est la version concrète des deux guides de référence, avec toutes les décisions prises plutôt que décrites.
Si vous cherchez plutôt le détail format par format, il est dans Formats de transcription.
Vérifier ce que vous avez gardé, décider qui étiquette les locuteurs, construire la configuration, exporter avec les temps
Étape 0 : regardez ce que vous avez vraiment
Avant toute chose, découvrez ce qu'il y a dans le dossier. Ça prend dix secondes et ça vous épargne une journée :
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).
Deux choses à en tirer. Aucun fichier n'est diarisé, donc rien dans ce corpus ne sait qui parle. Et interview_03.txt est passé comme un seul tour de durée nulle, parce qu'un .txt de Whisper contient du texte et rien d'autre. Il n'y a aucun temps à y récupérer.
Ce troisième fichier a besoin qu'on retrouve son .json, ou qu'on repasse l'audio dans le modèle. Rien en aval ne le réparera.
Étape 1 : choisissez l'unité d'annotation avant de choisir les étiquettes
La question de l'unité décide davantage de vos chiffres d'accord que le jeu d'étiquettes.
Les segments de Whisper font à peu près la taille d'un énoncé, et ils coupent aux pauses plutôt que sur quoi que ce soit de grammatical. Pour du codage d'entretiens, c'est en général la bonne unité : la réponse d'un interviewé arrive en plusieurs segments, et coder chacun séparément donne un relevé plus fin que coder toute la réponse d'un bloc.
Là où ça se gâte, c'est avec les sous-titres automatiques d'une plateforme vidéo, où les frontières de cue tombent là où la boîte de sous-titres s'est remplie. Demander à des annotateurs de noter « chaque phrase » sur des cues pareils produit du désaccord sur l'emplacement des phrases, pas sur ce que vous vouliez mesurer. Si c'est votre entrée, voyez Comment annoter des sous-titres YouTube.
Ici, les segments sont utilisables tels quels, donc l'unité est le tour.
Étape 2 : décidez qui attribue les locuteurs
Whisper ne diarise pas. Trois options, et c'est une vraie décision, pas une formalité :
Refaire tourner avec WhisperX. Automatique, rapide, et faux assez souvent sur les chevauchements de parole pour que quelqu'un doive de toute façon vérifier.
whisperx interview_01.mp3 --model medium --diarize --output_format jsonUtiliser une API cloud avec la diarisation activée. Deepgram avec diarize=true, AssemblyAI avec speaker_labels, AWS Transcribe ou Rev.ai. Potato lit les quatre nativement.
Laisser les annotateurs attribuer les locuteurs pendant qu'ils écoutent. Pour 40 entretiens à deux personnes, c'est l'option que nous choisirions. Deux locuteurs aux rôles nettement distincts, c'est le cas facile pour une personne et pas toujours le cas facile pour un modèle, et l'annotateur écoute l'audio de toute façon.
Nous prenons la troisième. Les tours non diarisés s'affichent comme Unassigned avec un sélecteur, et l'attribution est enregistrée avec les annotations.
Les tours non diarisés arrivent comme Unassigned, avec un sélecteur sur chacun
Étape 3 : construisez le fichier de données
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.jsonLes transcriptions s'apparient à leur audio par nom de base, donc interview_01.json trouve interview_01.mp3. Le doublement de nom de Whisper, interview_01.mp3.json, est géré, et l'identifiant d'item ressort comme interview_01.
Le résultat :
{
"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."}
]
}
}Vous pouvez sauter complètement cette étape si vous préférez garder les transcriptions sous forme de fichiers. Un fichier de données peut pointer directement dessus, et Potato lit et normalise au moment de l'affichage :
{"id": "interview_01", "conversation": {"audio": "audio/interview_01.mp3",
"transcript": "whisper_out/interview_01.json"}}Étape 4 : écrivez la configuration
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"Trois choses font le travail ici.
Le répertoire des locuteurs donne aux deux rôles des noms, des couleurs et des côtés stables avant que quiconque ait attribué le moindre tour. Sans lui, les locuteurs reçoivent quand même des couleurs, mais attribuées de façon déterministe par transcription plutôt que de façon cohérente à l'échelle du corpus.
turn_level: true avec turn_binding rattache la question de thème à chaque tour plutôt qu'à l'entretien pris comme un tout. C'est ce qui fait du tour l'unité d'annotation en pratique.
span_target: true plus le schéma span permet à un surlignage de courir au-delà des frontières de tours, ce qui compte quand le passage citable couvre une question et sa réponse. Les décalages restent stables quand un locuteur est réattribué.
Lancez-la :
python potato/flask_server.py start config.yaml -p 8000
Chaque tour reçoit un bouton de lecture qui ne joue que ce tour, plus sa propre question d'étiquetage
Chaque bulle a un bouton de lecture qui joue juste ce tour et s'arrête. En pratique, c'est la fonctionnalité que les annotateurs commentent : vérifier une réplique précise contre l'audio cesse d'être un exercice de navigation à la souris.
Étape 5 : contrôlez l'accord sur la bonne chose
Deux annotateurs par entretien, et vous avez maintenant deux sortes d'accord à regarder.
Les étiquettes de thème relèvent de l'accord catégoriel ordinaire, au niveau du tour. Comme les identifiants de tours sont déterministes, le même fichier produit toujours les mêmes identifiants, et les étiquettes des deux annotateurs s'alignent sans aucune étape d'alignement.
L'attribution des locuteurs mérite un contrôle à part. Si vos deux annotateurs sont en désaccord sur qui parle dans 15 % des tours, c'est un signal sur l'audio, et cela veut dire qu'une diarisation automatique se serait trompée au moins aussi souvent sans vous le dire.
Voir Accord inter-annotateurs pour les mesures, et Accord pour les empans et les sorties structurées pour les surlignages, qui demandent un traitement différent parce que les annotateurs choisissent les frontières en plus des étiquettes.
Étape 6 : exportez
Le JSON, le JSONL et le CSV standard fonctionnent comme d'habitude. Quand vous voulez que l'alignement temporel survive jusque dans un outil d'analyse de la parole, exportez les annotations en couches vers ELAN ou Praat :
python -m potato.export --config config.yaml --format eaf --output ./out/
python -m potato.export --config config.yaml --format textgrid --output ./out/Les deux font l'aller-retour, parce que Potato lit aussi EAF et TextGrid en entrée. Annotez ici, affinez dans ELAN, relisez le résultat.
Les quatre choses qui tournent mal
Quelqu'un a gardé le .txt. Aucun temps, rien à récupérer, il faut repasser l'audio dans le modèle. --dry-run l'attrape avant que vous ayez construit quoi que ce soit par-dessus.
Les temps sont 1000 fois faux. Quelque chose en amont a mélangé secondes et millisecondes. Whisper et Deepgram émettent des secondes en virgule flottante ; AssemblyAI, les décalages de whisper.cpp et le TSV de Whisper émettent des millisecondes entières.
Des schémas au niveau de la phrase sur une entrée découpée en cues. Traité plus haut, et le plus coûteux des quatre parce que vous ne le découvrez qu'au moment de calculer l'accord.
Faire confiance à une diarisation que personne n'a vérifiée. Une erreur de diarisation se propage dans chaque étiquette rattachée à ce tour, et elle ressemble à un désaccord entre annotateurs quand vous partez en chercher la cause.
Pour aller plus loin
- Comment annoter des transcriptions Whisper
- Comment annoter des sous-titres YouTube
- Formats de transcription
- Ingestion de formats de transcription, une conception exécutable avec six formats côte à côte
- Potato 2.7.1 : la transcription existe déjà