offiongbassey commited on
Commit
c6eeb00
·
verified ·
1 Parent(s): cb58064

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +290 -0
app.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PlotweaverNigerianVoice
3
+ A Gradio Space for the PlotweaverAI Nigerian-English fine-tuned F5-TTS model.
4
+
5
+ Two modes:
6
+ 1. Default voice: type text -> generated speech using the Nigerian-English
7
+ voice baked into this Space (sample.wav / sample.txt from the model repo).
8
+ 2. Custom voice clone: upload your own short reference clip (+ transcript,
9
+ or leave blank to auto-transcribe) -> generated speech in that voice.
10
+
11
+ Note: F5-TTS's own infer pipeline already auto-transcribes (Whisper, via
12
+ transformers) and auto-trims reference audio when ref_text is left blank,
13
+ so we lean on that built-in behavior rather than duplicating it here.
14
+
15
+ Model: PlotweaverAI/nigerian-english-ft-tts (private HF model repo)
16
+ Base architecture: F5-TTS (SWivid/F5-TTS)
17
+ """
18
+
19
+ import os
20
+ import threading
21
+
22
+ import gradio as gr
23
+ import torch
24
+ from huggingface_hub import hf_hub_download
25
+
26
+ # --- Optional: only present when running on HF Spaces with a GPU tier ----
27
+ try:
28
+ import spaces
29
+
30
+ ON_SPACES = True
31
+ except ImportError:
32
+ ON_SPACES = False
33
+
34
+
35
+ def gpu_decorator(func):
36
+ """No-op on CPU Spaces; enables ZeroGPU/queued GPU access if upgraded later."""
37
+ if ON_SPACES:
38
+ return spaces.GPU(func)
39
+ return func
40
+
41
+
42
+ # F5-TTS imports (package: f5-tts, installed from PyPI / git in requirements.txt)
43
+ from f5_tts.api import F5TTS
44
+
45
+
46
+ MODEL_REPO = "PlotweaverAI/nigerian-english-ft-tts"
47
+ HF_TOKEN = os.environ.get("HF_TOKEN") # set as a Space secret (repo is private)
48
+
49
+ # Architecture the checkpoint was fine-tuned from. F5TTS_v1_Base is the current
50
+ # default for new finetunes; override with the F5TTS_ARCH secret/variable if
51
+ # your training run used a different base (e.g. "F5TTS_Base").
52
+ MODEL_ARCH = os.environ.get("F5TTS_ARCH", "F5TTS_v1_Base")
53
+
54
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
55
+
56
+ MAX_GEN_CHARS = 600 # keep generations bounded on CPU
57
+
58
+
59
+ _model_lock = threading.Lock()
60
+ _tts_model = None
61
+ _default_ref_audio = None
62
+ _default_ref_text = None
63
+
64
+
65
+ def _download_model_files():
66
+ """Pull the fine-tuned checkpoint, vocab, and bundled sample voice
67
+ from the private model repo. Requires HF_TOKEN secret with read access."""
68
+ ckpt_path = hf_hub_download(
69
+ repo_id=MODEL_REPO, filename="model_last.pt", token=HF_TOKEN
70
+ )
71
+ vocab_path = hf_hub_download(
72
+ repo_id=MODEL_REPO, filename="vocab.txt", token=HF_TOKEN
73
+ )
74
+ sample_wav_path = hf_hub_download(
75
+ repo_id=MODEL_REPO, filename="sample.wav", token=HF_TOKEN
76
+ )
77
+ sample_txt_path = hf_hub_download(
78
+ repo_id=MODEL_REPO, filename="sample.txt", token=HF_TOKEN
79
+ )
80
+ with open(sample_txt_path, "r", encoding="utf-8") as f:
81
+ sample_text = f.read().strip()
82
+ return ckpt_path, vocab_path, sample_wav_path, sample_text
83
+
84
+
85
+ def get_model():
86
+ """Lazily load the F5-TTS model + default reference voice exactly once."""
87
+ global _tts_model, _default_ref_audio, _default_ref_text
88
+ if _tts_model is not None:
89
+ return _tts_model, _default_ref_audio, _default_ref_text
90
+
91
+ with _model_lock:
92
+ if _tts_model is not None:
93
+ return _tts_model, _default_ref_audio, _default_ref_text
94
+
95
+ ckpt_path, vocab_path, sample_wav_path, sample_text = _download_model_files()
96
+
97
+ try:
98
+ model = F5TTS(
99
+ model=MODEL_ARCH,
100
+ ckpt_file=ckpt_path,
101
+ vocab_file=vocab_path,
102
+ device=DEVICE,
103
+ )
104
+ except RuntimeError as e:
105
+ raise RuntimeError(
106
+ f"Failed to load checkpoint with architecture '{MODEL_ARCH}'. "
107
+ "If this fine-tune was trained from a different F5-TTS base "
108
+ "(e.g. 'F5TTS_Base' instead of 'F5TTS_v1_Base'), set the "
109
+ "F5TTS_ARCH variable in your Space settings to match. "
110
+ f"Original error: {e}"
111
+ ) from e
112
+
113
+ _tts_model = model
114
+ _default_ref_audio = sample_wav_path
115
+ _default_ref_text = sample_text
116
+
117
+ return _tts_model, _default_ref_audio, _default_ref_text
118
+
119
+
120
+ @gpu_decorator
121
+ def generate_default_voice(text: str, speed: float, nfe_steps: int):
122
+ if not text or not text.strip():
123
+ raise gr.Error("Please enter some text to generate speech for.")
124
+ if len(text) > MAX_GEN_CHARS:
125
+ raise gr.Error(
126
+ f"Text is too long ({len(text)} characters). "
127
+ f"Please keep it under {MAX_GEN_CHARS} characters on this CPU Space."
128
+ )
129
+
130
+ model, ref_audio, ref_text = get_model()
131
+
132
+ wav, sr, _ = model.infer(
133
+ ref_file=ref_audio,
134
+ ref_text=ref_text,
135
+ gen_text=text.strip(),
136
+ speed=speed,
137
+ nfe_step=int(nfe_steps),
138
+ remove_silence=True,
139
+ )
140
+
141
+ return (sr, wav)
142
+
143
+
144
+ @gpu_decorator
145
+ def generate_cloned_voice(
146
+ ref_audio_path: str,
147
+ ref_text: str,
148
+ gen_text: str,
149
+ speed: float,
150
+ nfe_steps: int,
151
+ ):
152
+ if ref_audio_path is None:
153
+ raise gr.Error("Please upload a reference voice clip first.")
154
+ if not gen_text or not gen_text.strip():
155
+ raise gr.Error("Please enter the text you want spoken in the cloned voice.")
156
+ if len(gen_text) > MAX_GEN_CHARS:
157
+ raise gr.Error(
158
+ f"Text is too long ({len(gen_text)} characters). "
159
+ f"Please keep it under {MAX_GEN_CHARS} characters on this CPU Space."
160
+ )
161
+
162
+ model, _, _ = get_model()
163
+
164
+ transcript = ref_text.strip() if ref_text else ""
165
+ if not transcript:
166
+ # F5-TTS's transcribe() uses a Whisper pipeline under the hood.
167
+ transcript = model.transcribe(ref_audio_path)
168
+ if not transcript:
169
+ raise gr.Error(
170
+ "Could not auto-transcribe the uploaded clip. "
171
+ "Please type the transcript manually and try again."
172
+ )
173
+
174
+ wav, sr, _ = model.infer(
175
+ ref_file=ref_audio_path,
176
+ ref_text=transcript,
177
+ gen_text=gen_text.strip(),
178
+ speed=speed,
179
+ nfe_step=int(nfe_steps),
180
+ remove_silence=True,
181
+ )
182
+
183
+ return (sr, wav), transcript
184
+
185
+ CSS = """
186
+ #title { text-align: center; margin-bottom: 0.5em; }
187
+ #subtitle { text-align: center; color: var(--body-text-color-subdued); margin-bottom: 1.5em; }
188
+ .cpu-note { font-size: 0.85em; color: var(--body-text-color-subdued); }
189
+ """
190
+
191
+ with gr.Blocks(css=CSS, title="Plotweaver Nigerian Voice") as demo:
192
+ gr.Markdown("#Plotweaver Nigerian Voice", elem_id="title")
193
+ gr.Markdown(
194
+ "Nigerian-English text-to-speech, fine-tuned from F5-TTS by Plotweaver AI. "
195
+ "Generate speech in our Nigerian voice, or clone a voice from your own clip.",
196
+ elem_id="subtitle",
197
+ )
198
+ gr.Markdown(
199
+ "Running on free CPU hardware — generation can take **30–90+ seconds** "
200
+ "per request, longer for longer text. Research / non-commercial use only "
201
+ "(base F5-TTS is CC-BY-NC).",
202
+ elem_classes="cpu-note",
203
+ )
204
+
205
+ with gr.Tabs():
206
+
207
+ with gr.Tab("Nigerian Voice (Text → Speech)"):
208
+ with gr.Row():
209
+ with gr.Column(scale=1):
210
+ default_text = gr.Textbox(
211
+ label="Text to speak",
212
+ placeholder="Type what you want the Nigerian voice to say...",
213
+ lines=6,
214
+ max_lines=12,
215
+ )
216
+ with gr.Accordion("Advanced settings", open=False):
217
+ default_speed = gr.Slider(
218
+ 0.5, 2.0, value=1.0, step=0.05, label="Speed"
219
+ )
220
+ default_nfe = gr.Slider(
221
+ 8, 64, value=24, step=2,
222
+ label="Quality steps (NFE)",
223
+ info="Higher = better quality but slower. 16-24 recommended on CPU.",
224
+ )
225
+ default_btn = gr.Button("Generate Speech", variant="primary")
226
+ with gr.Column(scale=1):
227
+ default_audio_out = gr.Audio(label="Generated audio", type="numpy")
228
+
229
+ default_btn.click(
230
+ fn=generate_default_voice,
231
+ inputs=[default_text, default_speed, default_nfe],
232
+ outputs=[default_audio_out],
233
+ )
234
+
235
+ with gr.Tab("Clone a Voice (Upload Clip → Speech)"):
236
+ gr.Markdown(
237
+ "Upload a short, clean voice clip (5-15 seconds works best). "
238
+ "Add the transcript of that clip if you have it — or leave it "
239
+ "blank and we'll auto-transcribe it with Whisper."
240
+ )
241
+ with gr.Row():
242
+ with gr.Column(scale=1):
243
+ ref_audio_in = gr.Audio(
244
+ label="Reference voice clip",
245
+ type="filepath",
246
+ sources=["upload", "microphone"],
247
+ )
248
+ ref_text_in = gr.Textbox(
249
+ label="Transcript of the clip (optional — auto-transcribed if left blank)",
250
+ placeholder="Leave blank to auto-transcribe with Whisper...",
251
+ lines=3,
252
+ )
253
+ clone_gen_text = gr.Textbox(
254
+ label="Text to speak in this cloned voice",
255
+ placeholder="Type what you want spoken in the uploaded voice...",
256
+ lines=5,
257
+ max_lines=12,
258
+ )
259
+ with gr.Accordion("Advanced settings", open=False):
260
+ clone_speed = gr.Slider(
261
+ 0.5, 2.0, value=1.0, step=0.05, label="Speed"
262
+ )
263
+ clone_nfe = gr.Slider(
264
+ 8, 64, value=24, step=2,
265
+ label="Quality steps (NFE)",
266
+ info="Higher = better quality but slower. 16-24 recommended on CPU.",
267
+ )
268
+ clone_btn = gr.Button("Generate Cloned Speech", variant="primary")
269
+ with gr.Column(scale=1):
270
+ clone_audio_out = gr.Audio(label="Generated audio", type="numpy")
271
+ used_transcript_out = gr.Textbox(
272
+ label="Transcript used for the reference clip",
273
+ interactive=False,
274
+ )
275
+
276
+ clone_btn.click(
277
+ fn=generate_cloned_voice,
278
+ inputs=[ref_audio_in, ref_text_in, clone_gen_text, clone_speed, clone_nfe],
279
+ outputs=[clone_audio_out, used_transcript_out],
280
+ )
281
+
282
+ gr.Markdown(
283
+ "---\nBuilt on [F5-TTS](https://github.com/SWivid/F5-TTS) "
284
+ "(CC-BY-NC license) · Fine-tuned voice by Plotweaver AI · "
285
+ "Non-commercial / research use.",
286
+ elem_classes="cpu-note",
287
+ )
288
+
289
+ if __name__ == "__main__":
290
+ demo.launch()