전편의 파이프라인은 통화 한 건을 전사해 분류 후보까지 만들었습니다. 운영은 다른 질문을 던집니다 — "이 전사, 얼마나 틀리는가?" 오류율을 모르는 전사 위에 세운 분류는 모래 위의 집입니다. 그리고 전사와 화자 분리를 따로 돌리면 "무슨 말이 있었는지"는 알아도 "누가 한 말인지" 는 모릅니다. 신고자의 "불이 났어요"와 접수자의 "불이 난 게 맞습니까?"가 같은 무게로 분류에 들어갑니다.
심화 4단
| 단계 | 무엇을 | 전편과의 차이 |
|---|---|---|
| 1 화자 결합 | 발화마다 화자 라벨 | 전사·화자분리를 결합해 한 스크립트로 |
| 2 용어 교정 | 소방 용어 오전사 수정 | 전사 전 힌트 + 전사 후 교정표, 두 겹 |
| 3 품질 측정 | 골든셋 CER | "감"이 아니라 오류율 숫자로 |
| 4 신뢰도 라우팅 | 낮은 신뢰 통화 분리 | 전부 자동이 아니라 재청취 큐로 |
1. 화자 결합 — "누가 말했나"가 붙은 스크립트
전편에서 따로 돌리던 faster-whisper(전사)와 pyannote(화자)를 WhisperX가 한 흐름으로 묶습니다. 정렬(alignment)로 단어 단위 타임스탬프를 정밀하게 만든 뒤, 그 위에 화자 구간을 겹쳐 발화마다 화자를 배정합니다. 추론은 전부 로컬입니다.
import whisperx device = "cpu" # 전편과 같은 로컬 원칙 audio = whisperx.load_audio("call_001.wav") # 1) 전사 (내부는 faster-whisper) model = whisperx.load_model("large-v3", device=device, compute_type="int8", language="ko") result = model.transcribe(audio, batch_size=4) # 2) 정렬 — 단어 단위 타임스탬프 model_a, meta = whisperx.load_align_model(language_code="ko", device=device) result = whisperx.align(result["segments"], model_a, meta, audio, device=device) # 3) 화자 분리 결합 — 신고 통화는 보통 2인(접수자·신고자) diarize = whisperx.diarize.DiarizationPipeline(token="hf_...", device=device) segments = diarize(audio, min_speakers=2, max_speakers=2) result = whisperx.assign_word_speakers(segments, result) for s in result["segments"]: print(f'[{s.get("speaker", "?")}] {s["text"].strip()}')
이제 분류 프롬프트에 신고자 발화만 골라 넣을 수 있습니다 — 접수자의 확인 질문이 유형 판단을 오염시키지 않습니다. 화자 라벨(SPEAKER_00/01) 중 누가 접수자인지는 첫 발화("119입니다…")로 규칙 판별하면 충분합니다.
2. 용어 교정 — 두 겹
일반 음성 모델은 소방 용어에 약합니다. "선착대"가 "선착 대기"로, "요구조자"가 "요 구조자"로 갈라지는 식입니다. 두 겹으로 잡습니다.
- 전사 전 — 용어 힌트: 전사 시작 프롬프트에 도메인 용어를 심어 모델이 그 표기를 선호하게 합니다(
initial_prompt계열 옵션 — 전편의 faster-whispertranscribe에도 있습니다). - 전사 후 — 교정표: 자주 틀리는 표기를 팀이 관리하는 교정표로 치환합니다. 교정표 자체가 자산입니다.
import re # 팀이 관리하는 교정표 — 재청취에서 발견될 때마다 한 줄씩 자란다 FIX = { r"선착\s*대기": "선착대", r"요\s*구조자": "요구조자", r"공기\s*호흡기": "공기호흡기", } def fix_terms(text: str) -> str: for pat, to in FIX.items(): text = re.sub(pat, to, text) return text
3. 품질 측정 — 골든셋 CER
전사 품질은 감상이 아니라 숫자로 관리합니다. 합성·익명 통화 20건을 골라 사람이 정확히 받아쓴 골든 전사를 만들고, 파이프라인 출력과 비교해 오류율을 잽니다. 띄어쓰기가 유동적인 한국어는 단어 오류율(WER)보다 글자 오류율(CER) 이 정직합니다.
import pandas as pd from jiwer import cer # pip install jiwer pairs = load_goldenset() # [{"ref": 사람 전사, "hyp": 파이프라인 전사, "잡음": "높음"}, ...] for p in pairs: p["cer"] = cer(p["ref"], fix_terms(p["hyp"])) # 전체 평균보다 '조건별' 평균이 운영 정보다 report = pd.DataFrame(pairs).groupby("잡음")["cer"].mean()
| 보는 법 | 의미 |
|---|---|
| 전체 CER 추세 | 모델·설정을 바꿀 때마다 회귀 비교(오르면 되돌린다) |
| 잡음·통화품질별 CER | 어떤 통화에서 취약한가 — 사이렌·바람·차량 소음 구간 |
| 교정표 적용 전후 CER | 교정표가 실제로 효과 있는지 검증 |
4. 신뢰도 라우팅 — 전부 자동이 아니라, 자신 없는 것만 사람에게
전사 모델은 자신 없음을 숫자로 남깁니다. 전편의 faster-whisper 세그먼트에는 평균 로그확률(avg_logprob)과 무음성 확률(no_speech_prob)이 붙어 있습니다. 이 값이 나쁜 통화를 분류로 보내지 말고 사람 재청취 큐로 보냅니다.
def route(segments) -> str: """전사 신뢰도 기반 분기 — 임계값은 자기 골든셋 CER과 맞춰 조정한다.""" bad = [s for s in segments if s.avg_logprob < -1.0 or s.no_speech_prob > 0.6] if len(bad) / max(len(segments), 1) > 0.3: return "재청취" # 사람이 듣고 직접 처리 return "자동분류" # 비식별 후 전편의 분류 단계로
낮은 신뢰 구간이 많은 통화는 대개 잡음·겹침말·아주 짧은 발화입니다 — 바로 CER가 가장 나쁘게 나오는 조건들이고, 자동 분류가 가장 위험한 통화들입니다. 라우팅은 그 위험을 사람 쪽으로 되돌리는 장치입니다.
운영 루틴 한 장
| 언제 | 무엇을 | 통과 기준 |
|---|---|---|
| 모델·설정 변경 시 | 골든셋 CER 회귀 | 이전 대비 오르지 않음 |
| 매주 | 재청취 큐 처리·원인 태깅 | 큐 비움 + 교정표/골든셋 보강 |
| 매월 | 조건별 CER 리포트 | 취약 조건이 개선 추세 |
| 상시 | 화자 결합 스크립트 점검 | 접수자/신고자 뒤바뀜 사례 기록 |
사람 검토 체크리스트
- [ ] 전사·정렬·화자분리 전 과정이 로컬에서 돌았습니다(외부 전송 없음). - [ ] 골든셋은 익명화·합성 통화로만 만들었습니다. - [ ] CER를 조건별(잡음·통화품질)로 나눠 보고 취약 조건을 파악했습니다. - [ ] 모델·설정 변경 시 골든셋 CER 회귀를 돌려 이전과 비교했습니다. - [ ] 신뢰도 낮은 통화가 자동분류로 새지 않고 재청취 큐로 갑니다. - [ ] 화자 라벨(접수자/신고자) 뒤바뀜을 사람이 표본 점검합니다.