Skip to content

Cómo anotar transcripciones de Whisper

Cómo convertir la salida de Whisper o WhisperX en un proyecto de anotación en marcha: qué archivo conservar, cuándo hace falta diarización, cómo asignar hablantes y cómo exportar sin perder las marcas de tiempo.

Whisper te da segmentos con tiempos, y una herramienta de anotación debería leerlos tal cual. Conserva la salida .json en lugar del .txt, ejecuta WhisperX si necesitas etiquetas de hablante y luego apunta Potato a la carpeta. Sin script de conversión ni paso de reformateo. Para el detalle formato por formato, consulta Formatos de transcripción.

Whisper es el modelo de reconocimiento de voz de código abierto de OpenAI, y se ha convertido en el primer paso por defecto para cualquiera que tenga una pila de audio por delante. Lo que produce es un conjunto de segmentos con tiempos, que es casi todo lo que necesita un proyecto de anotación. El hueco está en todo lo que va entre "tengo transcripciones" y "los anotadores están etiquetándolas".

Note: Potato no transcribe. El ASR se ejecuta antes y Potato ingiere su salida. Esta guía cubre qué decisiones previas importan, pero la transcripción ocurre antes de que Potato vea nada.

¿Qué archivo de salida de Whisper debo conservar?

Whisper escribe varios archivos y la elección importa más de lo que parece:

bash
whisper interview_01.mp3 --model medium --output_format json --word_timestamps True
ArchivoContiene¿Usarlo?
.jsonSegmentos con tiempos de inicio y fin, opcionalmente tiempos por palabraSí, este
.srt / .vttSegmentos con tiempos, sin metadatosSí, funciona bien
.tsvInicio y fin en milisegundos más el texto
.txtSolo texto, sin tiemposNo, no hay nada que sincronizar con el audio

Si el .txt es lo único que conservaste, la alineación se perdió y no la puedes recuperar sin volver a ejecutar el modelo. Esa es la razón más frecuente de que una transcripción llegue a Potato como un único bloque de texto indiferenciado.

¿Whisper etiqueta hablantes?

No, y esto sorprende a mucha gente constantemente. Whisper transcribe; no te dice quién habla. Todos los turnos llegan sin asignar, lo cual está bien para anotar contenido y es doloroso para cualquier cosa relacionada con hablantes.

La diarización de hablantes es un paso aparte. Tres formas de resolverlo:

Ejecuta WhisperX, que envuelve Whisper con diarización de pyannote:

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

Su salida lleva un campo speaker por segmento (SPEAKER_00, SPEAKER_01, y así) y Potato lo lee directamente.

Usa una API en la nube con diarización activada. Deepgram con diarize=true, AssemblyAI con speaker_labels, AWS Transcribe y Rev.ai producen etiquetas de hablante, y Potato lee las cuatro de forma nativa.

Tres turnos de transcripción con fondo gris rayado, cada uno etiquetado como Unassigned con una flecha desplegable.Los turnos sin diarizar se muestran como Unassigned con un selector de hablante

Deja que lo hagan los anotadores. Cuando los turnos llegan sin diarizar, cada burbuja se muestra como Unassigned con un selector. Con audio ruidoso, habla solapada o una sala con ruido de fondo, una persona escuchando suele ser más precisa que la diarización automática. Para un corpus pequeño esto es un intercambio razonable, no un recurso de emergencia.

¿Cómo construyo el archivo de datos?

Apunta el conversor a tu carpeta de salida de Whisper:

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

Las transcripciones se emparejan con el audio por nombre base, así que interview_01.json encuentra interview_01.mp3. Los identificadores de ítem salen del nombre de archivo, y la doble extensión interview_01.mp3.json de Whisper se maneja sin producir un id como interview_01.mp3.

Comprueba qué ha entendido antes de darlo por bueno:

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 archivo que informa plain text o cero turnos es tu archivo problemático. Casi siempre es un .txt que se coló entre la salida .json.

¿Puedo saltarme el paso de conversión?

Sí. Un archivo de datos puede apuntar directamente a las transcripciones en disco:

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

Potato lee el archivo, detecta el formato por su contenido y lo normaliza al renderizar la instancia. Tus transcripciones siguen siendo archivos que puedes comparar y volver a exportar, en lugar de quedar incrustadas en un bloque de datos.

¿Cómo es la configuración?

El archivo de datos generado tiene esta 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."}
    ]
  }
}

Y la configuración correspondiente, que potato transcripts --emit-config te imprime:

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"
    target_field: conversation
    labels:
      - name: policy
      - name: personal

Turnos de transcripción como burbujas de hablante de colores, con botones de reproducción por turno, marcas de tiempo y una pregunta de etiquetado bajo cada turno.La visualización audio_dialogue, con una pregunta por turno adjunta a cada turno

La visualización audio_dialogue muestra los turnos como burbujas de hablante sincronizadas con el audio, con un botón de reproducción en cada turno que reproduce solo ese turno. span_target: true permite que los tramos crucen fronteras de turno, y los desplazamientos se mantienen estables cuando se reasigna un hablante.

Si prefieres que los anotadores revisen la transcripción en lugar de etiquetar su contenido, speech_transcript te da tarjetas de segmento con etiquetas de error y una caja de corrección. Para trabajo sobre solapamientos e interrupciones, voice_interaction te da una línea de tiempo de doble pista. Ambos leen los mismos archivos.

¿Cómo asignan hablantes los anotadores?

Define una lista para que los hablantes tengan nombres, colores y lados estables:

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 activa el selector cuando hay turnos sin diarizar que etiquetar. Ponlo en true para permitir la reasignación incluso cuando la fuente sí etiquetó hablantes, que es lo que quieres si estás corrigiendo errores de diarización en lugar de rellenar huecos.

Las asignaciones se guardan con las anotaciones, ligadas a un identificador de turno estable, así que sobreviven a una recarga.

¿Cómo recupero las anotaciones?

La exportación a JSON, JSONL y CSV funciona como siempre. Cuando la alineación temporal tiene que sobrevivir, exporta las anotaciones por capas a ELAN EAF o Praat TextGrid:

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

Ambos hacen ida y vuelta. Potato también lee EAF y TextGrid como entrada, así que puedes anotar en Potato, refinar en ELAN o Praat, y volver a leer el resultado.

Lecturas adicionales