동시다발 신고 상황에서 통화 내용을 빠르게 유형·긴급도로 가르면 접수 부담이 줄어듭니다. 하지만 신고 음성과 그 전사 텍스트는 119 데이터 중 가장 민감합니다 — 신고자 목소리·이름·전화·위치·환자 상태가 그대로 담깁니다. 이 안내서는 음성을 전 과정 로컬에서 전사·비식별·분류하는 파이프라인이며, 분류 결과는 자동 확정이 아니라 사람 배정용 후보입니다.
- 음성·전사·비식별문·분류 후보의 민감도와 이동 경계를 구분할 수 있습니다.
- 로컬 전사와 선택적 화자 분리의 역할을 설명할 수 있습니다.
- 비식별 뒤 허용 라벨과 근거 문장으로 분류 결과를 검산할 수 있습니다.
- 불확실한 전사와 분류를 사람 재청취·확인 대상으로 라우팅할 수 있습니다.
왜 전 과정을 로컬에서 하나
신고 음성을 외부 전사 API나 클라우드에 올리는 일은 여권 원본을 잠깐 맡기는 것과 같습니다. 돌려받더라도 그 사이 어디에 복사본이 남았는지, 로그에 무엇이 찍혔는지 우리는 알 수 없습니다. 음성 한 파일에는 신고자의 목소리·이름·전화·위치·환자 상태가 통째로 들어 있어, 통제권을 한 번 넘기면 되돌릴 수 없습니다.
그래서 전사·비식별·분류는 승인된 로컬 환경을 기본으로 둡니다. 외부 처리가 꼭 필요하면 비식별에 더해 개인정보·보안 담당자의 별도 승인을 먼저 받아야 하며, 비식별만으로 외부 전송 권한이 생기지 않습니다. 여기서 "로컬"의 뜻은 음성 파일이 우리 기기 밖으로 나가지 않는다는 것입니다.
예전에는 쓸 만한 음성 인식이 서버급 장비를 요구해 로컬 처리가 비현실적이었습니다. 지금은 모델을 가볍게 만드는 양자화(아래 코드의 compute_type="int8")와 faster-whisper 같은 최적화 덕분에, 사무실 CPU에서도 한국어 전사를 돌릴 수 있습니다. 민감 데이터를 지키려고 성능을 통째로 포기하지 않아도 된다는 뜻입니다.
동작 원리
파이프라인은 한 모델이 통화를 이해해 결론을 내는 구조가 아닙니다. 전사 모델은 음성을 짧은 시간 구간의 텍스트 후보로 바꾸고, 화자 분리는 같은 시간축에 누가 말했는지 구간 후보를 붙입니다. 두 결과를 시간으로 맞추면 발화별 텍스트와 화자 표지가 생깁니다. 화자 분리를 생략하면 전사 자체는 가능하지만 접수자의 질문과 신고자의 진술을 별도 신호로 다루기 어렵습니다.
전사 직후 텍스트는 음성과 같은 민감 자료로 봅니다. 비식별 단계는 문자열 패턴과 문맥 검토로 신원 식별 값을 가리고, 분류에 필요한 사건 유형 표현만 남기려 합니다. 이때 마스킹 전·후를 공개 로그에 함께 남기지 않고, 자동 도구가 놓친 이름·주소는 사람이 최종 확인합니다.
분류 단계는 비식별된 텍스트를 고정 라벨 집합에 매핑하고, 선택 근거가 된 문장을 함께 냅니다. 아래 최소 코드는 결과 수와 입력 수, type 허용집합만 확인합니다. urgency 허용집합과 근거 필드의 존재·내용 검사는 별도로 구현하고, 근거가 실제 발화 의미를 지지하는지는 사람이 봐야 합니다. 따라서 각 단계의 출력과 실패 상태를 다음 단계 입력과 섞지 않고, 불확실하면 확인필요 경로로 보냅니다.
가상 상황: 새벽 3시, 접수석 세 자리에 벨이 일곱 통
가상의 상황을 하나 들어 보겠습니다. 건조한 겨울 새벽, 한 지역에 신고가 몰립니다. 접수석은 세 자리인데 벨은 일곱 통이 겹쳐 울립니다. 요원들은 통화 하나하나를 끝까지 듣고 유형과 긴급도를 가릴 시간이 없습니다.
이럴 때 녹음된 통화를 파이프라인에 넣으면, 로컬에서 전사 → 비식별 → 분류 후보가 흘러나옵니다. 요원은 "긴급도 높음"과 "확인필요"로 표시된 건부터 눈을 줍니다. 파이프라인은 통화를 대신 판단하지 않습니다 — 어디부터 사람이 봐야 하는지 순서를 제안할 뿐입니다. 실제 배정과 우선순위는 끝까지 사람이 정합니다.
가상 사례: 품질 담당자의 합성 통화 전사 검수
다음은 접수 폭주 사례와 다른 가상 사례입니다. 품질 담당자가 전사 모델을 바꾼 뒤 소방 용어와 겹침 발화가 어떻게 달라졌는지 확인하려 합니다. 입력은 실제 신고가 아닌 교육용 합성 음성, 사람이 미리 작성한 기준 전사, 허용된 분류 라벨입니다. 목적은 자동 배정이 아니라 오류 유형을 찾는 것입니다.
담당자는 음성별 전사 차이를 용어 오류, 숫자 오류, 겹침 발화 누락, 빈 구간으로 나눕니다. 비식별 단계도 함께 돌려 합성 이름·주소가 가려지는지 보지만, 합성값이므로 곧바로 실통화 안전성을 입증한다고 쓰지 않습니다. 분류 후보가 달라진 경우에는 근거 문장을 기준 전사와 대조하고, 전사 오류가 원인인지 분류 규칙이 원인인지 분리합니다.
실패 가능성은 전체 문장이 그럴듯하다는 이유로 숫자·지명 오류를 놓치는 것입니다. 사람은 오류가 난 시간 구간의 합성 음성을 다시 듣고, 모델 변경 전후 결과와 검토 기록을 남깁니다. 실제 자료 적용 여부는 별도 승인과 안전 검토 대상으로 남습니다.
파이프라인 4단
| 단계 | 무엇을 | 도구 | 데이터 위치 |
|---|---|---|---|
| 1 전사 | 음성 → 텍스트 | faster-whisper | 로컬 |
| 2 화자 분리(선택) | 신고자/접수자 구분 | pyannote | 로컬 |
| 3 비식별 | 전사에서 개인정보 제거 | PII 가드 | 로컬 |
| 4 분류 | 유형·긴급도 후보 | 프롬프트 + 검산 | 승인된 로컬 환경; 외부 전송은 별도 승인 |
선택지와 트레이드오프
모든 통화에 같은 처리를 강제하기보다 품질·민감도·운영 목적에 맞춰 단계를 고릅니다.
| 선택지 | 맞는 상황 | 장점 | 주의할 점 |
|---|---|---|---|
| 로컬 전사만 수행 | 텍스트 초안과 시간 구간만 필요할 때 | 구성 요소와 데이터 이동을 줄임 | 접수자 질문과 신고자 진술이 섞여 분류를 흐릴 수 있음 |
| 로컬 전사 + 화자 분리 | 화자별 발화를 나눠 검토해야 할 때 | 신고자 발화 후보를 별도로 볼 수 있음 | 화자 바뀜·겹침 오류를 사람이 확인해야 함 |
| 규칙 기반 키워드 후보 | 라벨 규칙이 좁고 생성형 출력이 필요 없을 때 | 동작과 근거를 비교적 직접 설명 가능 | 표현 변화와 문맥을 놓쳐 확인필요가 늘 수 있음 |
| 비식별 후 제한된 분류 프롬프트 | 다양한 표현을 고정 라벨로 묶어야 할 때 | 근거 문장과 후보를 정해진 형식으로 요청 가능 | 비식별 미탐·환각·잘못된 근거를 사람 검토해야 함 |
1. 로컬 전사 — faster-whisper
한국어 음성을 로컬 CPU에서 전사합니다. 외부로 나가는 요청이 없습니다.
from faster_whisper import WhisperModel # CPU + INT8 양자화 (메모리 적게, 외부 전송 없음) model = WhisperModel("large-v3", device="cpu", compute_type="int8") # language="ko"로 한국어 고정, 타임스탬프 포함 segments, info = model.transcribe("call_001.wav", language="ko", beam_size=5) lines = [f"[{s.start:.1f}-{s.end:.1f}] {s.text.strip()}" for s in segments] transcript = "\n".join(lines) # 전사는 여기서부터 '민감 데이터'로 취급
전사 모델이 소리를 글자로 옮기는 원리는 받아쓰기 시험과 닮았습니다. 음성 파형을 짧은 구간으로 잘라 각 구간이 어떤 소리인지 확률로 가늠하고, 그 위에서 "이어질 법한 한국어"를 골라 문장으로 잇습니다. 코드의 beam_size=5는 여러 후보 문장을 동시에 저울질하는 폭으로, 값이 클수록 그럴듯한 경로를 더 넓게 살펴 오전사를 줄이는 대신 느려집니다. 완벽하지는 않습니다 — 소방 용어나 주소를 엉뚱하게 받아쓰기도 하는데, 그 교정은 심화편에서 다룹니다.
2. 화자 분리 (선택) — 신고자 발화만 추리기
접수자·신고자가 섞인 통화를 화자별로 나누면, 분류에 신고자 발화만 써서 노이즈를 줄일 수 있습니다. pyannote는 모델 사용 약관 동의 후 받은 토큰이 필요하지만 추론은 로컬에서 돕니다.
from pyannote.audio import Pipeline pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1", use_auth_token="hf_...") # 로컬 추론 diarization = pipeline("call_001.wav") for turn, _, speaker in diarization.itertracks(yield_label=True): print(f"{turn.start:.1f}-{turn.end:.1f} {speaker}")
왜 굳이 화자를 나눌까요. 분류에 통화 전체를 넣으면 접수자의 확인 질문("불이 난 게 맞습니까?")이 신고자의 진술과 같은 무게로 섞여 유형 판단을 흐립니다. 신고자 발화만 골라 넣으면 신호가 또렷해집니다. 전사·정렬·화자 분리를 한 흐름으로 묶어 발화마다 화자를 붙이는 운영 수준 기법은 신고 전사 심화, 화자 결합과 품질 관리에서 이어집니다.
3. 전사 비식별 — 분류에 넣기 전
전사 텍스트에는 이름·전화·상세주소가 평문으로 남습니다. 외부 처리가 별도로 승인된 경우에도 LLM 분류에 넣기 전 반드시 비식별합니다.
→ 개인정보, AI에 넣기 전에 점검하기의 3단 점검(정규식 → 문맥 → 사람)을 전사 텍스트에 그대로 적용하세요. 자동 마스킹 뒤에도 이름·상세주소는 사람이 최종 확인합니다.
비식별이 실제로 무엇을 바꾸는지 가상·합성 예시로 보겠습니다. 아래 값은 모두 지어낸 합성값입니다 — 전사 직후의 원문은 이렇습니다.
[비식별 전 — 전부 합성 예시] [6.0-9.5] 저는 김하늘이고요 한빛로 12길 34, 301호인데 베란다에서 연기가 나요
여기서 이름·주소·호수를 가리면 분류에 넣을 형태가 됩니다.
[비식별 후] [6.0-9.5] 저는 [이름]이고요 [주소]인데 베란다에서 연기가 나요
유형·긴급도를 가르는 데 필요한 정보("베란다에서 연기가 나요")는 남고, 신원을 특정하는 값만 빠졌습니다. 자동 마스킹은 한국어 이름·상세주소를 놓칠 수 있으니, 위 3단 점검의 마지막 "사람 확인"은 생략하지 않습니다.
4. 유형·긴급도 분류 — 후보를 만들되 단정하지 않기
분류는 고정 라벨셋 안에서만 고르고, 모호하면 "확인필요"로 빼고, 각 분류에 근거 문장을 강제합니다.
아래는 비식별된 신고 전사입니다. 유형은 [화재, 구조, 구급, 기타] 중에서만 고르세요. 긴급도는 [높음, 보통, 낮음] 중에서만 고르세요. 판단 근거가 된 전사 문장을 그대로 인용해 함께 적으세요. 근거가 약하면 유형/긴급도를 "확인필요"로 두세요. 추측해서 채우지 마세요.
분류가 끝나면 먼저 결과 수가 입력과 맞는지, type이 허용집합 안인지 검산합니다.
ALLOWED_TYPE = {"화재", "구조", "구급", "기타", "확인필요"}
ALLOWED_URGENCY = {"높음", "보통", "낮음", "확인필요"}
assert len(results) == len(transcripts), "검산 실패: 분류 수 ≠ 입력 수"
assert all(r["type"] in ALLOWED_TYPE for r in results), "허용 라벨 밖의 type이 있습니다"
assert all(r.get("urgency") in ALLOWED_URGENCY for r in results), "허용 라벨 밖의 urgency가 있습니다"
assert all(r.get("evidence") and isinstance(r["evidence"], str) for r in results), "근거 필드가 비어 있거나 문자열이 아닙니다"
현재 네 assert는 필드의 존재와 기본 형식만 검사합니다. 운영 검산에는 추가로 근거 문장이 실제로 비식별 전사에 있는지, 그 문장이 분류를 논리적으로 지지하는지를 사람이 검토해야 합니다. 이 검사를 통과해도 근거의 의미가 분류를 실제로 뒷받침하는지는 담당자가 확인합니다.
같은 비식별 전사에 이 프롬프트를 태우면, 후보는 이런 모양으로 나옵니다(가상 예시).
유형: 화재 긴급도: 높음 근거: "베란다에서 연기가 나요" 확인필요: 정확한 동·호수 — 비식별로 지워졌으니 접수자가 통화로 재확인
근거 문장을 요구했기 때문에 요원은 "왜 화재로 분류됐는지"를 한눈에 보고, 동의하지 않으면 즉시 바꿉니다. 다만 현재 최소 코드에는 근거 누락 검사와 라우팅이 없으므로, 이를 별도로 구현한 뒤에만 근거가 비었거나 확인필요인 건을 자동 배정 대상에서 빼 사람에게 올릴 수 있습니다.
자주 하는 실수와 한계
- 오전사를 사실로 믿기. 전사 모델은 소방 용어·지명·숫자를 잘못 받아쓸 수 있습니다("삼 동"↔"산 동"). 기록·분류의 근거로 삼기 전, 신뢰도 낮은 통화는 원음성을 다시 듣습니다.
- "확인필요"를 흘려보내기. 근거가 약한 건을 자동으로 채우면, 긴급 신고가 조용히 후순위로 밀릴 수 있습니다. "확인필요"는 통과가 아니라 사람에게 올리라는 신호입니다.
- 짧거나 겹치는 통화에 과신. 두 사람이 동시에 말하거나 통화가 몇 초뿐이면 전사와 화자 분리가 함께 흔들립니다. 이런 통화를 신뢰도로 걸러 사람 재청취로 돌리는 방법은 심화편에서 다룹니다.
- 실통화로 연습. 성능이 궁금해도 실제 신고 음성으로 시험하지 않습니다 — 119 합성 데이터로 안전하게 연습하기의 합성·익명 통화만 씁니다.
이 파이프라인을 오류율(CER) 측정·화자 결합·신뢰도 라우팅까지 운영 수준으로 끌어올리는 이야기는 신고 전사 심화, 화자 결합과 품질 관리로 이어집니다.
사람 검토 체크리스트
- [ ] 음성·전사를 외부 전사 서비스/클라우드에 올리지 않았습니다. - [ ] 분류에 넣기 전 전사에서 개인정보를 비식별했습니다. - [ ] 분류 수가 입력 수와 일치하고, 라벨이 허용집합 안입니다. - [ ] "확인필요"로 빠진 건은 사람이 직접 분류했습니다. - [ ] 연습은 익명화/합성 통화로만 했습니다.
문제 해결
| 증상 | 가능한 원인 | 처방 | 확인 |
|---|---|---|---|
| 전사 일부가 비거나 반복됨 | 음질·겹침 발화·구간 분할 문제 | 해당 시간 구간을 실패 후보로 남기고 사람이 재청취 | 원음성 시간축과 전사 구간의 누락·중복 확인 |
| 접수자 질문이 신고자 진술로 분류됨 | 화자 분리 오류 또는 전체 전사를 그대로 사용 | 화자 구간을 재확인하고 불확실 발화는 혼합 상태로 유지 | 화자별 샘플을 사람이 듣고 표지와 비교 |
| 비식별 뒤 이름·주소 일부가 남음 | 자동 패턴이 한국어 문맥형 정보를 놓침 | 외부 분류를 멈추고 문맥 점검과 사람 확인으로 되돌림 | 승인된 합성 예시와 별도 검토에서 미탐 여부 확인 |
| 분류 결과 수가 입력보다 적음 | 빈 전사·파싱 실패·응답 누락 | 누락 ID를 확인필요로 분리하고 자동 배정 금지 | 입력 ID와 결과 ID를 양방향 대조 |
자주 묻는 질문
Q. 로컬에서 전사하면 개인정보 검토는 필요 없나요?
로컬 처리는 외부 전송 경계를 줄이는 선택입니다. 전사 텍스트 자체는 여전히 민감하므로 접근·저장·비식별·사람 확인이 필요합니다.
Q. 화자 분리가 틀리면 전사도 다시 해야 하나요?
먼저 전사 텍스트 오류인지 화자 시간 구간 오류인지 분리합니다. 텍스트가 맞다면 화자 정렬만 검토할 수 있고, 둘 다 흔들리면 해당 구간을 재청취 대상으로 보냅니다.
Q. 비식별로 주소가 가려지면 분류 근거가 부족하지 않나요?
유형 분류에 필요한 사건 표현과 신원 식별 값은 구분해야 합니다. 필요한 맥락까지 사라졌다면 추측으로 채우지 않고 확인필요로 남겨 접수자가 원통화에서 확인합니다.
Q. 긴급도 후보가 높음이면 먼저 자동 배정해도 되나요?
이 결과는 사람 검토 순서를 돕는 후보입니다. 전사·화자·분류 오류가 남으므로 출동 자원 배정과 우선순위는 담당자가 원음성과 업무 기준으로 결정합니다.
핵심 정리
- 음성, 원전사, 비식별문, 분류 후보는 단계별 민감도와 사용 범위가 다릅니다.
- 화자 분리는 선택 단계이며 오류가 있으면 신고자·접수자 발화가 뒤섞일 수 있습니다.
- 현재 최소 코드는 결과 수와
type허용집합만 확인하며,urgency·근거 필드 검사는 별도로 구현하고 근거 의미와 비식별 미탐은 사람이 확인합니다. - 불확실한 통화는 자동 확정하지 않고
확인필요와 재청취 경로로 보냅니다.
다음 단계
- 개인정보, AI에 넣기 전에 점검하기 — 원전사에서 이름·전화·상세주소를 분류 전에 거르는 기본 관문입니다.
- 신고 전사 심화, 화자 결합과 품질 관리 — 전사·화자 시간축 결합과 오류율·신뢰도 라우팅으로 확장합니다.
- AI 결과, 제출 전 1분 점검 — 분류 후보를 공유하거나 보고할 때 근거와 공식 판단 경계를 최종 확인합니다.