A-5 스파이크 — 어눌한 한국어 STT 정확도를 잰다
무엇을
구현이 아니라 “잴 수 있는가”를 먼저 확인하는 스파이크다. 외국인이 어눌한 한국어로 말할 때 상용 STT 가 얼마나 알아듣는지를 숙련도 등급별로 잰다.
⚠ 2026-08-28 ID 정정:
A-3→A-5. 기획서의A-3은 이미 「브라우저 실시간 전달(WebSocket)」이고A-4는 발화 구간 검출이다.decisions/201초안이 같은 번호를 통번역에 붙여 한 ID 가 두 뜻을 갖게 됐다 — 추적성 규칙(rfp-harness §1.3)이 깨진다. 코드 주석에는# Requirement: A-5를 쓴다. 근거:_project/plan.mdrev.5 요약.
범위 — ⓑ 어눌한 한국어만 (2026-08-28 확정)
_project/decisions/201 부록 A. 두 시나리오 중 ⓑ 만 1차 범위다.
| ⓐ 고객이 모국어로 말함 | ⓑ 고객이 어눌한 한국어로 말함 | |
|---|---|---|
| 상용 API | 대체로 됨 | 가장 못하는 지점 |
| 평가 데이터 | ❌ 못 찾았다 | ✅ AI Hub 505 · 71479 |
ⓐ 와 상담원→고객 TTS 는 폐기가 아니라 여유 시 확장이다.
데이터 — 확보 경로가 있다
| 데이터셋 | 규모 | 비고 |
|---|---|---|
AI Hub dataSetSn=505 |
4,302시간 | 베트남·영어·일본·중국·태국어 모어 화자 |
AI Hub dataSetSn=71479 |
1,500시간 | 숙련도 4등급 라벨 + 발음오류 태깅 |
전량이 필요하지 않다. 학습이 아니라 측정이 목적이므로 정답 전사가 붙은 발화 수십~수백 건이면 WER/CER 이 나온다. 등급별로 고르게 뽑는 것이 규모보다 중요하다.
⚠ 반드시 함께 적을 한계
- 전화 음성이 아니다. 이 데이터는 44.1kHz 스튜디오/온라인이고, 우리 실제 통화는 8kHz 협대역이다(실측: 다산콜DB 8000Hz/2ch · 저품질전화 8000Hz/1ch). 8kHz 로 다운샘플링해서 잰다.
- 민원 대화가 아니다 — 낭독·질문답변이라 어휘가 다르다.
- 그래서 이 수치는 상한이다. 실제 통화에서는 더 나쁘다. 이 문장 없이 숫자만 옮기면 절대 원칙 10 위반이다.
완료 조건
- AI Hub 71479 신청·수령 — 2026-09-21 받았다(Validation 라벨 3 + 발화 원천 1, 20 GB). 505 는 안 받았다(중·일어 화자가 필요해지면)
- 8kHz 다운샘플링 후 숙련도 등급별로 표본을 뽑았다 (등급당 최소 20건) — 2026-09-21, 등급당 20건 · 3~6초 창 · 화자당 최대 2건 · 시드 20260921(아래)
Chirp 3 로Google STT v1 기본 모델(ko-KR)로 WER/CER 을 등급별로 냈다. 전체 평균 하나로 뭉개지 않는다 — 2026-09-21. ⚠ Chirp 3 가 아니다 —transcribe_batch.py의 v1recognize호출을 그대로 썼다(09-09 8 kHz 실험과 같은 호출이라 비교가 된다). Chirp 3 로 다시 재려면 별도 예산이 든다- 같은 표본을 원본으로도 재서 8kHz 가 얼마나 깎아먹는지 분리했다 (2026-09-09, 아래)
- 수치에 측정일·커밋·명령·표본 수 네 가지가 붙어 있다(§5) — 아래 09-21 절 ·
jekyll/assets/a5-wer-2026-09-21.json - “이 수치는 상한”이라는 한계가 결과 옆에 적혀 있다 — 스크립트 출력·JSON
limits·아래 절 세 곳 - 왕복 지연(STT→번역→화면)을 실측해 예산표를 다시 만들었다. 목표치를 지어내지 않는다 — 미착수(09-21 현재). 번역 단계가 아직 없어 잴 파이프라인이 없다
- 결과가 나쁘면 A-5 를 코어에서 뺄지 팀에 알린다 — 스파이크의 목적이 이 판단이다 — 판단 보류(09-21). 수치는 나왔지만(아래) 상한이고 민원 어휘가 아니라, 팀이 보고 정한다
참고 — API 를 써도 데이터는 필요하다
Chirp 3 가 85개 이상 언어를 지원하므로 학습·파인튜닝은 불필요하다. 그러나 측정에는 필요하다 — 정답 전사가 붙은 외국인 화자 음성이 없으면 A-5 가 되는지 안 되는지 말할 수 없다(절대 원칙 10). API 는 “만들 데이터”를 없애지 “잴 데이터”를 없애지 않는다.
2026-09-09 — 절반은 막혔고 절반은 쟀다
막힌 것 — 외국인 화자 데이터가 없다
AI Hub 505·71479 를 아직 신청하지 않았다. 이 티켓의 본체(숙련도 등급별 WER/CER)는
그 데이터 없이는 한 걸음도 못 나간다. 지금 data/raw/ 에 있는 다섯 데이터셋은 전부
내국인 발화다(ktelspeech · krespspeech · lowquality-phone · minwon-qa · seoul-minwon-audio).
미결 항목의 “외국인 화자 한국어 음성 데이터 0건” 이 그대로 유효하다.
승인에 시간이 걸리므로 신청이 먼저다. 이 티켓을 in-progress 로 두는 이유가 그것이다.
잰 것 — 8kHz 협대역 페널티 (완료 조건 4)
외국인 데이터가 도착해 WER 이 나쁘게 나왔을 때 그것이 「어눌한 한국어라서」인지 「8kHz 로 낮춰서」인지 구분하려면 기준선이 필요하다. 그 교란 요인은 지금 가진 데이터로 먼저 분리할 수 있다.
측정일 2026-09-09 · 커밋 c9da0a3-dirty
.venv/bin/python scripts/measure_bandwidth_penalty.py --limit 20
데이터 aihub-krespspeech/validation (16kHz, 내국인) · 시드 20260909 · 표본 20건 · 오디오 260초
original(16kHz) WER 0.354 · CER 0.150 · n 20
8000Hz WER 0.358 · CER 0.129 · n 20
차이 WER +0.004 · CER −0.021
⚠ 「8kHz 는 안전하다」가 아니라 「이 표본으로는 차이가 보이지 않는다」다. WER 은 8kHz 가 조금 나쁘고 CER 은 조금 좋다 — 방향조차 일정하지 않으므로 n=20 에서 두 조건의 차이를 검출하지 못한 것이다. 표본을 늘리면 달라질 수 있다.
⚠ 다운샘플링만 분리한 값이다. 실제 전화망에는 코덱 압축·잡음·라인 손실이 더해지므로 여기서 나온 값보다 나쁘다. 그대로 「8kHz 페널티」로 옮겨 적지 않는다(절대 원칙 10).
⚠ 44.1kHz 가 아니라 16kHz 를 원본으로 썼다. 티켓 원문은 44.1kHz 를 적었는데, 그건 AI Hub 505·71479 의 사양이고 지금 가진 krespspeech 는 16kHz 다. 데이터가 오면 그때 44.1 → 8 로 다시 잰다.
이번에 한 번 틀렸다 — 도구 버그가 결과처럼 보였다
첫 실행에서 WER 1.115 가 나왔다. 「8kHz 가 이만큼 나쁘다」로 읽힐 수치였지만
측정 도구의 버그였다 — AI Hub 파일이 전부 0001.wav 라(세션 폴더로 구분된다)
다운샘플 사본이 한 파일에 덮여 쓰였고, 캐시가 첫 번째 것을 20번 돌려줬다.
전사 원문을 열어 보고 20건이 전부 같은 문장인 것을 발견해 잡았다.
숫자만 보고 넘어갔으면 기획서에 틀린 수치가 들어갈 뻔했다. narrow_path() 독스트링에
경위를 남겼다(절대 원칙 8).
만든 것
ai/apps/evaluation/metrics/asr.py WER/CER — 규칙 기반, 그룹별 집계 강제
ai/apps/evaluation/tests/test_asr_metrics.py
scripts/measure_bandwidth_penalty.py 협대역 실험 (COST-1 가드 재사용)
aggregate_by_group() 이 그룹별로 나눠 내도록 강제한다 — 이 티켓의 완료 조건이
“전체 평균 하나로 뭉개지 않는다” 이기 때문이다. 데이터가 도착하면 숙련도 등급을 그룹으로
넣기만 하면 된다.
AI Hub 라벨 규약((A-4)/(에이 다시 사) 이중 전사 · n/ 잡음 표시)을 정규화하지 않으면
STT 가 무엇을 뱉든 전부 오류가 된다. 적용한 규칙 5개를 NORMALIZATION_RULES 로 드러내
두었고 리포트에 함께 찍는다 — 정규화는 점수를 올리는 조치라 감추면 안 된다.
비용
STT 사용 520초(오늘). .env 의 STT_MAX_SECONDS_PER_DAY=600 안. 첫 실행이 버그로
260초를 헛되이 썼다 — 재실행분 260초와 합쳐 520초다. data/processed/stt-usage.json 에 누적된다.
다음
- AI Hub 505 또는 71479 신청 ← 여기서 막혀 있다. 승인에 시간이 걸린다
- 수령 후 숙련도 등급별로 표본 추출(등급당 20건 이상) →
aggregate_by_group에 넣는다 - 왕복 지연(STT→번역→화면) 실측 — 아직 안 했다
- 결과가 나쁘면 A-5 를 코어에서 뺄지 팀에 알린다
2026-09-15 — 상태 그대로다
AI Hub 505·71479 신청이 여전히 안 됐다. 신청은 AI Hub 계정으로 사람이 해야 하는 일이라 이번 세션에서 할 수 없었다. 외국인 화자 음성이 0건인 상태에서 이 티켓으로 잴 수 있는 것은 없다(09-09 에 8kHz 페널티 분리만 끝냈다). 데이터가 오기 전에는 「측정 불가 — 표본 없음」이다.
2026-09-21 — AI Hub 71479 파일 목록 확인 (신청 전)
정성윤 님 지시로 다운로드를 맡았다. 로그인 상태에서 「파일 목록 (API 다운로드)」 탭을 읽었다 — 299.78 GB 전체가 아니라 Validation 만 받는다(data/README.md 규칙).
승인은 소개 페이지의 「다운로드」 버튼 → 이용 신청으로 받는다(내국인만 신청 가능 — 본인 계정).
| 파일 | 용량 | filekey | 받나 |
|---|---|---|---|
VL_Speech_lab.zip (Validation 라벨 · 발화) |
15.30 MB | 475451 | 먼저 — 숙련도 등급 필드 확인 |
VL_pronunciation_lab.zip (라벨 · 발음) |
54.33 MB | 475452 | 먼저 |
VL_pronunciation_tag.zip (발음 오류 태깅) |
34.61 MB | 475453 | 먼저 |
VS_Speech_sound.zip (원천 · 발화) |
19.91 GB | 475449 | 라벨 확인 뒤 — WER/CER 은 이쪽 |
VS_pronunciation_sound.zip (원천 · 발음) |
13.17 GB | 475450 | 보류 — 발음 평가용이라 1차 범위 밖 |
| Training 원천 5개 · 라벨 3개 | 266 GB | 475441~475448 | 안 받는다 |
- 분할 압축 병합(사이트 안내 그대로):
find "<폴더>" -name "<파일명>.zip.part*" -print0 | sort -zt'.' -k2V | xargs -0 cat > "<파일명>.zip" - 넣을 곳:
data/raw/aihub-foreign-proficiency-71479/validation/{label,wav}/.505는 별도 폴더. - 주의: 이 데이터셋은 중·일어 모어 화자를 제외한 아시아어 화자다. 중·일어가 필요하면
505를 추가로 본다.
2026-09-21 — 71479 적재 완료 · 라벨 구조 확인
- 위치
data/raw/aihub-foreign-proficiency-71479/validation/— 라벨 3개 zip 은 풀었고(112 MB → 370 MB), 발화 원천VS_Speech_sound.zip(20 GB, wav 22,447개, 풀면 29.7 GB)은 풀지 않았다 — 표본만unzip <zip> <파일>로 꺼낸다(zip 은 임의 접근이 된다). 무결성 검사 통과. - 잴 수 있다:
VL_Speech_lab/*.json에 정답 전사RecordingMetadata.orthographic, 숙련도SpeakerMetadata.proficiency(Beginner 13,710 · Intermediate 4,764 · Advance 3,647 · Fluent 326),topik_level(0~6),recording_level(Beginner/Advanced),RecordedTime(중앙값 7.5초 · p90 36초 · 합 86.5시간). 화자 3,243명, 모어는 인도네시아 10,357 · 베트남 7,469 · 몽골 1,271 · 아제르바이잔·우즈벡·아르메니아·카자흐·태국·미얀마·힌디 …. 과제 유형 LAR 9,327 · ATQ 8,939 · SPT · PDT · EIG · SSO. - 등급 축은
proficiency4등급을 쓴다(완료 조건 「등급별, 등급당 20건 이상」 — Fluent 도 326건이라 된다).topik_level0 이 12,796건(미응시)이라 보조 축으로만. - 한계(원문 그대로): 온라인 녹음(48000Hz/1ch/16bit)이지 전화 음성이 아니다 → 8 kHz 다운샘플. 과제가 질문답변·낭독(ATQ/LAR)이라 민원 어휘가 아니다. 이 수치는 상한이다.
- 다음: ① 등급당 20건 이상 층화 표본(시드 고정, 화자 중복 최소) ② 표본 wav 만 꺼내 8 kHz ③ Chirp 3 →
aggregate_by_group(등급) → WER/CER ④ 원본 샘플레이트로도 재서 8 kHz 몫 분리 ⑤ STT 예산: 등급당 20건 × 4 × 중앙값 7.5초 ≈ 600초 — 일 캡 600초에 딱 걸린다. 표본을 짧은 발화 위주로 뽑거나 이틀에 나눠 잰다. - S3: 자격증명은 이 맥에 있고(
assist-apne2읽기 확인 — 현재uploads/객체 1개뿐) 20 GB 는 용량·비용에 문제 없다(런북 S3 100 GB ≈ $4/월). 경로는decisions/210대로 정성윤 님이 정한다 — 아직 안 올렸다.
2026-09-21 — 실측 · 숙련도 등급별 WER/CER
측정일 2026-09-21 · 커밋 97f7b25-dirty(스크립트·티켓이 아직 커밋 전) · 시드 20260921
PYTHONPATH= .venv/bin/python scripts/measure_a5_proficiency.py --dry-run # 표본·482초 확인
PYTHONPATH= .venv/bin/python scripts/measure_a5_proficiency.py
데이터 aihub-foreign-proficiency-71479/validation · 표본 80건(등급당 20 · 3~6초 · 화자당 ≤2)
STT Google Cloud Speech v1 recognize · ko-KR · 기본 모델 · 자동 구두점 (09-09 와 같은 호출)
STT 사용 482.3초(8 kHz 387.1 + 원본 95.3) · 장부 오늘 0 → 482.3/600 · 이달 513.2 → 995.6/3600
주 측정 — 8 kHz (정규화 후 / 정규화 전)
| 등급 | n | 화자 | 초 | WER | CER | WER(정규화 전) | CER(정규화 전) |
|---|---|---|---|---|---|---|---|
| Beginner | 20 | 20 | 97.4 | 0.412 | 0.221 | 0.431 | 0.212 |
| Intermediate | 20 | 20 | 90.6 | 0.266 | 0.198 | 0.287 | 0.190 |
| Advance | 20 | 19 | 102.3 | 0.225 | 0.087 | 0.234 | 0.087 |
| Fluent | 20 | 16 | 96.8 | 0.198 | 0.067 | 0.216 | 0.078 |
| (참고) 전체 | 80 | 75 | 387.1 | 0.273 | 0.139 | 0.289 | 0.138 |
등급이 내려갈수록 나빠지는 방향은 일관되고, Beginner 는 단어 다섯 중 둘을 틀린다(CER 도 Advance 의 2.5배). Intermediate 와 Advance 는 WER 로는 가깝고 CER 로 갈린다(0.198 vs 0.087) — 초급·중급은 음절 단위로 틀리고 고급은 띄어쓰기·조사 정도라는 뜻이다. 빈 가설(아무것도 못 알아들음) 0건.
정규화 전 CER 이 낮아 보이는 것은 분모에 문장부호 글자가 들어가서다 — 좋아진 것이 아니다. 정규화가 WER 을 올린 폭은 등급마다 0.01~0.02 — 정규화가 결과를 만든 것은 아니다.
원본(48 kHz) vs 8 kHz — 같은 20건(등급당 5), 정규화 후
| 등급 | n | WER 원본 → 8 kHz | CER 원본 → 8 kHz |
|---|---|---|---|
| Beginner | 5 | 0.400 → 0.560 (+0.160) | 0.250 → 0.281 (+0.031) |
| Intermediate | 5 | 0.263 → 0.263 (±0) | 0.179 → 0.196 (+0.018) |
| Advance | 5 | 0.160 → 0.240 (+0.080) | 0.047 → 0.109 (+0.062) |
| Fluent | 5 | 0.261 → 0.261 (±0) | 0.033 → 0.033 (±0) |
| 전체 | 20 | 0.272 → 0.337 (+0.065) | 0.127 → 0.155 (+0.029) |
전사가 두 조건에서 같은 것이 20건 중 13건, 다른 것이 7건. 방향은 8 kHz 쪽이 나쁘다(09-09 내국인 표본에서는 방향조차 안 보였다) — 하지만 등급당 5건이라 등급별 숫자는 표본 잡음 안이다. 전체 +0.065 만 인용한다.
⚠ 이 수치는 상한이다 — 함께 옮겨 적는다
- 온라인 녹음(48 kHz, 잡음 없음)을 다운샘플만 했다. 실제 전화망의 코덱·잡음·라인 손실이 없다 → 실제 통화는 이보다 나쁘다.
- 민원 어휘가 아니다 — 과제가 낭독(LAR 55건)·질문답변(ATQ 25건)이다. 낭독은 같은 문장을 여러 화자가 읽어 정답 문장이 80건 중 70종이다.
- 3~6초 짧은 발화만 골랐다(데이터 중앙값 7.5초) — STT 일 캡 600초 때문이다. 긴 발화는 다를 수 있다.
- 모어 편중 — 인도네시아 29 · 베트남 21 · 몽골 11 · 그 외 19. 데이터셋 자체의 분포다(중·일어 화자는 없다).
- 등급당 n=20 — 등급 간 차이가 표본 잡음보다 작을 수 있다. 원본 재측정은 등급당 5건뿐이다.
- 정답 전사가 학습자의 오류 발화를 그대로 적는다(
지나 생일에·용화가 좋). STT 가 그럴듯하게 「교정」하면(지난 생일을) 오류로 잡힌다 — 우리 목적(상담원이 알아듣기)에는 그 교정이 오히려 도움일 수 있어, WER 이 곧 「못 알아들은 정도」는 아니다. - 숫자 표기(라벨
십오년vs STT15년)·%는 정규화하지 않았다. 71479 라벨용으로 추가한 규칙 2개(제어 문자\x08제거 · 굽은따옴표 → 공백)는EXTRA_NORMALIZATION_RULES로 드러냈고 JSON 에도 들어 있다. - 모델이 Chirp 3 가 아니라 v1 기본 모델이다(위 완료 조건 3).
목표치는 정하지 않았다 — 이것은 측정값이고, 「A-5 를 코어에 두는가」는 팀이 이 표를 보고 정한다.
파일
scripts/measure_a5_proficiency.py 표본 추출(층화·시드·화자 상한·길이 창) → unzip 한 건씩 → 8 kHz → STT → aggregate_by_group
scripts/tests/test_measure_a5_proficiency.py 순수 함수 8건 (.venv/bin/python -m pytest scripts/tests -q)
data/processed/a5/2026-09-21-proficiency.json 건별 정답·가설·조건 (gitignore — AI Hub 재배포 금지)
data/processed/a5/{wav,8k,stt}/ 꺼낸 원본 80개 · 8 kHz 사본 · 전사 캐시(내용 해시 — 재실행이 예산을 다시 쓰지 않는다)
jekyll/assets/a5-wer-2026-09-21.json 공개 요약 — 등급별 n·WER·CER·초·조건 3종×정규화 전/후·한계. 건별 전사 없음
다음
- 왕복 지연(STT→번역→화면) 실측 — 번역 단계가 생긴 뒤.
- A-5 코어 유지 판단 — 팀. 판단 재료: Beginner WER 0.41(상한) 이 「상담원이 알아듣게 한다」에 충분한지.
- 필요하면 Chirp 3(v2) 로 같은 표본을 다시 잰다 — 캐시가 있어 8 kHz 사본은 그대로, STT 예산 ~390초.
2026-09-21 — 닫음 (내 몫 끝)
스파이크의 목적(「잴 수 있는가」)에 답이 나왔다 — 잴 수 있고, 쟀다(위 실측 절). 남은 둘은 이 티켓 안에서 할 수 없다: ① 왕복 지연(STT→번역→화면)은 번역 단계가 아직 없어 잴 파이프라인이 없다 ② A-5 를 코어에 둘지는 팀 판단(재료: Beginner WER 0.41 — 상한). Chirp 3(v2) 재측정은 8 kHz 사본·표본이 그대로 있어 다음 날 캡으로 ~390초면 된다(--min-sec/--max-sec 그대로, 스크립트에 모델 인자만). 셋 다 미결.
2026-09-21 — Chirp 3 재측정 시도: 권한에 막혔다 (STT 0초 사용)
measure_a5_proficiency.py 에 --stt-model {v1,chirp_3,chirp_2} · --region · --probe(1건만) 를 붙이고 v2 호출(transcribe_batch.transcribe_v2)을 넣었다. 같은 표본·같은 8 kHz 사본을 모델만 바꿔 재도록 했고, 캐시 키·결과 파일에 모델 이름이 붙어 v1 결과와 섞이지 않는다(테스트 3건 추가, 11 통과). 일 캡은 사용자 지시로 이 실행에만 프로세스 환경변수 STT_MAX_SECONDS_PER_DAY=900 으로 올리고 .env 는 그대로 뒀다(리포트 cap_override 에 남는다).
--probe 1건이 403 PERMISSION_DENIED speech.recognizers.recognize 로 실패했다 — us(chirp_3) · us-central1 · asia-southeast1(chirp_2) 모두 같다. v2 API 는 v1 과 달리 서비스 계정에 「Cloud Speech 클라이언트」(roles/speech.client) 역할이 있어야 한다. 프로젝트 IAM 변경은 GCP 관리자(정성윤 님) 몫이라 하지 않았다. 역할이 붙으면 아래 한 줄이다(약 390초 — 그날 캡 안이면 캡 상향 불필요):
PYTHONPATH= .venv/bin/python scripts/measure_a5_proficiency.py --stt-model chirp_3 --region us --original-subset 0 --probe # 1건 확인
PYTHONPATH= .venv/bin/python scripts/measure_a5_proficiency.py --stt-model chirp_3 --region us --original-subset 0