Metadata-Version: 2.5
Name: kocer
Version: 0.1.4
Summary: 한국어 STT를 공정하게 채점하는 CER/WER — 띄어쓰기·숫자·문장부호 차이를 오류로 세지 않음
Project-URL: Repository, https://github.com/Choi-kwan-u/kocer
Author: Choi Kwan-u
License-Expression: MIT
License-File: LICENSE
Keywords: asr,cer,evaluation,korean,stt,wer
Classifier: Natural Language :: Korean
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: rapidfuzz; extra == 'dev'
Provides-Extra: fast
Requires-Dist: rapidfuzz; extra == 'fast'
Description-Content-Type: text/markdown

# kocer

한국어 STT를 공정하게 채점하는 CER — 띄어쓰기·숫자·문장부호·영문 약어 표기 차이를 오류로 세지 않습니다.

*Fair CER/WER for Korean speech recognition. Spacing, punctuation, number notation (3개 ↔ 세 개) and Latin acronyms (AI ↔ 에이아이) are not counted as errors. Unlike jiwer and nlptutti, kocer scores numbers against every valid Korean reading, keeps the original CER alongside, and tells you where your errors come from. Pure Python, zero dependencies, `jiwer`-compatible API.*

```bash
pip install kocer
```

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Choi-kwan-u/kocer/main/docs/breakdown-dark.svg">
  <img alt="공개 한국어 ASR 모델 6개의 raw CER 오류를 띄어쓰기·문장부호·숫자/영문·진짜 오류로 나눈 누적 막대. 모델에 따라 46~68%가 표기 차이다." src="https://raw.githubusercontent.com/Choi-kwan-u/kocer/main/docs/breakdown-light.svg">
</picture>

위 그림을 **내 모델 출력으로 한 줄에** 얻습니다. 기존 CER 숫자는 그대로 두고, 그 오류가 어디서 왔는지만 나눠 보여줍니다.

```python
>>> import kocer
>>> print(kocer.breakdown(refs, hyps))      # whisper-large-v3, Zeroth test 457발화
raw CER 8.60%  →  kocer CER 2.93%   (2,191 raw errors, 457 pairs)
띄어쓰기     1,142   52%  █████████████████████
문장부호       226   10%  ████
숫자·영문      258   12%  █████
진짜 오류      565   26%  ██████████
```

문장 하나로 보면 이렇습니다.

```
ref: 사과 세 개 주세요
hyp: 사과 3개 주세요
jiwer CER 0.200   nlptutti CER 0.143   kocer CER 0.000
```

## 공개 모델 재채점: 같은 출력, 네 가지 채점

공개 한국어 ASR 6개 설정을 공개 테스트셋 세 개(Zeroth 457발화, FLEURS ko 382발화, Common Voice 27 ko 579발화, 합계 1,418발화·약 2.9시간)에서 같은 조건(greedy, 언어 고정, 프롬프트 없음)으로 돌리고, **모델 출력은 그대로 둔 채 채점만** 네 단계로 바꿨습니다. 각 단계는 정답과 출력에 똑같이 적용됩니다. 단위 %, 코퍼스 단위(총 오류 / 총 정답 글자). 맨 위 그림이 이 실험의 오류 분해이고, 아래가 숫자입니다.

### 데이터셋별 kocer CER

| 모델 | Zeroth | FLEURS | Common Voice 27 | 합산 |
|---|---|---|---|---|
| BuzzASR/korean | 3.16 | 4.72 | **3.88** | **3.90** |
| TeamUNIVA/qwen3_asr_1.7b_ko_beta | 2.46 | **4.31** | 6.08 | 3.99 |
| openai/whisper-large-v3 | 2.93 | 4.37 | 5.80 | 4.13 |
| Qwen/Qwen3-ASR-1.7B | 2.91 | **4.31** | 6.61 | 4.30 |
| FunAudioLLM/SenseVoiceSmall (itn 켬) | **2.40** | 6.95 | 9.68 | 5.78 |
| FunAudioLLM/SenseVoiceSmall (itn 끔) | 2.41 | 6.91 | 9.72 | 5.79 |

모든 데이터셋에서 이기는 모델은 없습니다. Common Voice·FLEURS 학습 데이터로 파인튜닝한 BuzzASR는 Common Voice에서 크게 앞서고 Zeroth에서는 뒤집니다. SenseVoice는 Zeroth에서 가장 좋고 나머지에서는 꼴찌입니다. 합산 순위는 어떤 데이터셋을 섞느냐에 따라 달라지니 데이터셋별 열을 먼저 보세요.

### 채점 단계별 합산 CER

| 모델 | raw | 공백 제거 | 공백·부호 제거 | kocer | 순위 raw → 공백·부호 → kocer |
|---|---|---|---|---|---|
| BuzzASR/korean | 7.83 | 7.81 | 5.02 | 3.90 | 3 → 3 → **1** |
| TeamUNIVA ko | 7.61 | 7.45 | 5.26 | 3.99 | 2 → 4 → **2** |
| whisper-large-v3 | 9.58 | 7.18 | 4.91 | 4.13 | 6 → 2 → 3 |
| Qwen3-ASR-1.7B | 6.97 | 6.72 | 4.73 | 4.30 | 1 → 1 → 4 |
| SenseVoice (itn 켬) | 7.98 | 8.43 | 6.36 | 5.78 | 4 → 6 → 5 |
| SenseVoice (itn 끔) | 8.45 | 9.05 | 6.36 | 5.79 | 5 → 5 → 6 |

열끼리 비율로 비교하면 안 됩니다. 공백을 지우면 분모가 25% 줄어서(정답 글자 64,833 → 48,470), 띄어쓰기를 이미 잘 맞춘 모델은 CER이 오히려 오릅니다(SenseVoice). 그래서 오류 **개수**로 다시 봅니다.

| 모델 | raw 오류 수 | 띄어쓰기 | 문장부호 | 숫자·약어 (kocer만 처리) | 진짜 오류 | 표기 차이 비율 |
|---|---|---|---|---|---|---|
| BuzzASR/korean | 5,077 | 23% | 29% | 11% | 37% | 63% |
| TeamUNIVA ko | 4,935 | 25% | 24% | 12% | 39% | 61% |
| whisper-large-v3 | 6,210 | 42% | 19% | 6% | 32% | 68% |
| Qwen3-ASR-1.7B | 4,521 | 26% | 23% | 5% | 46% | 54% |
| SenseVoice (itn 켬) | 5,171 | 19% | 22% | 5% | 54% | 46% |
| SenseVoice (itn 끔) | 5,479 | 18% | 26% | 5% | 51% | 49% |

- raw CER이 오류로 센 것의 **46~68%는 표기 차이**였습니다. 대부분은 띄어쓰기와 문장부호이고, 이 둘만 지워도(nlptutti 등이 이미 하는 일) whisper-large-v3가 꼴찌에서 2위로 올라옵니다.
- 숫자·영문 약어는 kocer만 처리하는 몫입니다. 합산 raw 오류의 5~12%이고 거의 전부 **Zeroth**에서 나옵니다. Zeroth 정답은 숫자를 전부 한글로 적는데(`이천 십 육 년`) 대부분의 모델은 아라비아 숫자를 뱉어서(`2016년`), 숫자를 뱉는 모델의 Zeroth raw 오류 중 8~27%가 이 경우였습니다. FLEURS와 Common Voice에서는 0~3%입니다(예외: 숫자를 한글로만 뱉는 SenseVoice의 FLEURS 9~11%, 반대 방향). 이 몫만으로도 합산 상위권이 바뀝니다. TeamUNIVA 4위 → 2위, BuzzASR 3위 → 1위.
- 어디까지 믿을 수 있나. 문장 단위 paired bootstrap 2,000회(kocer 기준): BuzzASR ≈ TeamUNIVA(−0.09%p, 95% 구간 [−0.32, +0.13]), 둘 다 Qwen3-ASR보다 나음(0.40 [0.16, 0.64], 0.31 [0.15, 0.46]), BuzzASR > whisper(0.23 [0.06, 0.40]), whisper는 TeamUNIVA·Qwen3-ASR과 구분 안 됨, SenseVoice는 모두에게 1.5~1.9%p 뒤짐. 순위 구간: BuzzASR [1,2], TeamUNIVA [1,3], whisper [2,4], Qwen3-ASR [3,4], SenseVoice [5,6].

**정규화가 진짜 오류를 숨기지 않나.** raw > 0이고 kocer = 0인 문장을 Zeroth·FLEURS 전 모델 출력에서 모아(중복 제거 1,325건) 무작위 100건(seed 20261024)을 저자가 정답과 나란히 읽었습니다. 99건은 띄어쓰기·문장부호·숫자 표기 차이만, 0건이 실제 오류를 숨김, 1건이 애매(`일 세기 전`/`일세기 전`: 띄어쓰기가 "한 세기 전"과 "1세기"를 가름). 애매한 유형은 띄어쓰기가 동형이의어를 만드는 경우로, 공백을 지우는 모든 CER이 0으로 세며 kocer의 숫자 후보 때문에 생긴 건 없었습니다. 목록은 [benchmarks/results/fake_review.tsv](benchmarks/results/fake_review.tsv)에 있습니다.

**주의할 것.** 디코딩은 전 모델 greedy로 각 모델의 최고점 설정이 아닙니다(BuzzASR 모델카드는 반복 억제 옵션을 권장). Common Voice는 Hugging Face 사본이 비어 있어 Mozilla Data Collective의 Scripted Speech 27.0을 썼고, BuzzASR 모델카드(버전 25)와 test가 완전히 같지 않습니다. 데이터셋은 각 0.8~1.1시간이라 0.3%p 이하 차이는 위 신뢰구간과 함께 읽어야 합니다.

모델 출력 전체(jsonl), 추론·채점 스크립트, 부트스트랩 코드, 전체 표는 [benchmarks/](benchmarks/)에 있습니다. kocer 0.1.3으로 다시 채점하면 표의 kocer 열이 소수점 둘째 자리까지 재현됩니다.

## 기존 도구와 무엇이 다른가

한국어 STT 채점에는 보통 [jiwer](https://github.com/jitsi/jiwer)나 [nlptutti](https://github.com/hyeonsangjeon/computing-Korean-STT-error-rates)를 씁니다. 셋 다 같은 편집거리로 CER을 계산합니다. 다른 점은 **무엇을 오류로 세느냐**입니다.

- **jiwer**는 언어를 가리지 않는 범용 도구입니다. 띄어쓰기와 문장부호도 기본으로 오류로 셉니다.
- **nlptutti**는 한국어용입니다. 띄어쓰기와 문장부호는 무시하지만, 숫자와 영문 표기 차이는 오류로 셉니다.
- **kocer**는 여기에 더해 숫자 읽기(3개 = 세 개)와 영문 약어 읽기(AI = 에이아이)까지 같은 말로 봅니다.

### 같은 문장, 세 도구의 CER

2026-10-24 실측. jiwer 4.0.0, nlptutti 0.0.0.24, kocer 0.1.2, 모두 기본 설정.

| 정답 | STT 결과 | jiwer | nlptutti | kocer |
|---|---|---|---|---|
| 네, 알겠습니다. | 네 알겠습니다 | 0.222 | 0.000 | 0.000 |
| 오늘날씨가좋다 | 오늘 날씨가 좋다 | 0.286 | 0.000 | 0.000 |
| 사과 세 개 주세요 | 사과 3개 주세요 | 0.200 | 0.143 | **0.000** |
| 삼 층으로 가세요 | 3층으로 가세요 | 0.222 | 0.143 | **0.000** |
| 세 시 삼십 분 | 3시 30분 | 0.625 | 0.600 | **0.000** |
| 이천이십사 년 십이 월 | 2024년 12월 | 0.750 | 0.778 | **0.000** |
| 오천 원 | 5,000원 | 1.250 | 0.800 | **0.000** |
| 오십 퍼센트 | 50% | 1.000 | 1.000 | **0.000** |
| 에이아이 모델 | AI 모델 | 0.571 | 0.667 | **0.000** |
| 나사 우주선 | NASA 우주선 | 0.667 | 0.571 | **0.000** |
| 사과 세 개 주세요 | 사과 **네** 개 주세요 | 0.100 | 0.143 | 0.143 |
| 세 시 삼십 분 | 3시 **13**분 | 0.625 | 0.600 | 0.400 |

마지막 두 줄은 진짜 인식 오류입니다. kocer도 오류로 셉니다. "세 시 삼십 분"과 "3시 13분"에서 kocer가 낮은 이유는 "3시"는 맞게 인식한 부분이라 감점하지 않고, 틀린 "13분"만 세기 때문입니다.

### 기능 비교

| | jiwer | nlptutti | kocer |
|---|---|---|---|
| 띄어쓰기·문장부호 무시 | 직접 설정 | 기본 | 기본 |
| 숫자 읽기 차이 무시 (3개 = 세 개) | 없음 | 없음 | 기본 |
| 영문 약어 읽기 차이 무시 (AI = 에이아이) | 없음 | 없음 | 기본, 사용자 사전 지원 |
| 정규화 전·후 CER을 한 번에 반환 | 없음 | 없음 | `raw_cer`, `cer` |
| 오류를 띄어쓰기·문장부호·숫자·진짜 오류로 분해 | 없음 | 없음 | `breakdown()` |
| 여러 모델 비교 리포트, 품질 게이트 | 없음 | 있음 | 없음 |
| 개체명·키워드 오류율 | 없음 | 있음 | 없음 |
| 지원 지표 | WER, CER, MER, WIL, WIP | CER, WER, CRR | CER, WER |
| 필수 의존성 | rapidfuzz, click | jiwer | 없음 |

**이럴 때 kocer를 쓰세요.** 정답 전사와 STT 결과의 숫자·영문 표기 방식이 다를 때입니다. 예를 들어 정답은 한글로 읽은 대로 적었는데 모델은 아라비아 숫자로 출력하는 경우입니다. 이 차이를 오류로 세면 모델 간 비교가 표기 습관 비교가 됩니다.

**이럴 때는 nlptutti가 낫습니다.** 여러 모델의 리포트를 만들거나, 개체명·키워드 단위로 평가하거나, 기준 점수로 CI를 막고 싶을 때입니다. 두 도구를 같이 써도 됩니다.

## 설치

```bash
pip install kocer          # 순수 파이썬, 의존성 없음
pip install kocer[fast]    # rapidfuzz로 채점 수십 배 빠름. 긴 문장(자막·회의록)을 많이 재면 권장
```

## 사용

```python
import kocer

kocer.cer("사과 세 개 주세요", "사과 3개 주세요")   # 0.0
kocer.cer(refs, hyps)                                # 리스트도 됨 (jiwer와 같은 코퍼스 집계)
kocer.wer(ref, hyp)

r = kocer.measure("세 시 삼십 분", "3시 30분")
r.cer         # 0.0  정규화 후
r.raw_cer     # 0.625  정규화 전 (jiwer와 같은 값). 둘의 차이가 "가짜 오류"의 크기
r.ops         # {'S': 0, 'D': 0, 'I': 0}
r.alignment   # [('H', '세', '세'), ...]
r.chosen_ref  # 숫자 후보 중 실제로 채택된 정답
r.chosen_hyp  # '세시삼십분'

b = kocer.breakdown(refs, hyps)   # 오류가 어디서 왔는지 개수로 분해
b.spacing, b.punctuation, b.numbers_letters, b.remaining   # 합이 b.raw_errors
b.shares                          # 비율
b.cer                             # {'raw':…, 'nospace':…, 'nopunct':…, 'kocer':…}
print(b)                          # 위의 막대 출력

# 단어처럼 읽는 약어는 직접 넘김 (내장 사전 위에 덮어씀)
kocer.cer("엘지 트윈스", "LG 트윈스")                      # 0.0  철자 읽기
kocer.cer("케이비오 리그", "KBO 리그")                      # 0.0
kocer.cer("유니스트 입학", "UNIST 입학", aliases={"UNIST": "유니스트"})  # 0.0
```

```bash
kocer ref.txt hyp.txt          # 줄 단위 쌍. raw CER, kocer CER, 오류 분해를 출력
kocer ref.txt hyp.txt --diff   # 틀린 글자만 색으로 표시
```

## 무엇을 무시하고, 무엇을 세는가

정규화는 순서대로 적용되고 각 단계를 끌 수 있습니다.

| 단계 | 기본 | 끄기 | 예 |
|---|---|---|---|
| 유니코드 NFC | 켬 | - | 자모 분리 입력 |
| 문장부호 제거 | 켬 | `punct=False` | `네, 알겠습니다.` = `네 알겠습니다` |
| 필러 제거 (음, 어, 그) | **끔** | `fillers=True` | 실제 단어 "그"도 지워질 수 있어 기본은 끔 |
| 영문 약어·단위 | 켬 | `letters=False` | `AI` = `에이아이`, `NASA` = `나사`, `50%` = `50퍼센트` |
| 숫자 후보 전개 | 켬 | `numbers=False` | `3개` = `세 개`, `3층` = `삼 층` |
| 공백 제거 | CER만 | - | WER은 공백 유지 |

`normalize=False`면 jiwer와 같은 순수 CER을 돌려줍니다.

### 숫자: 정답 하나가 아니라 후보 집합

숫자는 조수사에 따라 읽기가 달라서(3개=세 개, 3층=삼 층, 3시 30분=세 시 삼십 분) 규칙 하나로 정답을 만들 수 없습니다. kocer는 숫자마다 `{원문 숫자, 한자어 읽기, 고유어 읽기, 자릿수 읽기}` 후보를 만들고 CER이 최소가 되는 조합을 고릅니다.

- 조수사 사전([counters.py](src/kocer/counters.py))에 있으면 그 읽기만, **없으면 두 읽기 모두 허용**합니다. 틀리게 감점하는 것보다 관대한 쪽이 안전하다는 정책입니다.
- 반대 방향(ref가 한글 수사, hyp가 숫자)도 처리합니다.
- 전화번호·연도는 자릿수 읽기(010 = 공일공, 2024 = 이공이사)도 후보에 들어갑니다.
- 고유어 읽기는 1~99까지. `100개`는 `백 개`만 허용하고, `백스무 명`(120명)은 아직 안 됩니다.
- 고유어는 관형형(세, 스무)과 단독형(셋, 스물) 둘 다 후보에 넣습니다. 소수점은 `점`만 허용하고 구어 `쩜`은 받지 않습니다.
- 숫자 하나에 후보가 많아 조합이 64개를 넘으면 숫자 구간마다 따로 최적 후보를 고르는 근사를 씁니다.

### "너무 관대하다"는 비판에 대해

정규화는 실제 오류를 숨길 수 있습니다. 그래서 `raw_cer`를 항상 같이 돌려주고, 모든 단계를 끌 수 있게 했습니다. 두 값을 같이 보고하세요.

## 한계

- 대문자 연속은 철자로 읽습니다(`AI` = `에이아이`). 단어처럼 읽는 약어는 `aliases`로 넘기세요. `NASA`, `UNESCO`, `COVID` 등 16개만 내장되어 있습니다.
- 단위 기호는 `%`, `km`, `kg`, `cm`, `mm`, `ml`만 한글로 바꿉니다. `3m`은 `삼 미터`와 다른 것으로 봅니다.
- WER은 띄어쓰기 차이를 오류로 셉니다. 한국어 WER의 본질적 한계이고, CER을 권합니다.
- 조수사 사전은 완전하지 않습니다. 사전에 없는 조수사는 두 읽기를 다 허용합니다. 케이스를 [tests/cases.tsv](tests/cases.tsv)에 추가하는 PR을 환영합니다.
- 입력은 텍스트 쌍만 받습니다. 음성 파일·모델 실행은 다루지 않습니다.

## 라이선스

MIT
