솔로 해커톤
부산 편 - 프로젝트 [ Ryo ] - RVC 모델 학습
작성일: 2026-03-07
**"일단 드럽게 복잡한 마인크래프트 서버는 집어 치고, 이전에 작살난 프로젝트를 살리자."**
이번 <솔로 해커톤 - 부산 편>의 핵심 목표는 'Discord라는 게이머들의 SNS 플랫폼의 API를 활용해 채팅을 실시간으로 읽어주는 챗봇'을 완성하는 것.
이미 이전에 진행했던 사이드 프로젝트였습니다. 과거에 호기롭게 시도했다가 봇이 요청을 감당하지 못하고 마치 DoS 공격을 맞은 것처럼 뻗어버리며 프로젝트를 엎을 수밖에 없던 뼈아픈 추억이 있습니다. 이번 해커톤의 미션은 바로 작살났던 프로젝트의 아키텍처를 재기획하고, 새로 구축한 홈 서버 인프라 위에서 되살려내는 것이었습니다.
처음 기획 단계에서는 '이걸 굳이 또 해야 하나?' 하는 회의감도 있었습니다. Google의 gTTS, Edge 기반의 edge-tts와 같은 보편적인 모듈들이 내는 딱딱하고 기계적인 목소리는 이미 예전 프로젝트에서 지겹도록 써봤기 때문입니다.
2월 28일 **더 현대**의 행사 방문 이후 제 생각은 180도 바뀌었습니다. 제가 가장 좋아하는 캐릭터에 대한 '덕질' 스위치가 On 상태로 바뀐 것이었습니다.
'료슈(Ryoshu)의 목소리를 직접 학습시킨다면? 그 봇이 디스코드 보이스 채널에 난입해 그 캐릭터가 연상되는 특유의 톤으로 사람들의 채팅을 읽어준다면?'
이미 다른 같은 유형의 챗봇 이용자들에게서 ~~'얘.. 톤 변태같...'~~ 여러 불만을 듣고 있었고, 무엇보다 '덕질' 스위치가 제 가슴을 엄청 뛰게 만들어주었습니다.
이를 위해 꺼내든 기술이 바로 **RVC(Retrieval-based Voice Conversion)입니다.** ~~사실 원래 목적은 Text to Voice 모델을 쓰려했지만 찾을 때만 해도 그냥 막 붙들고 쓴 터라 몰랐어요..~~
기존의 밋밋한 기계음에 타깃 캐릭터의 목소리 가중치를 덮어씌우는 무거운 AI 기술을 방구석에 위치한 서버에서 직접 돌려 보기로 결심했습니다.
**[ Part 1. 보이스 타깃 설정 및 전처리 ]**
마침 이번 신규 스토리로 좋아하는 캐릭터의 내레이션이 생겼다, 내레이션만 나오는 영상도 찾았겠다 바로 mp3 추출부터 시작합니다.
근데 여기엔 몇 가지 문제점이 있죠.
첫 번째 문제 : 영상(당시는 추출되어 저장된 mp3)의 길이. 7분 길이나 되는 내레이션 모음을 통째로 사용할 수는 없습니다.
두 번째 문제 : 비음성 사운드의 존재입니다. 휘파람, 악기(기타), 담배를 깊게 빨아들이고 내쉬는 숨소리 그대로 포함된 상태입니다.
일단 비음성 사운드 문제부터 해결하기로 했습니다.
UVR5(Ultimate Vocal Remover v5) 모듈을 통해 담배 피우는 사운드와 악기(기타) 소리를 나누어 3개 또는 4개의 파일로 나누었습니다. 휘파람 소리는 미처 자르지 못했습니다.
다음으로는 오디오 파일의 슬라이싱입니다. 이때 사용한 것은 pydub.silence입니다.
단순히 길이로 자르기보단 오디오의 데시벨(dB)이 특정 임계값(Threshold) 이하로 떨어지는 묵음 구간과 그 구간의 길이를 감지해 자르도록 도입했습니다.
```bash
chunks = split_on_silence(
audio,
# 문장 사이의 긴 공백만 자르기 위해 800ms(0.8초)로 넉넉하게 설정
# (단어 사이의 짧은 띄어쓰기 공백은 무시하고 이어붙임)
min_silence_len=1800,
# 무음으로 간주할 데시벨 기준 (배경음이 거의 없다면 -45 ~ -50 적당)
silence_thresh=-45,
# 잘라낸 대사 앞뒤에 남길 자연스러운 여백 (250ms)
# RVC 학습 시 앞뒤 공백이 살짝 있어야 발음 끝이 날아가지 않습니다.
keep_silence=300
)
```
남은 휘파람 소리는 Mac OS의 QuickTime Player의 편집 기능을 통해 파형과 길이를 확인하여 남은 비음성 사운드(휘파람)를 잘라내었습니다.
**[ Part 2. RVC WebUI와 Trobleshooting ]**
정말 말 많은 구간이었습니다. data process에서 전처리 했던 보이스 데이터를 주입하는데 성공은 하였지만 특징 추출(Feature Extraction)과 모델 훈련(Model Training)이 문제였습니다.
문제 1. Hubert의 침묵
특징 추출 단계 진행 중 마지막 특징 추출 과정(hubert 사용하는 단계)에서 멈춰버리는 사태가 발생합니다. GPU 사용하고 있을 거라 생각하고 다른 터미널을 열어 nvidia-smi를 쳐봤지만 gnome shell 말곤 찾을 수 없었습니다.
다음으론 top 명령어로 CPU 코어 할당 상태 확인해 보았더니 10%는커녕 1.0% 미만이었습니다.
원인은 torch에 있었습니다.
heckpoint_utils.py 파일을 찾아 편집기로 아래 내용대로 수정 진행하였습니다.
기존 코드 (/torch.load로 찾기)
```bash
state = torch.load(f, map_location=torch.device("cpu"))
```
수정 코드 (weights_only=False 추가/수정)
```bash
state = torch.load(f, map_location=torch.device("cpu"), weights_only=False)
```
최신 버전의 PyTorch가 보안을 이유로 "안전하지 않은 데이터"로 인식하고 있었다는 게 원인이었습니다.
문제 2. 1차 모델 버전의 혼란
분명 1차 모델 학습은 v2로 진행 중이었는데 왠지 모르게 G path와 D path에서 버전 불일치 문제가 발생하였습니다.
그래서 v1으로 버전을 변경한 뒤 옵션 그대로 학습을 했더니 다음엔 Matplotlib에서 문제가 생기네요? 이놈도 최신버전으로 업데이트를 했는지 구형 함수(tostring_rgb) 인식 불가와 변경 이후 RGBA와 RGB(4 채널과 3 채널)의 호환성이 발목을 잡습니다.
이번엔 WebUI 폴더에서 infer/lib/train/utils.py를 찾아 아래처럼 수정하였습니다.
기존 코드
```bash
data = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep="")
data = data.reshape(fig.canvas.get_width_height()[::-1] + (3,))
```
수정 코드 - tostring_rgb()를 buffer_rgba()로 변경 / 기존 두 번째 data 변환의 마지막 숫자 4로 변경, 다시 한번 변환하여 3 채널로 만들도록 수정
```bash
data = np.frombuffer(fig.canvas.buffer_rgba(), dtype=np.uint8)
data = data.reshape(fig.canvas.get_width_height()[::-1] + (4,))
data = data[:, :, :3]
```
이렇게 두 문제를 해결하며 잘 된다고 생각했던 찰나 자고 일어나니 아주 아주 아주 심각한 문제가 발생합니다.
수정과 정상적으로 학습과정이 돌아가는 것을 확인하고 잠자리에 들었습니다.
문제 3, 4. 2차 모델 버전의 혼란과 인덱스 생성 오류 및 기계음의 습격
원래 나눠서 설명해야 하지만 어쩌다 인덱스 파일을 생성한 것이 해결이 아닌 더 극악의 지옥으로 내몰아버리는 결과를 만들어 내어 묶어 쓰게 되었습니다.
3시간 정도 자고 일어나니 학습 과정은 마무리되었으나 추론 과정에서 인덱스 파일이 없어 생성하려는 찰나 앞선 트러블슈팅 과정에서 버전을 하드웨어 QC때보다 몇 배로 체크하고 v1으로 진행하였으나 파일 생성 시작 단계에서 256차원(v1)으로 인식돼야 할 것이 왠지 모르게 768차원(v2)으로 하려 해서 프로세스 꼬임으로 인한 실행 중단 증상이 있었습니다.
이번에는 임의로 v1때 특징 추론 값은 두고 모델만 v2로 재생성하여 인덱스 파일을 얻어 내었으나 이내 4번째 문제인 기계음의 습격으로 이어지게 됩니다.
마지막에는 그냥 잘 실행되는 RVC WebUI를 바탕으로 v2로 학습 과정을 다시 진행하여 간신히 기계음에서 사람 목소리로 결과가 나오는 상태를 만드는 데 성공합니다.