Annotare le trascrizioni ASR: un esempio pratico
Un percorso completo da una cartella di output di Whisper a turni di parlante etichettati: scegliere l'unità di annotazione, gestire la diarizzazione, scrivere la configurazione, far girare il task ed esportare con l'allineamento temporale intatto.
Qui seguiamo un progetto dall'inizio alla fine: 40 interviste di ricerca registrate, già trascritte con Whisper, da codificare per argomento e per chi ha detto cosa. È la versione concreta delle due guide di riferimento, con ogni decisione presa davvero invece che descritta.
Se invece cerchi il dettaglio formato per formato, sta in Formati di trascrizione.
Controlla cosa hai tenuto, decidi chi etichetta i parlanti, scrivi la configurazione, esporta con i tempi
Passo 0: guarda che cosa hai davvero
Prima di ogni altra cosa, scopri che cosa c'è nella cartella. Ci vogliono dieci secondi e ti risparmiano una giornata:
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).
Da qui si leggono due cose. Nessun file è diarizzato, quindi in questo corpus niente sa chi sta parlando. E interview_03.txt è arrivato come un turno solo con durata zero, perché un .txt di Whisper contiene testo e nient'altro. Non ci sono tempi da recuperarci dentro.
Per quel terzo file bisogna ritrovare il suo .json, oppure rieseguire il modello sull'audio. Niente a valle lo sistemerà.
Passo 1: scegli l'unità di annotazione prima delle etichette
La questione dell'unità pesa sui tuoi numeri di accordo più di quanto pesi l'insieme delle etichette.
I segmenti di Whisper hanno grosso modo la dimensione di un enunciato e si interrompono alle pause, non su qualcosa di grammaticale. Per la codifica di interviste di solito è l'unità giusta: la risposta di un intervistato arriva come più segmenti, e codificarli uno per uno dà un registro più fine che codificare l'intera risposta in blocco.
Dove la cosa si rompe è con i sottotitoli automatici di una piattaforma video, dove i confini dei blocchi cadono dove è capitato che si riempisse il riquadro dei sottotitoli. Chiedere agli annotatori di valutare «ogni frase» su blocchi del genere produce disaccordo su dove siano le frasi, non sulla cosa che volevi misurare. Se il tuo input è quello, vedi Annotare i sottotitoli di YouTube.
Qui i segmenti si possono usare così come sono, quindi l'unità è il turno.
Passo 2: decidi chi assegna i parlanti
Whisper non diarizza. Tre opzioni, ed è una decisione vera e non una formalità:
Rieseguire con WhisperX. Automatico, veloce e sbagliato abbastanza spesso sul parlato sovrapposto che qualcuno dovrà comunque controllarlo.
whisperx interview_01.mp3 --model medium --diarize --output_format jsonUsare un'API cloud con la diarizzazione attiva. Deepgram con diarize=true, AssemblyAI con speaker_labels, AWS Transcribe o Rev.ai. Potato li legge tutti e quattro nativamente.
Lasciare che siano gli annotatori ad assegnare i parlanti mentre ascoltano. Per 40 interviste a due voci è l'opzione che sceglieremmo. Due parlanti con ruoli chiaramente diversi sono il caso facile per una persona e non sempre il caso facile per un modello, e l'annotatore sta comunque ascoltando l'audio.
Andiamo con la terza. I turni non diarizzati compaiono come Unassigned con un selettore, e l'assegnazione viene salvata insieme alle annotazioni.
I turni non diarizzati arrivano come Unassigned, con un selettore su ciascuno
Passo 3: costruisci il file di dati
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.jsonLe trascrizioni si abbinano al proprio audio per nome base, quindi interview_01.json trova interview_01.mp3. La doppia denominazione interview_01.mp3.json di Whisper è gestita, e l'id dell'elemento esce come interview_01.
Il risultato:
{
"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."}
]
}
}Puoi saltare del tutto questo passaggio se preferisci tenere le trascrizioni come file. Un file di dati può puntarci direttamente, e Potato le legge e le normalizza al momento della visualizzazione:
{"id": "interview_01", "conversation": {"audio": "audio/interview_01.mp3",
"transcript": "whisper_out/interview_01.json"}}Passo 4: scrivi la configurazione
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"Qui ci sono tre cose che lavorano.
L'elenco dei parlanti dà ai due ruoli nomi, colori e lati stabili prima che qualcuno abbia assegnato un solo turno. Senza, i parlanti ricevono comunque dei colori, ma assegnati in modo deterministico dentro ciascuna trascrizione invece che coerente su tutto il corpus.
turn_level: true insieme a turn_binding attacca la domanda sull'argomento a ogni turno invece che all'intervista nel suo insieme. È questo a rendere il turno l'unità di annotazione nella pratica.
span_target: true più lo schema span permette a un'evidenziazione di attraversare i confini dei turni, cosa che conta quando il passaggio citabile copre una domanda e la sua risposta. Gli offset restano stabili quando un parlante viene riassegnato.
Avvialo:
python potato/flask_server.py start config.yaml -p 8000
Ogni turno ha un pulsante di riproduzione che riproduce solo quel turno, più la sua domanda di etichettatura
Ogni fumetto ha un pulsante di riproduzione che riproduce solo quel turno e si ferma. Nella pratica è la funzione su cui gli annotatori fanno commenti: verificare una battuta precisa contro l'audio smette di essere un esercizio di scorrimento avanti e indietro.
Passo 5: misura l'accordo sulla cosa giusta
Due annotatori per intervista, e ora hai due tipi di accordo da guardare.
Le etichette di argomento sono un normale accordo categorico a livello di turno. Poiché gli id dei turni sono deterministici, lo stesso file produce sempre gli stessi id, e le etichette dei due annotatori si allineano senza alcun passaggio di allineamento.
L'assegnazione dei parlanti vale la pena controllarla a parte. Se i tuoi due annotatori sono in disaccordo su chi parla nel 15% dei turni, quello è un segnale sull'audio, e significa che la diarizzazione automatica avrebbe sbagliato almeno altrettanto spesso senza dirtelo.
Vedi Accordo tra annotatori per le misure e Accordo sugli span per le evidenziazioni, che richiedono un trattamento diverso perché gli annotatori scelgono i confini oltre alle etichette.
Passo 6: esporta
JSON, JSONL e CSV standard funzionano come sempre. Quando vuoi che l'allineamento temporale sopravviva dentro uno strumento di analisi del parlato, esporta le annotazioni a livelli verso ELAN o Praat:
python -m potato.export --config config.yaml --format eaf --output ./out/
python -m potato.export --config config.yaml --format textgrid --output ./out/Entrambi fanno andata e ritorno, perché Potato legge EAF e TextGrid anche in ingresso. Annota qui, rifinisci in ELAN, rileggi dentro il risultato.
Le quattro cose che vanno storte
Qualcuno ha tenuto il .txt. Nessun tempo, niente da recuperare, serve rieseguire. --dry-run lo intercetta prima che tu ci abbia costruito sopra qualcosa.
I tempi sono sbagliati di mille volte. Qualcosa a monte ha mescolato secondi e millisecondi. Whisper e Deepgram producono secondi in virgola mobile; AssemblyAI, gli offset di whisper.cpp e il TSV di Whisper producono millisecondi interi.
Schemi a livello di frase su input diviso in blocchi di sottotitolo. Ne abbiamo parlato sopra, ed è la più costosa delle quattro perché non te ne accorgi finché non calcoli l'accordo.
Fidarsi di una diarizzazione che nessuno ha controllato. Un errore di diarizzazione si propaga in ogni etichetta attaccata a quel turno, e quando vai a cercare la causa sembra disaccordo tra annotatori.
Ulteriori letture
- Come annotare trascrizioni Whisper
- Come annotare sottotitoli di YouTube
- Formati di trascrizione
- Transcript Format Ingestion, un progetto eseguibile con sei formati affiancati
- Potato 2.7.1: la trascrizione esiste già