voice-cloner-studio 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- voice_cloner_studio-2.0.0/LICENSE +21 -0
- voice_cloner_studio-2.0.0/PKG-INFO +92 -0
- voice_cloner_studio-2.0.0/README.md +61 -0
- voice_cloner_studio-2.0.0/pyproject.toml +49 -0
- voice_cloner_studio-2.0.0/setup.cfg +4 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio/__init__.py +10 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio/__main__.py +4 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio/app.py +296 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio/assets/LICENSE-resemblyzer.txt +191 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio/assets/speaker_encoder.pt +0 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio/cli.py +63 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio/engine.py +311 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio/speaker.py +78 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio.egg-info/PKG-INFO +92 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio.egg-info/SOURCES.txt +19 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio.egg-info/dependency_links.txt +1 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio.egg-info/entry_points.txt +3 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio.egg-info/requires.txt +13 -0
- voice_cloner_studio-2.0.0/src/voice_cloner_studio.egg-info/top_level.txt +1 -0
- voice_cloner_studio-2.0.0/tests/test_engine.py +109 -0
- voice_cloner_studio-2.0.0/tests/test_gui.py +48 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mubashir
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voice-cloner-studio
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Natural voice cloning in 23 languages (incl. Hindi) with a browser GUI, CLI and Python API - powered by Chatterbox Multilingual.
|
|
5
|
+
Author: Mubashir
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: voice cloning,tts,text-to-speech,chatterbox,hindi,gradio
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Requires-Python: <3.14,>=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
License-File: src/voice_cloner_studio/assets/LICENSE-resemblyzer.txt
|
|
19
|
+
Requires-Dist: chatterbox-tts>=0.1.7
|
|
20
|
+
Requires-Dist: soundfile>=0.12
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: librosa
|
|
23
|
+
Requires-Dist: pyloudnorm
|
|
24
|
+
Requires-Dist: gradio
|
|
25
|
+
Provides-Extra: denoise
|
|
26
|
+
Requires-Dist: noisereduce>=3.0; extra == "denoise"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
29
|
+
Requires-Dist: gradio_client; extra == "test"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# 🎙️ Voice Cloner Studio
|
|
33
|
+
|
|
34
|
+
Natural voice cloning in **23 languages, including Hindi**. You get a browser GUI, a one-line command and a simple Python API.
|
|
35
|
+
It is powered by [Chatterbox Multilingual](https://github.com/resemble-ai/chatterbox) from Resemble AI, an open model (MIT licence) whose naturalness is in the same league as commercial services.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install voice-cloner-studio
|
|
39
|
+
voice-cloner-studio # opens the GUI at http://127.0.0.1:7860
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Python, in 4 lines
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from voice_cloner_studio import VoiceCloner
|
|
46
|
+
|
|
47
|
+
vc = VoiceCloner()
|
|
48
|
+
vc.clone("my_voice", "my_voice.wav") # once - saved for next time
|
|
49
|
+
vc.speak("Hello everyone! Welcome to my channel.", voice="my_voice") # -> dict with "path", "similarity"
|
|
50
|
+
vc.speak("नमस्ते दोस्तों!", voice="my_voice", language="Hindi", style="Expressive / energetic")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Command line
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
voice-clone --ref my_voice.wav --voice me --text "Hello there!" --out hello.wav
|
|
57
|
+
voice-clone --voice me --language Hindi --text "नमस्ते, आप कैसे हैं?"
|
|
58
|
+
voice-clone --voice me --text-file story.txt --style "Calm narrator" --takes 3
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- **Clone** a voice from a clip or a microphone recording. The app cleans the clip, normalizes loudness and shows a quality report.
|
|
64
|
+
- **Choose a style:** Natural, Expressive, Calm narrator, Fast speaker, or Accent fix for speaking another language. You can also fine-tune emotion, pace and variation.
|
|
65
|
+
- **Best of N takes:** the app keeps the take that sounds most like the voice. A built-in speaker-verification model scores how close each result is.
|
|
66
|
+
- **Built-in default voice:** you can generate speech without cloning anything.
|
|
67
|
+
- **Long texts** are split into sentences automatically. A blank line adds a paragraph pause.
|
|
68
|
+
- **Voice library:** clone once and reuse forever.
|
|
69
|
+
- **Clear errors:** the GUI shows the real reason, and full details go to `error.log`.
|
|
70
|
+
|
|
71
|
+
Voices and outputs are saved in `VoiceClonerStudio` in your home folder. Change this with `--data-dir`.
|
|
72
|
+
The model (~2 GB) downloads once, on first use.
|
|
73
|
+
|
|
74
|
+
## Best results
|
|
75
|
+
|
|
76
|
+
| | Do this |
|
|
77
|
+
|---|---|
|
|
78
|
+
| Reference | **One clean 10-20 s clip.** The model listens to the first ~10 s |
|
|
79
|
+
| Recording | Quiet room, no music or echo, normal volume |
|
|
80
|
+
| Delivery | Speak in the tone you want, because the clone copies it |
|
|
81
|
+
| Other language | Use the *Other-language accent fix* style |
|
|
82
|
+
| Bad take | Set Takes to 3, or change the seed |
|
|
83
|
+
|
|
84
|
+
An NVIDIA GPU makes generation real-time. On a laptop CPU, expect about 3-10 s of waiting per second of speech.
|
|
85
|
+
|
|
86
|
+
## Responsible use
|
|
87
|
+
|
|
88
|
+
Clone only your own voice or voices you have permission to use. Chatterbox adds an inaudible watermark ([Perth](https://github.com/resemble-ai/perth)) to generated audio, so it can be identified as AI-made.
|
|
89
|
+
|
|
90
|
+
## Licence
|
|
91
|
+
|
|
92
|
+
The code is under the MIT licence. Chatterbox is MIT, and the bundled Resemblyzer speaker encoder is Apache-2.0.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 🎙️ Voice Cloner Studio
|
|
2
|
+
|
|
3
|
+
Natural voice cloning in **23 languages, including Hindi**. You get a browser GUI, a one-line command and a simple Python API.
|
|
4
|
+
It is powered by [Chatterbox Multilingual](https://github.com/resemble-ai/chatterbox) from Resemble AI, an open model (MIT licence) whose naturalness is in the same league as commercial services.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install voice-cloner-studio
|
|
8
|
+
voice-cloner-studio # opens the GUI at http://127.0.0.1:7860
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Python, in 4 lines
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from voice_cloner_studio import VoiceCloner
|
|
15
|
+
|
|
16
|
+
vc = VoiceCloner()
|
|
17
|
+
vc.clone("my_voice", "my_voice.wav") # once - saved for next time
|
|
18
|
+
vc.speak("Hello everyone! Welcome to my channel.", voice="my_voice") # -> dict with "path", "similarity"
|
|
19
|
+
vc.speak("नमस्ते दोस्तों!", voice="my_voice", language="Hindi", style="Expressive / energetic")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Command line
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
voice-clone --ref my_voice.wav --voice me --text "Hello there!" --out hello.wav
|
|
26
|
+
voice-clone --voice me --language Hindi --text "नमस्ते, आप कैसे हैं?"
|
|
27
|
+
voice-clone --voice me --text-file story.txt --style "Calm narrator" --takes 3
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Clone** a voice from a clip or a microphone recording. The app cleans the clip, normalizes loudness and shows a quality report.
|
|
33
|
+
- **Choose a style:** Natural, Expressive, Calm narrator, Fast speaker, or Accent fix for speaking another language. You can also fine-tune emotion, pace and variation.
|
|
34
|
+
- **Best of N takes:** the app keeps the take that sounds most like the voice. A built-in speaker-verification model scores how close each result is.
|
|
35
|
+
- **Built-in default voice:** you can generate speech without cloning anything.
|
|
36
|
+
- **Long texts** are split into sentences automatically. A blank line adds a paragraph pause.
|
|
37
|
+
- **Voice library:** clone once and reuse forever.
|
|
38
|
+
- **Clear errors:** the GUI shows the real reason, and full details go to `error.log`.
|
|
39
|
+
|
|
40
|
+
Voices and outputs are saved in `VoiceClonerStudio` in your home folder. Change this with `--data-dir`.
|
|
41
|
+
The model (~2 GB) downloads once, on first use.
|
|
42
|
+
|
|
43
|
+
## Best results
|
|
44
|
+
|
|
45
|
+
| | Do this |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Reference | **One clean 10-20 s clip.** The model listens to the first ~10 s |
|
|
48
|
+
| Recording | Quiet room, no music or echo, normal volume |
|
|
49
|
+
| Delivery | Speak in the tone you want, because the clone copies it |
|
|
50
|
+
| Other language | Use the *Other-language accent fix* style |
|
|
51
|
+
| Bad take | Set Takes to 3, or change the seed |
|
|
52
|
+
|
|
53
|
+
An NVIDIA GPU makes generation real-time. On a laptop CPU, expect about 3-10 s of waiting per second of speech.
|
|
54
|
+
|
|
55
|
+
## Responsible use
|
|
56
|
+
|
|
57
|
+
Clone only your own voice or voices you have permission to use. Chatterbox adds an inaudible watermark ([Perth](https://github.com/resemble-ai/perth)) to generated audio, so it can be identified as AI-made.
|
|
58
|
+
|
|
59
|
+
## Licence
|
|
60
|
+
|
|
61
|
+
The code is under the MIT licence. Chatterbox is MIT, and the bundled Resemblyzer speaker encoder is Apache-2.0.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "voice-cloner-studio"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Natural voice cloning in 23 languages (incl. Hindi) with a browser GUI, CLI and Python API - powered by Chatterbox Multilingual."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10,<3.14"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE", "src/voice_cloner_studio/assets/LICENSE-resemblyzer.txt"]
|
|
13
|
+
authors = [{ name = "Mubashir" }]
|
|
14
|
+
keywords = ["voice cloning", "tts", "text-to-speech", "chatterbox", "hindi", "gradio"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Topic :: Multimedia :: Sound/Audio :: Speech",
|
|
22
|
+
"Environment :: Web Environment",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"chatterbox-tts>=0.1.7",
|
|
26
|
+
"soundfile>=0.12",
|
|
27
|
+
"numpy",
|
|
28
|
+
"librosa",
|
|
29
|
+
"pyloudnorm",
|
|
30
|
+
"gradio",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
denoise = ["noisereduce>=3.0"]
|
|
35
|
+
test = ["pytest>=8", "gradio_client"]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
voice-cloner-studio = "voice_cloner_studio.app:main"
|
|
39
|
+
voice-clone = "voice_cloner_studio.cli:main"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
voice_cloner_studio = ["assets/*"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
filterwarnings = ["ignore::DeprecationWarning", "ignore::FutureWarning", "ignore::UserWarning"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Voice Cloner Studio - natural voice cloning (Chatterbox Multilingual) with a browser GUI.
|
|
2
|
+
|
|
3
|
+
from voice_cloner_studio import VoiceCloner
|
|
4
|
+
vc = VoiceCloner()
|
|
5
|
+
vc.clone("me", "my_voice.wav")
|
|
6
|
+
print(vc.speak("Hello world!", voice="me")["path"])
|
|
7
|
+
"""
|
|
8
|
+
__version__ = "2.0.0"
|
|
9
|
+
|
|
10
|
+
from .engine import BUILTIN, LANGUAGES, STYLES, VoiceCloner # noqa: E402,F401
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Voice Cloner Studio - browser GUI.
|
|
3
|
+
|
|
4
|
+
Run: voice-cloner-studio (or: python -m voice_cloner_studio)
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import datetime as dt
|
|
8
|
+
import functools
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import traceback
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import gradio as gr
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from .engine import BUILTIN, LANGUAGES, STYLES, VoiceCloner, default_data_dir, split_text
|
|
19
|
+
|
|
20
|
+
SIM_LEVELS = [(0.85, "🟢 Excellent - very hard to tell apart"),
|
|
21
|
+
(0.75, "🟢 Good - in the same range as two real recordings of this person"),
|
|
22
|
+
(0.65, "🟡 Fair - similar, but differences are noticeable"),
|
|
23
|
+
(0.00, "🔴 Weak - use a cleaner / longer reference clip")]
|
|
24
|
+
|
|
25
|
+
TIPS = """
|
|
26
|
+
### For the most natural, attractive clone
|
|
27
|
+
1. **One clean clip of 10-20 s** beats many clips - the model listens to the first ~10 s.
|
|
28
|
+
2. Quiet room, no music/echo, mic ~15 cm away, normal speaking volume.
|
|
29
|
+
3. **Speak the way you want the output to sound** - warm reference → warm clone.
|
|
30
|
+
4. Pick a **Style**: *Expressive* for ads/YouTube, *Calm narrator* for audiobooks.
|
|
31
|
+
5. Speaking another language than the reference? Use *Other-language accent fix*.
|
|
32
|
+
6. Set **Takes = 3** - the app keeps the take that sounds most like you.
|
|
33
|
+
7. Hindi: type in Devanagari (नमस्ते). Urdu: type it in Devanagari or Roman script.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
CSS = "#title {text-align:center} #title h1 {font-size:2.2rem;margin-bottom:0}"
|
|
37
|
+
|
|
38
|
+
engine: VoiceCloner = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def sim_label(score):
|
|
42
|
+
return next(label for th, label in SIM_LEVELS if score >= th)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def friendly_errors(fn):
|
|
46
|
+
"""Show the real reason in the GUI (not a bare "Error") and keep the traceback in error.log."""
|
|
47
|
+
@functools.wraps(fn)
|
|
48
|
+
def wrapper(*args, **kwargs):
|
|
49
|
+
try:
|
|
50
|
+
return fn(*args, **kwargs)
|
|
51
|
+
except gr.Error:
|
|
52
|
+
raise
|
|
53
|
+
except Exception as e:
|
|
54
|
+
log = engine.data_dir / "error.log"
|
|
55
|
+
with open(log, "a", encoding="utf-8") as f:
|
|
56
|
+
f.write(f"\n===== {dt.datetime.now():%Y-%m-%d %H:%M:%S} {fn.__name__} (v{__version__})\n"
|
|
57
|
+
f"{traceback.format_exc()}")
|
|
58
|
+
traceback.print_exc()
|
|
59
|
+
msg = f"{type(e).__name__}: {e}"
|
|
60
|
+
if any(k in msg for k in ("ProxyError", "ConnectionError", "ConnectError", "Max retries",
|
|
61
|
+
"getaddrinfo", "403", "timed out", "LocalEntryNotFound")):
|
|
62
|
+
msg = ("Couldn't download the voice model (~2 GB). Check your internet / firewall and that "
|
|
63
|
+
"huggingface.co is reachable, then try again. Details: " + msg)
|
|
64
|
+
raise gr.Error(f"{msg[:600]} (full details in {log})")
|
|
65
|
+
return wrapper
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def voice_choices():
|
|
69
|
+
return [BUILTIN] + engine.list_voices()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def library_rows():
|
|
73
|
+
rows = []
|
|
74
|
+
for v in engine.list_voices():
|
|
75
|
+
m = engine.voice_info(v)
|
|
76
|
+
rows.append([v, m.get("created", ""), m.get("clips", ""), m.get("seconds", ""), m.get("notes", "")])
|
|
77
|
+
return rows
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@friendly_errors
|
|
81
|
+
def ui_clone(name, files, mic, denoise, notes, consent, progress=gr.Progress()):
|
|
82
|
+
if not consent:
|
|
83
|
+
raise gr.Error("Please confirm you have the speaker's consent before cloning a voice.")
|
|
84
|
+
sources = [f if isinstance(f, str) else f.name for f in (files or [])]
|
|
85
|
+
if mic:
|
|
86
|
+
sources.append(mic)
|
|
87
|
+
if not engine.safe_name(name):
|
|
88
|
+
raise gr.Error("Give the voice a name.")
|
|
89
|
+
if not sources:
|
|
90
|
+
raise gr.Error("Upload or record at least one reference clip.")
|
|
91
|
+
progress(0.2, desc="Loading model (first time downloads ~2 GB)")
|
|
92
|
+
_ = engine.model
|
|
93
|
+
progress(0.6, desc="Learning the voice")
|
|
94
|
+
name, rows, total = engine.clone(name, sources, denoise=denoise, notes=notes)
|
|
95
|
+
|
|
96
|
+
lines = [f"### ✅ Voice **{name}** created", "", "| Clip | Speech | Level | Clipping |", "|---|---|---|---|"]
|
|
97
|
+
lines += [f"| {n} | {d:.1f}s | {lv:.1f} dBFS | {c:.2f}% |" for n, d, lv, c in rows]
|
|
98
|
+
lines.append(f"\n**Total clean speech: {total:.1f}s**")
|
|
99
|
+
if total < 5:
|
|
100
|
+
lines.append("\n⚠️ Under 5 s - add a longer clip (10-20 s is ideal).")
|
|
101
|
+
elif total > 25:
|
|
102
|
+
lines.append("\nℹ️ Only the first ~15 s are used - put your best clip first.")
|
|
103
|
+
else:
|
|
104
|
+
lines.append("\n🟢 Good length.")
|
|
105
|
+
if any(r[3] > 0.5 for r in rows):
|
|
106
|
+
lines.append("\n⚠️ Some audio is clipping/distorted - re-record a bit quieter.")
|
|
107
|
+
choices = voice_choices()
|
|
108
|
+
return ("\n".join(lines), gr.update(choices=choices, value=name),
|
|
109
|
+
gr.update(choices=engine.list_voices(), value=name), library_rows())
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@friendly_errors
|
|
113
|
+
def ui_speak(voice, text, language, style, exaggeration, cfg_weight, temperature, seed, takes,
|
|
114
|
+
progress=gr.Progress()):
|
|
115
|
+
if not (text or "").strip():
|
|
116
|
+
raise gr.Error("Type some text to speak.")
|
|
117
|
+
progress(0.02, desc="Loading model (first time downloads ~2 GB)")
|
|
118
|
+
r = engine.speak(text, voice=voice, language=language, exaggeration=exaggeration, cfg_weight=cfg_weight,
|
|
119
|
+
temperature=temperature, seed=seed, takes=takes,
|
|
120
|
+
on_progress=lambda f, d: progress(f, desc=d))
|
|
121
|
+
info = [f"Generated **{r['seconds']:.1f}s** · seed **{r['seed']}** · `{Path(r['path']).name}`"]
|
|
122
|
+
if r["similarity"] is not None:
|
|
123
|
+
info.append(f"\n### Voice similarity: **{r['similarity'] * 100:.1f}%** \n{sim_label(r['similarity'])}")
|
|
124
|
+
if len(r["scores"]) > 1:
|
|
125
|
+
info.append("\nBest of {} takes (scores: {}).".format(
|
|
126
|
+
len(r["scores"]), ", ".join(f"{s * 100:.1f}%" for s in r["scores"])))
|
|
127
|
+
info.append("\n<sub>Independent speaker-verification score. Two real recordings of the same "
|
|
128
|
+
"person typically score 75-90%; different people 45-70%.</sub>")
|
|
129
|
+
return r["path"], r["path"], "\n".join(info), history()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def ui_style(style):
|
|
133
|
+
e, c, t = STYLES[style]
|
|
134
|
+
return e, c, t
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def ui_delete(name):
|
|
138
|
+
if name:
|
|
139
|
+
engine.delete_voice(name)
|
|
140
|
+
vs = engine.list_voices()
|
|
141
|
+
return (gr.update(choices=[BUILTIN] + vs, value=BUILTIN), gr.update(choices=vs, value=vs[0] if vs else None),
|
|
142
|
+
library_rows(), f"Deleted **{name}**." if name else "Nothing selected.")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def ui_preview(name):
|
|
146
|
+
p = engine.reference_path(name) if name else None
|
|
147
|
+
return str(p) if p else None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def history():
|
|
151
|
+
files = sorted(engine.output_dir.glob("*.wav"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
152
|
+
return [str(p) for p in files[:30]]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def build_ui():
|
|
156
|
+
voices = engine.list_voices()
|
|
157
|
+
with gr.Blocks(title="Voice Cloner Studio") as demo:
|
|
158
|
+
gr.Markdown(f"# 🎙️ Voice Cloner Studio <small>v{__version__}</small>\n"
|
|
159
|
+
f"Natural voice cloning in {len(LANGUAGES)} languages (incl. Hindi) · "
|
|
160
|
+
f"engine: Chatterbox Multilingual · running on **{engine.device.upper()}**", elem_id="title")
|
|
161
|
+
with gr.Tabs():
|
|
162
|
+
with gr.Tab("1 · Clone a voice"):
|
|
163
|
+
with gr.Row():
|
|
164
|
+
with gr.Column(scale=3):
|
|
165
|
+
c_name = gr.Textbox(label="Voice name", placeholder="e.g. my_voice")
|
|
166
|
+
c_files = gr.File(label="Reference clip(s) - one clean 10-20 s clip is ideal",
|
|
167
|
+
file_count="multiple", file_types=["audio"])
|
|
168
|
+
c_mic = gr.Audio(label="…or record: read a paragraph naturally for 15 s",
|
|
169
|
+
sources=["microphone"], type="filepath")
|
|
170
|
+
c_denoise = gr.Checkbox(label="Reduce background noise (only if your recording is noisy)",
|
|
171
|
+
value=False)
|
|
172
|
+
c_notes = gr.Textbox(label="Notes (optional)")
|
|
173
|
+
c_consent = gr.Checkbox(label="I own this voice or have the speaker's explicit permission "
|
|
174
|
+
"to clone it, and won't use it to deceive or impersonate.")
|
|
175
|
+
c_btn = gr.Button("🧬 Create voice", variant="primary", size="lg")
|
|
176
|
+
with gr.Column(scale=2):
|
|
177
|
+
c_report = gr.Markdown("Upload or record audio, then press **Create voice**.")
|
|
178
|
+
gr.Markdown(TIPS)
|
|
179
|
+
|
|
180
|
+
with gr.Tab("2 · Speak"):
|
|
181
|
+
with gr.Row():
|
|
182
|
+
with gr.Column(scale=3):
|
|
183
|
+
with gr.Row():
|
|
184
|
+
s_voice = gr.Dropdown([BUILTIN] + voices, value=voices[0] if voices else BUILTIN,
|
|
185
|
+
label="Voice", scale=3)
|
|
186
|
+
s_lang = gr.Dropdown(list(LANGUAGES), value="English", label="Language", scale=2)
|
|
187
|
+
s_text = gr.Textbox(label="Text", lines=7,
|
|
188
|
+
value="Hello! This is my cloned voice. Sounds natural, doesn't it?",
|
|
189
|
+
placeholder="Type anything. Blank line = paragraph pause.")
|
|
190
|
+
s_style = gr.Radio(list(STYLES), value="Natural", label="Style")
|
|
191
|
+
with gr.Accordion("Fine-tune", open=False):
|
|
192
|
+
s_exag = gr.Slider(0.25, 2.0, 0.5, step=0.05, label="Emotion / expressiveness")
|
|
193
|
+
s_cfg = gr.Slider(0.0, 1.0, 0.5, step=0.05,
|
|
194
|
+
label="Pace & accent guidance (lower = slower, less accent carry-over)")
|
|
195
|
+
s_temp = gr.Slider(0.05, 1.5, 0.8, step=0.05, label="Variation (temperature)")
|
|
196
|
+
s_seed = gr.Number(value=-1, precision=0, label="Seed (-1 = random)")
|
|
197
|
+
s_takes = gr.Slider(1, 5, 1, step=1, label="Takes - keep the one most like the voice")
|
|
198
|
+
s_btn = gr.Button("🔊 Generate speech", variant="primary", size="lg")
|
|
199
|
+
with gr.Column(scale=2):
|
|
200
|
+
s_audio = gr.Audio(label="Result", type="filepath", interactive=False)
|
|
201
|
+
s_file = gr.File(label="Download WAV")
|
|
202
|
+
s_info = gr.Markdown()
|
|
203
|
+
s_hist = gr.Files(label="Recent generations", value=history())
|
|
204
|
+
|
|
205
|
+
with gr.Tab("3 · Voice library"):
|
|
206
|
+
l_table = gr.Dataframe(headers=["Voice", "Created", "Clips", "Seconds", "Notes"],
|
|
207
|
+
value=library_rows(), interactive=False)
|
|
208
|
+
with gr.Row():
|
|
209
|
+
l_voice = gr.Dropdown(voices, value=voices[0] if voices else None, label="Voice")
|
|
210
|
+
l_play = gr.Audio(label="Reference used for this voice", interactive=False)
|
|
211
|
+
with gr.Row():
|
|
212
|
+
l_refresh = gr.Button("🔄 Refresh")
|
|
213
|
+
l_del = gr.Button("🗑️ Delete voice", variant="stop")
|
|
214
|
+
l_msg = gr.Markdown()
|
|
215
|
+
|
|
216
|
+
c_btn.click(ui_clone, [c_name, c_files, c_mic, c_denoise, c_notes, c_consent],
|
|
217
|
+
[c_report, s_voice, l_voice, l_table], api_name="clone")
|
|
218
|
+
s_style.change(ui_style, s_style, [s_exag, s_cfg, s_temp], api_name="style")
|
|
219
|
+
s_btn.click(ui_speak, [s_voice, s_text, s_lang, s_style, s_exag, s_cfg, s_temp, s_seed, s_takes],
|
|
220
|
+
[s_audio, s_file, s_info, s_hist], api_name="speak")
|
|
221
|
+
l_voice.change(ui_preview, l_voice, l_play, api_name="preview")
|
|
222
|
+
l_del.click(ui_delete, l_voice, [s_voice, l_voice, l_table, l_msg], api_name="delete")
|
|
223
|
+
l_refresh.click(lambda: (library_rows(), gr.update(choices=voice_choices()),
|
|
224
|
+
gr.update(choices=engine.list_voices())),
|
|
225
|
+
None, [l_table, s_voice, l_voice], api_name="refresh")
|
|
226
|
+
return demo
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def launch(data_dir=None, host="127.0.0.1", port=7860, share=False, inbrowser=True, prevent_thread_lock=False,
|
|
230
|
+
cloner=None):
|
|
231
|
+
global engine
|
|
232
|
+
engine = cloner or VoiceCloner(data_dir)
|
|
233
|
+
demo = build_ui().queue()
|
|
234
|
+
# Gradio only serves files from temp/cwd unless told otherwise; voices and outputs live in data_dir.
|
|
235
|
+
demo.launch(server_name=host, server_port=port, share=share, inbrowser=inbrowser, show_error=True,
|
|
236
|
+
prevent_thread_lock=prevent_thread_lock, theme=gr.themes.Soft(primary_hue="violet"), css=CSS,
|
|
237
|
+
allowed_paths=[str(engine.voices_dir), str(engine.output_dir)])
|
|
238
|
+
return demo
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def self_check(data_dir=None):
|
|
242
|
+
import importlib
|
|
243
|
+
import platform
|
|
244
|
+
import urllib.request
|
|
245
|
+
ok = True
|
|
246
|
+
|
|
247
|
+
def item(name, fn):
|
|
248
|
+
nonlocal ok
|
|
249
|
+
try:
|
|
250
|
+
detail = fn()
|
|
251
|
+
print(f" [OK] {name}{': ' + str(detail) if detail not in (None, '') else ''}")
|
|
252
|
+
except Exception as e:
|
|
253
|
+
ok = False
|
|
254
|
+
print(f" [FAIL] {name}: {type(e).__name__}: {e}")
|
|
255
|
+
|
|
256
|
+
def writable(d):
|
|
257
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
(d / ".write_test").write_text("ok")
|
|
259
|
+
(d / ".write_test").unlink()
|
|
260
|
+
return d
|
|
261
|
+
|
|
262
|
+
d = Path(data_dir or default_data_dir())
|
|
263
|
+
print(f"Voice Cloner Studio v{__version__} - self check")
|
|
264
|
+
item("Python", lambda: f"{platform.python_version()} ({platform.architecture()[0]})")
|
|
265
|
+
import torch
|
|
266
|
+
item("PyTorch", lambda: f"{torch.__version__}, GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'none (CPU mode, slower)'}")
|
|
267
|
+
for mod in ["chatterbox", "gradio", "librosa", "soundfile", "pyloudnorm"]:
|
|
268
|
+
item(f"package {mod}", lambda m=mod: getattr(importlib.import_module(m), "__version__", ""))
|
|
269
|
+
item("text splitter", lambda: f"{len(split_text('Hello there. नमस्ते।', 'hi'))} chunk(s)")
|
|
270
|
+
item("similarity scorer", lambda: VoiceCloner(d)._embed(np.random.randn(16000).astype(np.float32) * .1, 16000).shape)
|
|
271
|
+
item("data folder writable", lambda: writable(d))
|
|
272
|
+
item("huggingface.co reachable (model download)",
|
|
273
|
+
lambda: urllib.request.urlopen("https://huggingface.co/api/models/ResembleAI/chatterbox", timeout=15).status)
|
|
274
|
+
print("\nAll good - start the app with: voice-cloner-studio" if ok else "\nSome checks failed - see above.")
|
|
275
|
+
return 0 if ok else 1
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def main(argv=None):
|
|
279
|
+
ap = argparse.ArgumentParser(prog="voice-cloner-studio", description="Voice Cloner Studio GUI")
|
|
280
|
+
ap.add_argument("--host", default="127.0.0.1")
|
|
281
|
+
ap.add_argument("--port", type=int, default=7860)
|
|
282
|
+
ap.add_argument("--share", action="store_true", help="also create a public gradio.live link")
|
|
283
|
+
ap.add_argument("--data-dir", help=f"where voices/outputs are stored (default: {default_data_dir()})")
|
|
284
|
+
ap.add_argument("--no-browser", action="store_true", help="don't open a browser tab")
|
|
285
|
+
ap.add_argument("--check", action="store_true", help="check this PC's setup and exit")
|
|
286
|
+
args = ap.parse_args(argv)
|
|
287
|
+
if args.check:
|
|
288
|
+
sys.exit(self_check(args.data_dir))
|
|
289
|
+
print(f"[voice-cloner] Voice Cloner Studio v{__version__}")
|
|
290
|
+
print(f"[voice-cloner] Data folder: {args.data_dir or default_data_dir()}")
|
|
291
|
+
print(f"[voice-cloner] Opening http://{args.host}:{args.port} - keep this window open while using the app.")
|
|
292
|
+
launch(args.data_dir, args.host, args.port, args.share, not args.no_browser)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
if __name__ == "__main__":
|
|
296
|
+
main()
|