데스크탑 앱
38740bf 기준입니다.
VoiceStudio는 내 하드웨어에서 음성을 생성하고 가공하는 오픈소스 제작 도구입니다. 참고 녹음으로 목소리를 복제하고, 글로 목소리의 성격을 지정하고, 영상의 음성을 다른 언어로 다시 입힐 수 있습니다. README는 받아쓰기·전사·오디오북까지 묶어 “로컬 ElevenLabs 대안”으로 소개합니다.
이 저장소의 핵심 학습 대상은 음성 모델 자체와 그 모델을 오래 안전하게 돌리는 제품 코드 사이의 연결입니다. 화면에서 생성 버튼을 누르는 일과 GPU에서 신경망을 계산하는 일은 서로 다른 프로그램에서 일어납니다. 그 사이에는 입력 검증, 모델 선택, 메모리 관리, 진행 상황 전달, 파일 저장이 필요합니다.
모델은 노래하는 사람에 가깝습니다. 하지만 녹음실에는 접수하는 직원, 녹음 순서를 정하는 사람, 음향을 보정하는 장비, 결과를 보관하는 서랍도 있어야 합니다.
React가 주문을 받고, FastAPI가 작업을 배분하며, TTS 엔진이 소리를 만듭니다. FFmpeg와 오디오 처리 코드가 결과를 다듬고, SQLite와 파일 시스템이 작업을 기억합니다.
| 기능 | 넣는 것 → 받는 것 | 코드에서 볼 경계 |
|---|---|---|
| 음성 합성 | 글 + 선택한 목소리 → 음성 | backend/api/routers/generation.py |
| 음성 복제 | 참고 녹음 + 글 → 참고 목소리를 닮은 음성 | backend/services/tts_backend.py의 참고 음성 처리 |
| 영상 더빙 | 영상 + 목표 언어 → 시간에 맞춘 새 음성·영상 | backend/api/routers/dub_core.py, backend/api/routers/dub_translate.py, backend/api/routers/dub_generate.py |
| 전사·받아쓰기 | 녹음 또는 마이크 음성 → 글 | backend/api/routers/dictation.py 등 |
제품 이름은 VoiceStudio지만 기본 모델과 Python 패키지에는 OmniVoice라는 이름이 남아 있습니다.
pyproject.toml의 배포 이름은 omnivoice이고,
기본 모델 어댑터는 OmniVoiceBackend입니다.
이름이 다르다고 별도 서버 두 개로 오해하지 않는 것이 첫 번째 독해 요령입니다.
현재 데스크톱과 웹 화면은 모두 electron/에 있습니다.
README는 v0.5.3을 마지막 Tauri 릴리스로 안내합니다.
Rust 코드가 보이더라도 현재 앱 셸을 Tauri라고 설명하면 틀립니다.
TrendShift Weekly #18의 분석 대상으로 선정되었고, 2026-10-23 GitHub 기준 스타는 82,109개, 포크는 9,065개입니다. 2026-04-09 생성 이후 빠르게 관심을 모은 프로젝트입니다. 다만 이 시점의 누적 수치만으로 주간 증가량이나 특정 홍보 효과를 계산할 수는 없습니다.
다음 세 가지는 기능과 코드에서 읽어낼 수 있는 관심 요인에 대한 해석입니다. 실제 이용자 설문이나 트래픽 분석으로 입증한 유행의 원인은 아닙니다.
VoiceStudio의 매력은 모델 하나의 승부보다, 사용자가 자기 장비와 목적에 맞게 음성 작업 경로를 고를 수 있다는 데 있습니다.
아래는 저장소의 제품 지향과 구조를 비교한 표입니다. ElevenLabs 등 특정 서비스의 현재 가격·요금제·기능 전체를 대조한 벤치마크는 아닙니다. 클라우드 음성 서비스를 이용하는 방식과 로컬 제작 도구를 운영하는 방식의 차이에 초점을 맞춥니다.
| 비교 관점 | 클라우드 음성 서비스 이용 방식 | VoiceStudio의 선택과 대가 |
|---|---|---|
| 계산 위치 | 제공자가 운영하는 서버에 작업 요청 | 로컬 엔진 사용 시 내 CPU·GPU에서 처리. 장비 관리가 필요합니다. |
| 자료 흐름 | 서비스에 전달하는 자료와 정책을 확인 | 로컬 경로를 선택하면 원본을 외부 추론 서버로 보낼 필요가 없습니다. |
| 비용 구조 | 각 서비스의 사용량·구독 조건에 따름 | 장비·전기·저장 공간 비용이 듭니다. 선택한 유료 외부 공급자 비용은 별도입니다. |
| 모델 선택 | 제공자가 노출하는 모델·기능 사용 | TTSBackend 어댑터로 엔진 교체. 설치·호환성 차이도 함께 다뤄야 합니다. |
| 개발 학습 | 주로 API 호출과 결과 처리 학습 | API 뒤의 수명 관리·오류 복구·미디어 파이프라인까지 읽을 수 있습니다. |
| 품질·속도 | 제공 환경과 모델에 좌우됨 | 모델·언어·참고 녹음·하드웨어에 좌우됩니다. 일괄 우위를 뜻하지 않습니다. |
모델 로딩이 늦는 경우, GPU 메모리가 부족한 경우, 사용자가 요청을 취소한 경우가 별도 함수와 테스트로 남아 있습니다. “모델을 호출했다” 다음에 무엇을 해야 제품이 되는지 볼 수 있습니다.
README도 원격 서비스는 선택 사항이고 분석 수집에는 동의가 필요하다고 설명합니다. 모델 다운로드, 업데이트, 선택적 외부 번역·LLM, 계정·동기화 코드가 존재합니다. 로컬 작업 가능성을 모든 설정에서의 무통신 보장으로 확대해서 읽으면 안 됩니다.
버전 표에는 두 종류의 숫자가 나옵니다. >=는 허용하는 하한,
^는 호환되는 범위 선언입니다. uv.lock과 bun.lock은
재현 가능한 설치에 필요한 해석 결과를 보관합니다.
의존성 파일의 허용 범위와 실제 선택된 버전을 구분해야 같은 환경을 재현할 수 있습니다.
| 기술 | 실제 버전 근거 | 이 프로젝트에서의 역할 |
|---|---|---|
| Python | pyproject.toml: >=3.11 | API, 모델 실행, 파일·프로세스 제어. CI 주 테스트는 Python 3.11입니다. |
| FastAPI·Pydantic | 선언 fastapi<0.137; 잠금 0.136.3, Pydantic 2.13.4 | 폼·업로드 입력 검증, HTTP·WebSocket 라우트 |
| Uvicorn | 잠금 0.52.4 | FastAPI의 ASGI 실행 서버 |
| PyTorch 계열 | 선언 torch>=2.4; 잠금 torch·torchaudio 2.8.0, torchvision 0.23.0 | 텐서 계산과 신경망 추론. 잠금 파일에는 CPU·CUDA 변형도 있습니다. |
| Transformers | 선언 >=5.10.0; 잠금 5.15.1 | PreTrainedModel 기반 모델 구성·가중치 로딩 |
| 음성 인식 | whisperx>=3.1.0, faster-whisper>=1.0.0 | 전사와 단어 시간 정렬. WhisperX 잠금에는 3.4.5와 3.7.4가 함께 존재합니다. |
| 미디어 처리 | demucs>=4.0.1, imageio-ffmpeg>=0.6.0, soundfile 잠금 0.14.0 | 음원 분리, FFmpeg 탐색, 오디오 읽기·쓰기 |
| 저장·이력 | 표준 라이브러리 sqlite3; alembic>=1.16, 잠금 1.19.1 | 프로필·생성 이력·작업 저장과 스키마 이행 |
| 번역·표식·연결 | Argos 잠금 1.11.0, AudioSeal 0.2.0, MCP 1.29.0 | 로컬 번역, 합성 음성 표식, 에이전트 도구 연결 |
표의 잠금 버전은 uv.lock에 나타나는 항목 기준입니다.
플랫폼 조건이나 별도 엔진 환경에 따라 설치 조합은 달라집니다.
예를 들어 mlx-audio>=0.4.4는
sys_platform == 'darwin' and platform_machine == 'arm64' 조건으로 제한됩니다.
Gradio도 >=6.15.1 의존성에 남아 있지만, 현재 제품 화면은 React·Electron입니다.
패키지 목록에 있다는 이유만으로 메인 UI 기술이라고 판단하지 않는 것이 중요합니다.
| 계층 | electron/package.json 선언 | 읽어볼 구현 |
|---|---|---|
| 화면·타입 | React ^19.3.0, TypeScript ^7.0.2 | electron/src/renderer/src/features/ |
| 서버 상태·경로 | TanStack Query ^5.102.8, Router ^1.170.36 | 요청 상태, 캐시, 기능별 화면 이동 |
| 스타일·상태 | Tailwind CSS ^4.3.3, Zustand ^5.0.15 | 테마, 재사용 UI, 화면 상태 |
| 오디오 UI | wavesurfer.js ^7.12.12 | 음성 파형과 재생 인터페이스 |
| 데스크톱 실행 | Electron ^44.3.0, electron-updater ^6.8.9 | electron/src/main/, electron/src/preload/ |
| 번들·테스트 | Vite ^8.3.0, electron-vite 6.0.0-beta.1, Vitest 4.1.11 | 웹 번들과 main·preload·renderer 빌드, 단위 테스트 |
package.json은 workspaces: ["electron"],
packageManager: "bun@1.4.2"를 선언합니다. JavaScript 잠금 파일은 루트 bun.lock입니다.native/desktop-bridge/Cargo.toml은 Rust
edition = "2021", global-hotkey = "=0.8.0",
reqwest 0.13, enigo 0.3을 씁니다. 전역 단축키·입력 전달처럼 OS와 가까운 일을 맡습니다.deploy/Dockerfile은
oven/bun:1.4.2-alpine에서 웹을 빌드하고,
기본 실행 이미지 pytorch/pytorch:2.8.0-cuda12.8-cudnn9-runtime에 옮깁니다..github/workflows/ci.yml은 Python 3.11, Node.js 22, Bun 1.4.2로
백엔드·프론트엔드·Electron 패키징 계약을 확인합니다.
모델 다운로드가 테스트를 가리지 않도록 HF_HUB_OFFLINE=1도 사용합니다.앱 버전의 기준은 루트 package.json의 0.5.7-beta.1입니다.
electron/electron-builder.config.mjs가 이 값을 직접 읽습니다.
최신 안정 릴리스 v0.5.6과 현재 개발 커밋의 버전을 혼동하지 마세요.
iOS는 Swift·SwiftUI이며 mobile/ios/VoiceStudioKit/Package.swift에
Swift 도구 버전 6.0, iOS 최소 17이 선언되어 있습니다.
Android의 mobile/android/gradle/libs.versions.toml에는
Kotlin 2.2.21, AGP 8.12.0, Compose BOM 2025.10.01이 있습니다.
docs/mobile.md와 mobile/contract/SPEC.md는 현재 경로를
컴퓨터의 백엔드에 연결하는 보조 앱으로 설명합니다.
iOS mobile/ios/VoiceStudioKit/Sources/VoiceStudioKit/Networking/APIClient.swift에서도 HTTP 요청과 페어링된 서버 검증을 확인할 수 있습니다.
이 클라이언트의 음성 생성 요청은 페어링된 서버에서 처리됩니다.
전체를 “Python 프로그램에 화면을 붙였다”라고만 보면 중요한 경계를 놓칩니다. 데스크톱 셸, 화면, HTTP 서버, 모델 계산, 외부 엔진 프로세스가 서로 다른 책임을 가집니다. 아래 도식의 화살표는 대표적인 요청 경로이며 모든 라우트를 표시한 것은 아닙니다.
config/app.json은 기본 포트를 백엔드 3900, 웹 개발 3901,
Electron 개발 3902로 나눕니다.
실제 포트는 실행 설정에 따라 바뀔 수 있습니다.
웹 배포의 API 경로는 electron/src/renderer/src/lib/api/client.ts의 resolveApiBase()가 구분합니다.
electron/src/main/backend.ts의 resolveSpawnPlan()은
패키지 앱인지 개발 실행인지에 따라 Python 실행 계획을 만듭니다.
패키지에서는 관리되는 런타임의 Python을 쓰며, 개발 환경에서 가상환경이 없으면 설정 안내를 반환합니다.
서버를 띄우는 순간에 무거운 의존성 설치까지 몰래 시작하지 않도록 구분한 구조입니다.
electron/src/preload/index.ts는 contextBridge와
ipcRenderer.invoke()로 기능별 통로를 노출합니다.
예를 들어 업데이트 확인과 파일 저장을 화면이 임의의 시스템 명령으로 실행하는 대신
이름과 자료형이 정해진 요청으로 전달합니다.
// electron/src/main/index.ts의 실제 창 설정 일부
webPreferences: {
preload: PRELOAD_PATH,
contextIsolation: true,
sandbox: false,
nodeIntegration: false,
}
contextIsolation과 nodeIntegration: false는 화면과 시스템 기능의 경계를 만듭니다.
동시에 sandbox: false도 실제 설정입니다.
따라서 “Electron의 모든 격리 장치가 켜져 있다”라고 해석해서는 안 됩니다.
코드 주석은 ESM preload 사용을 그 이유로 설명합니다.
매장 손님이 창고의 모든 문 열쇠를 받는 대신, 접수 창구에 정해진 요청서를 내는 구조입니다. preload가 요청서의 형식을 정하고, main이 실제 OS 작업을 수행합니다.
backend/services/tts_backend.py의 TTSBackend는
엔진마다 달라지는 호출 방식을 공통 인터페이스로 감쌉니다.
generate()의 계약은 형태가 (1, n_samples)인 텐서 반환입니다.
샘플레이트는 별도 속성이므로 결과 배열만 보고 재생 속도를 추측하지 않아도 됩니다.
# 실제 인터페이스의 핵심만 축약한 코드
class TTSBackend(ABC):
@property
@abstractmethod
def sample_rate(self) -> int: ...
@property
@abstractmethod
def supported_languages(self) -> list[str]: ...
# generate(...) -> torch.Tensor
# ensure_ready(): 계산 전에 모델을 준비
# unload(): 사용이 끝난 모델 자원을 해제
_REGISTRY에는 omnivoice, kittentts,
mlx-audio 등의 ID와 클래스가 연결됩니다.
_LazyRegistry.__getitem__()은 일부 엔진 모듈을 처음 조회할 때 가져옵니다.
선택하지 않은 엔진 때문에 앱 시작부터 모든 무거운 모듈을 읽는 부담을 줄이는 방법입니다.
get_active_tts_backend()는 선택한 엔진 인스턴스를 재사용합니다.
엔진을 바꿀 때 이전 인스턴스의 사용이 끝나면 자원을 해제하고,
MLX 모델은 엔진 ID가 같더라도 세부 모델 선택이 달라지면 교체합니다.
get_backend_class()는 요청에서 지정한 엔진을 찾는 공통 진입점입니다.
backend/services/model_manager.py의 _workers_for_free_vram()은
남은 GPU 메모리로 작업자 수를 정합니다. 작업당 예산은 5.0 GiB, 자동 상한은 4입니다.
코드의 단위 표기는 GB이지만, 바이트 수를 1,073,741,824로 나누므로 실제 계산 단위는 GiB입니다.
이 GPU 작업 풀은 CPU·MPS 환경에서 자동으로 작업자 한 개를 선택합니다.
# backend/services/model_manager.py의 실제 정책
_GPU_VRAM_PER_JOB_GB = 5.0
_GPU_WORKER_CAP = 4
def _workers_for_free_vram(free_gb: float) -> int:
return max(1, min(_GPU_WORKER_CAP,
int(free_gb // _GPU_VRAM_PER_JOB_GB)))
# 예: 여유 8 GiB → 1개, 16 GiB → 3개
# 카드 전체 용량이 아니라 조회 당시의 여유 메모리 기준
run_on_gpu_pool_guarded()는 두 개의 시계를 씁니다.
queue_timeout은 대기 제한 시간이고, timeout은 실제 작업을 시작한 뒤의 실행 제한 시간입니다.
앞 작업 때문에 기다리기만 한 요청을 “계산이 너무 느리다”라고 잘못 진단하지 않으려는 설계입니다.
| 상황 | 구분 | 소스의 처리 단서 |
|---|---|---|
| 아직 순서를 기다림 | 자원 혼잡 | GpuPoolBusyError, 대기열에서 취소 |
| 실행 후 예산 초과 | 계산 지연·정지 가능성 | GpuJobTimeoutError, 풀 복구 처리 |
| 클라이언트가 떠남 | 응답 대기 종료와 작업 종료가 다름 | on_abandon으로 입력 자원의 뒤늦은 해제 연결 |
backend/api/routers/generation.py의 _TempReferenceLease는 이 차이를 파일 수명에 적용합니다.
요청이 취소되어도 백그라운드 스레드가 참고 녹음을 읽고 있다면 곧바로 지우지 않습니다.
마지막 독자가 끝난 뒤 삭제하도록 참조 횟수를 관리합니다.
요청이 끝났다는 사실과 계산·파일 사용이 끝났다는 사실은 같지 않으며, 이 차이를 관리해야 취소 기능도 안전해집니다. Python 스레드는 Future를 취소했다고 강제로 멈추지 않습니다. 강제 종료가 필요한 엔진에 별도 프로세스 방식이 있는 이유도 여기에서 이해할 수 있습니다.
backend/services/dub_pipeline.py의 ingest_pipeline()은
입력 확보, 오디오 추출, 음원 분리, 장면 처리의 진행 이벤트를 내보내는 비동기 생성기입니다.
이후 전사·번역·합성은 backend/api/routers/dub_*.py 등에서 이어집니다.
한 파일이 더빙 전체를 모두 처리한다고 생각하지 마세요.
영상 입력
-> 오디오 추출
-> 음성 / 배경음 분리
-> 전사와 구간별 시간 정보
-> 목표 언어로 번역
-> 구간별 음성 합성
-> 길이 맞추기와 배경음 혼합
-> 최종 영상 또는 음성 출력
compute_file_hash()는 파일을 256 KiB씩 읽어 SHA-256을 계산합니다.
find_cached_job()은 같은 내용의 과거 작업을 최대 5개 찾고,
중간 파일이 실제로 남았는지 확인합니다.
특히 audio_hq.wav가 없는 예전 결과는 재사용하지 않습니다.
이 검사는 과거의 저품질 오디오 추출 결과가 새 파이프라인에도 계속 섞이는 일을 막습니다.
캐시는 “같은 입력이 있었나”뿐 아니라 지금 요구하는 품질의 결과인가도 확인해야 합니다.
get_job()은 메모리 조회가 실패하면 SQLite의 dub_history.job_data를 읽어 복구합니다.
generate_speech()가 폼 입력을 받고, Unicode NFC 정규화를 적용합니다.engine 또는 현재 설정으로 엔진을 찾습니다.
모델 이용 조건 확인과 참고 음성·설계 기능 검증은 무거운 로딩 전에 수행합니다.normalize_for_tts() 다음에 발음 사전을 적용하고, 긴 글은 나눠 합성합니다.mark_synthetic_async()를 거쳐 WAV와 생성 이력을 저장합니다.정규화 뒤에 발음 사전이 오는 것은 우연이 아닙니다. 사용자가 지정한 발음 표기를 후속 정규화가 다시 바꾸지 않도록 순서를 정했습니다. 이런 처리 순서의 계약도 함수 이름만큼 중요한 설계 정보입니다.
backend/와 omnivoice/를 구분하면 읽을 양이 줄어듭니다.
전자는 “사용자 요청을 어떻게 처리할까”, 후자는 “음성을 어떻게 계산할까”에 가깝습니다.
웹 개발 입문자는 라우터부터, 머신러닝에 관심 있는 독자는 모델의 generate()부터 시작할 수 있습니다.
electron/src/shared/는 독립 실행되는 옛 앱의 입구가 아닙니다.
현재 renderer가 가져다 쓰는 공통 모듈도 이곳에 있습니다.
디렉토리 이름에 “shared”가 있다고 데스크톱·웹·모바일의 UI가 모두 같은 코드인 것도 아닙니다.
| 언어 | 파일 수 | 물리적 줄 수 |
|---|---|---|
| Python | 1,131 | 282,303줄 |
| TypeScript·TSX | 922 | 129,521줄 |
| JavaScript·JSX | 650 | 113,004줄 |
| Swift | 51 | 5,746줄 |
| Kotlin | 45 | 5,052줄 |
| Rust | 10 | 4,220줄 |
| CSS | 24 | 5,272줄 |
커밋 38740bf의 Git 추적 파일 중 위 확장자를 가진 일반 파일을 세었습니다.
주석·공백·테스트·생성 파일을 포함하며, 마지막 개행이 없어도 마지막 줄은 한 줄로 셉니다.
JSON·문서·이미지·바이너리는 이 표에서 제외했습니다.
따라서 “직접 작성한 제품 코드의 순수 줄 수”와는 다릅니다.
위 언어 합계는 545,118줄입니다.
같은 집계에서 tests/만 134,565줄이며,
각 기능 폴더 안에도 테스트가 더 있습니다.
큰 규모를 처음부터 정독하기보다 기능 하나와 그 테스트를 짝지어 읽는 편이 낫습니다.
backend/api/routers/generation.py → backend/services/tts_backend.py → backend/services/generation_audio.pyelectron/src/renderer/src/lib/api/generate.ts → electron/src/renderer/src/lib/api/client.ts → electron/src/main/backend.tsbackend/services/chunked_tts.py → backend/services/dub_pipeline.py → tests/test_perf_operation_budgets.pygenerate_speech()는 JSON 본문 대신 Form과 UploadFile을 받습니다.
글과 참고 녹음을 한 요청으로 보내기 위한 선택입니다.
max_chunk_chars의 하한은 0,
crossfade_ms는 0~1000으로 선언됩니다.
# backend/api/routers/generation.py의 실제 입력 선언 일부
text: str = Form(...),
ref_audio: Optional[UploadFile] = File(None),
max_chunk_chars: int = Form(800, ge=0),
crossfade_ms: int = Form(50, ge=0, le=1000),
stream: bool = Form(False),
배울 것: 자료형 검증과 “이 엔진에서 이 기능을 쓸 수 있는가”라는 의미 검증은 다릅니다.
후자는 _conditioning_refusal() 같은 코드가 맡습니다.
실습 아이디어: 각 입력이 빠졌을 때, 범위를 벗어났을 때, 엔진 능력과 충돌할 때를 표로 나눠보세요.
함수에 async를 붙여도 모델 추론이 자동으로 가벼워지지 않습니다.
run_on_gpu_pool_guarded()는 막히는 계산을 제한된 풀에 맡기고,
WAV 저장은 asyncio.to_thread()로 이벤트 루프 밖에서 처리합니다.
배울 것: 이벤트 루프는 요청을 조율하는 역할을 유지해야 합니다. 실습 아이디어: 종이 위에서 작업자 1명, 요청 3개를 배치하고 각 요청의 도착·계산 시작·완료 시각을 기록해 대기 시간과 계산 시간을 따로 구하세요.
omnivoice/models/omnivoice.py의 OmniVoice는
PreTrainedModel을 상속합니다.
from_pretrained()는 모델과 토크나이저 등 필요한 자산을 준비하고,
generate()가 입력과 생성 설정을 받아 음성 생성 흐름을 시작합니다.
_generate_iterative()는 가려진 오디오 토큰을 단계적으로 채웁니다.
실제 반복문은 range(gen_config.num_step)이며,
매 단계 모델의 점수와 토큰 선택 정책을 사용합니다.
따라서 단계 수는 단순한 화면 장식이 아니라 계산량에 직접 연결되는 설정입니다.
OmniVoiceGenerationConfig의 기본 단계 수는 32이지만,
앱의 backend/services/performance_profiles.py는 빠름 8, 균형 16,
품질 32, 최대 64를 정의합니다.
라이브러리 기본값과 제품의 실행 기본값이 다른 좋은 사례입니다.
실습 아이디어: 함수 인자가 설정 객체를 덮어쓰는 경로를 표시해보세요. 모델을 로딩하지 않고도 “화면에서 고른 값이 최종 반복 횟수가 되는 과정”을 추적할 수 있습니다.
backend/services/chunked_tts.py는 기본 800자 단위로 글을 나누고,
기본 50 ms 크로스페이드로 음성을 잇습니다.
한글·한자·가나가 전체의 30% 이상이면
_effective_max_chars()가 글자 한 개당 발화량을 고려해 한도를 줄입니다.
# 실제 분할 한도 정책 일부
_DENSE_FRACTION_THRESHOLD = 0.3
_DENSE_SPEECH_FACTOR = 2.5
# 조건을 만족하면 적용되는 계산
return max(120, min(max_chars,
round(max_chars / _DENSE_SPEECH_FACTOR)))
# 기본 800자라면 계산된 한도는 320자
배울 것: 입력의 길이 제한은 문자 수만의 문제가 아닙니다. 최종 음성의 길이와 모델의 안정적인 처리 범위를 고려해야 합니다. 실습 아이디어: 영어·한국어·숫자가 섞인 글을 만들어 분할 규칙의 예상 결과를 적어보세요.
24,000 Hz는 1초를 24,000개 값으로 나타낸다는 뜻입니다.
모노 음성의 모양이 (1, 48,000)이고 샘플레이트가 24,000이면 길이는 2초입니다.
같은 값들을 다른 샘플레이트로 재생하면 시간과 음높이도 달라집니다.backend/core/db.py의 get_db()는
PRAGMA journal_mode=WAL과 PRAGMA foreign_keys=ON을 켭니다.
db_conn()은 정상 종료 시 커밋, 예외 시 롤백, 마지막에 연결 닫기를 담당합니다.
오디오 자체는 파일로, 프로필과 이력은 테이블로 관리하는 경계도 볼 수 있습니다.
실습 아이디어: voice_profiles, generation_history,
dub_history 세 테이블에서 파일 경로와 작업 ID가 어떤 관계인지 그려보세요.
파일 저장 성공과 DB 저장 성공 사이에 앱이 종료되면 어떤 흔적이 남을지도 생각해보세요.
electron/src/renderer/src/lib/api/client.ts의 ApiError는 HTTP 상태, 설명, 파싱한 응답을 함께 보관합니다.
electron/src/renderer/src/lib/api/generate.ts의 sanitizeInstruct()는
허용된 지시 태그, 중복, 충돌, 지원하지 않는 문장을 구분합니다.
모든 실패를 “오류 발생” 하나로 뭉개지 않는 설계입니다.
실습 아이디어: 같은 분류의 태그 두 개와 알 수 없는 설명을 넣었을 때 각각 어느 배열에 들어갈지 추적해보세요. 검증 결과를 사용자 안내로 바꾸는 과정을 배울 수 있습니다.
backend/mcp_server.py의 create_mcp_server(app=None)는
앱이 주어지고 환경변수 OMNIVOICE_API_URL이 비어 있으면
httpx.ASGITransport로 같은 API를 프로세스 안에서 호출합니다.
독립 실행하거나 이 환경변수를 지정하면 HTTP로 백엔드와 통신합니다.
도구마다 별도의 합성 구현을 만드는 대신 기존 동작을 재사용합니다.
tests/test_perf_operation_budgets.py는 실행 시간만 재지 않고
합성·배치 호출 횟수 같은 연산 예산을 확인합니다.
테스트 장비마다 속도는 달라도, 불필요한 모델 호출이 추가되었는지는 일관되게 검사할 수 있습니다.
저장소는 모든 엔진에 통하는 단일 RAM 최소치를 제시하지 않습니다. 아래 OS·디스크 조건은 설치 문서 기준이고, 메모리 여유에 관한 권장은 학습을 위한 선택 기준입니다. 설치가 가능하다는 것과 원하는 엔진을 충분한 속도로 돌릴 수 있다는 것은 서로 다른 조건입니다.
| 환경 | 문서·소스에서 확인한 조건 | 학습할 때의 판단 |
|---|---|---|
| Apple Silicon | macOS 13.3+, MPS 가속; 설치 문서상 약 10 GB 여유 디스크 | 통합 메모리를 다른 앱과 공유합니다. 무거운 전사·합성을 동시에 잡지 마세요. |
| Intel Mac | 현재 문서는 앱 UI + 원격 백엔드만 지원 | 로컬 Python 백엔드 설치를 기본 실습으로 잡지 않습니다. |
| Windows x64 | Windows 10/11, 약 10 GB 여유 디스크; NVIDIA CUDA 경로 | CPU 엔진으로도 시작할 수 있습니다. GPU 이름보다 실제 실행 장치를 확인하세요. |
| Linux 데스크톱 | x86_64, X11 또는 Wayland; 약 10 GB 여유 디스크 | NVIDIA CUDA, 조건에 맞는 AMD ROCm을 검토합니다. |
| GPU 없는 PC | CPU 실행 가능; CPU PyTorch 환경은 문서상 약 5 GiB 여유 공간 안내 | 가벼운 엔진과 짧은 글부터 시작합니다. 모델·출력 공간은 별도입니다. |
| Windows ARM | 실험적 지원. 네이티브 셸 + x64 Python 에뮬레이션, CPU 전용 안내 | 실기기 검증이 끝난 일반 지원 환경으로 취급하지 않습니다. |
근거는 docs/install/macos.md, docs/install/windows.md, docs/install/linux.md입니다.
약 10 GB는 여러 엔진과 장시간 영상까지 포함한 상한이 아닙니다.
별도 엔진의 가상환경과 가중치가 수 GB씩 추가될 수 있습니다.
OmniVoiceBackend.min_vram_gb는 6.0으로 선언됩니다.
이는 기본 엔진의 전용 GPU 메모리 판단에 쓰이는 기준이며,
모든 엔진의 최소 조건이나 “6 GB만 있으면 항상 안전하다”는 보장은 아닙니다.
자동 작업 수 정책의 5 GiB/작업과도 목적이 다릅니다.
학습용 권장: 가벼운 CPU 엔진으로 짧은 문장을 다루거나, 여유 있는 16 GB 이상 시스템에서 한 번에 한 작업부터 시작하세요. 이는 공식 최소 사양이 아닙니다. 더빙은 전사·분리·합성 모델과 중간 오디오가 필요하므로 메모리와 디스크를 더 넉넉히 잡는 편이 좋습니다.
Windows에서 AMD·Intel GPU가 감지되어도 기본 PyTorch 경로가 그 GPU를 쓰는 것은 아닙니다.
설치 문서는 Windows의 기본 PyTorch 가속을 NVIDIA/CUDA로 안내하며,
audio.cpp의 Vulkan 경로와 별도 고급 설정을 구분합니다.
get_best_device()에 여러 장치 분기가 있다는 사실을 모든 설치의 지원 보장으로 읽지 마세요.
3.11+, Node.js 22+,
Bun 1.4.2, Rust/Cargo와 OS 빌드 도구가 관련됩니다. 실제 설치 절차는 공식 플랫폼 문서를 따릅니다.냉장고에서 재료를 꺼내는 시간과 요리하는 시간은 다릅니다. 첫 생성에는 모델 로딩과 가속 커널 준비가 섞일 수 있습니다. 첫 실행만 재서 모든 문장의 합성 속도라고 부르면 비교가 어긋납니다.
처음에는 실제 녹음이나 GPU를 준비하지 않아도 됩니다. 아래 과제는 소스 읽기와 독립된 연습 코드로 진행할 수 있습니다. 좋은 실습의 목표는 기능을 크게 늘리는 것이 아니라, 하나의 계약을 설명하고 실패 조건까지 확인하는 것입니다.
출발 파일: backend/api/routers/generation.py의 generate_speech().
할 일: text, engine, ref_audio,
max_chunk_chars, crossfade_ms, stream을 골라
필수 여부·자료형·기본값·검증 조건을 표로 정리합니다.
완료 기준: “텍스트 누락”, “알 수 없는 엔진”, “음성 설계를 지원하지 않는 엔진”이 각각 어느 단계에서 거절되는지 설명합니다. 네트워크 요청을 보내지 않고 라우터와 검증 함수를 따라가면 됩니다.
출발 파일: backend/services/chunked_tts.py의
split_text_into_chunks()와 _effective_max_chars().
할 일: 독립된 연습 파일에 마침표·물음표·느낌표를 기준으로 나누는 함수를 작성합니다.
먼저 단순 버전을 만든 뒤 약어, 한국어 문장, [pause 300ms] 같은 태그를 반례로 추가합니다.
완료 기준: 빈 문자열과 한 문장은 불필요하게 쪼개지지 않고, 태그 내부는 나뉘지 않아야 합니다. 영문 800자와 한국어 800자가 왜 같은 발화 길이가 아닌지도 설명합니다.
출발 파일: backend/services/tts_backend.py의
TTSBackend, generate_batch().
할 일: 표준 라이브러리만으로 작은 공통 인터페이스와 두 구현을 만듭니다. 하나는 길이 100의 0 목록, 다른 하나는 길이 200의 0 목록을 반환하게 합니다. 이는 계약을 익히기 위한 축소 모형이며 실제 앱의 텐서 계약과는 구분합니다.
# 독립 연습 코드의 골격: 실제 앱에 붙이는 엔진이 아님
class SilentEngine:
sample_rate = 100
def generate(self, text):
return [0.0] * len(text)
engine = SilentEngine()
samples = engine.generate("가나다라")
duration = len(samples) / engine.sample_rate
assert duration == 0.04
완료 기준: 호출하는 쪽은 엔진 이름을 몰라도 길이를 계산할 수 있어야 합니다. 샘플레이트가 0이거나 결과가 비었을 때 누가 오류를 내야 하는지도 계약에 적습니다.
출발 파일: backend/services/model_manager.py의 run_on_gpu_pool_guarded(),
backend/api/routers/generation.py의 _TempReferenceLease.
할 일: 작업자 한 개에 A·B 두 요청이 들어오는 시간표를 그립니다. B가 대기 중 취소되는 경우와, 계산 시작 후 응답 대기가 끝나는 경우를 각각 표시합니다.
완료 기준: 참고 파일을 지울 수 있는 가장 이른 시각을 두 경우에 대해 답합니다. 지연·파일 읽기를 흉내 낸 독립 코드로 확장할 때에도 “취소 = 스레드 종료”라고 가정하면 안 됩니다.
출발 파일: backend/services/dub_pipeline.py의 find_cached_job(),
backend/services/longform_resume.py.
할 일: 입력 내용, 엔진 ID, 모델 식별자, 목표 언어, 분할 정책 버전을
캐시 키에 넣는 설계안을 작성합니다.
긴 글 재개용 resume.json에서 계획을 보존하는 방식도 비교합니다.
완료 기준: “엔진만 바뀜”, “중간 파일 삭제”, “합성 도중 종료” 세 경우에 재사용 가능한 단계와 다시 해야 하는 단계를 표로 제시합니다. 설계안의 키 구성이 현재 더빙 캐시와 똑같이 구현되어 있다고 주장하지 않습니다.
실습을 확장할 때는 tests/test_perf_operation_budgets.py의
test_dub_remix_budget_zero_tts_calls()를 읽어보세요.
이미 생성된 음성을 다시 섞는 작업에서 TTS 호출이 0회여야 한다는 식의
검증 가능한 목표를 세우는 방법을 배울 수 있습니다.
주당 3~5시간을 기준으로 잡은 학습 제안입니다. Python의 함수·클래스, JavaScript의 객체·비동기 문법이 낯설다면 1주차를 더 길게 잡으세요. 한 기능의 입력부터 저장까지 설명할 수 있게 된 뒤 다른 엔진과 화면으로 범위를 넓히면, 큰 저장소도 읽을 수 있는 크기로 줄어듭니다.
| 주차 | 배울 주제·읽을 곳 | 남길 결과물 |
|---|---|---|
| 1주차 | Python 자료형·함수·예외, HTTP·폼 입력.
backend/api/routers/generation.py의 선언과 검증을 읽습니다. | 생성 요청 계약 표, 오류 사례 5개 |
| 2주차 | 추상 클래스·어댑터·레지스트리.
TTSBackend와 두 엔진 구현을 비교합니다. | 가짜 엔진 2개와 공통 호출 코드 |
| 3주차 | asyncio·스레드·취소·자원 수명.
run_on_gpu_pool_guarded()와 lease를 읽습니다. | 대기·실행·취소 시퀀스 표 |
| 4주차 | 샘플레이트·텐서·WAV·크로스페이드.
backend/services/chunked_tts.py, backend/services/generation_audio.py를 읽습니다. | 분할기 반례 모음, 음성 길이 계산기 |
| 5주차 | React 상태·HTTP 오류·Electron IPC.
electron/src/renderer/src/lib/api/client.ts, electron/src/preload/index.ts를 연결합니다. | 오류 상태별 UI 설계, 요청 경로 도식 |
| 6주차 | 캐시·SQLite·재개·CI 계약.
backend/services/dub_pipeline.py, backend/services/longform_resume.py, CI 설정을 읽습니다. | 캐시 키 설계와 회귀 테스트 계획 |
OmniVoice.generate() → _preprocess_all()
→ _generate_iterative() → _decode_and_post_process() 순으로 자료 형태를 추적합니다.deploy/Dockerfile과 Electron 패키징을 비교해
같은 앱이 서로 다른 실행 환경을 갖는 이유를 설명합니다.“이 함수는 어떤 입력을 믿는가”, “누가 자원을 해제하는가”, “어떤 결과를 재사용해도 되는가”에 답할 수 있으면 구조를 이해한 것입니다. 추론 수식 전체를 먼저 이해해야만 앱 코드를 읽을 수 있는 것은 아닙니다.
| 용어 | 쉬운 설명 |
|---|---|
| TTS | 글을 음성으로 바꾸기. 이 프로젝트에서는 여러 엔진을 공통 인터페이스로 호출합니다. |
| ASR | 소리를 글로 바꾸기. 더빙의 출발점이며 참고 녹음의 전사에도 쓰입니다. |
| 음성 복제 | 참고 녹음의 목소리 특성을 조건으로 새 글을 읽게 하는 일입니다. |
| 제로샷 | 대상 목소리를 위해 별도 재학습하지 않고 참고 자료로 생성하는 방식입니다. |
| 음성 설계 | 참고 녹음 대신 성별·음높이·스타일 같은 설명으로 목소리를 지정하는 기능입니다. |
| 텐서 | 숫자를 여러 축으로 담은 배열입니다. 오디오는 채널과 샘플 축으로 표현할 수 있습니다. |
| 오디오 토큰 | 음성을 모델이 다룰 수 있는 이산 기호로 나타낸 값입니다. 텍스트의 글자와 같은 단위는 아닙니다. |
| 샘플레이트 | 1초에 들어가는 오디오 값의 수입니다. 배열 길이를 시간으로 바꾸는 데 필요합니다. |
| 크로스페이드 | 앞 소리를 줄이며 뒤 소리를 키워 짧게 겹치는 연결 방식입니다. |
| 화자 분리 | 녹음의 어느 구간에서 누가 말했는지 구분하는 작업입니다. 음성·배경음 분리와 다릅니다. |
| 강제 정렬 | 전사한 단어가 음성의 어느 시간에 놓이는지 맞추는 작업입니다. |
| 어댑터 | 서로 다른 엔진 호출을 같은 모양으로 바꾸는 연결 코드입니다. |
| 레지스트리 | 엔진 ID와 구현을 연결한 명부입니다. 지연 방식이면 필요할 때 구현을 가져옵니다. |
| 지연 로딩 | 앱 시작 때가 아니라 실제로 필요해질 때 모델이나 모듈을 읽는 방식입니다. |
| 작업 풀 | 정해진 수의 작업자가 요청을 나눠 처리하는 구조입니다. 무제한 동시 실행을 막습니다. |
| lease | 자원을 쓰는 동안 유효함을 보장하는 사용권입니다. 마지막 사용자가 끝날 때 해제할 수 있습니다. |
| sidecar | 주 프로그램 옆에서 별도 프로세스로 돌아가는 보조 실행체입니다. |
| IPC·preload | IPC는 프로세스 사이 통신입니다. Electron preload는 화면에 제한된 시스템 기능 통로를 제공합니다. |
| SSE·WebSocket | SSE는 서버가 이벤트를 이어 보내는 방식, WebSocket은 연결을 유지하며 양방향으로 주고받는 방식입니다. |
| NDJSON | 한 줄에 JSON 하나를 보내는 형식입니다. 생성 미리 듣기 이벤트를 순서대로 읽을 수 있습니다. |
| WAL | SQLite가 변경 내용을 로그에 먼저 기록하는 모드입니다. 읽기·쓰기 동시 처리에 도움을 줍니다. |
| 캐시 무효화 | 예전에 만든 결과를 더 이상 재사용하면 안 되는 조건을 정하는 일입니다. |
| MCP | AI 도구와 자료를 공통 방식으로 노출하는 프로토콜입니다. 음성 생성도 도구 호출로 연결합니다. |
| 워터마크 | 합성 여부를 식별하도록 음성에 넣는 표식입니다. 구현이 있다는 사실과 항상 삽입된다는 보장은 다릅니다. |
| 항목 | 상태·확인 기준 |
|---|---|
| 저장소 | debpalash/VoiceStudio |
| 선정 근거 | TrendShift Weekly #18 · 2026-10-23 분석 |
| 스타 / 포크 | 82,109 / 9,065 · 2026-10-23 GitHub 기준 |
| 생성일 / 최근 push | 2026-04-09 / 2026-10-23 · GitHub 기준 |
| 주 언어 | Python · GitHub 기준 |
| 최신 안정 릴리스 | v0.5.6 · 2026-09-23 |
| 분석 커밋 | 38740bf · 2026-10-23test(ios): expect the release triple as the bundle marketing version (#3203) |
| 개발 코드 버전 | 0.5.7-beta.1 · 루트 package.json, pyproject.toml |
| 코드 규모 | 주요 7개 언어 묶음 545,118줄. 테스트·생성 파일·주석·공백 포함. Git 추적 항목 총 3,615개. |
| 라이선스 | 앱은 AGPL-3.0-only. LICENSE-NOTICE.md는
omnivoice/의 별도 Apache-2.0 출처와 모델 자산의 별도 조건을 구분합니다. |
| 현재 UI | Electron 데스크톱·React 웹. Tauri는 종료된 배포 계열입니다. |
646개는 언어 매핑의 수: omnivoice/utils/lang_map.py의
LANG_NAME_TO_ID에 646개 항목이 있습니다.
모든 TTS·ASR·번역 엔진이 646개 언어에서 같은 품질을 낸다는 뜻은 아닙니다.
로컬 기능과 선택적 외부 연결: 다운로드·업데이트·외부 공급자·동기화·분석 설정을 구분해야 합니다. 사용할 경로와 설정을 확인하세요. 로컬 작업에 외부 계정이 일괄 필수인 구조는 아닙니다.
앱과 모델은 조건이 다름: 저장소의 LICENSE-NOTICE.md는
기본 모델 가중치의 CC-BY-NC 표기와 오디오 토크나이저의 별도 조건을 명시합니다.
앱 라이선스나 앱의 상용 라이선스만으로 모든 모델·목소리의 이용 조건을 대신할 수 없습니다.
목소리 복제에는 당사자의 허락을 확인하세요.
워터마크는 설정과 성공 여부에 좌우됨: mark_synthetic()는
표식 기능이 꺼졌거나 사용할 수 없거나 실패하면 원본 음성을 반환하도록 설계되어 있습니다.
함수 호출 경로가 있다는 이유로 모든 출력에 표식이 보장된다고 설명하면 안 됩니다.
모바일 설명의 차이: AGENTS.md는 휴대전화 자체 추론을 지원한다고 서술하지만,
docs/mobile.md와 mobile/contract/SPEC.md는 보조 앱 단계로 설명합니다.
문서 간 설명이 일치하지 않습니다. iOS의 APIClient.generate()는 페어링된 서버의
/generate에 HTTP 요청을 보냅니다.
정적 구조와 실측은 다름: 코드 구조 설명은 실제 음질·처리 속도·모든 장치의 동작 검증을 대신하지 않습니다. 개발 커밋과 안정 릴리스 사이에도 기능·화면·설치 동작 차이가 있을 수 있습니다.