STT 품질 개선기(2) - 파라미터 조정

Posted: Updated:
#STT#Whisper#Benchmark
이 글은 Convene 의 STT 품질 개선 과정을 다룬 2편짜리 글의 두 번째 글입니다. 1편: STT 품질 개선기(1) - 성능 측정 도구

1편에서는 측정 하네스를 만들고, 오디오의 98%가 사라지던 파이프라인 결함과 품질을 망치던 고정 프롬프트를 찾아 CER 을 67.8%에서 57.5%까지 낮췄습니다. 하지만 그 뒤로는 디코딩 파라미터를 무엇을 바꿔도 CER이 움직이지 않았습니다. 이번 글에서는 파라미터 너머의 원인을 찾아간 과정과, 그 과정에서 틀렸던 판단들을 기록합니다.

1. 진단부터 틀렸다: 출력이 짧으니 버려지고 있는 거겠지?

프롬프트를 뺀 뒤에도 눈에 띄는 증상이 하나 남아 있었습니다. 출력 길이가 정답의 78% 수준이었습니다. 저는 이걸 보고 Whisper가 세그먼트를 폐기하고 있다고 판단했습니다. Whisper에는 무음으로 판정되거나 확률이 낮은 구간을 버리는 장치가 여럿 있기 때문입니다.

그래서 폐기에 관여하는 지표(VAD, 무음 판정 임계값, 확률 임계값)를 전부 풀어 6종을 측정했습니다.

설정CER길이비RTF
기준 (VAD 켬)59.00%0.780.442
VAD 끔58.03%0.770.366
무음 판정 완화58.31%0.780.386
확률 임계 완화60.15%0.740.315
둘 다 완화59.73%0.750.292
VAD 민감도 완화58.62%0.790.330

CER은 전부 노이즈 폭 안이었고, 길이비는 0.74 ~ 0.79에서 꿈쩍도 하지 않았습니다.

결정적인 증거는 실제 출력이었습니다. 영어 필드명을 나열하는 구간에서 6개 설정이 문자 하나 다르지 않게 같은 답을 냈습니다.

  • 정답(126자): "…상품 테이블에는 product_id, name, description, price, stock, category_id, image_url, created_at, updated_at 정도를 넣으려고 합니다…"
  • 6종 공통 출력(11자): "고조로 생각하고 있습니다."

정말 폐기가 원인이라면 임계값을 풀었을 때 무엇이든 달라져야 합니다. 달라지지 않았다는 것은 모델이 그 지점에서 스스로 디코딩을 끝내고 있다는 뜻이고, 디코딩 파라미터로는 닿을 수 없는 층위라는 뜻이었습니다. 즉, 문제는 누락이 아니라 짧고 틀리게 인식하는 것이었습니다.


2. 모델 교체: 두 번 측정했고, 첫 결론은 틀렸다

파라미터로 안 된다면 모델을 바꿔 보자는 생각으로 넘어갔습니다.

2.1. 1차 측정: small 을 이긴 모델이 없었다

모델CERRTF피크 RSS
small (기존)57.48%0.3921143MB
base73.41%0.457503MB
tiny99.31%1.057333MB
한국어 파인튜닝 A95.39%0.8623177MB
한국어 파인튜닝 B98.65%1.4392673MB

기대했던 한국어 파인튜닝 모델들이 가장 크게 무너졌습니다. 출력을 보니 실패 양상이 "품질 저하"가 아니라 “출력 부족”이었습니다. 파인튜닝 A는 34건 중 13건이 빈 출력이었고, B는 사실상 전부 비어 있었습니다.

원인은 학습 데이터였습니다. 두 모델 모두 짧은 낭독 문장 데이터셋 기반이라, 문장을 일찍 끝내도록 편향되어 있었습니다. 15~20초 연속 대화에서 한두 마디만 내고 멈춰 버린 것입니다. 이 결과를 보고 저는 자체 호스팅으로 갈 수 있는 길은 소진됐다고 결론을 내렸습니다.

2.2. 전제를 다시 보니: 런타임이 1년 4개월 낡아 있었다

그런데 결론을 정리하며 전제를 다시 점검하다가 이상한 점을 발견했습니다. 저희가 쓰던 faster-whisper는 1.0.3(2024-07)이었고, 최신은 1.2.1(2025-10)이었습니다. 마이너 릴리스 4개를 건너뛴 상태였습니다.

더 결정적인 것은, 제가 기각했던 모델 중 하나인 large-v3-turbo 가 저희 런타임보다 3개월 늦게 나온 모델이었다는 점입니다. 즉, 런타임이 그 모델을 지원조차 하지 않는 상태에서 "성능이 나쁘다"고 기각한 것이었습니다.

런타임을 올리고 같은 34건으로 전량 재측정했습니다.

모델CERRTF피크 RSS1차 판정
small58.97%0.2841089MB동일
base76.80%0.333469MB동일
medium52.04%1.0413203MB조기 종료로 기각
large-v3-turbo49.41%0.8851655MB런타임 미지원
한국어 파인튜닝 B54.47%0.6201337MB98.65% → 잘못된 기각

흥미로운 점은 버전 자체가 품질을 바꾸지는 않았다는 것입니다. small 은 양쪽에서 같은 수준이었습니다. 바뀐 것은 어떤 모델을 쓸 수 있느냐였습니다.

부수적으로 VAD 구현도 개선되어 속도 구조가 뒤집혔습니다. 구버전에서는 VAD를 켜면 17% 느려졌는데, 신버전에서는 켠 쪽이 더 빨랐습니다(RTF 0.345 → 0.284). 앞서 "속도 때문에 VAD를 끈다"고 내렸던 결정도 함께 되돌렸습니다.

2.3. 노이즈의 원인을 파고들었더니

1편에서 같은 설정을 반복하면 CER이 약 2pp 흔들린다고 했습니다. 그런데 모델을 바꿔 가며 측정해 보니, small만 흔들리고 다른 모델들은 소수점까지 같은 값이 재현됐습니다. 왜 small만 그럴까?

원인은 Whisper의 temperature fallback이었습니다. 디코딩 결과가 품질 기준(반복이 심하거나 확률이 너무 낮은 경우 등)을 위반하면 temperature를 올려 재시도하는데, 이 재시도가 무작위성을 가집니다.

그렇다면 fallback을 끄면 결과가 안정되지 않을까? 싶어서 꺼 봤더니, small의 CER 이 60.63% → 69.53% 로 무너졌습니다.

비결정성은 결함이 아니라, 품질을 8.9pp 떠받치는 재시도가 실제로 자주 걸리고 있다는 신호였습니다. 노이즈의 크기만 볼 게 아니라 원인을 봤어야 했던 것입니다.


3. 가장 값싼 개선: 30초로 자르는 게 문제였다

3.1. 발상

Whisper는 30초 창 안에서만 문맥을 봅니다. 그리고 저희 시스템은 오디오를 30초 주기로 잘라 한 조각씩 전사하고 있었습니다. 청크 하나가 정확히 한 창이고, 청크 사이의 문맥은 완전히 끊기는 구조였습니다.

문맥을 복원하는 방법은 두 가지를 생각할 수 있었습니다.

  • (A) 슬라이딩 프롬프트: 앞 청크의 전사 결과를 다음 청크의 프롬프트로 넘겨주기
  • (B) 오디오 이어붙이기: 여러 청크의 오디오를 이어붙여 한 번에 전사하기
실험small한국어 파인튜닝
기준 (개별 전사)64.04%56.02%
(B) 오디오 이어붙이기52.78% (−11.26pp)62.21% (+6.19pp)
(A) 직전 전사를 프롬프트로63.48% (−0.56pp)72.55% (+16.53pp)
(A') 직전 정답을 프롬프트로62.77% (−1.27pp)68.61% (+12.59pp)

(A)는 폐기했습니다. 정답을 그대로 넘겨주는 이상적인 조건에서조차 노이즈 폭 안이었고, 파인튜닝 모델에서는 재앙이었습니다. 프롬프트가 디코딩을 바꾸기는 하지만 맞는 방향으로 바꾸지 못할 뿐이었습니다. 1편의 서술형 프롬프트, 무관 단어 대조군에 이어 세 번째로 확인한 셈입니다.

(B)는 모델을 탔습니다. small은 11pp 좋아졌지만 파인튜닝 모델은 오히려 나빠졌습니다. 짧은 낭독 문장으로 학습된 모델에게 긴 입력은 학습 분포 밖이기 때문입니다.

3.2. 청크 길이 곡선: 90초에서 최저

그렇다면 몇 초로 묶는 게 가장 좋을까? 이를 확인하기 위해 한 화자가 끊김 없이 이어서 말한 12청크 구간을 골라, 묶음 크기만 바꿔 가며 같은 오디오로 측정했습니다. (처음에는 파일 번호가 인접한 청크끼리 묶었는데, 번호가 화자와 무관한 전역 카운터라 같은 화자의 연속 청크가 아니었다는 걸 중간에 발견해 표본을 다시 잡았습니다.)

배치CER전사 시간드리프트
30초 (기존)61.52%52.2초−15.94초
60초54.97%74.2초−3.14초
90초49.91%37.8초+1.33초
120초51.75%31.1초−3.15초
180초50.00%36.0초−3.17초
360초50.87%57.7초—

여기서 드리프트는 "전사가 말하는 마지막 시각 − 실제 오디오 길이"입니다.

  • 30초 → 90초에서 −11.6pp, 그 이후로는 평평합니다. 이득이 3청크에서 이미 다 회수됩니다.
  • 드리프트는 배치가 길어질수록 오히려 줄어듭니다. 타임스탬프가 밀리는 것이라면 비례해서 커져야 하니, 30초의 −15.94초는 오디오 뒷부분이 통째로 전사되지 않았다는 뜻이었습니다.
  • 전사 시간도 약 30% 줄었습니다. 청크마다 드는 고정 오버헤드(모델 워밍, 후처리)가 묶음 수만큼 줄었기 때문입니다. 앞서 "RTF를 더 낮출 수단이 없다"고 했던 결론도 여기서 뒤집혔습니다.

3.3. 숫자 너머: 사라졌던 내용이 돌아왔다

수치만 보면 표기가 조금 나아진 정도로 읽힙니다. 하지만 실제 출력을 보면 다릅니다. 1장에서 "파라미터로 닿는 층위가 아니다"라고 결론 냈던 바로 그 구간이 살아났습니다.

  • 정답: "…상품 테이블에는 product_id, name, description, price, stock, category_id, image_url, created_at, updated_at 정도를 넣으려고 합니다…"
  • 30초 개별 전사: "고조로 생각하고 있습니다."
  • 90초 배치: "…하나의 카테고리에 여러 상품이 들어갈 수 있는 구조로 생각하고 있습니다. 맞습니다. 테이블에는 프로덕트 ID, 네임, 디스크립션, 프라이스, 스톡, 카테고리 ID, 이미지 URL, … 정도를 넣으려고 합니다."

음차되어 있지만 정보가 살아났습니다. 개별 전사에서는 필드 목록 논의가 통째로 사라져 요약이 복원할 방법이 없었지만, 90초 배치에서는 요약이 이 내용을 회의록에 담을 수 있습니다. 1편에서 이야기한 "전사는 요약의 입력"이라는 전제가 이 차이를 결정적으로 만듭니다.

3.4. 구조는 거의 바꾸지 않아도 됐다

배치로 전사하면 결과 세그먼트를 원래 30초 청크 단위로 되돌려야 할까? 라는 고민이 있었지만, 답은 아니오였습니다. 최종 회의록은 모든 세그먼트를 시간순으로 이어붙인 transcript 를 쓰기 때문에, 세그먼트에 절대 시각만 찍어 저장하면 정보 손실이 없습니다.

그래서 청크는 오디오를 모으는 단위로만 남기고, 전사 결과의 단위와는 분리했습니다. 이후 배치 크기를 바꾸거나 모델을 교체해도 "몇 개를 모아 언제 보내나"만 바뀌고 나머지 구조는 그대로 둘 수 있습니다.


4. 클라우드 STT 와 비교: "프롬프트가 정반대로 작동했다"

자체 호스팅 쪽 실험을 마친 뒤, 클라우드 모델(Gemini)은 같은 오디오와 같은 채점 방식에서 어떤 결과를 내는지 측정했습니다. 전사 기능이 인터페이스로 추상화되어 있어서 어댑터 하나만 새로 작성하면 되는 작업이었습니다.

엔진CER영어 토큰 recall
whisper small57.48%9.6%
gemini-2.5-flash · 기본 프롬프트35.08%40.7%
gemini-2.5-flash · 영어 표기 지시24.24%80.8%

Whisper에서는 매번 손해였던 프롬프트가 여기서는 정반대로 작동했습니다. "영어 기술 용어는 영어 철자로 쓰라"는 지시 한 줄로 −10.8pp가 나왔습니다.

  • 지시 전: "프라이스가 마이너스 5천이면 밸리데이션 에러고 스톡이…"
  • 지시 후: "그럼 price가 -5000이면 validation error고 stock이…"

다만 규칙을 세게만 걸면 역효과가 났습니다. 34건 중 7건이 오히려 악화됐는데, 전부 스프린트 → sprint, 프론트 → front 처럼 한국어로 굳은 외래어까지 영어로 바꾼 경우였습니다. 경계는 "코드나 스펙에 문자 그대로 나타나는 단어인가"였고, 이 경계를 프롬프트에 명시해야 했습니다. 또 프롬프트 예시로 든 단어는 표본 정답에 하나도 등장하지 않는 것으로만 골랐습니다. 정답에 있는 단어를 예시로 넣으면, 개선이 규칙 덕인지 정답 유출 덕인지 구분할 수 없기 때문입니다.

그렇다면 같은 처방을 Whisper에도 쓸 수 있지 않을까? 확인하기 위해 양쪽 출력에서 영어 알파벳을 지우고 재채점해 봤는데, Whisper는 CER 이 오히려 올랐습니다(57.48% → 59.27%). 영어 표기는 Whisper 쪽 문제의 원인이 아니었습니다. 엔진이 다르면 실패하는 층위도 다른 것입니다.

품질은 압도적이었지만 채택하지 않았습니다. 청크마다 따로 호출하는 구조에서는 프롬프트가 매번 반복되어 비용이 커지고, 무엇보다 오디오가 외부로 나가게 되어 전사 후 즉시 폐기한다는 원칙과 충돌합니다. 나중에 다시 검토한다면, 실시간 전사는 Whisper로 유지하고 회의 종료 후 최종 회의록용 전사만 한 번 호출하는 하이브리드 구조를 먼저 고려해 볼 생각입니다.


5. 최종 선택: "서버를 두 배로 올릴 이유가 없었다"

전사는 서버에서 직렬로 처리되기 때문에, RTF × 동시 발화 인원이 1.0 을 넘으면 실시간 처리가 밀립니다. 그래서 RTF 를 "몇 명까지 감당할 수 있는가"로 환산해 비교했습니다.

구성CERRTFRSS감당 인원비고
small + 90초 배치49.91%0.2841089MB4명+채택
small + 30초 (기존)61.52%0.2841089MB3명기준
한국어 파인튜닝54.47%0.6201337MB1명배치를 늘리면 악화
large-v3-turbo49.41%0.8851655MB1명서버 2배 필요
Gemini24.24%———오디오 외부 전송

결정적인 비교는 첫 줄과 large-v3-turbo 줄입니다. large-v3-turbo 로 49.41% 를 얻으려면 서버 사양을 두 배로 올려야 하는데, small + 90초 배치가 거의 같은 49.91% 를 현재 서버에서 냅니다. 게다가 감당 인원도 더 많습니다. 서버를 올릴 이유가 없었습니다.

최종적으로 확정한 설정은 다음과 같습니다.

  • 고정 프롬프트 제거: 대조군으로 과적합을 확인 (1편)
  • 모델은 small 유지: 대안이 더 나쁘거나 서버 예산을 초과
  • VAD 켬: 런타임 업그레이드 후 켠 쪽이 더 빠름
  • CPU 스레드 1 → 2: 메모리는 그대로, RTF 개선
  • 전사 배치 30초 → 90초: −11.6pp, 전사 시간 약 30% 감소

6. 결과

구분작업 전작업 후
CER67.83%49.91%
RTF0.7870.284
실시간 감당 인원3명4명+
회의 종료 blocking447초25초
서버 / 모델Lightsail 2GB / small변경 없음

7. 마치며: 틀린 판단에서 배운 것

돌아보면 이번 작업에서 가장 많이 배운 것은 맞힌 판단이 아니라 틀린 판단들이었습니다.

  • "길이비 0.78 = 세그먼트 폐기" → 폐기 임계값을 전부 풀어도 출력이 그대로였습니다. 진짜 원인은 청크 길이였습니다.
  • "모델 대안은 전부 기각됐다" → 런타임이 1년 4개월 낡아서 그렇게 보였을 뿐입니다.
  • "small 의 CER 변동은 측정 노이즈" → 품질을 8.9pp 떠받치는 재시도가 자주 걸린다는 신호였습니다.

이 경험을 통해 몇 가지 원칙이 생겼습니다. 기각하기 전에 도구 버전부터 확인하고, 기각할 때는 반드시 근거를 함께 기록합니다. 실제로 이번 작업 중에는 근거 없이 "기각됨"으로만 적혀 있던 항목이 한 번도 측정된 적 없었던 경우도 있었고, 그 기록이 재측정을 막고 있었습니다. 그리고 수치가 흔들리거나 예상과 다를 때는 그 값보다 원인을 먼저 봐야 한다는 것도 배웠습니다.

마지막으로, CER 49.9%라는 숫자 자체보다 중요한 것은 제품이 무엇을 요구하는가였습니다. 전사가 요약의 입력이라는 기준으로 보면, 표기가 조금 흔들리더라도 사라졌던 논의가 돌아오는 것이 훨씬 큰 개선이었습니다. 앞으로도 수치를 쫓기 전에, 그 수치가 서비스에서 무엇을 의미하는지부터 고민해 보려고 합니다.

Comments (0)

댓글을 불러오는 중...