2년 전쯤 Bert-VITS2 라는 일본어 TTS 라이브러리를 알게 돼서, 학습 방법을 한번 정리한 글을 적은 적이 있습니다.
그 후로는 학습한 모델을 홈 클러스터 서버에 올려 평소에 쓰고 있었습니다. 원본 코드는 Fork 한 뒤, 음성 합성에 필요한 path 만 따로 추려내 정리해서 쓰고 있었습니다.
그런데 시간이 지나니 작은 불편들이 슬슬 쌓이기 시작했습니다.
- 일본어 텍스트를 발음 정보로 바꿔 주는
openjtalk라이브러리가, 외부에서 뭔가를 계속 내려받는데 그게 어떻게 돌아가는지는 사실 잘 모름. 일단 쓰고는 있었지만, 혹시 저 다운받는 서버가 죽기라도 하면 내 서버도 같이 죽을 것 같아 영 찜찜했습니다. openjtalk때문에 Python 버전업이 막혀 있었음. 의존성 호환 문제로 한참 동안 올리지 못한 채 방치 중이었습니다.- TTS 가 영어를 지원하지 않음 (묵음으로 나옴). 앞단에 번역 레이어를 붙여 일본어로 한번 옮긴 다음 합성하는 식으로 우회하고 있었는데, 동작은 했지만 소소하게 거슬렸습니다.
- 그리고 결정적으로, 기존 라이브러리는 대부분 fish-speech 라는 새 라이브러리로 넘어갔고, 이를 계승한 Style-Bert-VITS2 (SBV2) 도 8개월 전에 업데이트가 멈춘 상태 였습니다.
어차피 유지만 할 거라면, 이 기회에 한번 다 갈아엎고 내가 쓰기 편한 형태로 만들어 보자.
가 이 프로젝트의 시작이었습니다. 그리고 이왕 손대는 김에, 모델 코드까지 깊숙이 들여다보고 최적화 여지가 있는지 공부해 보자는 것이 두 번째 목적이었습니다.
결과물 — hayakoe
- Repository: github.com/LemonDouble/hayakoe
- Documentation: lemondouble.github.io/hayakoe
- PyPI:
pip install hayakoe
대략 이렇게 쓰는 라이브러리가 됐습니다.
from hayakoe import TTS
tts = TTS().load("jvnv-F1-jp").prepare()
tts.speakers["jvnv-F1-jp"].generate("こんにちは").save("output.wav")특징을 압축하면:
openjtalk외부 다운로드 제거 — 사전을 함께 패키징해 첫 실행에도 외부 서버 의존 없음- 최신 Python 지원 — Python 3.x 신버전 환경에서 바로 설치/동작
- 영어 입력 직접 합성 / 커스텀 사전 등록 — 22만 엔트리 외래어 사전 내장으로 영어→카타카나 자동 변환, 모르는 고유명사는 직접 발음 등록 가능
- CPU 실시간 추론 — PyTorch 대비 1.5× ~ 3.6× 빠르고, RAM 은 절반 정도 (5,122 MB → 2,346 MB, -54%) 로 감소
- torch 불필요 — CPU 추론에서는 PyTorch 없이 동작, 의존성 가벼움
- 간결한 API —
TTS().load(...).prepare()한 줄로 시작 / HuggingFace transformers 스타일 - 소스 플러그형 —
hf://,s3://,file://를 한 인스턴스에서 섞어 쓸 수 있음 - FastAPI 서버 통합 지원 — 동기 / 비동기 핸들러 모두 지원
- ARM64 지원 — 라즈베리 파이 4B 위에서도 동작, 다만 속도는 느림
“그래서 뭘 했길래 더 빨라진 걸까요?”
처음에는 양자화나 한번 돌려볼까 정도의 생각으로 시작했습니다.
weight 파일 크기만 봐도 BERT 가 모델에서 차지하는 비중이 가장 크더군요. 그래서 “이거 양자화만 하면 되겠지?” 싶었는데, 막상 측정해보니 그게 아니었습니다.
- 텍스트가 길어질수록 Synthesizer 가 CPU 시간의 80~91% 를 가져가고, BERT 양자화로 줄어드는 시간은 전체의 5% 도 안 됐습니다.
- BERT INT8 양자화는 속도가 아니라 메모리 절감 (1,698 MB → 368 MB, -78%) 에 실제 가치가 있었습니다.
- “그럼 Synthesizer 를 양자화하면 되겠다” 싶었는데, Synthesizer 는 양자화 자체가 불가능했습니다 — Flow 레이어 안의
rational_quadratic_spline이 FP16 부터 수치 불안정으로 깨지기 때문입니다.
즉, 어디가 실제 병목인지 정확히 측정하지 않으면 엉뚱한 부분에 시간만 쏟게 된다는 것을 매번 다시 배운 셈입니다.
그래서 결국 작업은 대략 이런 흐름으로 흘러갔습니다.
모델부터 다시 고르기 — 2년이란 시간이 지났으니 어차피 고쳐 쓸 거라면 다른 라이브러리도 한번 둘러보자 싶었습니다. 한국어 지원이 잘 된다는 GPT-SoVITS, autoregressive 계열 (Qwen-3-TTS 등) 을 후보로 검수했습니다. 실시간 알람/브리핑 용도라 지연 시간이 나름 중요한 팩터였고, 결국 음질·속도 균형이 가장 좋았던 Style-Bert-VITS2 (JP-Extra v2.7.0) 로 정착했습니다.
- GPT-SoVITS — 실시간 속도는 만족스러웠지만 추론 음성의 체감 품질이 SBV2 보다 떨어져 탈락.
- Qwen-TTS — 연산량이 너무 많아서 탈락. 기존 GPU 가 RTX 2070 Super 였는데 flash-attn 이 RTX 3000 번대 이상 카드에서만 지원되길래, “이거 flash-attn 안 깔아서 생긴 문제인가?” 싶어 RTX 3080 까지 새로 사 봤지만 생성시간이 오래 걸려 실시간 사용은 무리라는 결론을 냈습니다.
컴포넌트별로 다른 처방 적용 — 측정 결과를 바탕으로 BERT 와 Synthesizer 에 각각 다른 최적화를 적용했습니다.
- BERT — INT8 dynamic quantization 으로 메모리만 줄임. 속도엔 큰 영향 없지만, 한 서버에서 여러 프로그램 돌리려면 메모리가 꽤 중요해서요.
- Synthesizer — ONNX 로 변환해 그래프 수준 최적화를 적용했습니다. ONNX Runtime 이 kernel fusion (여러 연산을 하나로 합치기), constant folding (상수 연산을 로드 시점에 미리 계산) 같은 최적화를 자동으로 적용해 줍니다.
자잘한 디테일 마무리 — 큰 줄기 외에 세부 사항들도 함께 정리했습니다.
torch.compile— GPU 추론에서 PyTorch JIT 컴파일로 속도 추가 향상.- BERT 배치 추론 — 다문장 합성 시 BERT 호출을 한 번으로 묶어 오버헤드 감소.
- 자연스러운 pause 복원 — 원본 SBV2 는 구두점 뒤에 자연스러운 쉼이 들어가는데, 다문장을 분할 합성하면 그 쉼이 사라집니다. Duration Predictor 만 따로 ONNX 로 export 해 구두점 위치의 쉼 길이를 예측하도록 처리했습니다.
- ARM64 빌드 — 라즈베리 파이 4B 같은 ARM 기기에서도 동작하도록 사전 빌드.
라이브러리로서의 설계 — 단순히 동작하는 데서 그치지 않고, 다른 사람도 쉽게 가져다 쓸 수 있게 만드는 부분에 신경 썼습니다.
- 화자별 메모리 분리 — BERT, WavLM 같은 공유 리소스는 한 번만 로드하고, 화자마다 자기 가중치만 따로 들고 있도록 구조화. 화자가 늘어도 메모리가 폭증하지 않습니다.
- multi-source 지원 — 한 인스턴스에서
hf://,s3://,file://를 섞어서 사용 가능. 공식 모델은 HuggingFace, S3 를 이미 쓰고 있다면 S3, 개발 중인 모델은 로컬 식으로. - Thread-safe 싱글톤 서빙 — FastAPI 서버에서 하나의 TTS 인스턴스를 여러 핸들러가 공유해도 안전.
- Docker 빌드 패턴 — 빌드 시점에 모델을 이미지에 포함시키고, 런타임에는 캐시에서 즉시 로드.
앞으로 쓸 글
이 시리즈는 4부작으로 나눠 작성할 예정입니다.
- (Part 1) 2년만에 다시 TTS 모델 이것저것 보다 또 다시 VITS로 정착한 이야기 GPT-SoVITS v4 를 시도했다가 왜 갈아탔는지, 학습 데이터셋을 어떻게 모았는지, 10개 체크포인트 청감 평가로 최적 step 을 어떻게 정했는지.
- (Part 2) 메모리는 절반으로, 속도는 1.5배 빠르게 한 이야기 병목 측정 방법론, 양자화 실험 (왜 Synthesizer 는 양자화가 안 되는가), ONNX 변환, 그리고 메모리가 89MB 로 잘못 측정됐던 사건.
- (Part 3) 마지막 1% 까지 — torch.compile, 배치 추론, Pause 복원, ARM64 GPU 추론 잔디 다듬기와, Duration Predictor 만 따로 떼서 ONNX 로 export 한 이야기.
- (Part 4) 라이브러리로 패키징하면서 고민한 것들
API 설계 철학, multi-source (
hf:///s3:///file://), thread-safe 싱글톤 서빙, FastAPI / Docker 배포.
서론을 마치며
처음에 학습 글을 썼을 땐 “이거 한 번 학습하고 끝” 정도로 가볍게 생각했었는데, 막상 모델 손에 쥐고 보니 학습 그 자체보다 이걸 어떻게 다른 사람도 쉽게 가져다 쓰게 만들지 가 훨씬 더 큰 일이었습니다.
그 과정에서 평소 다뤄보지 않았던 양자화·ONNX 그래프·ML 서빙 영역까지 발을 들이게 됐는데, 기왕 한 김에 정리해 두면 비슷한 길을 가는 분께도 도움이 될 것 같아 시리즈로 남겨봅니다.

Comments