-
Whisper Small에서 Turbo로 — 아이 발음을 위한 STT 모델 선택IT 2026. 5. 10. 21:00
아들에게 작은 음성 챗봇을 만들어줬습니다. 마이크 버튼 하나, 말로 묻고 답을 음성으로 듣는 단순한 화면입니다. 그런데 처음 며칠 동안 가장 자주 듣는 소리가 이거였습니다.
"잘 못 들었어요. 다시 한번!"
옆에서 들을 땐 분명히 "공룡은 왜 멸종했어?"라고 또박또박 말했는데, 화면에는 엉뚱한 글자가 찍혀 있곤 했습니다. 같은 챗봇 백엔드를 쓰는 제 메인 챗봇은 멀쩡한데, 아이 목소리만 들어가면 자꾸 헛소리를 합니다. 모델을 의심해봐야 했습니다.
Whisper-small이 아이 목소리에 약한 이유
저는 음성 인식기로 OpenAI의 Whisper를 씁니다. Whisper는 오픈소스로 풀려 있는 다국어 음성-텍스트(STT, Speech-to-Text) 모델 가족입니다. 작게는 39M(메가) 파라미터의
tiny부터, 가장 큰large-v3는 1.55B(빌리언, 약 15억) 파라미터까지 일곱 단계가 있습니다.가정용 서버에서 실시간 대화를 하려면 너무 큰 모델은 응답이 느려서 못 씁니다. 그래서 처음엔 가장 흔하게 쓰이는 중간 사이즈인
small(244M)을 골랐습니다. 어른 음성에는 충분히 잘 동작하는 평판이 있는 모델입니다.문제는 Whisper의 학습 데이터가 대부분 성인 화자의 음성이라는 점입니다. 인터넷에서 모은 다국어 영상·팟캐스트가 주된 재료라서, 발음이 또렷하지 않거나 음역대가 높은 어린이 음성에는 상대적으로 약합니다. 게다가
small처럼 모델 용량이 작을수록 그런 분포 가장자리(tail) 발화에서 오차가 더 커집니다. 큰 모델은 같은 음향 패턴에서도 "이건 이런 단어겠지"라고 더 정밀하게 골라냅니다.이 차이가 우리 집 거실에서는 "또박또박 말한 어린이 → 엉뚱한 글자"로 나타났던 셈입니다.
모델 라인업 — 정확도와 지연의 트레이드오프
Whisper의 주요 모델 네 개를 정확도·지연·크기 축으로 비교하면 다음과 같습니다.
그림 설명 — 가로축은 지연(추론 시간), 세로축은 정확도입니다. 좌상단(빠르고 정확함)이 이상적입니다. Small은 빠르지만 어린이 발음에서 약합니다. Large-v3는 정확하지만 느립니다(우리 환경에선 5초 발화에 1.5~2.5초 추론). Turbo가 좌상단의 sweet spot에 있습니다 — small에 가까운 속도로 large-v3에 가까운 정확도를 냅니다. 점선 화살표는 이번 글에서 한 교체 방향입니다.
Turbo 모델의 정체 — 디코더만 슬림화한 large-v3
OpenAI는 2024년 9월에
large-v3-turbo(별칭turbo)라는 모델을 추가로 공개했습니다. 이름 그대로large-v3의 빠른 버전인데, 흥미로운 점은 디코더 층 수만 줄였다는 것입니다.Whisper 같은 인코더-디코더 구조에서 인코더는 음성 신호를 잠재 표현으로 압축하고, 디코더는 그 표현을 단어 토큰으로 옮깁니다. 인코더는 그대로 두고 디코더만 32층에서 4층으로 줄였더니 — 정확도는 large-v3의 95~98%를 유지하면서, 추론 속도는 약 4배 빨라졌습니다. 파라미터도 1.55B에서 809M으로 절반에 가깝게 줄었고요.
이게 가능했던 이유는 직관적입니다. 음성을 잘 "듣는" 부분(인코더)이 정확도의 대부분을 결정하지, 그것을 단어로 "옮기는" 부분(디코더)은 적당히 깊으면 충분하다는 것이죠. Speculative decoding이나 distillation 같은 다른 가속 기법과 달리, 그냥 학습된 모델의 디코더 층을 잘라내고 미세 조정한 단순한 접근입니다. 그래서 동작도 large-v3와 거의 동일하게 흉내 냅니다.
가정용 서버에서 어린이 음성 챗봇을 굴리는 입장에선 이거야말로 찾던 모델입니다.
적용 — 환경변수 한 줄과 디코딩 옵션 한 줄
저는 Whisper를 데몬 프로세스로 띄워두고 챗봇이 HTTP로 호출하는 구조로 운영합니다. 모델 이름은 환경변수로 받아 띄우고, 디코딩 옵션은 transcribe 함수 호출 인자로 줍니다. 변경한 곳은 두 군데입니다.
# whisper_daemon.py 일부 # turbo: large-v3-turbo (809M) — 정확도 ≈ large-v3, 지연 ≈ small. # 어린이 음성 인식을 위해 small에서 승격. MODEL_NAME = os.environ.get("WHISPER_MODEL", "turbo") LANG = os.environ.get("WHISPER_LANGUAGE", "ko") DEVICE = os.environ.get("WHISPER_DEVICE", "cuda") def load_model(): global _model if _model is not None: return _model import whisper _model = whisper.load_model(MODEL_NAME, device=DEVICE) return _model코드 설명 —
WHISPER_MODEL기본값을"small"에서"turbo"로 바꾼 것이 핵심입니다. 환경변수로 빼둔 이유는 모델 비교 실험을 systemd 유닛 파일만 고쳐서 돌릴 수 있게 하기 위함입니다.load_model()은 한 번 로드한 후 모듈 전역에 캐시해서 재호출 시 즉시 반환합니다 — 첫 요청에서만 모델 로딩 비용을 치르고, 그다음부터는 GPU에 올라간 상태 그대로 추론합니다.def transcribe_blocking(audio_path: str) -> str: model = load_model() result = model.transcribe( audio_path, language=LANG, beam_size=5, fp16=(DEVICE == "cuda"), condition_on_previous_text=False, initial_prompt=INITIAL_PROMPT or None, ) return (result.get("text") or "").strip()코드 설명 — 실제 추론을 수행하는 함수입니다. 모델 교체와 함께
beam_size=5(디코딩 후보 다양성)와initial_prompt(어휘 편향)도 같이 적용했는데, 이 두 가지는 별도 글에서 따로 다룰 예정입니다. 지금 글에서 핵심은 모델 자체를 바꾼 첫 줄(load_model 안의 모델 이름)이고, 나머지는 그 모델의 잠재력을 더 끌어내기 위한 보조 장치라고 생각하시면 됩니다.모델 파일은 처음 호출할 때 자동으로 캐시 디렉토리에 다운로드됩니다.
turbo는 약 1.6GB이고, GB10 GPU에서 로드하는 데 5초 정도 걸립니다. 이후 메모리에 상주하면서 idle 타임아웃 전까지는 이어지는 모든 요청을 빠르게 처리합니다.실제 사용 결과
아이가 며칠 동안 같은 챗봇을 쓰는 동안 체감 변화는 분명했습니다. "잘 못 들었어요. 다시 한번!"을 듣는 빈도가 눈에 띄게 줄었습니다. 똑같은 발화를 두 모델에 동시에 통과시키면,
small이 자주 빠뜨리던 받침이나 발음 변화가turbo에서는 안정적으로 잡혔습니다.지연은 거의 그대로였습니다. 5초 발화에 small이 0.3~0.5초였다면 turbo는 0.4~0.6초 — 차이를 인지하기 어려운 수준입니다. GB10에서는 turbo의 작은 디코더 덕분에 small과의 격차가 더 좁아지는 것 같습니다.
다만 트레이드오프는 있습니다.
- 모델 다운로드 1.6GB — small의 약 6배 크기. 처음 한 번이지만 인터넷 환경에 따라 시간이 걸립니다.
- GPU 메모리 점유 — small은 약 2GB, turbo는 약 5GB를 씁니다. 다른 GPU 작업과 자원 경쟁이 있다면 스케줄러로 조정해야 합니다.
- 모델 재시작 시 콜드 스타트 — idle 타임아웃 후 다시 깨울 때 5초 정도 모델 로딩이 필요합니다. 이 비용은 데몬 프로세스 + 사전 워밍업으로 가립니다(이것도 다른 글의 주제).
마치며 — 트레이드오프 곡선의 구석을 찾는 일
모델 선택은 보통 "정확도 vs 속도" 두 축의 trade-off로 설명됩니다. 그래서 머릿속에 일직선이 그려지죠 — 한쪽을 얻으려면 다른 쪽을 내줘야 한다는 그림입니다. 그런데 실제로는 곡선이고, 가끔 곡선의 구석에 작고 흥미로운 점이 하나 추가됩니다.
turbo같은 모델이 그렇습니다.제 경우엔 그저 "디폴트로 small을 쓰고 있었으니 그대로 가야겠다"고 생각했고, 실제로 두 달 가까이 그렇게 썼습니다. 사용자(아이)가 어려워하는 모습을 며칠 본 뒤에야 모델 라인업을 다시 들여다봤고, 그제야
turbo의 존재를 발견했습니다. 환경변수 한 줄을 바꾸는 데 5초 걸렸고, 효과는 그날 저녁부터 확인됐습니다."같은 도구라도 사용자 분포 가장자리에서 다시 검토하라"는 평범한 결론이지만, 이번엔 그게 어린이 목소리라는 구체적인 모습으로 다가왔습니다. 다음 글에서는 같은 맥락에서 디코딩 옵션 두 가지(
beam_size와initial_prompt)를 어떻게 같이 적용했는지 이어 다룹니다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
JSON으로 페르소나를 운영한다는 것 — 호칭 규칙부터 퀴즈 상태머신까지 (0) 2026.05.11 위임받는 쪽의 30줄 — X-Bot-ID 주입과 X-Session-ID 양방향 전파 (0) 2026.05.11 음성 챗봇이 '구독 부탁드립니다'를 말한 날 — STT 환각과 false-negative bias (0) 2026.05.11 Whisper 디코더에 어휘를 박아 넣기 — initial_prompt 한 줄의 효과 (1) 2026.05.10 Whisper의 빔 서치를 살리는 한 줄 — beam_size 1과 5의 차이 (0) 2026.05.10 로컬 챗봇 시리즈 #12 (완) — 정책을 데이터로 표현하기: jobs.conf 한 줄이 모든 GPU 동거 정책을 결정한다 (0) 2026.05.09 로컬 챗봇 시리즈 #11 — Esc 한 키가 깨끗해야 한다: UI 임시 상태의 우선순위 스택 디자인 (0) 2026.05.09 로컬 챗봇 시리즈 #10 — [hidden] 속성이 안 먹는 한 시간: HTML5의 작은 속성과 컴포넌트 CSS의 충돌 (0) 2026.05.09 로컬 챗봇 시리즈 #9 — MCP 외부 서버 장애를 graceful하게 흡수하는 디자인: '채팅이 안 막히는 게 우선' (0) 2026.05.09 로컬 챗봇 시리즈 #8 — 봇은 도구 풀을 좁히는 장치다: '지식금고 검색가' 한 도구 봇이 가장 효과적인 이유 (0) 2026.05.09