로컬 TTS 뽑기 · 1화
첫 TTS LoRA는 긴 문단에서 음량이 5%까지 무너졌다: 단문만 배운 탓
채널 나레이션 목소리를 Fish Speech S2 Pro에 LoRA로 학습시켰다. 검증 손실도 발음도 좋았지만, 긴 문단을 읽히면 뒤로 갈수록 목소리가 작아졌다. 원인은 단문뿐인 학습 데이터였다.
영상마다 나레이션이 들어간다. 같은 목소리로, 긴 원고도 중간에 끊기지 않게, 필요한 만큼 뽑을 수 있어야 한다. 그걸 집 GPU에서 하고 싶었다. 그래서 오픈소스 TTS인 Fish Speech S2 Pro에 채널 나레이션 목소리를 LoRA로 학습시켜 로컬 TTS를 만들어 봤다. 이 연재는 그 과정을 세 편으로 정리한다. 1화는 첫 LoRA가 실패한 이야기다.
먼저 알아야 했던 것: 레퍼런스와 LoRA
Fish S2 Pro는 4B 크기의 음성 합성 모델이다. 한국어를 읽을 수 있고, 10~30초짜리 참조 음성(레퍼런스)과 그 대본을 같이 넣으면 그 목소리를 흉내 내서 읽는다. 학습 없이 매번 레퍼런스로 목소리를 따라 하는 방식이다. 반면 LoRA는 모델 일부에 작은 가중치를 덧붙여 따로 학습시키는 방법이다. 목소리를 모델 쪽에 새겨 두는 셈이다.
설치는 맥 스튜디오(M4 Max)와 RTX 5090 양쪽에 했다. 같은 대본으로 재 보니 5090이 초당 6.39토큰, 맥이 2.11토큰으로 5090이 약 3배 빨랐다. 생산은 5090이 맡기로 했다. 참고로 코드와 모델은 Fish Audio Research License다. 연구·비상업 용도는 허용되고, 상업적으로 쓰려면 별도 라이선스가 필요하다.
LoRA를 시작하기 전에 하나 헷갈렸다. 한국어를 잘 읽게 하는 LoRA와 목소리를 복제하는 LoRA는 다른 것인가. 기술적으로는 같은 방식이고 데이터만 다르다. 한 사람의 한국어 음성으로 학습하면 한국어 읽기 습관과 그 사람의 목소리가 한 LoRA 안에 같이 들어간다. 그래서 LoRA는 목표 목소리 하나로 만들고, 숫자나 영문 약어 읽기는 대본 단계에서 푼다. “35억 달러”는 “삼십오억 달러”로, “HBM”은 “에이치비엠”으로 적어서 넣는다.
첫 번째 LoRA: 41분 데이터, 300스텝
데이터는 그동안 유튜브에 올린 영상에서 모았다. 영상 108개에서 오디오를 뽑고, 완전히 겹치는 파일과 소리 없는 파일을 뺐더니 1시간 22분이 남았다. 여기서 다른 목소리가 섞인 영상은 빼고 같은 화자의 원본 나레이션 20개만 골랐다. 이것을 5~15초 단위로 잘라 문장마다 대본 파일을 붙였다. 402개, 41.5분짜리 데이터셋이다.
5090에서 공식 기본값(rank 8, alpha 16)으로 300스텝을 학습하는 데 10분쯤 걸렸다. 검증 손실은 마지막 체크포인트에서 가장 낮았고, 한국어 시험 문장도 음성 인식으로 되돌려 보면 틀린 글자가 없었다. 숫자로는 성공이었다.
목소리는 달랐다. 원래 목소리와 얼마나 닮았는지 화자 유사도로 재 보면, 기본 모델에 레퍼런스만 넣은 쪽이 0.859, LoRA에 레퍼런스를 넣은 쪽이 0.831이었다. LoRA만 쓰고 레퍼런스를 빼면 0.380으로 떨어졌다. LoRA가 목소리를 새겨 넣지 못했다는 뜻이다.
끝이 죽는다
이 LoRA로 나레이션을 만들어 보니 문장이 뒤로 갈수록 목소리가 작아졌다. 이걸 고치느라 몇 시간을 헛돌았다. 작업하던 AI는 마침표를 여러 개 찍게 하고, 마지막 단어 음량을 재서 기준에 못 미치면 다시 만들게 하고, 끝 단어만 살짝 키우기도 했다. 수치상으로는 매번 통과였는데 들어 보면 그대로였다. 나중에 확인해 보니 그 AI는 이 작업 환경에서 소리를 직접 들을 수 없었다. 파형과 음량 숫자만 보고 고쳤다고 판단한 것이다. 보정을 쌓을수록 원래 없던 음량 출렁임까지 생겼다.
결국 후처리를 전부 빼고 같은 다섯 문장 대본, 같은 시드로 세 조건을 비교했다.
| 조건 | 문장별 음량 (첫 문장 = 100) | 결과 |
|---|---|---|
| 기본 모델 + 레퍼런스 | 100 → 89 → 90 → 77 → 79 | 다섯 문장 정상 |
| LoRA 300스텝, 레퍼런스 없음 | 100 → 53 → 17 → 10 → 5 | 뒤로 갈수록 사그라듦 |
| LoRA 300스텝 + 레퍼런스 | 첫 문장만 | 3.1초에서 끝남 |
문장부호나 후처리 문제가 아니라 LoRA 자체의 문제였다. 학습 데이터 402개가 전부 2.9~13.1초짜리 단문이었고 중앙값은 5.7초였다. 검증도 같은 단문으로 했다. 모델은 짧게 말하고 끝내는 패턴을 익혔고, 여러 문장을 이어 말하는 상황은 학습에서도 검증에서도 본 적이 없었다. 학습 원본의 문장 끝 음량은 정상이었으니 데이터가 작게 녹음된 탓도 아니었다.
다시 레퍼런스로
이 LoRA는 폐기하고, 한동안 기본 모델에 13초짜리 레퍼런스를 넣는 방식으로 돌아갔다. 같은 다섯 문장에서 음량이 끝까지 버틴 쪽이 이것이었다.
이 방식에도 함정이 하나 있었다. 2분짜리 나레이션 중간에 한 문장이 갑자기 여성 목소리로 나왔다. 85~113Hz이던 목소리가 한 문장 동안 260~300Hz로 뛰었다. 다른 음원이 섞인 게 아니라 모델이 문장 경계에서 레퍼런스 화자를 놓친 것이다. 레퍼런스로 따라 하는 방식은 확률적으로 화자를 놓칠 수 있다. 그 장면만 다시 생성해서 넘어갔고, 그 뒤로는 문장마다 화자 유사도도 검사한다.
배운 것
검증 손실과 발음 검사는 학습 데이터와 같은 짧은 문장만 봤다. 긴 문단에서 무너지는 것은 실제로 쓸 길이로 시험해야 보인다. 첫 LoRA는 시험한 범위 안에서는 성공이었고, 쓰려는 범위에서는 실패였다.
AI는 숫자로 소리를 판단한다. 음량과 음성 인식 결과는 정확히 재지만, 끝이 죽는다는 것은 사람 귀가 먼저 잡았다. 숫자에 맞춰 보정을 쌓으면 문제를 숨기거나 새로 만든다. 그래서 규칙을 바꿨다. 자동 검사는 불량을 거르는 데만 쓰고, 채택은 사람이 듣고 정한다. 문제는 후처리로 덮지 않고 생성 단계에서 바로잡는다.
다음 화에서는 긴 샘플을 새로 만들어 LoRA를 다시 학습한다.