Jonathan Mellette Contact

Applied AI · Released v1.1.0 · Open source (AGPL-3.0)

OpenVoice

A self-hosted voice studio: type text and get speech, design a new voice from a written description, clone one from a 15-second clip, or fine-tune a LoRA adapter from about 30 minutes of speech. Two open-weight engines run in isolated environments behind one API.

Status
Released: tagged v1.1.0 on 23 Jun 2026, source public on GitHub; later commits on main carry no version bump. Self-hosted: it runs on a machine you own.
Licence
AGPL-3.0-or-later for the code. Model weights are downloaded at install, never bundled: Chatterbox under MIT, VoxCPM2 under Apache-2.0.
Runs on
Windows with an NVIDIA GPU (PyTorch from CUDA 12.4 wheels)
Stack
Python 3.11 · FastAPI · PyTorch 2.6 (CUDA 12.4) · Chatterbox 0.1.7 · VoxCPM2 + LoRA · React 19
Links
GitHub: TheRevDrJ/OpenVoice

Measured · how

By hand

The 30-minute clone was a hand-run recipe: convert, slice, transcribe, build a manifest, train, register.

Now

One button, or a 30-minute tab recording that starts the build itself. Five monitored stages, and a failure shows the failing tool’s own error.

At a glance

engines, each in its own locked environment and process
2
ways to make a voice: design from text, quick clone, fine-tune
3
monitored stages in the fine-tuning job
5
API routes on one network-facing port, plus 2 on a localhost-only engine worker
20 + 2
How these were measured
  • 2, 130, 161, 10: every package entry in the two lock files, server/uv.lock and voxcpm/uv.lock, parsed 12 Oct 2026, leaving out each project’s own entry. 104 names appear in both; 10 resolve to different versions, among them numpy 1.26.4 against 2.4.6 and transformers 5.2.0 against 5.12.1. Both use torch 2.6.0 built for CUDA 12.4.
  • 3: the Voice Lab’s three modes in web/src/App.tsx. The quick clone records exactly 15 seconds.
  • 5: the stage list in server/app/voicelab.py (convert, slice, transcribe, train, register, then ready). The training config it writes sets rank 32, alpha 32 and 100 iterations.
  • 20 + 2: route decorators counted in server/app/main.py (20) and in the engine worker (2: health and generate). openvoice.bat starts the worker on localhost and the backend on all interfaces; the backend also serves the built UI.

All read from the public repository’s main branch, which matches GitHub’s latest commit.

OpenVoice’s Voice Lab in 30-Minute Clone mode: a voice named Narrator, an Internal use only checkbox, a file named narration.wav, and a stage tracker reading convert, slice, transcribe, train, register, ready, with Train in progress.
Fig. 1Voice Lab’s 30-Minute Clone partway through a build, with its tracker at Train. Built from the public main branch and shown with sample data; the job state is a mocked response.

The short version

  1. Isolated two engines by process and lockfile instead of forcing one dependency resolution. One API hides the seam, and the browser sees a single port.
  2. Removed a working engine before release because its weights licence reaches model outputs. The released stack downloads only MIT and Apache-2.0 weights, checked against Hugging Face’s metadata on 12 Oct 2026.
  3. Turned a hand-run fine-tuning recipe into a monitored background job that fails loudly. Five monitored stages, a check that the trained adapter exists before anything is registered, and the failing tool’s own error output on screen.

Case 1 of 5

Two engines, isolated by design, behind one API

Each engine gets its own locked environment and its own process, and one API hides the seam.

Chatterbox, from Resemble AI, supplies the default voice, expressive controls and zero-shot cloning. Its package pins much of its stack exactly: torch 2.6.0, transformers 5.2.0, safetensors 0.5.3, gradio 6.8.0, and numpy below 2. VoxCPM2, from OpenBMB, designs voices from text and accepts LoRA adapters. Rather than force VoxCPM2 into Chatterbox’s pins, I gave each engine its own uv-locked environment and its own process. The two lock files resolve 130 and 161 packages; of the 104 they share, 10 land on different versions, including numpy 1.26 against 2.4.

The FastAPI backend fronts both through one abstract engine interface, and a single speech route dispatches on each voice’s engine. VoxCPM2 runs as an always-on worker that listens only on localhost, and the backend calls it over HTTP. The worker loads the base model once with LoRA layers attached, then hot-swaps a per-voice adapter on request. The current adapter is cached, so repeat calls skip the reload, and torch.compile stays off so the swaps stay clean. The browser sees one port that serves both the UI and the API.

OpenVoice’s Voice Synthesis tab: a list of three voices (Computer, River, Space Cowboy), a text box reading Hello from OpenVoice, sliders for expressiveness, guidance and speed, and a Generate button.
Fig. 2Voice Synthesis: voices from both engines in one list, one text box, expressiveness, guidance and speed controls, and one Generate button. River and Space Cowboy are invented voices designed from written descriptions, not modelled on anyone. Built from the public main branch and shown with sample data.
How we know

PyPI’s dependency metadata for chatterbox-tts 0.1.7 and both lock files, read 12 Oct 2026. The engine interface is in server/app/engines/base.py, the dispatch in server/app/main.py, and the adapter cache and compile setting in server/workers/voxcpm_worker.py. The start order is in openvoice.bat; the frozen builds are in setup.bat.

Case 2 of 5

Licence as an architectural constraint: a working engine removed before release

When the plan became a free AGPL-3.0 release, licence stopped being a tie-breaker and became a filter, and an engine that worked didn’t pass it.

The release belongs to the HonedEdge project, a nonprofit I’m standing up, so every component had to pass that filter, weights included.

A third engine, IndexTTS2, had been integrated for emotion and inflection, and it ran, though it saw little use. Its weights come under bilibili’s own Model Use License Agreement. That licence calls itself open-source, yet it counts model outputs within its definition of derivative works, requires a separate licence above a scale threshold, and makes whoever distributes the model answerable for downstream recipients. I judged it incompatible with an AGPL release and removed it before the public tag: its adapter, its worker and its UI.

One remnant stays on purpose: a tag stripper on the speech route, so an inline emotion tag left in older text is never read aloud.

How we know

The removed engine’s licence text, fetched from its public model page on 12 Oct 2026 (sections 1.5, 2.2, 2.3 and 3.4). Hugging Face’s model API for ResembleAI/chatterbox (mit) and openbmb/VoxCPM2 (apache-2.0), and PyPI for both packages, queried the same day. download_models.py lists only the two shipped models; the tag stripper is in server/app/main.py.

Case 3 of 5

A hand-run ML recipe turned into a fail-loud background job

Voice Lab makes the 30-minute clone one button; the real work was the failures the hand-run recipe never met.

Voice Lab runs that recipe as one job with five monitored stages. You can record a browser tab for 30 minutes and walk away: the build starts itself when the capture ends. Most stages shell out to blocking external tools (ffmpeg, an audio slicer, Whisper speech recognition, the LoRA trainer), so the job runs on a background thread. The browser polls a job endpoint every two seconds, and the state is also written to a status file on disk. The failure modes were the hard part:

  • The slicer exits with an error on a Unicode print even when it succeeds, so success is judged by the clips it produced, not by its exit code.
  • Speech recognition failed silently because it inherited the app’s model-cache setting. The job now strips that variable for the slicing and speech-recognition tools, and if either produces nothing, it shows that tool’s error output.
  • A five-clip test crawled, because on Windows every data-loader worker re-imports PyTorch. Below 64 samples, the job now loads data in-thread.
  • An idle-sleeping PC killed a long build. The server now holds a Windows keep-awake for the length of the job, and the browser holds a screen wake lock while recording.

Browser capture turns off auto-gain, echo cancellation and noise suppression, so the training audio is a faithful copy. Each build trains into a fresh checkpoint directory and checks that the step-100 adapter exists before registering anything. If the external tools aren’t configured, the build endpoint answers 503 “not configured” and everything else keeps working.

How we know

Read in server/app/voicelab.py on 12 Oct 2026: the stage list, the background thread, the clip count after slicing, the cache-variable strip, the 64-sample threshold, the step-100 check, and the keep-awake call. The two-second poll, the capture settings and the screen wake lock are in web/src/App.tsx; the 503 path is in server/app/main.py. A comment beside the threshold records about 115 seconds per training step on that five-clip test; it was not re-measured.

Case 4 of 5

Ears as the test oracle

An AI coding agent can’t hear, so audio quality was settled by blind listening, and the answers were written into code.

The agent built blind A/B sets: the same script and voice, one change at a time, several generations each. I judged them by ear, and every verdict was logged so a settled question stayed settled. For fine-tuned voices, that produced a recipe that now lives in code rather than in anyone’s memory:

  • LoRA rank 32 and alpha 32, on both the language-model and diffusion layers
  • 100 training steps
  • guidance 2.0
  • 28 diffusion steps instead of the worker’s default 10, the one setting that clearly improved quality

The same sessions showed that a voice’s delivery comes from sampling, not from any setting, and voice design behaves the same way. So Text to Voice renders three takes around guidance 2.0, at 1.75, 2.0 and 2.25, and you keep the best. A studio-recording steer is added to every description so the model doesn’t invent a room, and the chosen take is frozen as a reference clip, which turns a random design into a stable, repeatable voice. Library previews are a real synthesis in the voice, not its source clip.

Listening also caught a bug no type checker would. Reading the wrong sample-rate attribute wrote 48 kHz audio under a 16 kHz header, so it played back slow and low. The worker now reads the model’s true output rate.

Voice Lab in Text to Voice mode: a voice named River with a written description of a mid-range, non-binary narrator, the line A gentle rain fell over the city as the evening lights came on, a Generate 3 Takes button, and Take A, B and C players, each with a Save button.
Fig. 3Text to Voice: a written description of an invented voice, a line to audition, and three takes to choose from. Built from the public main branch; the takes are mocked responses with silent placeholder audio.
How we know

Read 12 Oct 2026: the training config in server/app/voicelab.py; the 28 steps for fine-tuned voices, the studio steer, the three-take offsets, the frozen reference clip and the synthesized preview in server/app/main.py; the worker’s defaults (guidance 2.0, 10 steps) and its output-rate read in server/workers/voxcpm_worker.py. VoxCPM2’s model config on Hugging Face lists a 16 kHz sample_rate and a 48 kHz out_sample_rate (read 12 Oct 2026), and the training config in server/app/voicelab.py carries the same pair. The listening trials themselves are not published.

Case 5 of 5

Releasing a voice-cloning tool so that no recorded voice ships with it

Voices, adapters and machine-specific paths all stay out of the repository, and the installer builds both environments from frozen lock files.

  • Voice clips, the voice registry and the adapters stay out of git by where they live: a gitignored data/voices folder (adapters included), plus a blanket ignore on adapter weights. The only built-in voice is the engine’s default, defined in code, so a fresh clone works with no data at all.
  • The registry stores bare filenames, resolved per machine, so a fine-tuned voice moves between machines as three pieces: its registry entry, its reference clip and its adapter folder.
  • Eight environment variables set every machine-specific location: model cache, built UI, build cache, Voice Lab scratch space and four external training tools. Model weights and the build cache stay out of the backed-up, synced tree; irreplaceable data stays in it.
  • Rewritten commits stay reachable by hash on GitHub, so the public repository was recreated with one clean initial commit rather than force-pushed.

For whoever installs it, one admin run of setup.bat builds both engines from their frozen lock files with CUDA 12.4 PyTorch, builds the UI, pre-downloads both models, and checks that the CUDA runtime library actually loads, because a basic Windows display driver can provide nvidia-smi without it. openvoice.bat starts, stops and reports on the two processes.

How we know

Read 12 Oct 2026: .gitignore at the public main branch; the default voice and filename resolution in server/app/voices.py; the environment variables in server/app/config.py, web/vite.config.ts and the other server modules (8 distinct names); setup.bat; the moving-voices section of SETUP.txt; and GitHub’s API, which shows the repository created on 23 Jun 2026.