Linta1994 Claude Opus 4.8 commited on
Commit
a082bc6
Β·
1 Parent(s): 5fa178f

Add ZeroGPU Gradio Space: free on-demand GPU, always-on, no manual start

Browse files

- huggingface_space/ : Gradio app with @spaces.GPU, same two-section workflow
(create profile / generate), tuning sliders, chunked long-script stitching
- DEPLOY.md: ZeroGPU as recommended always-on free-GPU option; notes why an
automatic Colab backend isn't possible

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

DEPLOY.md CHANGED
@@ -1,14 +1,42 @@
1
  # πŸš€ Running Voice Clone Studio online
2
 
3
- GitHub stores the code; it doesn't run it. Below are two ready-to-use ways to run
4
- the app online. Pick whichever fits β€” the files for both are already in this repo.
5
-
6
- | | πŸ€— Hugging Face Spaces | ▢️ Google Colab |
7
- |---|---|---|
8
- | Cost | Free (CPU) | Free |
9
- | Speed | Slow (~min/clip on CPU) | **Fast** (free GPU) |
10
- | Stays online? | βœ… Always-on, public URL | ❌ Only while the notebook runs |
11
- | Best for | A permanent public demo | Quick, fast personal use |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  ---
14
 
 
1
  # πŸš€ Running Voice Clone Studio online
2
 
3
+ GitHub stores the code; it doesn't run it. Below are ready-to-use ways to run the
4
+ app online. The files for all of them are already in this repo.
5
+
6
+ | | ⭐ HF Space (ZeroGPU) | πŸ€— HF Space (Docker, CPU) | ▢️ Google Colab |
7
+ |---|---|---|---|
8
+ | Cost | Free | Free | Free |
9
+ | Speed | **Fast** (on-demand GPU) | Slow (~min/clip) | Fast (GPU) |
10
+ | Stays online? | βœ… Always-on | βœ… Always-on | ❌ Only while notebook runs |
11
+ | Manual start each time? | ❌ No | ❌ No | βœ… Yes |
12
+ | UI | Gradio | Your custom Flask UI | Your custom Flask UI |
13
+ | Files | `huggingface_space/` | `Dockerfile` + `deploy/huggingface/` | `notebooks/` |
14
+
15
+ > **Recommended: ZeroGPU.** Always-on, free GPU, nothing to launch. The only
16
+ > trade-off is it uses a Gradio UI instead of the custom Flask one. This is the
17
+ > closest thing to "free GPU hosting with nothing to start."
18
+ >
19
+ > **Why there's no automatic Colab option:** Google Colab is an interactive
20
+ > notebook β€” it can't stay running on its own and hands out a new URL each run, so
21
+ > it always requires manually starting it. ZeroGPU replaces that need.
22
+
23
+ ---
24
+
25
+ ## Option ⭐ β€” Hugging Face Space with ZeroGPU (free GPU, always-on)
26
+
27
+ 1. Create a free account at **https://huggingface.co**.
28
+ 2. **New β–Έ Space** β†’ **SDK: Gradio**, name it, Create.
29
+ 3. Upload the **contents of the `huggingface_space/` folder** to the Space root
30
+ (`app.py`, `requirements.txt`, `README.md`). Easiest: clone the Space repo,
31
+ copy those 3 files in, commit & push. (The Space's `README.md` must be the one
32
+ from `huggingface_space/` β€” it has the Gradio + config header.)
33
+ 4. **Settings β–Έ Hardware β–Έ select `ZeroGPU`** (free; may require a quick eligibility
34
+ opt-in / PRO for higher limits, but the free tier works).
35
+ 5. The Space builds and starts. First generation downloads the model (~1 min),
36
+ then runs on a GPU. Share the Space URL β€” done, nothing to launch.
37
+
38
+ > ⚠️ Free-Space storage is ephemeral: saved profiles reset on restart. To keep them,
39
+ > add Persistent Storage in Settings or back them with a HF Dataset.
40
 
41
  ---
42
 
huggingface_space/README.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Voice Clone Studio
3
+ emoji: πŸŽ™οΈ
4
+ colorFrom: purple
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 4.44.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Voice Clone Studio β€” Hugging Face Space (ZeroGPU)
14
+
15
+ Clone a voice and generate speech with Chatterbox TTS, on a **free on-demand GPU**.
16
+
17
+ ## How this Space is set up
18
+ - **SDK:** Gradio
19
+ - **Hardware:** **ZeroGPU** (select it in *Settings β–Έ Hardware β–Έ ZeroGPU*). The
20
+ `@spaces.GPU` decorator on `generate()` requests a GPU only while generating.
21
+
22
+ ## Notes
23
+ - Storage on a free Space is **ephemeral** β€” saved voice profiles reset when the
24
+ Space restarts/rebuilds. For permanent profiles, attach Persistent Storage
25
+ (Settings) or back them with a Hugging Face Dataset.
26
+ - First generation downloads the model (~1 min). After that, GPU generation is fast.
huggingface_space/app.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Voice Clone Studio β€” Gradio app for Hugging Face Spaces (ZeroGPU).
3
+
4
+ Free on-demand GPU via the `spaces` library: the @GPU-decorated generate()
5
+ function gets a GPU allocated for its duration. Runs on CPU locally as a no-op.
6
+
7
+ Same two-section workflow as the Flask app:
8
+ β‘  Create Voice Profile (upload + name + optional transcript, save/delete)
9
+ β‘‘ Generate Audio (pick profile, paste script, tune, generate, download)
10
+ """
11
+
12
+ import os
13
+ import re
14
+ import glob
15
+ import uuid
16
+ import shutil
17
+ from datetime import datetime
18
+
19
+ import torch
20
+ import numpy as np
21
+ import soundfile as sf
22
+ import gradio as gr
23
+
24
+ # ZeroGPU decorator β€” real GPU on HF Spaces, transparent no-op locally.
25
+ try:
26
+ import spaces
27
+
28
+ gpu = spaces.GPU(duration=120)
29
+ except Exception: # not on ZeroGPU / package missing -> run inline
30
+ def gpu(fn):
31
+ return fn
32
+
33
+ # --------------------------------------------------------------------------- #
34
+ PROFILE_DIR = "profiles"
35
+ OUTPUT_DIR = "outputs"
36
+ for _d in (PROFILE_DIR, OUTPUT_DIR):
37
+ os.makedirs(_d, exist_ok=True)
38
+
39
+ ALLOWED = {"wav", "mp3", "flac", "ogg", "m4a"}
40
+ CHUNK_MAX = 280
41
+ GAP_SECONDS = 0.35
42
+
43
+ _model = None
44
+
45
+
46
+ def get_model():
47
+ global _model
48
+ if _model is None:
49
+ from chatterbox.tts import ChatterboxTTS
50
+
51
+ device = "cuda" if torch.cuda.is_available() else "cpu"
52
+ _model = ChatterboxTTS.from_pretrained(device=device)
53
+ return _model
54
+
55
+
56
+ # ---- profile helpers ------------------------------------------------------ #
57
+ def _safe(name: str) -> str:
58
+ return re.sub(r"[^A-Za-z0-9_-]+", "_", (name or "").strip()).strip("_")
59
+
60
+
61
+ def profile_audio_path(name: str):
62
+ safe = _safe(name)
63
+ if not safe:
64
+ return None
65
+ for ext in ALLOWED:
66
+ p = os.path.join(PROFILE_DIR, f"{safe}.{ext}")
67
+ if os.path.exists(p):
68
+ return p
69
+ return None
70
+
71
+
72
+ def list_profile_names():
73
+ names, seen = [], set()
74
+ for p in sorted(glob.glob(os.path.join(PROFILE_DIR, "*.*"))):
75
+ base, ext = os.path.splitext(os.path.basename(p))
76
+ if ext.lstrip(".").lower() in ALLOWED and base not in seen:
77
+ seen.add(base)
78
+ names.append(base)
79
+ return names
80
+
81
+
82
+ def save_profile(name, audio_path, transcript):
83
+ safe = _safe(name)
84
+ if not safe:
85
+ return gr.update(), gr.update(), "❌ Please enter a profile name."
86
+ if not audio_path:
87
+ return gr.update(), gr.update(), "❌ Please upload a reference clip."
88
+ ext = os.path.splitext(audio_path)[1].lstrip(".").lower()
89
+ if ext not in ALLOWED:
90
+ ext = "wav"
91
+ for old in glob.glob(os.path.join(PROFILE_DIR, safe + ".*")):
92
+ try:
93
+ os.remove(old)
94
+ except OSError:
95
+ pass
96
+ shutil.copy(audio_path, os.path.join(PROFILE_DIR, f"{safe}.{ext}"))
97
+ if (transcript or "").strip():
98
+ with open(os.path.join(PROFILE_DIR, f"{safe}.txt"), "w", encoding="utf-8") as fh:
99
+ fh.write(transcript.strip())
100
+ names = list_profile_names()
101
+ return (
102
+ gr.update(choices=names, value=safe),
103
+ gr.update(choices=names, value=safe),
104
+ f"βœ… Saved voice profile β€œ{safe}”.",
105
+ )
106
+
107
+
108
+ def delete_profile(name):
109
+ safe = _safe(name)
110
+ for f in glob.glob(os.path.join(PROFILE_DIR, safe + ".*")):
111
+ try:
112
+ os.remove(f)
113
+ except OSError:
114
+ pass
115
+ names = list_profile_names()
116
+ val = names[0] if names else None
117
+ return (
118
+ gr.update(choices=names, value=val),
119
+ gr.update(choices=names, value=val),
120
+ (f"πŸ—‘ Deleted β€œ{safe}”." if safe else ""),
121
+ )
122
+
123
+
124
+ # ---- text chunking -------------------------------------------------------- #
125
+ def split_text(text, max_len=CHUNK_MAX):
126
+ text = " ".join(text.split())
127
+ chunks, cur = [], ""
128
+ for s in re.split(r"(?<=[.!?])\s+", text):
129
+ if not s:
130
+ continue
131
+ if len(s) > max_len:
132
+ pieces = []
133
+ for part in re.split(r"(?<=[,;:])\s+", s):
134
+ while len(part) > max_len:
135
+ pieces.append(part[:max_len])
136
+ part = part[max_len:]
137
+ if part:
138
+ pieces.append(part)
139
+ else:
140
+ pieces = [s]
141
+ for p in pieces:
142
+ if cur and len(cur) + len(p) + 1 > max_len:
143
+ chunks.append(cur)
144
+ cur = p
145
+ else:
146
+ cur = (cur + " " + p).strip()
147
+ if cur:
148
+ chunks.append(cur)
149
+ return chunks or [text]
150
+
151
+
152
+ def recent_outputs(limit=10):
153
+ files = sorted(
154
+ glob.glob(os.path.join(OUTPUT_DIR, "*.wav")),
155
+ key=os.path.getmtime,
156
+ reverse=True,
157
+ )
158
+ return files[:limit]
159
+
160
+
161
+ # ---- generation (runs on GPU via ZeroGPU) --------------------------------- #
162
+ @gpu
163
+ def generate(profile, text, cfg_weight, temperature, exaggeration,
164
+ progress=gr.Progress()):
165
+ if not profile:
166
+ raise gr.Error("Choose a saved voice profile (create one in tab β‘ ).")
167
+ if not (text or "").strip():
168
+ raise gr.Error("Paste or type a script to speak.")
169
+ ref = profile_audio_path(profile)
170
+ if not ref:
171
+ raise gr.Error(f"Voice profile '{profile}' not found.")
172
+
173
+ model = get_model()
174
+ chunks = split_text(text)
175
+ gap = torch.zeros(int(GAP_SECONDS * model.sr))
176
+ pieces = []
177
+ for i, ch in enumerate(progress.tqdm(chunks, desc="Generating")):
178
+ wav = model.generate(
179
+ ch,
180
+ audio_prompt_path=ref,
181
+ exaggeration=exaggeration,
182
+ cfg_weight=cfg_weight,
183
+ temperature=temperature,
184
+ )
185
+ pieces.append(wav.squeeze(0).cpu())
186
+ if i < len(chunks) - 1:
187
+ pieces.append(gap)
188
+
189
+ full = torch.cat(pieces).numpy()
190
+ slug = re.sub(r"[^a-zA-Z0-9]+", "-", text.strip().lower())[:32].strip("-") or "clip"
191
+ out = os.path.join(OUTPUT_DIR, f"{slug}-{datetime.now():%Y%m%d-%H%M%S}-{uuid.uuid4().hex[:6]}.wav")
192
+ sf.write(out, full, model.sr)
193
+ return (model.sr, full), recent_outputs()
194
+
195
+
196
+ # --------------------------------------------------------------------------- #
197
+ # UI
198
+ # --------------------------------------------------------------------------- #
199
+ def build_ui():
200
+ names = list_profile_names()
201
+ init = names[0] if names else None
202
+ with gr.Blocks(title="Voice Clone Studio", theme=gr.themes.Soft()) as demo:
203
+ gr.Markdown(
204
+ "# πŸŽ™οΈ Voice Clone Studio\n"
205
+ "Clone a voice and generate speech β€” powered by Chatterbox TTS on a free GPU."
206
+ )
207
+
208
+ with gr.Tab("β‘  Create Voice Profile"):
209
+ with gr.Row():
210
+ with gr.Column():
211
+ p_name = gr.Textbox(label="Profile name", placeholder="e.g. gayan")
212
+ p_audio = gr.Audio(label="Reference audio (10–60 s of clear speech)", type="filepath")
213
+ p_trans = gr.Textbox(label="Transcript / notes (optional)", lines=3,
214
+ placeholder="Optional β€” Chatterbox clones from the audio alone.")
215
+ p_save = gr.Button("πŸ’Ύ Save voice profile", variant="primary")
216
+ p_msg = gr.Markdown()
217
+ with gr.Column():
218
+ gr.Markdown("### Your voice profiles")
219
+ del_dd = gr.Dropdown(label="Saved profiles", choices=names, value=init)
220
+ del_btn = gr.Button("πŸ—‘ Delete selected")
221
+
222
+ with gr.Tab("β‘‘ Generate Audio"):
223
+ gen_dd = gr.Dropdown(label="Choose voice profile", choices=names, value=init)
224
+ g_text = gr.Textbox(label="Script", lines=8,
225
+ placeholder="Paste your script. Long scripts are split and stitched into one track…")
226
+ with gr.Accordion("βš™οΈ Voice tuning (lower = cleaner / less artifacts)", open=False):
227
+ cfg = gr.Slider(0, 1, value=0.3, step=0.05, label="CFG weight")
228
+ temp = gr.Slider(0.05, 1.2, value=0.7, step=0.05, label="Temperature")
229
+ exag = gr.Slider(0.25, 1, value=0.5, step=0.05, label="Exaggeration")
230
+ g_btn = gr.Button("Generate Audio", variant="primary")
231
+ g_audio = gr.Audio(label="Output", type="numpy", autoplay=True)
232
+ g_hist = gr.Files(label="Recent generations (click to download)", value=recent_outputs())
233
+
234
+ # refresh dropdown choices when this tab gains focus is automatic via events below
235
+ p_save.click(save_profile, [p_name, p_audio, p_trans], [gen_dd, del_dd, p_msg])
236
+ del_btn.click(delete_profile, [del_dd], [gen_dd, del_dd, p_msg])
237
+ g_btn.click(generate, [gen_dd, g_text, cfg, temp, exag], [g_audio, g_hist])
238
+
239
+ return demo
240
+
241
+
242
+ demo = build_ui()
243
+
244
+ if __name__ == "__main__":
245
+ demo.queue().launch()
huggingface_space/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ spaces
3
+ chatterbox-tts>=0.1.7
4
+ numpy<2
5
+ soundfile