Skip to content
Tutorials8 min read

Anotar transcrições de ASR: um exemplo na prática

Um passo a passo completo, de uma pasta de saída do Whisper até turnos de fala rotulados: escolher a unidade de anotação, lidar com a diarização, escrever a configuração, rodar a tarefa e exportar com o alinhamento temporal intacto.

Potato Team

Este texto percorre um projeto de ponta a ponta: 40 entrevistas de pesquisa gravadas, já transcritas com o Whisper, que precisam ser codificadas por tema e por quem disse o quê. É a versão concreta dos dois guias de referência, com cada decisão tomada em vez de descrita.

Se o que você quer é o detalhe formato a formato, isso mora em Formatos de transcrição.

De uma pasta de saída de ASR a turnos rotulados, em quatro passosConfira o que você guardou, decida quem rotula os falantes, monte a configuração, exporte com as marcações de tempo

Passo 0: olhe o que você tem de fato

Antes de qualquer outra coisa, descubra o que está na pasta. Isso leva dez segundos e economiza um dia:

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

Duas coisas a ler aí. Todos os arquivos estão não diarizados, então nada neste corpus sabe quem está falando. E o interview_03.txt entrou como um único turno de duração zero, porque um .txt do Whisper contém texto e mais nada. Não há marcações de tempo ali para recuperar.

Esse terceiro arquivo precisa que se ache o .json dele, ou que o áudio seja processado de novo. Nada rio abaixo vai consertar isso.

Passo 1: escolha a unidade de anotação antes de escolher os rótulos

A questão da unidade decide mais sobre os seus números de concordância do que o conjunto de rótulos decide.

Os segmentos do Whisper têm mais ou menos o tamanho de um enunciado, e quebram em pausas e não em nada gramatical. Para codificação de entrevistas isso costuma ser a unidade certa: a resposta de um entrevistado chega em vários segmentos, e codificar cada um separadamente dá um registro mais fino do que codificar a resposta inteira de uma vez.

Onde isso dá errado é com legendas automáticas de uma plataforma de vídeo, em que as fronteiras das falas caem onde a caixa de legenda encheu. Pedir que os anotadores avaliem "cada frase" sobre falas assim produz discordância sobre onde estão as frases, e não sobre a coisa que você queria medir. Se essa é a sua entrada, veja Como anotar legendas do YouTube.

Aqui os segmentos são usáveis como estão, então a unidade é o turno.

Passo 2: decida quem atribui os falantes

O Whisper não diariza. Três opções, e essa é uma decisão de verdade e não uma formalidade:

Rodar de novo com o WhisperX. Automático, rápido e errado com frequência suficiente em fala sobreposta para que alguém tenha de conferir de qualquer jeito.

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

Usar uma API na nuvem com diarização ligada. Deepgram com diarize=true, AssemblyAI com speaker_labels, AWS Transcribe ou Rev.ai. O Potato lê os quatro nativamente.

Deixar os anotadores atribuírem os falantes enquanto escutam. Para 40 entrevistas de duas pessoas, é a opção que escolheríamos. Dois falantes com papéis claramente diferentes é o caso fácil para uma pessoa e nem sempre o caso fácil para um modelo, e o anotador vai escutar o áudio de todo modo.

Vamos com a terceira. Turnos não diarizados aparecem como Unassigned com um seletor, e a atribuição é salva junto com as anotações.

Turnos de transcrição com fundo cinza tracejado, cada um rotulado como Unassigned com um menu suspensoTurnos não diarizados chegam como Unassigned, com um seletor em cada um

Passo 3: monte o arquivo de dados

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

As transcrições são pareadas com seu áudio pelo nome-base, então interview_01.json encontra interview_01.mp3. A nomenclatura duplicada do Whisper, interview_01.mp3.json, é tratada, e o id do item sai como interview_01.

O resultado:

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

Dá para pular esse passo inteiro se você preferir manter as transcrições como arquivos. Um arquivo de dados pode apontar direto para elas, e o Potato lê e normaliza na hora de renderizar:

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

Passo 4: escreva a configuração

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]
    turn_level: true
    turn_binding:
      field: conversation
 
  - annotation_type: span
    name: quotes
    description: "Highlight anything quotable in the writeup"
    target_field: conversation
    labels:
      - name: quotable
        key_value: "q"

Três coisas estão fazendo trabalho aqui.

A lista de falantes dá aos dois papéis nomes, cores e lados estáveis antes de alguém ter atribuído um único turno. Sem ela, os falantes ainda ganham cores, mas atribuídas de forma determinística por transcrição em vez de consistente ao longo do corpus.

turn_level: true com turn_binding prende a pergunta de tema a cada turno em vez de à entrevista inteira. É isso que faz do turno a unidade de anotação na prática.

span_target: true mais o esquema span deixam um destaque atravessar fronteiras de turno, o que importa quando a passagem citável abrange uma pergunta e sua resposta. Os deslocamentos continuam estáveis quando um falante é reatribuído.

Rode:

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

Turnos de transcrição apresentados como balões de falante coloridos com botões de reprodução por turno e uma pergunta de rótulo embutidaCada turno ganha um botão de reprodução que toca só aquele turno, mais sua própria pergunta de rótulo

Cada balão tem um botão de reprodução que toca apenas aquele turno e para. Na prática é esse o recurso que os anotadores comentam: conferir uma fala específica contra o áudio deixa de ser um exercício de arrastar a barra.

Passo 5: confira a concordância na coisa certa

Dois anotadores por entrevista, e agora você tem dois tipos de concordância para olhar.

Os rótulos de tema são concordância categórica comum em nível de turno. Como os ids de turno são determinísticos, o mesmo arquivo sempre produz os mesmos ids, e os rótulos dos dois anotadores se alinham sem nenhum passo de alinhamento.

A atribuição de falante vale ser conferida à parte. Se seus dois anotadores discordam sobre quem está falando em 15% dos turnos, isso é um sinal sobre o áudio, e significa que a diarização automática teria errado pelo menos essa fração das vezes sem te avisar.

Veja Concordância entre anotadores para as medidas, e Concordância para trechos e saídas estruturadas para os destaques, que precisam de outro tratamento porque os anotadores escolhem fronteiras além de rótulos.

Passo 6: exporte

JSON, JSONL e CSV padrão funcionam como sempre. Quando você quer que o alinhamento temporal sobreviva até uma ferramenta de análise de fala, exporte anotações em camadas para 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/

Os dois fazem o caminho de ida e volta, porque o Potato também lê EAF e TextGrid como entrada. Anote aqui, refine no ELAN, leia o resultado de volta.

As quatro coisas que dão errado

Alguém guardou o .txt. Sem marcações de tempo, irrecuperável, exige rodar de novo. O --dry-run pega isso antes de você ter construído qualquer coisa em cima.

As marcações de tempo estão 1000× fora. Alguma coisa a montante misturou segundos e milissegundos. Whisper e Deepgram emitem segundos em ponto flutuante; AssemblyAI, os deslocamentos do whisper.cpp e o TSV do Whisper emitem milissegundos inteiros.

Esquemas em nível de frase sobre entrada baseada em falas de legenda. Coberto acima, e o mais caro dos quatro porque você só descobre quando vai calcular a concordância.

Confiar numa diarização que ninguém conferiu. Um erro de diarização se propaga para todo rótulo preso àquele turno, e parece discordância entre anotadores quando você vai procurar a causa.

Leitura adicional