Skip to content
Tutorials6 min read

ASR 전사본 주석하기: 실제 사례 하나

Whisper 출력이 담긴 폴더에서 라벨이 붙은 화자 발화 차례까지 전 과정을 따라갑니다. 주석 단위 고르기, 화자 분리 처리하기, 설정 작성하기, 작업 돌리기, 그리고 시간 정렬을 유지한 채 내보내기까지 다룹니다.

Potato Team

이 글은 프로젝트 하나를 처음부터 끝까지 따라갑니다. 녹음된 연구 인터뷰 40건이 이미 Whisper로 전사돼 있고, 주제와 화자를 기준으로 코딩해야 합니다. 두 참조 가이드의 구체판이며, 결정을 설명하는 대신 전부 실제로 내립니다.

형식별 세부 사항 쪽이 필요하다면 그것은 전사 형식에 있습니다.

ASR 출력이 담긴 폴더에서 라벨이 붙은 차례까지, 네 단계로무엇을 남겼는지 확인하고, 화자를 누가 라벨링할지 정하고, 설정을 만들고, 시간 정보를 유지한 채 내보내기

0단계: 실제로 무엇을 가지고 있는지 보기

무엇보다 먼저 폴더에 무엇이 들어 있는지 확인하십시오. 10초가 걸리고 하루를 아껴 줍니다.

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

여기서 읽어 낼 것이 둘입니다. 모든 파일이 화자 분리가 되지 않았으므로 이 코퍼스에는 누가 말하는지 아는 것이 하나도 없습니다. 그리고 interview_03.txt는 길이가 0인 차례 하나로 들어왔는데, Whisper의 .txt에는 텍스트만 들어 있기 때문입니다. 되살릴 시간 정보가 아예 없습니다.

세 번째 파일은 .json을 찾아내거나 오디오를 다시 돌려야 합니다. 뒤쪽 단계에서 고쳐지는 것은 아무것도 없습니다.

1단계: 라벨보다 먼저 주석 단위를 고르기

단위를 무엇으로 하느냐가 라벨 집합보다 일치도 수치를 더 크게 좌우합니다.

Whisper의 구간은 대체로 발화 하나 정도 크기이고, 문법이 아니라 멈춤에서 끊깁니다. 인터뷰 코딩에서는 보통 그게 맞는 단위입니다. 응답자의 답변 하나가 여러 구간으로 들어오고, 각 구간을 따로 코딩하면 답변 전체를 한꺼번에 코딩할 때보다 더 촘촘한 기록을 얻습니다.

이 방식이 어긋나는 곳은 영상 플랫폼의 자동 생성 자막인데, 여기서는 자막 상자가 다 찬 자리마다 조각 경계가 떨어집니다. 그런 조각을 두고 어노테이터에게 “문장마다” 평가하라고 하면, 재려던 대상이 아니라 문장이 어디서 끝나는지에 대한 불일치가 생깁니다. 그런 입력을 다루고 있다면 YouTube 자막을 주석하는 방법을 보십시오.

여기서는 구간을 그대로 쓸 수 있으므로 단위는 발화 차례입니다.

2단계: 화자를 누가 배정할지 정하기

Whisper는 화자 분리를 하지 않습니다. 선택지가 셋이고, 형식적인 절차가 아니라 진짜 결정입니다.

WhisperX로 다시 돌리기. 자동이고 빠르지만, 발화가 겹치는 구간에서 충분히 자주 틀려서 결국 누군가는 확인해야 합니다.

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

화자 분리를 켠 클라우드 API 쓰기. diarize=true를 켠 Deepgram, speaker_labels를 켠 AssemblyAI, AWS Transcribe, Rev.ai입니다. Potato는 넷 다 그대로 읽습니다.

어노테이터가 들으면서 화자를 배정하게 하기. 두 사람이 나오는 인터뷰 40건이라면 우리는 이쪽을 고릅니다. 역할이 뚜렷이 다른 화자 둘은 사람에게는 쉬운 경우이고 모델에게는 늘 그렇지도 않으며, 어차피 어노테이터는 오디오를 듣고 있습니다.

우리는 세 번째로 갑니다. 화자 분리가 되지 않은 차례는 선택기와 함께 Unassigned로 표시되고, 배정은 주석과 함께 저장됩니다.

회색 빗금 배경의 전사 차례들. 각각 드롭다운과 함께 Unassigned로 표시되어 있다화자 분리가 되지 않은 차례는 각각 선택기가 붙은 채 Unassigned로 도착합니다

3단계: 데이터 파일 만들기

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

전사본은 기본 이름으로 오디오와 짝지어지므로 interview_01.jsoninterview_01.mp3를 찾아냅니다. Whisper가 이름을 겹쳐 쓰는 interview_01.mp3.json 방식도 처리되고, 항목 id는 interview_01로 나옵니다.

결과는 이렇습니다.

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

전사본을 파일로 두고 싶다면 이 단계는 통째로 건너뛰어도 됩니다. 데이터 파일이 그쪽을 바로 가리킬 수 있고, Potato가 화면에 그릴 때 읽어서 정규화합니다.

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

4단계: 설정 쓰기

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"

여기서 일을 하는 것이 셋 있습니다.

화자 명단은 아무도 차례 하나 배정하기 전에 두 역할에 안정된 이름과 색, 좌우 위치를 줍니다. 이것이 없어도 화자에게 색은 붙지만, 코퍼스 전체에 걸쳐 일관되게가 아니라 전사본마다 결정론적으로 배정됩니다.

turn_level: trueturn_binding은 주제 질문을 인터뷰 전체가 아니라 모든 차례에 붙입니다. 실제로 차례를 주석 단위로 만드는 것이 이 부분입니다.

span_target: truespan 스킴을 더하면 강조 표시가 차례 경계를 넘어 이어질 수 있는데, 인용할 만한 대목이 질문과 그 답에 걸쳐 있을 때 중요합니다. 화자가 다시 배정돼도 오프셋은 그대로 유지됩니다.

돌려 봅니다.

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

색으로 구분된 화자 말풍선으로 표시된 전사 차례들. 차례마다 재생 버튼과 라벨 질문이 붙어 있다차례마다 그 차례만 재생하는 버튼과 자기 라벨 질문이 붙습니다

말풍선마다 그 차례만 재생하고 멈추는 재생 버튼이 있습니다. 실제로 어노테이터들이 언급하는 기능이 이것입니다. 특정 대사를 오디오와 대조하는 일이 더 이상 스크러빙 작업이 아니게 됩니다.

5단계: 맞는 대상에 대해 일치도 확인하기

인터뷰당 어노테이터 두 명이고, 이제 살펴볼 일치도가 두 종류입니다.

주제 라벨은 차례 수준의 평범한 범주형 일치도입니다. 차례 id가 결정론적이라 같은 파일은 언제나 같은 id를 만들어 내므로, 두 어노테이터의 라벨은 별도의 정렬 단계 없이 그대로 맞물립니다.

화자 배정은 따로 확인해 볼 값어치가 있습니다. 두 어노테이터가 차례의 15%에서 누가 말하는지를 두고 갈린다면 그것은 오디오에 대한 신호이고, 자동 화자 분리였다면 최소한 그만큼은 아무 말 없이 틀렸으리라는 뜻입니다.

지표는 주석자 간 일치도를, 강조 표시 쪽은 스팬 및 구조화 출력의 일치도를 보십시오. 후자는 어노테이터가 라벨뿐 아니라 경계도 고르기 때문에 다른 처리가 필요합니다.

6단계: 내보내기

표준 JSON, JSONL, CSV는 평소대로 됩니다. 시간 정렬을 음성 분석 도구까지 살려 보내고 싶다면 계층 주석을 ELAN이나 Praat로 내보내십시오.

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

Potato가 EAF와 TextGrid를 입력으로도 읽으므로 둘 다 왕복합니다. 여기서 주석하고, ELAN에서 다듬고, 그 결과를 다시 읽어 들이면 됩니다.

잘못되는 네 가지

누군가 .txt를 남겨 두었다. 시간 정보가 없고, 되살릴 수 없고, 다시 돌려야 합니다. --dry-run이 그 위에 뭔가를 쌓기 전에 잡아냅니다.

시간이 1000배 어긋난다. 앞단 어딘가에서 초와 밀리초를 섞었습니다. Whisper와 Deepgram은 실수 초를 내보내고, AssemblyAI와 whisper.cpp 오프셋, Whisper의 TSV는 정수 밀리초를 내보냅니다.

조각 단위 입력에 문장 단위 스킴을 씌웠다. 위에서 다룬 경우이고, 넷 중 가장 비쌉니다. 일치도를 계산하기 전까지는 알아채지 못하기 때문입니다.

아무도 확인하지 않은 화자 분리를 믿었다. 화자 분리 오류 하나는 그 차례에 붙은 모든 라벨로 번지고, 원인을 찾으러 갔을 때는 어노테이터 불일치처럼 보입니다.

더 읽을거리