ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Whisper Small에서 Turbo로 — 아이 발음을 위한 STT 모델 선택
    IT 2026. 5. 10. 21:00
    Whisper Small에서 Turbo로 — 아이 발음을 위한 STT 모델 선택

    아들에게 작은 음성 챗봇을 만들어줬습니다. 마이크 버튼 하나, 말로 묻고 답을 음성으로 듣는 단순한 화면입니다. 그런데 처음 며칠 동안 가장 자주 듣는 소리가 이거였습니다.

    "잘 못 들었어요. 다시 한번!"

    옆에서 들을 땐 분명히 "공룡은 왜 멸종했어?"라고 또박또박 말했는데, 화면에는 엉뚱한 글자가 찍혀 있곤 했습니다. 같은 챗봇 백엔드를 쓰는 제 메인 챗봇은 멀쩡한데, 아이 목소리만 들어가면 자꾸 헛소리를 합니다. 모델을 의심해봐야 했습니다.

    Whisper-small이 아이 목소리에 약한 이유

    저는 음성 인식기로 OpenAI의 Whisper를 씁니다. Whisper는 오픈소스로 풀려 있는 다국어 음성-텍스트(STT, Speech-to-Text) 모델 가족입니다. 작게는 39M(메가) 파라미터의 tiny부터, 가장 큰 large-v3는 1.55B(빌리언, 약 15억) 파라미터까지 일곱 단계가 있습니다.

    가정용 서버에서 실시간 대화를 하려면 너무 큰 모델은 응답이 느려서 못 씁니다. 그래서 처음엔 가장 흔하게 쓰이는 중간 사이즈인 small(244M)을 골랐습니다. 어른 음성에는 충분히 잘 동작하는 평판이 있는 모델입니다.

    문제는 Whisper의 학습 데이터가 대부분 성인 화자의 음성이라는 점입니다. 인터넷에서 모은 다국어 영상·팟캐스트가 주된 재료라서, 발음이 또렷하지 않거나 음역대가 높은 어린이 음성에는 상대적으로 약합니다. 게다가 small처럼 모델 용량이 작을수록 그런 분포 가장자리(tail) 발화에서 오차가 더 커집니다. 큰 모델은 같은 음향 패턴에서도 "이건 이런 단어겠지"라고 더 정밀하게 골라냅니다.

    이 차이가 우리 집 거실에서는 "또박또박 말한 어린이 → 엉뚱한 글자"로 나타났던 셈입니다.

    모델 라인업 — 정확도와 지연의 트레이드오프

    Whisper의 주요 모델 네 개를 정확도·지연·크기 축으로 비교하면 다음과 같습니다.

    Whisper 모델: 지연 vs 정확도 (5초 발화 기준)

    그림 설명 — 가로축은 지연(추론 시간), 세로축은 정확도입니다. 좌상단(빠르고 정확함)이 이상적입니다. 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_sizeinitial_prompt)를 어떻게 같이 적용했는지 이어 다룹니다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.