통화 온도(D-5) — 감정분석을 음성 톤 이상치로 바꾼다

무엇을

D(감정분석)의 판정 입력을 전사 텍스트에서 음성 신호로 바꾼다. 화자별 기준선 대비 톤이 얼마나 튀는지를 규칙으로 판정한다. 근거: _project/decisions/203.

왜

상담사는 베테랑이라 톤이 항상 일정하다. 상담사 발화를 감정 분류해도 거의 전부 「중립」이라 변별력이 없다. 그런데 그 일정함이 약점이 아니라 기준선이다 — 일정한 쪽을 빼면 남는 것이 고객의 톤이고, 그것이 튀는 정도가 신호다.

텍스트는 이미 C-1~C-4(상담원)와 C-6(고객)이 본다. D 까지 텍스트를 보면 같은 입력을 세 번 재고 새 정보가 없다. 비속어 없는 불만은 셋 다 놓친다.

완료 조건


실측 (2026-09-09) — 가설이 반대로 나왔다

.venv/bin/python scripts/measure_call_temperature.py --per-role 200
데이터 seoul-minwon-audio/다산콜DB · 시드 20260909 · 커밋 c9da0a3-dirty

                    발화 안 F0 흔들림(중앙값)   에너지 중앙값   발화 길이 중앙값
  민원인 (n=200)          63.2 Hz              -25.15 dB        5.86 s
  상담사 (n=200)          78.7 Hz              -30.53 dB        8.66 s

  민원인 / 상담사 흔들림 비 = 0.80   ← 가설 2 는 > 1 을 예상했다

상담사가 오히려 더 흔들린다. n=150 과 n=200 에서 같은 값(0.80)이라 표본 잡음이 아니다.

왜 그런가 — 가설이 틀린 게 아니라 데이터에 전제가 없다

이 데이터셋에는 격한 통화가 한 건도 없다. 서울 열린데이터광장이 「56개 시나리오에 대한 음성데이터」라고 명시한 연기된 정상 상담이다. 가설 2 는 “고객이 격해지면 톤이 튄다” 인데, 격해지는 고객이 표본에 없으므로 검증 자체가 성립하지 않는다.

반대로 상담사 쪽이 넓은 것은 설명이 된다 — 응대 억양은 의도적으로 높낮이를 준다. 평탄하게 묻는 민원인보다 폭이 넓은 것이 자연스럽다.

「가설이 기각됐다」가 아니라 「이 데이터로는 못 잰다」다. 둘을 구분하지 않으면 D-5 를 잘못 접게 된다(절대 원칙 10).

그래도 설계 하나는 검증됐다 — 절대 임계값을 쓰면 안 된다

상담사의 기본 억양 폭이 민원인보다 넓다(78.7 vs 63.2 Hz). 절대 임계값을 두면 베테랑 상담사가 통화 내내 걸린다. decisions/203 이 화자별 기준선을 고른 판단이 실측으로 뒷받침됐다 — 이건 데이터가 확인해 준 것이다.

가설 3 은 데이터 구조 때문에 거의 못 쟀다

시나리오당 화자별 발화가 8건을 넘는 경우가 민원인 2개 · 상담사 1개뿐이다. 다산콜DB 는 발화 단위 클립이고 통화 단위가 아니다. 통화 온도는 통화 안에서 기준선을 만드는 것이라 통화 단위 데이터가 없으면 오프라인 검증이 안 된다. 실시간 데모에서는 동작하지만 수치로 증명할 수 없다 — 이 한계를 지우지 않는다.


도구를 만들다가 도구의 오류를 잰 적이 있다

첫 측정에서 발화 하나 안의 F0 폭이 100Hz 로 나왔다. 사람이 한 문장 안에서 그렇게 오르내리지 않는다 — 8kHz 협대역에서 자기상관이 2·3배 배음에 걸린 것이었다. 억양을 잰 게 아니라 추정기의 오류를 재고 있었다.

옥타브 보정을 넣었더니 63~79Hz 로 내려왔다. 그런데 첫 보정판이 더 나빴다 — 「기준 대비 1.6배 창을 벗어나면 되돌린다」로 썼는데 창 폭이 1.6² = 2.56 배라 옥타브 간격(2배)보다 넓어서 어떤 값이든 ×2 나 ÷2 중 하나가 창 안에 들어왔다. 결과적으로 진짜 억양의 극단값까지 스냅됐고, 우리가 재려는 것이 바로 그 억양 폭이라 측정 도구가 측정 대상을 깎고 있었다. 「확실히 더 가까워질 때만 되돌린다」로 바꿨다.

테스트가 설계 결함 둘을 잡았다

남은 것


2026-09-21 — 하네스 배선

ai/apps/evaluation/harness.py 가 D-5 를 call_temperature 섹션으로 낸다. 접점은 새 hub 포트 VoiceOutlierPort.judge(call_id, speaker, [(segment_id, 오디오 경로)]) → VoiceOutlierVerdict(judged, outliers) (server/apps/hub/app/ports/output/voice_outlier_port.py). 골든셋에는 선택 필드 call_temperature (통화 1건·화자 1명 — 발화별 오디오 경로 + 튀어야 할/차분한 segment_id 라벨, 형식은 golden-set/README.md)를 뒀고 기존 156건에는 없다. eval_run_repository._MODULE_ID 에 call_temperature → D-5 를 더했다 — 16자라 매핑이 없으면 --record 가 VARCHAR(10) 을 넘친다.

ES 없이 돌린 리포트 원문:

[call_temperature]
  측정 불가 — 골든셋에 채점 대상이 없다        ← 포트를 꽂았을 때(NO_SAMPLES)
  측정 불가 — 모듈 미구현                      ← Ports() 빈 채(NOT_IMPLEMENTED)

둘을 갈라 찍는 것이 이 배선의 목적이다(절대 원칙 10). 채점 값은 손으로 만든 케이스 2건으로만 확인했다 (테스트 test_D5_케이스가_있으면_골든셋에서_채점기까지_흐른다) — 실제 수치가 아니다.

⚠ 어댑터가 없다. voice_signal/adapter/outbound/ 에는 기록 어댑터와 wav_reader 뿐이라 run_eval.py 에 꽂을 VoiceOutlierPort 구현체가 없다. 실제 run_eval.py 리포트는 그래서 아직 「미구현」으로 나온다. 오디오 → 특징 → segment_outliers 어댑터는 음성 골든셋(통화 단위 + 톤 라벨)이 생길 때 같이 만든다.


2026-09-21 — 어댑터

VoiceOutlierPort 구현체 ai/apps/voice_signal/adapter/outbound/wav_voice_outlier_adapter.py(WavVoiceOutlierAdapter). (segment_id, WAV 경로) → read_mono → extract → F0 중앙값 → segment_outliers — scripts/measure_call_temperature.py 가설 3 이 잇던 순서 그대로이고 판정 로직·임계값·특징 추출은 손대지 않았다. voiced_ratio 가 낮거나 F0 를 못 잡은 발화는 nan 으로 넘겨 도메인이 기준선·판정에서 빼게 한다(「값 없음」이지 「튀지 않음」이 아니다). 기준선을 못 만들면 judged=False, 파일이 없거나 못 읽으면 예외를 그대로 올린다(절대 원칙 10). scripts/run_eval.py 의 build_ports 양쪽 분기에 voice_outlier= 로 꽂았다. 테스트 5건(tests/adapter/test_wav_voice_outlier_adapter.py) — 합성 톤 wav 로 도메인 함수 결과와 일치·발화 부족·무음·없는 파일.

ELASTICSEARCH_URL=http://localhost:9200 .venv/bin/python scripts/run_eval.py --runs 1 --no-ner --retriever bm25 원문:

[call_temperature]
  측정 불가 — 골든셋에 채점 대상이 없다

「모듈 미구현」에서 「표본 없음」으로 바뀌었고 나머지 모듈은 그대로다(retrieval 0.833/0.666 n 96 · masking 누락 0 n 28 · closure_gate 1.0 n 99 · no_answer n 24 · compliance 1.0/1.0 · call_guard 1.0/1.0).

한계 — ① 음성 골든셋이 0건이라 채점 수치는 없다. 통화 단위 + 톤 라벨이 생겨야 한다(golden-set/README.md 형식). ② 실제 통화 음성이 이 어댑터로 흘러오는 서버 경로는 없다 — server/ 는 텍스트만 받는다(voice_outlier_recorder.py 머리말과 같은 상태). 이 어댑터가 부르는 곳은 평가 하네스뿐이다. ③ 특징 축은 F0 중앙값 하나다 — 에너지 축은 측정 스크립트가 가설 3 에서 쓰지 않았으므로 여기서도 더하지 않았다. 축을 더하려면 도메인이 먼저 정해야 한다.

2026-09-21 — 닫음 (내 몫 끝)

판정 규칙·화자 기준선·실측(가설 1~3)·하네스 배선·어댑터까지 끝났다. 남은 둘은 이 티켓 안에서 할 수 없는 것이다 — ① 음성 골든셋 0건: 통화 단위 + 톤 라벨이 필요한데 다산콜DB 는 발화 단위 클립이고 AI Hub 71479 도 단일 화자 과제 녹음이라 출처가 없다. 출처가 생기면 새 티켓으로 채점한다(케이스 형식은 golden-set/README.md) ② 용어 확정은 팀. D-5 는 시연에선 동작하고 수치로는 「측정 불가 — 표본 없음」 이다(절대 원칙 10).