영상 더빙용 한국어 TTS가 필요했다. ElevenLabs를 쓰면 가장 퀄리티 있고 빠르지만, 비용과 외부 API 의존을 줄이고 싶어서 오픈소스로 직접 워크벤치를 만들어보기로 했다. 품질을 완전히 따라잡겠다는 건 아니고, 사내 더빙 용도로 "충분히 쓸 만한" 수준이 목표다.
모델을 바로 고르기 전에 쓸 만한 한국어 오픈소스 TTS 3개를 먼저 비교해 봤다. 오픈소스 TTS는 많지만, 한국어를 우선순위로 학습하고 상업 이용 라이선스까지 갖춘 모델은 의외로 적다. XTTS-v2나 F5-TTS, Kokoro, Sesame CSM 같은 모델들은 한국어를 지원하더라도 학습 비중이 낮거나 품질 검증이 부족해서 후보에서 빠졌다. 조건을 추리니 Raon-Speech, Supertonic, Qwen3-TTS 이렇게 세 개가 남았다.
이번 포스팅에는 3가지 모델을 비교한 표와, 그중 Qwen3-TTS를 보이스 클로닝용으로 채택하면서 만난 후처리 이슈 핵심 2가지를 다룬다.
후보 3개 비교
| 항목 | Raon-Speech | Supertonic | Qwen3-TTS |
| 출시 | 2026-04 (Krafton) | 2025-11 (Supertone) | 2026-01 (Alibaba) |
| 파라미터 | 9B | 66M | 0.6B / 1.7B |
| 라이선스 | Apache-2.0 | MIT(코드) / OpenRAIL-M(모델) | Apache-2.0 |
| 지원 언어 | 한·영 | 한·영·스·포·프 | 한 포함 10개 + 방언 |
| 추론 환경 | CUDA GPU 필수 | CPU on-device · WebGPU | GPU 권장 |
| 보이스 클로닝 | 화자 조건 / 레퍼런스 연속 생성 | Voice Builder | 3초 클로닝 + Voice Design |
| 강점 | 풀듀플렉스 대화, 한국어 자연스러움 | RTF 0.012 (M4 Pro), 11개 SDK | 10개 언어, 자연어 보이스 디자인 |
요약하면,
- Raon-Speech: 9B짜리 큰 모델. 한·영 100만 시간 학습. 풀듀플렉스 대화(turn-taking, 끼어들기) 가능. GPU 필수.
- Supertonic: 66M 초경량. 모바일/웹 어디서든 돈다.
- Qwen3-TTS: 3초만 들려주면 화자 모방 가능. 자연어 instruction으로 "차분하게", "밝게" 같은 표현력 조절 가능.
[ Supertonic + Qwen3-TTS 조합 채택 ]
처음엔 Raon-Speech가 한국어 학습량(100만 시간)으로는 가장 매력적이었다. 그런데 9B 모델 + CUDA 필수라는 게 발목을 잡았다.
로컬 맥북(MPS)에서 돌릴 수 있는 게 사실상 Supertonic / Qwen3-TTS 둘이었다.
그래서 결국 목적별로 나눠 쓰기로 했다.
- 빠른 더빙 → Supertonic (프리셋 보이스 10개)
- 보이스 클로닝 → Qwen3-TTS (3초 reference로 화자 모방)
Qwen3-TTS 후처리 이슈
클로닝 품질 자체는 인상적인데, 합성 결과를 그대로 쓰기엔 아티팩트가 줄줄이 나왔다. 후처리는 vocal-fry 제거, tail trim, fade in/out, 묵음 조정 4단계로 했는데, 여기서는 가장 까다로웠던 두 가지만 다룬다.
1. 도입부 vocal-fry — "엄~" 소리 제거
Qwen3-TTS는 reference 음성을 보고 화자의 prosody(억양, 음색)를 모방하는데, 이 prosody buildup 과정에서 클립 맨 앞에 "엄~" 같은 vocal-fry가 약 200~300ms 정도 끼어든다. 테스트 음성 reference로 50번 합성해 보니 거의 매 클립마다 재현됐다.
1차 시도: 무식한 fixed chop
처음엔 단순하게 앞부분 250ms를 그냥 잘라냈다.
HARD_CHOP_LEAD_MS = 250
def hard_chop_lead(audio, sr, ms=HARD_CHOP_LEAD_MS):
n_drop = int(sr * ms / 1000)
return audio[n_drop:] if n_drop < audio.shape[0] else audio
대부분의 클립에서는 잘 동작했다. 그런데 합성 결과를 더 들어보니 앞쪽에 묵음이 길게 붙어 나오는 클립이 종종 있었다. 이런 클립에서는 250ms를 잘라도 vocal-fry 본체는 그대로 남고, 결과적으로 앞쪽 묵음만 깎이고 말았다.
2차 시도 — onset-aware chop
"음성이 실제로 시작되는 지점을 먼저 찾고, 거기서부터 300ms를 자르자"전략으로 바꿨다.
10ms 윈도우로 RMS를 계산해서 -45dB을 처음 넘는 지점을 voice onset으로 잡고, 거기서부터 300ms를 잘라낸다. 묵음이 긴 클립이든 짧은 클립이든 vocal-fry만 정확히 깎이는 것처럼 보였다.
그런데 여기서도 엣지 케이스가 있었다. "오늘", "아침" 처럼 모음으로 시작하는 단어는 attack이 vocal-fry의 RMS 패턴과 비슷해서, onset-aware가 vocal-fry가 아닌 첫 음절 자체를 잘라버리는 경우가 생겼다.
결국 RMS 하나만으로는 fry와 정상 모음 attack을 구분할 수 없었다. 그래서 조건을 두 개 추가했다. head 에너지가 tail보다 절반 이하인 ramp 패턴이거나, head가 tail보다 5dB 이상 크면서 -27dB 미만인 attack 패턴일 때만 chop하도록 바꿨다. 두 조건 중 하나라도 해당하지 않으면 자르지 않는다.
2. 로봇 같은 호흡 — sentence-aware 묵음
세그먼트 사이에 똑같은 0.5s 무음을 넣었더니 읽는 호흡이 로봇 같아졌다. 사람은 마침표 뒤에서 길게, 쉼표나 이어지는 구절에서는 짧게 쉰다. 따라서 세그먼트 마지막 글자를 보고 묵음 길이를 다르게 줬다. 그 결과 청취 인상이 확 달라졌다.
SENTENCE_END_PAUSE_S = 1.50 # . ! ? 다음
CONNECTOR_PAUSE_S = 0.70 # 그 외
SENTENCE_END_CHARS = {".", "!", "?", "。", "!", "?"}
구현 아키텍처) Qwen3-TTS를 사이드카로 격리
Qwen3-TTS는 Python 3.12와 transformers + torch + qwen-tts 의존성을 요구한다. Supertonic는 Python 3.11 + ONNX 환경이라 한 venv에 묶을 수 없었고, 묶을 수 있다 해도 가벼운 메인 서버에 수백 MB짜리 ML 의존성을 끌어들일 이유가 없었다. 그래서 메인서버에는 Supertonic를 두고, 별도 사이드카(포트 8001)로 분리하고, 메인 서버는 HTTP 프록시만 하도록 했다.
web (3100) → server (8000, FastAPI)
├─ Supertonic (in-process)
└─ Qwen3-TTS → server-qwen (8001) ← 모델은 여기만
덕분에 메인 서버는 의존성을 가볍게 유지하고, Qwen 모델 재시작도 사이드카만 띄웠다 내렸다 하면 돼서 개발이 훨씬 편해졌다.
정리
모델 선택
한국어 오픈 TTS 3개를 두고 봤을 때, 로컬 워크벤치 용도라면 Supertonic + Qwen3-TTS 조합이 현실적이다. Raon-Speech는
학습 스펙(100만 시간)은 매력적이지만 GPU 서버 전제가 부담이다.
Qwen3-TTS 후처리 핵심 2가지
- onset-aware chop — 음성 시작점에서 300ms를 잘라 vocal-fry 제거
- sentence-aware pause (1.50s / 0.70s) — 마침표 뒤엔 길게, 그 외엔 짧게
결과물
놀면뭐하니의 뉴스 앵커 클립에서 나오는 유재석 목소리 10초를 reference로 등록해서 아래 스크립트를 합성했다.
오늘 서울의 날씨를 전해 드리겠습니다.
아침에는 영상 7도, 한낮에는 18도까지 오르겠습니다.
다만 오후부터 서쪽 지방을 중심으로 비가 조금씩 시작되겠습니다.