무료로 내 목소리 AI로 복제하기 – SuPerTTS 앱 개발 완전 정복기 (Google Colab + Supertonic SDK v3)

무료로 내 목소리 AI 복제하기 – SuPerTTS 앱 개발 완전 정복기

Google Colab + Supertonic SDK v3 + SwiftUI로 약 $50짜리 유료 서비스를 완전히 대체한 온디바이스 TTS 앱 구축기. 기획부터 내 목소리 AI로 복제하기 성공(Loss 0.0356 달성), 수많은 트러블슈팅까지 모든 과정을 낱낱이 공개한다.(다만, 개발환경의 차이로 본 포스트대로 진행되지 않을 수 있으며, 그럴 때는 클로드 AI 등 유료 AI 서비스를 통해서 해결책을 찾아보시기를 바랍니다.)

SuPerTTS — 클립보드 텍스트를 내 목소리로 읽어주는 온디바이스 AI TTS 앱


📌 이 글에서 다루는 것

  • SuPerTTS 앱이란 무엇인가? — 기획 배경과 핵심 목표
  • 아키텍처 설계: Swift + Python Flask + Supertonic SDK v3
  • 무료 보이스 클로닝 워크플로우 전 단계 (Google Colab GPU 활용)
  • Loss 0.0356 달성까지의 실전 학습 과정
  • 맥OS 샌드박스, ATS, Swift Actor, 단축키 권한 등 4대 기술 난관과 해결책
  • 최종 결과물: my_voice_final.json 앱 적용 가이드

🎙️ 1. SuPerTTS란? — 기획 배경

긴 글을 읽어야 할 때, 또는 손이 자유롭지 않을 때 — 복사 한 번으로 AI가 내 목소리로 대신 읽어준다면? 이 단순한 질문에서 SuPerTTS 프로젝트가 시작되었다.

시중에는 이미 다양한 TTS 앱이 존재하지만, 대부분 두 가지 치명적인 한계를 갖는다. 첫 번째는 클라우드 의존성 — 인터넷이 없으면 동작하지 않는다. 두 번째는 비용 — 특히 ‘내 목소리’로 합성하려면 대부분 월정액이나 고액의 일시불 결제를 요구한다. S사의 경우 목소리 클로닝에 약 $50(약 6만원)를 요구했다.

SuPerTTS는 이 두 한계를 모두 정면 돌파하는 것을 목표로 삼았다.

목표구체적 방법
☁️ 클라우드 의존 탈피온디바이스(On-device) 모델로 오프라인 동작
💸 유료 클로닝 대체Google Colab + WavLM 기반 무료 추출 워크플로우
⚡ 즉각적인 반응클립보드 자동 감지 + 글로벌 단축키(Option+Space)
🇰🇷 고품질 한국어 지원Supertonic SDK v3 채택

🏗️ 2. 아키텍처 설계 — Swift와 Python의 분리

SuPerTTS 시스템 아키텍처 — Client(SwiftUI) ↔ Server(Python Flask)

SuPerTTS의 구조는 크게 두 계층으로 나뉜다. Swift(SwiftUI)가 사용자 인터페이스와 시스템 이벤트(클립보드 감지, 단축키)를 담당하고, Python(Flask)이 AI 엔진(Supertonic SDK v3, ONNX Runtime)을 실행한다. 두 계층은 로컬 호스트 127.0.0.1:7890을 통해 HTTP로 통신한다.

이 분리 구조를 채택한 이유는 명확하다.

  • Python 생태계 활용: PyTorch, ONNX, librosa 등 ML 라이브러리는 Python에서 가장 성숙하다.
  • 엔진 교체 용이성: Flask 서버만 바꾸면 Swift 앱 코드를 건드리지 않고도 다른 TTS 엔진으로 교체 가능하다.
  • UI 반응성 보장: 무거운 AI 연산이 별도 프로세스에서 돌아가므로 UI 스레드가 블로킹되지 않는다.

핵심 서비스 레이어 4종

Swift 앱의 비즈니스 로직은 4개의 전용 관리 클래스로 분리되어 있다:

班级역할
HotkeyManager시스템 전역 Option + Space 단축키 감지 (CGEventTap 기반)
ClipboardMonitor클립보드 변화 실시간 감지 → 텍스트 추출
ModelManagerAI 모델 파일 존재 여부 확인 및 자동 다운로드
TTSServicePython 서버에 HTTP 요청 → 오디오 데이터 수신·재생

모든 클래스는 @MainActor class로 선언되어 SwiftUI의 @Published 속성 변화가 메인 스레드에서 안전하게 UI에 반영된다. (이 결정에 이르기까지의 트러블슈팅은 3.2절에서 상세히 다룬다.)


🎤 3. 무료 보이스 클로닝 — Google Colab GPU 워크플로우

이 섹션이 이 글의 핵심이다. S사가 약 $50에 제공하는 목소리 클로닝을 완전 무료로 재현하는 전체 과정을 단계별로 정리한다.

🚫 먼저, Gemini(로컬 CPU)가 실패한 이유

초기에는 로컬 맥북 환경에서 목소리 특징 추출을 시도했다. 결과는 실패였다. 그 기술적 원인은 세 가지다.

  1. 무작위 투영(Random Projection)의 한계: 초기 스크립트는 torch.nn.Linear 레이어를 학습 없이 무작위 초기화하여 사용했다. 실행할 때마다 결과가 달라지며, 실제 목소리의 정체성을 전혀 담지 못한다.
  2. 데이터 규격 불일치: Supertonic 엔진은 style_ttl(차원: [1, 50, 256])과 운율을 담당하는 style_dp(차원: [1, 8, 16]) 두 키를 동시에 요구한다. 초기 스크립트는 이 규격을 충족하지 못했다. (이번에 완성된 my_voice_final.json은 두 키를 모두 정확한 차원으로 포함한다.)
  3. 연산 능력 부족: 고품질 보이스 클로닝은 수천 번의 최적화(Optimization) 반복이 필요하다. NVIDIA GPU(CUDA) 없이는 맥북 CPU만으로 감당할 수 없다.
Google Colab T4 GPU 환경에서 Loss가 0.05 미만으로 수렴하는 학습 과정

✅ 단계별 실행 가이드 (Google Colab)

1단계 — 런타임 설정 및 환경 구축

Google Colab에서 런타임 유형을 반드시 GPU(T4 이상)로 설정한 후 아래 셀을 실행한다.

!git clone https://github.com/saurabhv749/supertonic3-voice-clone
%cd supertonic3-voice-clone
!pip install -r requirements.txt
!pip install supertonic
!git lfs install
!git clone https://huggingface.co/Supertone/supertonic-3 supertonic3

⚠️ 의존성 충돌 주의: librosanumba의 버전 충돌로 AttributeError: module 'coverage.types' has no attribute 'Tracer'가 발생할 수 있다. 이 경우 아래 명령어로 버전을 고정한다:

!pip install coverage==6.5.0
!pip install numpy==1.23.5 numba==0.56.4

2단계 — 목소리 파일 준비 및 경로 트릭

학습 스크립트(train_style.py)가 내부적으로 샘플 파일 F6.wav를 강제로 참조하는 버그가 있다. 사용자의 파일을 해당 경로에 F6.wav라는 이름으로 복사해 스크립트를 속이는 것이 핵심이다.

  1. 녹음한 목소리 파일(15~30초 권장, WAV 포맷)을 Colab에 업로드한다.
  2. 아래 명령어로 파일을 스크립트가 기대하는 위치에 복사한다:
!mkdir -p voices
# my_voice.wav를 본인 파일 이름으로 변경
!cp /content/my_voice.wav /content/supertonic3-voice-clone/voices/F6.wav

💡 â 녹음 팁: 조용한 환경에서 다양한 억양과 속도가 담긴 15~30초 분량이 이상적이다. 단조로운 낭독보다는 자연스러운 대화체 발화가 더 풍부한 스타일 벡터를 생성한다.

3단계 — 학습 실행 (약 30분 소요)

!python train_style.py \
  --voice_name "my_real_voice" \
  --audio_path "voices" \
  --output_dir "output"

학습이 진행되면 터미널에 에폭(Epoch)과 함께 Loss 값이 출력된다. 성공 기준은 아래와 같다:

Loss 값평가
0.10 이상❌ 원본과 거리가 먼 합성음
0.05 ~ 0.10🟡 어느 정도 유사하나 특징이 약함
0.05 미만✅ 원본과 매우 흡사한 목소리 생성 성공
0.0356🏆 이번 프로젝트 실제 달성값

4단계 — 결과물 다운로드

학습이 완료되면 logs/F6/F6.json 파일이 생성된다. 이 파일이 바로 Supertonic 엔진이 목소리를 재현하는 데 사용하는 스타일 벡터(Style Vector)다.

이번 프로젝트의 결과물인 my_voice_final.json은 아래 두 핵심 텐서를 포함한다:

차원(Dims)역할
style_ttl[1, 50, 256]음색(Timbre) — 목소리의 음색적 정체성
style_dp[1, 8, 16]운율(Prosody) — 억양, 리듬, 강세 패턴

🔥 4. 4대 기술 난관과 해결책 — 개발 트러블슈팅 완전 기록

SuPerTTS 개발 과정에서 마주한 4대 기술 장벽

🚨 난관 1 — macOS 샌드박스 & ATS 통신 차단 (최대 난관)

증상: 앱이 정상 실행되고 Python 서버도 떠 있는데 통신이 안 되며 E003 오류가 반복 발생.

원인: macOS의 보안 샌드박스는 앱이 외부 네트워크에 접근하는 것을 기본적으로 차단한다. 심지어 로컬호스트(127.0.0.1)까지도 예외가 없다. 여기에 더해 ATS(App Transport Security) 설정이 누락되어 HTTP 요청 자체가 OS 레벨에서 차단되었다.

해결 과정 (3단계):

  1. project.pbxproj 파일에서 ENABLE_APP_SANDBOX 값을 YESNO로 변경
  2. Info.plistNSAppTransportSecurity 딕셔너리를 수동으로 추가하여 로컬호스트 HTTP 통신 허용
  3. codesign 명령으로 애드혹(ad-hoc) 서명을 강제 재적용하여 변경된 보안 정책을 OS에 반영
<!-- Info.plist에 추가할 ATS 예외 설정 -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsLocalNetworking</key>
    <true/>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

🚨 난관 2 — Swift Actor와 ObservableObject의 데이터 바인딩 충돌

증상: 상태 변화가 일어나도 UI가 업데이트되지 않음. 버튼을 눌러도 화면 반응이 없음.

원인: Swift의 actor는 동시성 안전성(concurrency safety)을 위해 설계된 타입으로, SwiftUI의 @Published 속성을 메인 스레드에서 안전하게 업데이트하는 ObservableObject 프로토콜과 완벽히 호환되지 않는다.

해결: 모든 서비스 클래스를 actor@MainActor class로 전환하고 Combine 프레임워크를 도입하여 데이터 흐름을 안정화했다.

// ❌ 문제 있는 코드
actor TTSService: ObservableObject {
    @Published var isPlaying: Bool = false
    // actor 내부에서 @Published 업데이트 → UI 반영 안 됨
}

// ✅ 해결된 코드
@MainActor
class TTSService: ObservableObject {
    @Published var isPlaying: Bool = false
    // 메인 스레드 보장 → UI 즉시 반영
}

🚨 난관 3 — 글로벌 단축키 가로채기 및 손쉬운 사용 권한 문제

증상: Option + Space를 눌러도 앱이 반응하지 않고 현재 포커스된 텍스트 필드에 공백 문자가 입력됨.

원인: 시스템 전체 이벤트를 가로채는 CGEventTap API는 macOS의 ‘손쉬운 사용(Accessibility)’ 권한이 명시적으로 부여되지 않으면 단순히 무력화되며, 이벤트가 원래 대상 앱으로 그대로 전달된다.

해결: 앱 실행 시 AXIsProcessTrusted() 함수로 권한 상태를 확인하고, 권한이 없으면 SwiftUI로 구현한 시각적 경고 배너와 시스템 설정 열기 버튼을 노출한다.

// 권한 확인 및 안내 처리
func checkAccessibilityPermission() {
    let trusted = AXIsProcessTrusted()
    if !trusted {
        // SwiftUI View에 경고 배너 표시
        showAccessibilityAlert = true
    }
}

// 설정 화면 바로 열기
func openAccessibilitySettings() {
    NSWorkspace.shared.open(
        URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!
    )
}

🚨 난관 4 — Python SDK 호환성 및 오디오 처리 오류

증상: 음성 합성 요청 시 Python 서버에서 TypeError가 발생하며 오디오가 생성되지 않음.

원인 1 — SDK API 변경: Supertonic SDK 업데이트로 synthesize() 메서드의 인자명이 변경됨.

원인 2 — 메모리 스트림 미지원: save_audio() 함수가 io.BytesIO 같은 메모리 스트림 객체를 지원하지 않고, 반드시 실제 파일 경로를 요구함.

원인 3 — Style 객체 타입 불일치: 추출된 JSON은 단순 Python dict인데 SDK는 전용 Style 객체를 요구함.

해결: tts_server.pyget_voice_style_from_path() 메서드를 도입하여 JSON 파일을 정식 Style 객체로 변환하고, 임시 파일(temp_output.wav)을 경유하는 오디오 처리 로직을 추가했다.

import tempfile
import os

def get_voice_style_from_path(json_path: str):
    """JSON 파일을 Supertonic SDK의 Style 객체로 변환"""
    import json
    with open(json_path, 'r') as f:
        data = json.load(f)
    # SDK의 Style 클래스로 래핑
    return tts_engine.load_style_from_dict(data)

def synthesize_with_style(text: str, style_json_path: str) -> bytes:
    style = get_voice_style_from_path(style_json_path)
    
    # 임시 파일 경유 방식으로 오디오 생성
    with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp:
        tmp_path = tmp.name
    
    try:
        tts_engine.synthesize(text=text, style=style, output_path=tmp_path)
        with open(tmp_path, 'rb') as f:
            return f.read()
    finally:
        os.unlink(tmp_path)  # 임시 파일 정리

📲 5. JSON 파일을 앱에 적용하는 방법

SuPerTTS 앱의 Voice Builder → JSON 임포트 화면

  1. Google Colab에서 생성된 JSON 파일(F6.json 또는 커스텀 이름)을 로컬에 다운로드한다.
  2. SuPerTTS 앱을 실행한 뒤 화자 탭 → 보이스 빌더로 이동한다.
  3. [JSON 선택] 버튼을 눌러 파일 선택 창을 열고 JSON 파일을 선택한다.
  4. 앱이 JSON을 voices/ 폴더로 자동 복사하고 목록에 등록한다.
  5. 등록된 목소리를 기본 화자로 설정하면 이후 모든 합성에 내 목소리가 사용된다.

⚠️ 파일 선택 창에서 JSON이 회색으로 보이는 경우: VoiceBuilderView.swift"(《世界人权宣言》) NSOpenPanel 在设置中 allowedContentTypes[.json, .text, .data]로 넓혀야 한다. 기본값이 너무 엄격하게 설정되어 있으면 JSON 파일이 선택 불가 상태로 표시된다.


📊 6. 최종 성과 정리

项目결과
💰 비용 절감목소리 클로닝 비용 $50 → $0 (무료)
🎯 클로닝 품질Loss 0.0356 달성 (목표 0.05 미만 초과 달성)
⚡ 응답 지연샌드박스 해제 + 로컬 서버 최적화로 실시간 합성 실현
🔒 보안 난관macOS Sandbox, ATS, Accessibility 권한 모두 해결
🔧 확장성Flask 서버 교체만으로 다른 TTS 엔진으로 손쉽게 마이그레이션
📦 최종 스타일 벡터style_ttl [1,50,256] + style_dp [1,8,16] 완전 규격 준수

🛣️ 7. 앞으로의 방향 (Roadmap)

  • 멀티 화자 지원: 여러 개의 JSON 스타일을 등록하고 즉석에서 전환하는 기능
  • iOS 포팅: Swift 코드베이스의 70%가 공유 가능하므로 iPhone/iPad 버전 확장 예정
  • 엔진 다각화: Kokoro, StyleTTS2 등 다른 오픈소스 TTS 엔진 교체 실험
  • 자동 클로닝 파이프라인: 앱 내에서 직접 Colab API를 호출하여 원클릭 클로닝 구현

🧩 마치며

이 프로젝트는 단순히 “TTS 앱을 만들었다”는 이야기가 아니다. macOS 보안 정책의 벽을 넘고, Swift와 Python의 경계를 연결하고, 유료 서비스의 대안을 직접 구축해낸 기록이다. 그 과정에서 핵심은 실패를 두려워하지 않고 원인을 끝까지 파고드는 것이었다.

Google Colab GPU로 30분간 돌린 최적화 결과 Loss 0.0356이 터미널에 찍히는 순간, 그리고 앱에서 내 목소리로 합성된 음성이 처음 재생되던 순간 — 그 모든 트러블슈팅의 시간이 보상받는 느낌이었다.

이 글이 비슷한 길을 걷고 있는 누군가에게 단 한 번의 오류 해결에라도 도움이 되기를 바란다.


📎 관련 링크 및 참고 자료

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注