STT 품질 개선기(1) - 성능 측정 도구
이 글은 Convene 의 STT 품질 개선 과정을 다룬 2편짜리 글의 첫 번째 글입니다.
Convene은 WebRTC 기반 화상 회의 도구로, 회의가 끝나면 음성을 자동으로 전사하고 LLM 이 그 전사본을 요약해 회의록을 만들어 줍니다. 그런데 이 전사 결과가 회의록으로 쓰기 어려울 만큼 부정확했습니다. 처음엔 Whisper 파라미터 몇 개만 조정하면 될 줄 알았으나, 측정 도구를 만들고 나니 문제는 전혀 다른 곳에 있었습니다.
1. 시작하기 전에: "CER 49.9%가 개선이라고?"
결과부터 이야기하면, 이번 작업으로 모델도 서버도 바꾸지 않고 CER을 67.8% → 49.9%, RTF를 0.787 → 0.284로 낮췄습니다.
49.9%라는 숫자만 보면 절반이 틀렸는데 개선이라고 할 수 있나? 라는 생각이 들 수 있습니다. 여기에는 이 시스템의 전제가 있습니다. Convene에서 전사는 최종 산출물이 아니라 요약의 입력입니다. 표기가 조금 흔들리는 것은 요약 단계에서 흡수되지만, 내용이 통째로 빠지면 요약은 그것을 복원할 방법이 없습니다. 그래서 이번 작업 내내 "글자가 정확한가"보다 "내용이 살아남는가"를 더 중요하게 봤습니다.
음성이 텍스트가 되기까지의 경로는 다음과 같습니다.
브라우저 마이크
→ Opus 인코딩 → WebRTC → mediasoup SFU
→ 참가자마다 ffmpeg 1개가 RTP 를 받아 16kHz PCM으로 디코딩
→ Redis에 참가자별 오디오 버퍼로 적재
→ 스케줄러가 주기적으로 꺼내 wav로 감싸 STT 워커에 전송
→ STT 워커(FastAPI + faster-whisper)가 전사해 세그먼트 생성
→ 모든 참가자의 세그먼트를 시각순으로 합쳐 transcript 완성
→ LLM이 transcript를 요약해 회의록 생성또 하나의 제약은 서버였습니다. 운영 서버는 2GB RAM, 2 vCPU 인스턴스 한 대이고, 이 위에서 미디어 서버, 백엔드, Redis, STT 워커가 동시에 돌아갑니다. 품질만 보고 모델을 고르면 배포 자체가 불가능한 환경이었습니다.
2. 왜 튜닝보다 측정이 먼저였나
전사 품질이 나쁜 원인이 무엇일까? 라는 고민으로 후보를 적어 보니 너무 많았습니다.
- 모델 크기, 디코딩 설정, 프롬프트
- 오디오 전처리, 마이크 편차, 네트워크 손실
- 청크 길이, 서버 사양
문제는 어느 것이 효과가 있었는지 판별할 수단이 없다는 점이었습니다. 설정을 바꾸고 결과 몇 개를 들여다보며 "좋아진 것 같다"고 판단하는 방식으로는 여러 가설을 처리할 수 없다고 판단했고, 그래서 첫 작업을 튜닝이 아니라 측정 하네스 구축으로 정했습니다.
2.1. 무엇을 쟀는가
하네스는 세 지표를 항상 한 표에 같이 출력합니다.
| 지표 | 의미 | 보는 이유 |
| CER (Character Error Rate) | 정답과 STT 출력의 글자 단위 오류율 | 주 지표. 낮을수록 정확 |
| RTF (Real-Time Factor) | 오디오 1초를 처리하는 데 걸리는 시간(초) | 동시 발화 인원을 곱한 값이 1.0 을 넘으면 실시간 처리가 밀림 |
| 피크 RSS | 전사 중 프로세스의 최대 메모리 | 컨테이너 메모리 상한을 넘으면 배포 불가 |
세 지표를 같이 보는 이유는 STT 설정 변경이 거의 항상 품질과 비용의 맞바꿈이기 때문입니다. CER만 보고 고르면 서버에서 OOM으로 죽거나 실시간 처리가 밀리게 됩니다.
2.2. WER 대신 CER을 쓴 이유
한국어에서는 어절 단위로 오류를 세는 WER이 띄어쓰기에 과민합니다. 실제로 띄어쓰기만 다르고 내용은 같은 문장에서 CER 0, WER 2가 나오는 것을 확인했습니다. WER 만 봤다면 존재하지 않는 오류를 쫓고 있었을 것입니다.
또한 CER은 샘플별 CER의 평균이 아니라 전체 편집거리의 합을 전체 정답 길이의 합으로 나눠 계산했습니다. 단순 평균을 쓰면 3초짜리 샘플 하나가 60초짜리 샘플과 같은 무게를 갖게 되어, 짧은 샘플이 과대 대표되기 때문입니다.
3. 하네스 설계: 결과를 믿을 수 있게 만드는 결정들
하네스 자체는 단순합니다. 샘플 폴더에서 wav 와 정답 txt 쌍을 찾고, 비교할 설정 목록을 읽어 설정마다 전 샘플을 전사한 뒤 지표와 실제 출력 텍스트를 남깁니다. 다만 몇 가지 결정이 결과의 신뢰도를 좌우했습니다.
- 설정마다 프로세스 격리: 피크 메모리는 OS가 알려 주는 "프로세스 생애 최대값"으로 측정합니다. 한 프로세스에서 여러 설정을 연속으로 측정하면 앞 설정의 피크가 뒤 설정의 값으로 그대로 남습니다. 무거운 모델을 한 번 돌리고 나면 그 뒤의 모든 메모리 측정이 오염되는 구조라, 설정마다 자식 프로세스를 새로 띄우도록 했습니다.
- 텍스트 정규화: 비교 전에 유니코드 정규형 통일, 소문자화, 문장부호 제거, 공백 정리를 거치고, CER은 띄어쓰기까지 제외하고 셉니다. 의미 없는 차이를 오류로 세지 않기 위해서입니다.
- 설정값 그대로 전달: 설정 JSON의 값을 모델과 전사 함수에 그대로 넘겨, 새 파라미터를 시험할 때마다 하네스 코드를 고칠 필요가 없게 했습니다. 30종이 넘는 설정을 돌릴 수 있었던 실질적인 이유입니다.
- 외부 의존성 0: 편집거리(Levenshtein)와 정규화를 직접 구현했습니다. 벤치 때문에 운영 컨테이너 이미지가 커질 이유는 없다고 판단했습니다.
3.1. 샘플은 운영 경로에서 떠낸다
깨끗한 녹음 파일로 튜닝하면 실제 환경에서는 통하지 않는 설정을 고르게 됩니다. 운영 오디오는 Opus 인코딩 → 네트워크 전송 → ffmpeg 디코딩 → 16kHz 변환을 거치는데, STT 품질을 떨어뜨리는 것이 바로 이 경로이기 때문입니다.
그래서 샘플을 운영 파이프라인에서 그대로 떠냈습니다. 로컬 환경에서만 켜는 환경 변수 하나로 STT 워커가 받은 오디오를 그대로 wav로 저장하게 했고, 이렇게 모은 샘플은 모델이 보는 입력과 바이트 단위로 동일합니다.
수집할 때는 다음 규칙을 정했습니다.
- 한 파일에 한 명: 파이프라인이 이미 참가자별로 오디오를 분리하므로, 단일 화자가 모델이 실제로 보는 형태입니다.
- 읽지 말고 대화하기: 낭독은 STT에 훨씬 쉬워서 개선 효과가 부풀려집니다.
- 기술 용어 섞기: 프롬프트 실험의 평가 대상이기 때문입니다.
- 화자 2명 이상: 마이크 편차를 판정하려면 목소리와 마이크가 달라야 합니다.
정답 전사는 사람이 직접 받아쓰기 때문에 규칙이 곧 채점 기준이 됩니다. 영어 기술 용어는 원하는 출력 형태(에이피아이가 아니라 API)로 적고, "어", "음" 같은 filler는 뺐습니다. Whisper는 filler를 떨구도록 학습되어 있어서, 정답에 넣으면 정상 출력이 오류로 잡히기 때문입니다.
최종 표본은 실제 회의 2회, 화자 2명, 34건, 오디오 460.7초, 정답 3,095자입니다. 이후 모든 수치는 예외 없이 이 동일 표본에서 나왔습니다. 규모가 크지 않기 때문에 이 글의 결론은 "이 조건에서 이렇게 나왔다" 정도로 봐주시면 좋을 것 같습니다.
3.2. 판정 노이즈부터 재기
같은 설정을 다시 돌려도 CER은 57.48% ~ 59.00% 사이로 흔들렸습니다. 약 ±1.5pp, 모델에 따라서는 2pp까지였습니다. 즉, 이보다 작은 차이는 개선이라고 부를 수 없습니다.
이 기준선이 없었다면 "1pp 좋아졌다"는 결과를 몇 번이고 개선으로 기록했을 것입니다. 그리고 이 노이즈가 왜 생기는지를 파고든 것이 나중에 중요한 발견으로 이어지는데, 이 이야기는 2편에서 다루겠습니다.
4. 장애 발견: "10분 회의에서 12초만 캡처됐다?"
튜닝을 시작하기도 전에, 샘플을 모으는 과정에서 문제가 터져 나왔습니다. 10분짜리 회의를 했는데 캡처된 오디오가 12.3초였습니다. 회의의 2%입니다.
원인을 따라가 보니 이랬습니다.
- DTX 활성화: 프론트엔드에서 대역폭을 아끼려고 Opus의 DTX(무음 구간에서 전송을 멈추는 기능)를 켜 두고 있었습니다.
- ffmpeg 종료: 참가자가 잠시 말을 멈추면 패킷이 끊기고, 서버의 ffmpeg은 입력이 10초간 없으면 종료됩니다.
- 재시작 없음: 종료된 ffmpeg을 다시 띄우는 로직이 없었습니다.
즉, 누구든 10초만 조용히 있으면 그 사람의 오디오 캡처가 회의가 끝날 때까지 죽어 있었던 것입니다.
그렇다면 ffmpeg의 타임아웃을 늘리면 되지 않을까? 싶었지만, 그 10초는 컴파일 타임 상수라 옵션으로 바꿀 수 없다는 것까지 확인했습니다. 결국 해법은 우리가 직접 켜 두었던 DTX를 끄는 것이었습니다. 하나를 고치자 다른 결함도 연달아 드러났습니다.
| 결함 | 증상 | 원인 |
| 캡처 사망 | 10분 회의에서 12.3초만 캡처 | DTX로 전송이 멈추면 ffmpeg 이 종료, 재시작 없음 |
| 스케줄러 무동작 | 회의 중 전사 0건, 종료 시 447초 blocking | Redis 키 prefix가 SCAN 패턴에만 적용되지 않아 참가자를 못 찾음 |
| 전사 유실 | 30초마다 2초씩 사라짐 | 중복 제거 로직이 전사된 적도 없는 꼬리 구간을 삭제 |
| 인코더 낭비 | 오디오가 평균 4.1초 조각으로 파편화 | 도착 시각 기반 타임스탬프의 지터 |
모두 수정한 결과 캡처량이 정상으로 돌아왔고, 회의 종료 시 blocking은 447초 → 25초, 인코더 낭비는 7.4배 → 2.2배로 줄었습니다.
돌이켜 보면 당연한 이야기입니다. 없는 오디오는 어떤 디코딩 설정으로도 전사할 수 없습니다. 이 결함을 고치기 전에 모은 샘플은 회의의 2%만 담고 있었으니, 그걸로 튜닝을 했다면 전부 헛수고였을 것입니다. 측정 도구를 만드는 과정 자체가 진짜 병목을 드러낸 셈입니다.
5. 범인은 프롬프트였다: "도움이 되라고 넣은 건데..."
파이프라인을 정상화한 뒤에야 본격적으로 디코딩 설정을 비교할 수 있었습니다. 파라미터를 하나씩 바꾼 20종을 같은 34건으로 돌렸습니다.
결과를 CER과 RTF 축에 찍어 보니 모양이 묘했습니다. small 모델의 설정들은 CER 56~62%의 좁은 띠 안에 전부 모여 있었고, 디코딩 설정을 어떻게 바꾸든 그 띠를 벗어나지 못했습니다. 그런데 띠에서 크게 벗어난 점이 딱 하나 있었고, 그게 당시 운영 설정(67.8%)이었습니다.
운영 설정이 다른 설정들과 달랐던 점은 프롬프트였습니다. Whisper는 initial_prompt 로 모델에게 힌트를 줄 수 있는데, 저희는 여기에 "이 녹음은 IT 개발 동아리의 화상 회의입니다…" 같은 서술형 문장을 넣어 두고 있었습니다. 회의 맥락을 알려 주면 전사가 나아지리라는 기대였습니다.
| 프롬프트 | CER |
| 서술형 문장 (당시 운영) | 67.83% |
| 없음 | 57.48% |
| hotwords (해당 회의의 실제 용어) | 57.10% |
| 단어 리스트 (해당 회의의 실제 용어) | 55.89% |
| 무관한 단어 리스트 (대조군) | 62.19% |
프롬프트를 빼기만 했는데 CER이 10.35pp 떨어졌습니다. 이번 프로젝트 전체에서 단일 변경으로 얻은 가장 큰 개선입니다.
5.1. 대조군이 결론을 바꿨다
표를 보면 용어 목록을 넣었을 때는 CER이 조금 내려갑니다. 그렇다면 서술형 문장 대신 용어 목록을 넣으면 되지 않을까?
여기서 한 가지가 걸렸습니다. 그 용어 목록은 벤치 샘플에 실제로 등장하는 단어들이었습니다. 정답을 알고 고른 단어를 힌트로 주면 좋아지는 게 당연합니다. 그래서 전혀 무관한 단어 목록(환율, 배당, 채권 같은 금융 용어)을 넣은 대조군을 함께 돌렸고, 결과는 프롬프트가 없을 때보다 4.7pp 나빠졌습니다.
실제 서비스에서 회의 주제는 매번 다릅니다. 고정된 용어 목록은 어떤 회의에는 맞고 대부분의 회의에는 맞지 않으니, 평균적으로는 손해입니다. 이득처럼 보였던 것은 벤치 표본에 대한 과적합이었습니다. 이 대조군 하나로 initial_prompt 와 hotwords 를 모두 제거하기로 결정했습니다.
5.2. 개선처럼 보였던 것의 정체
여기에 얽힌 착각이 하나 더 있었습니다. 프롬프트를 빼기 전, beam search 크기를 1에서 5로 늘리는 실험을 했고 CER이 4.1pp 좋아졌습니다. 저는 이걸 beam 5가 효과적이라고 기록해 두었습니다.
그런데 프롬프트를 빼고 다시 돌려 보니 beam 1, 2, 5의 CER은 57.48%, 58.41%, 57.86% 로 차이가 없었습니다. 그 4.1pp는 beam의 효과가 아니라, 서술형 프롬프트가 입힌 손상을 beam search가 일부 보정해 준 것이었습니다. 원인을 제거하자 보정할 것이 없어진 것입니다.
개선처럼 보이는 수치가 실은 다른 실수를 부분적으로 상쇄한 결과일 수 있다는 것을 이때 알게 되었고, 이후로는 어떤 설정이 효과가 있다고 결론 내리기 전에 그 효과가 다른 조건에 기대고 있지는 않은지 먼저 확인하게 되었습니다.
6. 결과
| 구분 | 작업 전 | 1편 작업 후 |
| 오디오 캡처 | 10분 회의 중 12.3초 (2%) | 정상 캡처 |
| 회의 종료 blocking | 447초 | 25초 |
| 인코더 낭비 | 7.4배 | 2.2배 |
| CER | 67.83% | 57.48% |
7. 마치며: 튜닝하기 전에 먼저 재야 한다
이번 과정을 통해 "파라미터만 조정하면 되겠지"라는 가정이 얼마나 많은 것을 가리고 있었는지 알 수 있었습니다. 측정 도구가 없었다면 2%의 오디오로 튜닝을 하고, 과적합된 용어 목록을 개선이라 믿고, beam 5를 운영에 적용했을 것입니다.
정리하면 다음과 같습니다.
- 튜닝보다 측정 도구가 먼저이고, 측정 도구를 만드는 과정 자체가 진짜 문제를 드러낸다.
- 샘플은 운영 경로에서 떠내야 한다.
- 노이즈 기준선을 먼저 재고, 그보다 작은 차이는 개선으로 인정하지 않는다.
- 좋아 보이는 결과에는 대조군을 붙인다.
하지만 여기서부터가 진짜 문제였습니다. 프롬프트를 빼서 CER 57%까지 왔지만, 무음 판정, VAD, 확률 임계값 등 무엇을 풀어도 CER은 노이즈 폭 안에서만 움직였고, 출력 길이는 정답의 78%에서 꿈쩍도 하지 않았습니다. 디코딩 파라미터로는 닿지 않는 층위가 있었던 것입니다.
2편에서는 그 층위를 찾아간 과정을 다루겠습니다. 모델 교체에 실패한 이유가 사실은 1년 4개월 낡은 런타임이었다는 것, 그리고 가장 값싼 개선이 청크 길이에 숨어 있었다는 이야기입니다.
Comments (0)
댓글을 불러오는 중...