Skip to content
Tutorials8 min read

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.

Potato Team

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.

D'un dossier de sorties de reconnaissance vocale à des tours étiquetés, en quatre étapesVé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 :

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

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.

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

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

Des tours de transcription sur fond gris hachuré, chacun étiqueté Unassigned avec un menu déroulantLes tours non diarisés arrivent comme Unassigned, avec un sélecteur sur chacun

Étape 3 : construisez le fichier de données

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

Les 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 :

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

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 :

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

Étape 4 : écrivez la configuration

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"

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 :

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

Des tours de transcription affichés en bulles de locuteur colorées, avec un bouton de lecture par tour et une question d'étiquetage en ligneChaque 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 :

bash
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