Anotar transcripciones de ASR: un ejemplo resuelto
Un recorrido completo desde una carpeta de salida de Whisper hasta turnos de hablante etiquetados: elegir la unidad de anotación, resolver la diarización, escribir la configuración, poner la tarea en marcha y exportar con la alineación temporal intacta.
Este recorrido sigue un proyecto de principio a fin: 40 entrevistas de investigación grabadas, ya transcritas con Whisper, que hay que codificar por tema y por quién dijo qué. Es la versión concreta de las dos guías de referencia, con cada decisión tomada en lugar de descrita.
Si lo que quieres es el detalle formato por formato, eso está en Formatos de transcripción.
Comprueba qué has conservado, decide quién etiqueta los hablantes, construye la configuración y exporta con los tiempos
Paso 0: mira qué tienes en realidad
Antes que nada, averigua qué hay en la carpeta. Lleva diez segundos y ahorra un día:
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).
Dos cosas que se leen ahí. Todos los archivos están sin diarizar, así que nada en este corpus sabe quién habla. Y interview_03.txt ha entrado como un único turno de duración cero, porque un .txt de Whisper contiene texto y nada más. No hay tiempos dentro que recuperar.
A ese tercer archivo hay que encontrarle su .json, o volver a pasar el audio. Nada de lo que venga después lo va a arreglar.
Paso 1: elige la unidad de anotación antes que las etiquetas
La cuestión de la unidad decide más sobre tus cifras de acuerdo que el propio conjunto de etiquetas.
Los segmentos de Whisper tienen aproximadamente el tamaño de un enunciado, y cortan en las pausas y no en nada gramatical. Para codificar entrevistas esa suele ser la unidad correcta: la respuesta de un entrevistado llega repartida en varios segmentos, y codificar cada uno por separado te da un registro más fino que codificar la respuesta entera de una vez.
Donde esto se tuerce es con los subtítulos automáticos de una plataforma de vídeo, donde las fronteras de cada rótulo caen justo donde se llenó la caja de subtítulos. Pedir a los anotadores que valoren «cada frase» sobre rótulos así produce desacuerdo sobre dónde están las frases, no sobre lo que querías medir. Si esa es tu entrada, consulta Cómo anotar subtítulos de YouTube.
Aquí los segmentos se pueden usar tal cual, así que la unidad es el turno.
Paso 2: decide quién asigna los hablantes
Whisper no diariza. Tres opciones, y esta es una decisión de verdad y no un trámite:
Vuelve a pasarlo con WhisperX. Automático, rápido y equivocado en el habla solapada con la frecuencia suficiente como para que alguien tenga que revisarlo igualmente.
whisperx interview_01.mp3 --model medium --diarize --output_format jsonUsa una API en la nube con la diarización activada. Deepgram con diarize=true, AssemblyAI con speaker_labels, AWS Transcribe o Rev.ai. Potato lee los cuatro de forma nativa.
Deja que los anotadores asignen los hablantes mientras escuchan. Para 40 entrevistas de dos personas, esta es la opción que elegiríamos. Dos hablantes con papeles claramente distintos es el caso fácil para una persona y no siempre el caso fácil para un modelo, y el anotador va a escuchar el audio de todas formas.
Nos quedamos con la tercera. Los turnos sin diarizar se muestran como Unassigned con un selector, y la asignación se guarda junto con las anotaciones.
Los turnos sin diarizar llegan como Unassigned, con un selector en cada uno
Paso 3: construye el archivo de datos
potato transcripts ./whisper_out --media-dir ./audio -o data/interviews.jsonLas transcripciones se emparejan con su audio por el nombre base, así que interview_01.json encuentra interview_01.mp3. El nombrado doble de Whisper, interview_01.mp3.json, está contemplado, y el identificador del elemento sale como interview_01.
El resultado:
{
"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."}
]
}
}Puedes saltarte este paso por completo si prefieres mantener las transcripciones como archivos. Un archivo de datos puede apuntar directamente a ellas, y Potato las lee y las normaliza al mostrarlas:
{"id": "interview_01", "conversation": {"audio": "audio/interview_01.mp3",
"transcript": "whisper_out/interview_01.json"}}Paso 4: escribe la configuración
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"Aquí hay tres cosas haciendo trabajo.
La lista de hablantes le da a los dos papeles nombres, colores y lados estables antes de que nadie haya asignado un solo turno. Sin ella, los hablantes siguen recibiendo colores, pero se asignan de forma determinista dentro de cada transcripción y no de forma consistente en todo el corpus.
turn_level: true junto con turn_binding engancha la pregunta de tema a cada turno en lugar de a la entrevista entera. Eso es lo que convierte al turno en la unidad de anotación en la práctica.
span_target: true más el esquema span permite que un resaltado cruce las fronteras entre turnos, lo que importa cuando el pasaje citable abarca una pregunta y su respuesta. Los desplazamientos se mantienen estables cuando se reasigna un hablante.
Ponlo en marcha:
python potato/flask_server.py start config.yaml -p 8000
Cada turno tiene un botón de reproducción que reproduce solo ese turno, más su propia pregunta de etiquetado
Cada burbuja tiene un botón de reproducción que reproduce solo ese turno y se para. En la práctica esa es la función que comentan los anotadores: comprobar una línea concreta contra el audio deja de ser un ejercicio de rebobinar a mano.
Paso 5: mide el acuerdo sobre lo que toca
Dos anotadores por entrevista, y ahora tienes dos tipos de acuerdo que mirar.
Las etiquetas de tema son acuerdo categórico corriente a nivel de turno. Como los identificadores de turno son deterministas, el mismo archivo produce siempre los mismos identificadores, y las etiquetas de los dos anotadores encajan sin ningún paso de alineación.
La asignación de hablantes merece una comprobación aparte. Si tus dos anotadores discrepan sobre quién habla en el 15 % de los turnos, eso es una señal sobre el audio, y significa que la diarización automática se habría equivocado al menos con esa frecuencia sin decírtelo.
Consulta La concordancia entre anotadores para las medidas, y Acuerdo para tramos para los resaltados, que necesitan otro tratamiento porque los anotadores eligen también las fronteras, y no solo las etiquetas.
Paso 6: exporta
JSON, JSONL y CSV estándar funcionan como siempre. Cuando quieras que la alineación temporal sobreviva hasta una herramienta de análisis del habla, exporta las anotaciones por capas a 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/Los dos hacen el viaje de ida y vuelta, porque Potato lee EAF y TextGrid también como entrada. Anota aquí, refina en ELAN y vuelve a leer el resultado.
Las cuatro cosas que salen mal
Alguien conservó el .txt. Sin tiempos, no recuperable, hay que volver a pasarlo. --dry-run lo detecta antes de que hayas construido nada encima.
Los tiempos están 1000 veces mal. Algo, antes en la cadena, mezcló segundos y milisegundos. Whisper y Deepgram emiten segundos en coma flotante; AssemblyAI, los desplazamientos de whisper.cpp y el TSV de Whisper emiten milisegundos enteros.
Esquemas a nivel de frase sobre una entrada basada en rótulos. Ya visto arriba, y el más caro de los cuatro porque no te enteras hasta que calculas el acuerdo.
Fiarte de una diarización que nadie revisó. Un error de diarización se propaga a todas las etiquetas enganchadas a ese turno, y parece desacuerdo entre anotadores cuando vas a buscar la causa.
Lecturas adicionales
- Cómo anotar transcripciones de Whisper
- Cómo anotar subtítulos de YouTube
- Formatos de transcripción
- Ingesta de formatos de transcripción, un diseño ejecutable con seis formatos en paralelo
- Potato 2.7.1: la transcripción ya existe