Skip to content

Come annotare le trascrizioni di Whisper

Come prendere l'output di Whisper o WhisperX e trasformarlo in un progetto di annotazione funzionante: quale file conservare, quando serve la diarizzazione, come assegnare i parlanti e come esportare senza perdere i tempi.

Whisper restituisce segmenti con i tempi, e uno strumento di annotazione dovrebbe leggerli così come sono. Conserva l'output .json invece del .txt, usa WhisperX se ti servono le etichette dei parlanti, poi punta Potato alla cartella. Nessuno script di conversione, nessun passo di riformattazione. Per il dettaglio formato per formato, vedi Formati di trascrizione.

Whisper è il modello open source di riconoscimento vocale di OpenAI, ed è diventato il primo passo standard per chiunque abbia una montagna di audio da smaltire. Quello che produce è un insieme di segmenti con i tempi, cioè quasi tutto ciò che serve a un progetto di annotazione. Manca il pezzo tra «ho le trascrizioni» e «gli annotatori le stanno etichettando».

Nota: Potato non trascrive. L'ASR gira a monte e Potato ne ingerisce l'output. Questa guida spiega quali scelte a monte contano, ma la trascrizione avviene prima che Potato veda qualcosa.

Quale file prodotto da Whisper conviene tenere?

Whisper scrive più file e la scelta pesa più di quanto sembri:

bash
whisper interview_01.mp3 --model medium --output_format json --word_timestamps True
FileContenutoDa usare?
.jsonSegmenti con tempo di inizio e fine, volendo anche i tempi per parolaSì, questo
.srt / .vttSegmenti con i tempi, senza metadatiSì, va bene
.tsvInizio e fine in millisecondi più il testo
.txtSolo testo, senza tempiNo, non c'è niente da sincronizzare con l'audio

Se hai tenuto solo il .txt, l'allineamento è perso e non lo recuperi senza rieseguire il modello. È il motivo più frequente per cui una trascrizione arriva in Potato come un unico blocco di testo indistinto.

Whisper etichetta i parlanti?

No, e la cosa sorprende in continuazione. Whisper puro trascrive e basta, non ti dice chi parla. Ogni turno arriva senza attribuzione, il che va benissimo per l'annotazione dei contenuti ed è un problema per tutto ciò che riguarda i parlanti.

La diarizzazione dei parlanti è un passo a parte. Tre modi per affrontarla:

Usa WhisperX, che avvolge Whisper con la diarizzazione di pyannote:

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

Il suo output porta un campo speaker per ogni segmento (SPEAKER_00, SPEAKER_01 e così via) e Potato lo legge direttamente.

Usa un'API cloud con la diarizzazione attiva. Deepgram con diarize=true, AssemblyAI con speaker_labels, AWS Transcribe e Rev.ai producono tutte etichette dei parlanti, e Potato le legge tutte e quattro in modo nativo.

Tre turni di trascrizione su sfondo grigio tratteggiato, ciascuno marcato come Non assegnato con una freccia a discesa.I turni non diarizzati compaiono come Non assegnati con un selettore del parlante

Lascia fare agli annotatori. Quando i turni arrivano senza diarizzazione, ogni fumetto compare come Non assegnato con un selettore. Su audio disturbato, parlato sovrapposto o stanze rumorose, una persona che ascolta è spesso più precisa della diarizzazione automatica. Su un corpus piccolo è uno scambio ragionevole, non un ripiego.

Come costruisco il file di dati?

Punta il convertitore alla cartella con l'output di Whisper:

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

Le trascrizioni si accoppiano al media in base al nome del file, quindi interview_01.json trova interview_01.mp3. Gli id degli elementi vengono dal nome del file, e l'estensione doppia di Whisper interview_01.mp3.json è gestita senza produrre un id interview_01.mp3.

Controlla cosa ha capito prima di fidarti:

bash
potato transcripts ./whisper_out --dry-run
text
Scanned 3 file(s):
  interview_01.json      Whisper JSON      42 turns    891.4s  undiarized
  interview_02.json      WhisperX JSON     51 turns   1120.8s  2 speaker(s): SPEAKER_00, SPEAKER_01
  interview_03.json      Whisper JSON      38 turns    754.2s  undiarized

3 item(s), 131 turn(s).

Un file che riporta plain text o zero turni è il tuo file problematico. Quasi sempre è un .txt finito per sbaglio insieme all'output .json.

Posso saltare il passo di conversione?

Sì. Un file di dati può puntare direttamente ai file di trascrizione su disco:

json
{"id": "int_001", "conversation": {"audio": "media/int_001.mp3",
                                   "transcript": "media/int_001.srt"}}

Potato legge il file, riconosce il formato dal contenuto e lo normalizza quando l'istanza viene visualizzata. Le trascrizioni restano file di cui puoi fare il diff e che puoi riesportare, invece di finire dentro un blob di dati.

Che aspetto ha la configurazione?

Il file di dati generato ha questa forma:

json
{
  "id": "interview_01",
  "conversation": {
    "audio": "audio/interview_01.mp3",
    "turns": [
      {"turn_id": "t0", "speaker": "SPEAKER_00", "start": 0.0, "end": 6.5,
       "text": "Welcome back."}
    ]
  }
}

E la configurazione corrispondente, che potato transcripts --emit-config stampa per te:

yaml
annotation_task_name: "Interview Annotation"
task_dir: .
data_files:
  - data/interviews.json
 
item_properties:
  id_key: id
  text_key: conversation
 
instance_display:
  fields:
    - key: conversation
      type: audio_dialogue
      label: "Transcript"
      span_target: true
      display_options:
        show_timestamps: true
        allow_speaker_assignment: auto
 
annotation_schemes:
  - annotation_type: span
    name: topics
    description: "Highlight topic mentions"
    labels:
      - name: policy
      - name: personal

Turni di trascrizione resi come fumetti colorati per parlante, con pulsanti di riproduzione per turno, marche temporali e una domanda di etichettatura in linea sotto ogni turno.La visualizzazione audio_dialogue, con una domanda agganciata a ogni turno

La visualizzazione audio_dialogue mostra i turni come fumetti dei parlanti sincronizzati con l'audio, e ogni turno ha un pulsante che riproduce solo quel turno. span_target: true permette agli span di attraversare i confini tra turni, e gli offset restano stabili quando un turno cambia parlante.

Se preferisci che gli annotatori verifichino la trascrizione stessa invece di etichettarne il contenuto, speech_transcript ti dà schede per segmento con tag di errore e una casella di correzione. Per il lavoro su sovrapposizioni e interruzioni, voice_interaction offre una timeline a doppia traccia. Entrambi leggono gli stessi file.

Come fanno gli annotatori ad assegnare i parlanti?

Definisci un elenco così i parlanti hanno nomi, colori e lati stabili:

yaml
display_options:
  allow_speaker_assignment: auto
  speakers:
    - id: interviewer
      name: "Interviewer"
      color: "#7c3aed"
      side: left
    - id: participant
      name: "Participant"
      color: "#059669"
      side: right

auto accende il selettore quando ci sono turni non diarizzati da etichettare. Impostalo a true per consentire la riassegnazione anche quando la sorgente aveva già etichettato i parlanti, che è quello che serve se stai correggendo errori di diarizzazione invece di riempire caselle vuote.

Le assegnazioni vengono salvate insieme alle annotazioni, agganciate a un id di turno stabile, quindi sopravvivono a un ricaricamento.

Come tiro fuori le annotazioni?

L'export in JSON, JSONL e CSV funziona come al solito. Quando l'allineamento temporale deve sopravvivere, esporta le annotazioni a livelli in EAF di ELAN o in TextGrid di Praat:

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

Entrambi fanno il giro completo. Potato legge EAF e TextGrid anche in ingresso, quindi puoi annotare in Potato, rifinire in ELAN o Praat e rileggere il risultato.

Approfondimenti