맥미니 M4 Pro 로컬 TTS 4종 비교 — 숫자는 Qwen3가 이겼는데, 결국 쓰는 건 Higgs Audio v3다

“맥미니로 로컬 TTS 되나요?” 이 질문에 답이 달린 글은 많다. 그런데 이상하게 결론이 다 다르다.
어떤 글은 Qwen3-TTS가 최고라 하고, 어떤 글은 Fish Audio 아니면 안 된다고 한다. 왜일까? 🤔

샘플 문장이 다르고, 참조 음성이 다르고, 결정적으로 음량이 다르기 때문이다. 사람 귀는 큰 소리를 더 좋게 듣는다.
그러니 조건이 제각각인 비교는 애초에 비교가 아니다.

그래서 직접 만들었다. 같은 문장, 같은 참조 음성, 같은 음량으로 네 모델을 나란히 듣는 웹앱을 클로드 코드로 짰다.
이 글은 그 구축 과정과, 실제로 써보고 정리한 모델별 장단점이다. 숫자는 전부 내 맥미니 M4 Pro에서 직접 잰 값이다.

한 줄 답변
맥미니 M4 Pro 64GB에서 한국어 로컬 TTS 4종(Qwen3-TTS Base·CustomVoice, Higgs Audio v3, Fish Audio S2 Pro)은 전부 mlx-audio로 돌아간다. 속도·메모리·라이선스는 Qwen3 계열이 압도적으로 유리하다. 그런데도 내가 채택한 건 가장 무겁고 느린 Higgs Audio v3다. 표에 없는 “자연스러움”이 표에 있는 단점을 전부 이겼기 때문이다.

맥미니 M4 Pro로 로컬 TTS가 돌아가긴 하나?

돌아간다. 다만 조건이 하나 붙는다.
이 맥에는 LLM 서버가 24시간 떠 있다. TTS가 메모리를 독점할 수 없다는 뜻이고, 이 제약이 뒤에 나오는 설계 대부분을 결정했다.

항목 구성
기기 Mac Mini M4 Pro · 통합 메모리 64GB
추론 MLX (mlx-audio)
백엔드 FastAPI + SQLite
프론트 React + Vite
가중치 합계 약 26GB

대상 모델은 넷이다. 셋으로 시작했다가 넷이 됐는데, 이유는 뒤에서 나온다.

  • Qwen3-TTS 12Hz 1.7B Base — Apache 2.0
  • Qwen3-TTS 12Hz 1.7B CustomVoice — Apache 2.0
  • Higgs Audio v3 TTS 4B — 비상업
  • Fish Audio S2 Pro — 비상업

클로드 코드로 무엇을 만들었나?

화면은 일곱 개지만 하는 일은 두 가지다. 비교하고, 만든다.

비교 모드는 참조 음성 하나와 문장 세트 하나를 골라 여러 모델에 똑같이 먹인다. 결과는 행=문장, 열=모델 격자로 나온다. 나란히 듣고 점수를 매기고, 모델명을 가린 블라인드 A/B도 있다. 끝나면 CSV로 뽑는다.

제작 모드는 모델을 정한 뒤에 쓴다. 대본을 통째로 붙여넣으면 잘라서 보여주고, 구간별로 감정을 지정하고, 아직 안 만든 덩어리만 생성해서 wav 묶음과 manifest.csv로 내보낸다.

여기에 ASR 자동 채점이 붙는다. 생성한 음성을 Qwen3-ASR로 다시 받아쓰고 원문과 글자 오류율(CER)을 계산한다.
귀로만 판단하면 스무 개쯤부터 기준이 흔들리기 때문이다.

만들면서 뭐가 제일 골치 아팠나?

모델을 “내렸다 올리는” 게 진짜 되는지부터 확인해야 했다. 파이썬에서 참조만 끊는다고 GPU 메모리가 돌아온다는 보장이 없다. 재봤더니 세 모델 모두 unload 후 0.00GB로 전부 반환됐다. 순서가 중요했다.

def unload(self) -> None:
    self._model = None
    gc.collect()
    import mlx.core as mx
    mx.clear_cache()

이게 확인되니 나머지가 편해졌다. 배치가 끝나면 내리고, 다른 모델이 필요하면 교체한다. 재로드 비용 1.4~9.8초는 배치 한 번(수십 초~수 분)에 비하면 무시할 만하다. fish_s2는 로드 피크가 15.33GB까지 올라가서, 둘을 겹쳐 올렸다면 64GB에서도 아슬아슬했을 것이다.

참조 음성 형식이 모델마다 다르다

qwen3와 higgs3는 파일 경로를 그대로 받는데, fish_s2는 아니다. AttributeError: 'str' object has no attribute 'ndim'이 뜬다. README 예시는 경로인데 실제 코드는 배열을 기대하고, 코덱은 44.1kHz를 원한다(다른 둘은 24kHz).
그래서 업로드본은 원본 샘플레이트로 저장하고, 모델별 변환본을 캐시하는 구조가 됐다.
설계 초안의 “16kHz로 정규화”는 버렸다. 거기서 낮추면 fish_s2용으로 다시 올려야 하는데, 없던 정보가 생기지는 않으니까.

음량 정규화, 비교와 제작이 정반대다

같은 문장·같은 참조 음성인데 모델 간 출력 음량이 최대 10.3dB 벌어졌다.
정규화 없이 들려주면 순위가 음량순으로 정해질 위험이 크다. 그래서 비교 화면은 클립마다 정규화한다.

그런데 제작에서 같은 걸 하면 안 된다. 같은 모델·같은 목소리로 감정만 바꿨더니 rms가 기쁨 0.1473 / 속삭임 0.0120으로 12배 벌어졌다. 이건 오차가 아니라 사용자가 지시한 표현이다. 클립마다 맞추면 속삭임이 환호만큼 커진다. 결국 비교는 클립별, 제작은 대본 전체 단일 이득으로 갈랐다.

문장 분할은 자르기가 아니라 묶기가 맞다

짧게 문장을 자를수록 손해다. 호출마다 고정 비용이 붙기 때문이다.

덩어리 길이 RTF (qwen3)
2자 1.59
20자 0.55
51자 0.40

같은 내용을 문장별로 4회 만들면 4.30초, 합쳐서 1회면 2.50초다. 재미있는 건 합친 쪽 음성이 더 길다는 것(6.24 vs 5.76초). 모델이 문장 사이에 호흡을 넣어주기 때문이다. 그래서 문장 경계를 찾은 뒤 길이 상한까지 다시 묶는 구조가 됐다.
긴 문장을 만들때 이득은 qwen3 20%, higgs3 12%, fish_s2 3%로 모델마다 달랐다.

한 모델만 조용히 무너졌다

묶는 게 이득이면 얼마나 길게 묶을 수 있을까. 생성한 음성을 ASR로 받아쓰고 원문과 대조했다.

입력 길이 qwen3 higgs3 fish_s2
192자 CER 0.7% 0.0% 0.0%
227자 0.0% 0.0% 0.6%
261자 0.0% 12.2% 0.0%
319자 0.4% 28.7% 0.0%

higgs3는 227자와 261자 사이에서 뒷문장을 통째로 버린다. 에러도 경고도 없다. 파일은 정상이고 소리도 자연스럽다. 대본 뒷부분이 없을 뿐이다 😱

처음엔 토큰 상한(2048 프레임)이라고 생각했다. 잘린 길이가 25.60초였고 2048 ÷ 25.60 = 80.0. 너무 딱 맞아서 믿을 뻔했는데, 상한을 6144로 올려도 길이가 한 샘플도 안 변했다. 우연의 일치였고, 모델이 스스로 멈춘 것이었다.

fish_s2가 멀쩡한 이유는 따로 있다. 내부에서 300바이트(한국어 약 100자)로 다시 쪼개되 조각들이 하나의 대화 문맥을 공유한다. 결국 안전 상한을 모델별 속성으로 뒀다(qwen3·fish_s2 300자, higgs3 200자). 이건 들어봐도 알 수 없는 손실이라 넘는 설정은 앱이 거절한다.

💡 교훈 — 숫자가 딱 맞아떨어진다고 원인이 맞는 건 아니다. 상한을 바꿔보고 결과가 그대로면 그 가설은 버려야 한다.

한국어 TTS 4종, 숫자로 보면 누가 이기나?

Qwen3 계열이 거의 전 항목에서 이긴다. 전부 이 맥에서 측정한 값이다.

qwen3 Base qwen3 CustomVoice higgs3 fish_s2
저장소 3.10GB 3.08GB 9.32GB 11.01GB
로드 시간 1.4s 2.5s 9.8s 2.7s
로드 후 메모리 2.95GB 약 2.9GB 8.22GB 10.81GB (피크 15.33)
출력 24kHz 24kHz 24kHz 44.1kHz
한계 RTF 0.31 0.31 0.84 2.08
안전 입력 상한 300자+ 300자+ 200자 300자+
목소리 복제 O X O O
감정 제어 없음 자유 서술문 41종 태그 34종 태그
발화 속도 8.0자/초 5.9자/초 8.2자/초 8.2자/초
라이선스 Apache 2.0 Apache 2.0 비상업 비상업

한 줄씩만 정리하면 이렇다.

  • Qwen3 Base — 3GB, 로드 1.4초, RTF 0.31. 가장 가볍고 상업 이용 가능. 대신 감정 제어가 아예 없다. instruct 인자가 있어도 조용히 버려진다.
  • Qwen3 CustomVoice — 감정을 쓰면서 상업 이용하는 유일한 길. 대신 참조 음성을 못 받고 내장 화자 9명 중에서 골라야 한다. 9명 전부 한국어를 CER 0.0%로 읽었다.
  • Higgs Audio v3 — 감정 태그 41종으로 표현력 최강. 대신 로드 9.8초, 메모리 8.22GB, 200자 상한, 비상업.
  • Fish Audio S2 Pro — 유일한 44.1kHz. 긴 입력에도 강하다. 대신 RTF 2.08이라 음성 1초에 2초가 걸리고, 메모리도 가장 무겁다.

그런데 왜 결국 Higgs Audio v3를 쓰게 됐나?

넷 중 가장 자연스러웠기 때문이다. 억양이 평평해지지 않고, 문장 끝이 부자연스럽게 끊기지 않는다. 감정 지시도 가장 잘 따라온다. 기쁘라면 기쁘게, 조용히 하라면 조용히.

위 표는 전부 기계로 잴 수 있는 값이다. 그런데 TTS를 고를 때 가장 궁금한 건 대개 표에 없다. 어느 목소리가 더 사람 같은가. ASR 채점이 잡아주는 건 “발음을 틀렸는가”까지고, “사람 같은가”는 여전히 귀의 영역이다. 그래서 같은 문장·같은 참조 음성으로 만든 결과를 나란히 놓고 들었다.

로드 9.8초, 메모리 8.22GB, 200자 상한, 비상업 라이선스. 표에 적힌 단점을 다 합쳐도, 결과물이 더 낫다는 사실 하나를 못 이겼다. 개인 작업이라 상업 라이선스가 필요 없었던 것도 컸다.

여기서 한 번 헛짚은 이야기를 해야겠다. CustomVoice의 감정 지시가 실제로 먹히는지 재봤는데, 처음엔 “오늘은 저비용 진단 센서를 소개하겠습니다” 같은 사실 나열문으로 쟀다. 음량 변화가 1.5배 안에 그쳐서 “효과가 불확실하다”고 적을 뻔했다. 감정이 실릴 수 있는 문장으로 바꾸니 신호가 분명해졌다.

지시문 길이 자/초 rms(배)
없음 5.76s 5.38 1.00
기쁨 4.56s 6.80 1.03
슬픔 7.20s 4.31 0.55
천천히 6.64s 4.67 0.97
속삭임 4.96s 6.25 0.98

이 모델은 감정을 음량이 아니라 말 속도로 표현한다. 슬픔과 기쁨의 속도 차이가 1.58배다. 측정 방법이 틀리면 멀쩡한 기능을 “없다”고 적게 된다. 그래서 이런 실험은 시험 문장부터 의심해야 한다 😅

결국 숫자는 고를 이유가 아니라 감당할 비용을 알려준다. 로드가 9.8초라는 걸 알면 모델을 자주 바꾸지 않도록 작업 순서를 짜게 되고, 200자에서 잘린다는 걸 알면 분할 기본값을 거기에 맞춘다. 이 앱이 필요했던 이유는 감으로 고르지 않기 위해서가 아니라, 감으로 고른 뒤에 그 대가를 미리 알기 위해서였다.

그래서 어떤 상황에 뭘 써야 하나?

가장 크게 갈리는 축은 라이선스, 두 번째는 분량이다.

상황 선택 이유
자연스러움·감정 최우선 (개인 용도) higgs3 넷 중 가장 자연스러움. 내가 쓰는 것
상업 이용 + 내 목소리 복제 qwen3 Base 유일한 조합. 감정은 포기
상업 이용 + 감정 qwen3 CustomVoice 유일한 조합. 목소리 복제는 포기
최종 음질 우선 fish_s2 44.1kHz. 시간은 각오할 것
분량이 많다 qwen3 계열 RTF 0.31 vs 2.08, 6.7배 차이
긴 문단을 한 번에 qwen3 / fish_s2 higgs3는 200자에서 잘린다


그리고 이번 비교의 가장 큰 결론 하나. “내 목소리 + 감정 + 상업 이용”은 현재 조합으로 불가능하다. 세 칸이 동시에 채워지는 모델이 없다. 그래서 내보내기 manifest.csv에 license 열을 넣었다.
나중에 상업으로 전환할 때 어떤 파일을 다시 만들어야 하는지 바로 보이게.

자주 묻는 질문

Q. 64GB가 아니라 16GB 맥에서도 되나?
qwen3 계열은 로드 후 약 3GB라 가능성이 높다. fish_s2는 피크 15.33GB라 사실상 어렵다. 다만 16GB 기기에서 직접 재보지는 않았다.

Q. 윈도우 RTX에서는 어떤가?
이 글은 MLX 기준이다. CUDA 환경은 로드 방식과 메모리 반환 동작이 다를 수 있어서 이 수치를 그대로 옮기면 안 된다.

Q. 상업적으로 쓸 수 있는 건 뭔가?
Qwen3-TTS Base와 CustomVoice 둘뿐이다(Apache 2.0). Higgs Audio v3와 Fish Audio S2 Pro는 비상업 라이선스다.

Q. ASR 자동 채점은 믿을 만한가?
대리 지표다. “오후 2시”를 “오후 두 시”로 받아쓰면 발음이 맞아도 CER이 뒤집힌다. 그래서 기준문과 표기 방식이 다르면 점수 보류로 처리한다. 이 장치가 실제 결함 두 건(USB-C의 C 누락, 문장 앞 환각 음절)을 잡아냈다.

마무리

이 앱을 만든 이유는 “어느 모델이 제일 좋은가”를 알아내기 위해서였다. 다 만들고 나서 얻은 답은 조금 달랐다. 제일 좋은 모델은 들어봐야 알 수 있었고, 비교표는 그 선택의 값을 알려줬다.

맥미니로 로컬 TTS를 고민 중이라면, 남의 비교 글보다 같은 문장 하나를 네 모델에 넣어보는 것부터 권한다.
참조 음성 하나, 문장 하나면 충분하다. 어느 쪽이 사람 같은지는 10초면 갈리고, 그다음에 표를 보면 된다 🎧


※ 이 글의 수치는 맥미니 M4 Pro 64GB에서 직접 측정한 값이다. “자연스러움” 같은 주관적 평가는 필자의 청취 기준이며, 같은 모델이라도 용도가 다르면 결론이 달라질 수 있다.

댓글 남기기