Daankular commited on
Commit
ee8cb08
·
verified ·
1 Parent(s): 46e31ca

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. README.md +21 -7
  2. app.py +350 -0
  3. requirements.txt +7 -0
  4. src/asr_backend.py +63 -0
  5. src/audio8_backend.py +95 -0
  6. voices.json +0 -0
README.md CHANGED
@@ -1,13 +1,27 @@
1
  ---
2
- title: Audio8TTS
3
- emoji: 📉
4
- colorFrom: gray
5
- colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.23.1
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Audio8 TTS Preview
3
+ emoji: 🗣️
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.23.1
 
8
  app_file: app.py
9
+ python_version: "3.12"
10
+ short_description: Zero-shot voice cloning TTS gallery for Audio8 0.6B
11
+ startup_duration_timeout: 30m
12
  ---
13
 
14
+ # Audio8 TTS Preview 0.6B
15
+
16
+ Demo for [Audio8/Audio8-TTS-Preview-0.6b](https://huggingface.co/Audio8/Audio8-TTS-Preview-0.6b),
17
+ a 0.6B-parameter DualAR multilingual TTS model with zero-shot voice cloning,
18
+ running on ZeroGPU.
19
+
20
+ Browse thousands of reference voices (sourced from
21
+ [Daankular/DramaboxTTS](https://huggingface.co/spaces/Daankular/DramaboxTTS)'s
22
+ `voices.json`), pick one to clone, or upload/record your own reference clip.
23
+ A CPU-side Whisper pass auto-transcribes the reference clip since Audio8 TTS
24
+ requires a matching transcript to condition cloning.
25
+
26
+ Supported generation languages: Cantonese, Chinese, Dutch, English, French,
27
+ German, Italian, Japanese, Korean, Polish, Spanish.
app.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Audio8 TTS Preview 0.6B voice gallery for Hugging Face ZeroGPU."""
3
+
4
+ import json
5
+ import logging
6
+ import os
7
+ import sys
8
+ import tempfile
9
+
10
+ import gradio as gr
11
+ import requests
12
+ import soundfile as sf
13
+ import spaces
14
+
15
+ _DIR = os.path.dirname(os.path.abspath(__file__))
16
+ sys.path.insert(0, os.path.join(_DIR, "src"))
17
+ import audio8_backend # noqa: E402
18
+ import asr_backend # noqa: E402
19
+
20
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
21
+
22
+ SUPPORTED_LANGUAGES = (
23
+ "Cantonese, Chinese, Dutch, English, French, German, Italian, "
24
+ "Japanese, Korean, Polish, Spanish"
25
+ )
26
+
27
+ # ── Voices ───────────────────────────────────────────────────────────────────
28
+ with open(os.path.join(_DIR, "voices.json"), encoding="utf-8") as _f:
29
+ VOICES = json.load(_f)
30
+
31
+ LANGUAGES = ["All"] + sorted({v.get("language", "") for v in VOICES if v.get("language")})
32
+ GENDERS = ["All", "female", "male", "neutral"]
33
+ PER_PAGE = 20
34
+
35
+ logging.info(f"Loaded {len(VOICES):,} reference voices")
36
+
37
+
38
+ def _filter(search, lang, gender, accent):
39
+ s = (search or "").lower()
40
+ return [
41
+ v for v in VOICES
42
+ if (lang == "All" or v.get("language") == lang)
43
+ and (gender == "All" or v.get("gender") == gender)
44
+ and (accent == "All" or v.get("accent") == accent)
45
+ and (not s or s in v.get("name", "").lower()
46
+ or s in (v.get("description") or "").lower())
47
+ ]
48
+
49
+
50
+ def _accents_for(lang):
51
+ pool = VOICES if lang == "All" else [v for v in VOICES if v.get("language") == lang]
52
+ return ["All"] + sorted({v.get("accent", "") for v in pool if v.get("accent")})
53
+
54
+
55
+ # ── Models ───────────────────────────────────────────────────────────────────
56
+ audio8_backend.load()
57
+ asr_backend.load()
58
+
59
+
60
+ @spaces.GPU(duration=60, size="large")
61
+ def on_generate(prompt, ref_audio_path, ref_text, temperature, top_p, top_k,
62
+ max_new_tok, seed, progress=gr.Progress()):
63
+ """Synthesize speech from text, optionally cloning the reference voice."""
64
+ if not (prompt or "").strip():
65
+ raise gr.Error("Prompt is empty.")
66
+
67
+ try:
68
+ progress(0.5, desc="Generating with Audio8 TTS…")
69
+ waveform, sr = audio8_backend.generate(
70
+ prompt.strip(), voice_ref=ref_audio_path, reference_text=ref_text,
71
+ temperature=float(temperature), top_p=float(top_p),
72
+ top_k=int(top_k), max_new_tokens=int(max_new_tok), seed=int(seed),
73
+ )
74
+ except ValueError as e:
75
+ raise gr.Error(str(e))
76
+
77
+ out = tempfile.mktemp(suffix=".wav", prefix="audio8_", dir="/tmp")
78
+ sf.write(out, waveform, sr)
79
+ return out
80
+
81
+
82
+ # ── CSS ──────────────────────────────────────────────────────────────────────
83
+ CSS = """
84
+ /* card grid */
85
+ .card-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
86
+ @media (max-width: 1200px) { .card-grid { grid-template-columns: repeat(3, 1fr); } }
87
+ @media (max-width: 800px) { .card-grid { grid-template-columns: repeat(2, 1fr); } }
88
+
89
+ /* individual card — scoped inside the Gradio column */
90
+ .voice-card { background: #14181f !important; border: 1px solid #26303f !important;
91
+ border-radius: 10px !important; padding: 14px !important; height: 100% !important; }
92
+ .voice-card:hover { border-color: #2dd4bf !important; }
93
+
94
+ /* card header line */
95
+ .card-header { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 6px; }
96
+ .badge-f { background: #3d0e2a; color: #e080b0; font-size: 11px; font-weight: 700;
97
+ padding: 2px 7px; border-radius: 4px; white-space: nowrap; }
98
+ .badge-m { background: #0e2a3d; color: #80c0e0; font-size: 11px; font-weight: 700;
99
+ padding: 2px 7px; border-radius: 4px; white-space: nowrap; }
100
+ .badge-n { background: #1e2a1e; color: #a0c8a0; font-size: 11px; font-weight: 700;
101
+ padding: 2px 7px; border-radius: 4px; white-space: nowrap; }
102
+ .card-name { font-size: 13px; font-weight: 600; color: #dde5f0; line-height: 1.35; }
103
+
104
+ /* tags row */
105
+ .card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; }
106
+ .card-tags span { font-size: 10px; padding: 2px 6px; border-radius: 3px; }
107
+ .t-lang { background: #123a2e; color: #6fd7b5; }
108
+ .t-acc { background: #16283a; color: #7fb0d9; }
109
+ .t-age { background: #2a1e2a; color: #b08cc0; }
110
+
111
+ /* description */
112
+ .card-desc { font-size: 11px; color: #6a7590; line-height: 1.4; margin-bottom: 4px; }
113
+
114
+ /* "Use this voice" button override */
115
+ .use-btn { background: #2dd4bf !important; color: #04231f !important; border: none !important;
116
+ font-weight: 700 !important; }
117
+ .use-btn:hover { background: #5fe4d3 !important; }
118
+
119
+ /* selected voice banner */
120
+ .sel-banner { background: #0d1a17; border: 1px solid #204a3f; border-radius: 8px;
121
+ padding: 10px 14px; margin: 6px 0; }
122
+
123
+ /* pagination */
124
+ .pager-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
125
+ """
126
+
127
+ # ── UI ───────────────────────────────────────────────────────────────────────
128
+ with gr.Blocks(title="Audio8 TTS Preview", analytics_enabled=False) as app:
129
+
130
+ gr.Markdown(
131
+ "# 🗣️ Audio8 TTS Preview 0.6B\n"
132
+ "A 0.6B-parameter multilingual TTS model with zero-shot voice cloning "
133
+ "([model card](https://huggingface.co/Audio8/Audio8-TTS-Preview-0.6b)). "
134
+ f"Browse **{len(VOICES):,} reference voices**, hit ▶ to preview, then "
135
+ "**Use this voice** to clone it — or upload/record your own reference clip.\n\n"
136
+ f"⚠️ Generated text should be one of the model's supported languages: "
137
+ f"**{SUPPORTED_LANGUAGES}**. Reference clips in other languages still work "
138
+ "as voice-timbre references, but transcription/cloning quality is best "
139
+ "within these 11."
140
+ )
141
+
142
+ # Filters
143
+ with gr.Row():
144
+ search_in = gr.Textbox(placeholder="Search by name or description…", label="Search", scale=3)
145
+ lang_in = gr.Dropdown(LANGUAGES, value="All", label="Language", scale=2)
146
+ gender_in = gr.Radio(GENDERS, value="All", label="Gender", scale=2)
147
+ accent_in = gr.Dropdown(["All"], value="All", label="Accent", scale=2)
148
+
149
+ result_md = gr.Markdown("")
150
+
151
+ # ── Fixed card grid (PER_PAGE slots) ─────────────────────────────────────
152
+ card_rows = [] # gr.Column slots (show/hide)
153
+ card_html = [] # gr.HTML — full card content incl. <audio> tag
154
+ card_btns = [] # gr.Button — "Use this voice"
155
+
156
+ page_voices = gr.State([]) # voice dicts on the current page
157
+
158
+ COLS = 4
159
+ for r_idx in range((PER_PAGE + COLS - 1) // COLS):
160
+ with gr.Row():
161
+ for c_idx in range(COLS):
162
+ slot = r_idx * COLS + c_idx
163
+ if slot >= PER_PAGE:
164
+ break
165
+ with gr.Column(elem_classes=["voice-card"]) as col:
166
+ html = gr.HTML("")
167
+ btn = gr.Button("✅ Use this voice", size="sm", elem_classes=["use-btn"])
168
+ card_html.append(html)
169
+ card_btns.append(btn)
170
+ card_rows.append(col)
171
+
172
+ # Pagination
173
+ with gr.Row(elem_classes=["pager-row"]):
174
+ prev_btn = gr.Button("← Prev", size="sm", interactive=False)
175
+ page_info = gr.Markdown("", elem_classes=["pager-info"])
176
+ next_btn = gr.Button("Next →", size="sm", interactive=False)
177
+
178
+ # Selected voice banner
179
+ with gr.Row(visible=False, elem_classes=["sel-banner"]) as sel_row:
180
+ with gr.Column(scale=2):
181
+ sel_md = gr.Markdown("**No voice selected**")
182
+ with gr.Column(scale=3):
183
+ sel_audio = gr.Audio(
184
+ label="Reference audio (auto-filled from gallery pick — or upload/record your own)",
185
+ sources=["upload", "microphone"], type="filepath", interactive=True,
186
+ )
187
+
188
+ # Generation
189
+ gr.Markdown("---\n## Write text to synthesize")
190
+ with gr.Row():
191
+ with gr.Column(scale=3):
192
+ prompt_box = gr.Textbox(
193
+ label="Text", lines=5,
194
+ placeholder="Type what you want the selected voice to say.",
195
+ )
196
+ gr.Examples(
197
+ examples=[
198
+ ["Welcome to Audio8 TTS, a compact model with zero-shot voice cloning."],
199
+ ["La qualité de la voix clonée dépend beaucoup de la clarté de l'échantillon de référence."],
200
+ ["Dieses Modell erzeugt Sprache in elf Sprachen bei nur 0,6 Milliarden Parametern."],
201
+ ["この音声合成モデルは、わずかな参照音声からも声質を再現できます。"],
202
+ ],
203
+ inputs=[prompt_box],
204
+ label="Example prompts",
205
+ )
206
+ gen_btn = gr.Button("Generate", variant="primary", size="lg")
207
+ with gr.Column(scale=2):
208
+ with gr.Accordion("Settings", open=False):
209
+ ref_text_in = gr.Textbox(
210
+ label="Reference transcript (auto-filled on selection, required for cloning)",
211
+ lines=2,
212
+ placeholder="Auto-transcribed from the reference audio. Must match it exactly.",
213
+ )
214
+ temperature_s = gr.Slider(0., 1.5, .8, step=.05, label="Temperature")
215
+ top_p_s = gr.Slider(.1, 1., .95, step=.01, label="Top-p")
216
+ top_k_s = gr.Slider(0, 200, 50, step=1, label="Top-k")
217
+ max_tok_s = gr.Slider(64, 2048, 1024, step=64, label="Max new tokens")
218
+ seed_n = gr.Number(-1, precision=0, label="Seed (-1 = random)")
219
+ audio_out = gr.Audio(label="Generated audio", type="filepath")
220
+
221
+ # ── Page state ────────────────────────────────────────────────────────────
222
+ page_state = gr.State(1)
223
+
224
+ # ── Helper: build all card + pagination outputs from a voice list + page ──
225
+ def _all_updates(filtered, page):
226
+ total = len(filtered)
227
+ total_pages = max(1, (total + PER_PAGE - 1) // PER_PAGE)
228
+ page = max(1, min(page, total_pages))
229
+ chunk = filtered[(page - 1) * PER_PAGE: page * PER_PAGE]
230
+
231
+ html_updates, vis_updates = [], []
232
+ for i in range(PER_PAGE):
233
+ if i < len(chunk):
234
+ v = chunk[i]
235
+ g = v.get("gender", "")
236
+ badge_cls = {"female": "badge-f", "male": "badge-m"}.get(g, "badge-n")
237
+ badge_sym = {"female": "♀", "male": "♂"}.get(g, "•")
238
+ badge = f'<span class="{badge_cls}">{badge_sym}</span>'
239
+ name = v.get("name", "Unknown")
240
+ lt, at, ag = v.get("language", "?"), v.get("accent", "?"), v.get("age", "?")
241
+ desc = (v.get("description") or "")[:100]
242
+ src = v.get("preview_url", "")
243
+ html = (
244
+ f'<div class="card-header">{badge}'
245
+ f'<span class="card-name">{name}</span></div>'
246
+ f'<div class="card-tags">'
247
+ f'<span class="t-lang">{lt}</span>'
248
+ f'<span class="t-acc">{at}</span>'
249
+ f'<span class="t-age">{ag}</span></div>'
250
+ + (f'<p class="card-desc">{desc}</p>' if desc else "")
251
+ + f'<audio controls preload="none" src="{src}" style="width:100%;height:32px;margin-top:4px"></audio>'
252
+ )
253
+ html_updates.append(gr.update(value=html))
254
+ vis_updates.append(gr.update(visible=True))
255
+ else:
256
+ html_updates.append(gr.update(value=""))
257
+ vis_updates.append(gr.update(visible=False))
258
+
259
+ return (
260
+ html_updates + vis_updates +
261
+ [gr.update(value=f"**{total:,}** voices found"),
262
+ gr.update(value=f"Page **{page}** / {total_pages}"),
263
+ gr.update(interactive=page > 1),
264
+ gr.update(interactive=page < total_pages),
265
+ chunk, page]
266
+ )
267
+
268
+ _gallery_outputs = (
269
+ card_html + card_rows +
270
+ [result_md, page_info, prev_btn, next_btn, page_voices, page_state]
271
+ )
272
+
273
+ # ── Filter change → reset to page 1 ──────────────────────────────────────
274
+ def on_filter(s, l, g, a):
275
+ filtered = _filter(s, l, g, a)
276
+ return _all_updates(filtered, 1)
277
+
278
+ def on_lang(l):
279
+ return gr.Dropdown(choices=_accents_for(l), value="All")
280
+
281
+ lang_in.change(on_lang, lang_in, accent_in)
282
+
283
+ for inp in [search_in, lang_in, gender_in, accent_in]:
284
+ inp.change(on_filter, [search_in, lang_in, gender_in, accent_in], _gallery_outputs)
285
+
286
+ # ── Pagination ────────────────────────────────────────────────────────────
287
+ def on_prev(s, l, g, a, pg):
288
+ return _all_updates(_filter(s, l, g, a), int(pg) - 1)
289
+
290
+ def on_next(s, l, g, a, pg):
291
+ return _all_updates(_filter(s, l, g, a), int(pg) + 1)
292
+
293
+ prev_btn.click(on_prev, [search_in, lang_in, gender_in, accent_in, page_state], _gallery_outputs)
294
+ next_btn.click(on_next, [search_in, lang_in, gender_in, accent_in, page_state], _gallery_outputs)
295
+
296
+ # ── "Use this voice" buttons ──────────────────────────────────────────────
297
+ def _make_use_handler(slot_idx):
298
+ def handler(voices):
299
+ if slot_idx >= len(voices):
300
+ return gr.update(), gr.update(), gr.update(visible=False)
301
+ v = voices[slot_idx]
302
+ name = v.get("name", "Unknown")
303
+ preview = v.get("preview_url", "")
304
+ tmp = None
305
+ if preview:
306
+ try:
307
+ r = requests.get(preview, timeout=15)
308
+ r.raise_for_status()
309
+ f = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
310
+ f.write(r.content)
311
+ f.close()
312
+ tmp = f.name
313
+ except Exception as e:
314
+ logging.warning(f"Preview download failed: {e}")
315
+ return (
316
+ gr.update(value=f"**Selected:** {name}"),
317
+ gr.update(value=tmp),
318
+ gr.update(visible=True),
319
+ )
320
+ return handler
321
+
322
+ for i, btn in enumerate(card_btns):
323
+ btn.click(_make_use_handler(i), inputs=[page_voices], outputs=[sel_md, sel_audio, sel_row])
324
+
325
+ # Auto-transcribe the reference clip on CPU (Whisper) so "Reference
326
+ # transcript" is pre-filled — Audio8 TTS requires a transcript whenever a
327
+ # reference clip is provided, so this fires whether the clip came from
328
+ # the gallery or a direct upload/recording. User can still edit it.
329
+ sel_audio.change(asr_backend.transcribe, inputs=[sel_audio], outputs=[ref_text_in])
330
+
331
+ # ── Generate ──────────────────────────────────────────────────────────────
332
+ gen_btn.click(
333
+ on_generate,
334
+ [prompt_box, sel_audio, ref_text_in, temperature_s, top_p_s, top_k_s, max_tok_s, seed_n],
335
+ [audio_out],
336
+ )
337
+
338
+ # ── Initial load ──────────────────────────────────────────────────────────
339
+ app.load(lambda: _all_updates(VOICES, 1), outputs=_gallery_outputs)
340
+
341
+
342
+ if __name__ == "__main__":
343
+ port = int(os.environ.get("GRADIO_SERVER_PORT", "7860"))
344
+ app.queue(max_size=10).launch(
345
+ server_name="0.0.0.0", server_port=port,
346
+ share=os.environ.get("GRADIO_SHARE", "1") == "1",
347
+ css=CSS,
348
+ ssr_mode=False,
349
+ mcp_server=True,
350
+ )
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ transformers>=4.57.0,<5
2
+ torch>=2.5.0
3
+ torchaudio>=2.5.0
4
+ accelerate>=0.25.0
5
+ soundfile>=0.12
6
+ safetensors>=0.4
7
+ requests>=2.31.0
src/asr_backend.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """CPU-only Whisper ASR for auto-transcribing voice reference clips.
3
+
4
+ Runs on CPU and is never wrapped in @spaces.GPU — keeps it off the ZeroGPU
5
+ quota entirely and lets it run any time, independent of whichever TTS
6
+ request currently holds the GPU. Audio8 TTS *requires* a reference
7
+ transcript whenever a reference clip is used, so this auto-fills
8
+ "Reference transcript" the moment a voice is picked from the gallery (or a
9
+ clip is uploaded), and the user can still edit it before generating.
10
+
11
+ Multilingual `whisper-base` (not the `.en` variant) since the voice gallery
12
+ spans many languages.
13
+ """
14
+ import logging
15
+
16
+ import torch
17
+
18
+ ASR_REPO = "openai/whisper-base"
19
+ ASR_SAMPLE_RATE = 16000
20
+
21
+ _processor = None
22
+ _model = None
23
+
24
+
25
+ def load():
26
+ """Load the Whisper processor + model onto CPU. Idempotent."""
27
+ global _processor, _model
28
+ if _model is not None:
29
+ return
30
+
31
+ from transformers import AutoProcessor, WhisperForConditionalGeneration
32
+
33
+ logging.info(f"Loading Whisper ASR ({ASR_REPO}) on CPU…")
34
+ _processor = AutoProcessor.from_pretrained(ASR_REPO)
35
+ _model = WhisperForConditionalGeneration.from_pretrained(ASR_REPO).eval()
36
+ logging.info("Whisper ASR ready.")
37
+
38
+
39
+ def transcribe(audio_path):
40
+ """Best-effort CPU transcription of a reference clip.
41
+
42
+ Returns the stripped transcript, or "" if there's no clip or
43
+ transcription fails — callers treat "" as "leave the field as-is /
44
+ let the user fill it in manually".
45
+ """
46
+ if not audio_path or _model is None:
47
+ return ""
48
+
49
+ import soundfile as sf
50
+ import torchaudio
51
+
52
+ try:
53
+ data, sr = sf.read(audio_path, dtype="float32", always_2d=True) # [L, C]
54
+ wav = torch.from_numpy(data).mean(dim=1) # mono [L]
55
+ if sr != ASR_SAMPLE_RATE:
56
+ wav = torchaudio.functional.resample(wav, orig_freq=sr, new_freq=ASR_SAMPLE_RATE)
57
+ inputs = _processor(wav.numpy(), sampling_rate=ASR_SAMPLE_RATE, return_tensors="pt")
58
+ with torch.no_grad():
59
+ tokens = _model.generate(**inputs)
60
+ return _processor.batch_decode(tokens, skip_special_tokens=True)[0].strip()
61
+ except Exception as e:
62
+ logging.warning(f"Reference transcription failed: {e}")
63
+ return ""
src/audio8_backend.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Audio8 TTS Preview 0.6B backend for ZeroGPU.
3
+
4
+ DualAR (Fish-Audio-S2-Pro-style) 0.6B multilingual TTS with zero-shot voice
5
+ cloning: https://huggingface.co/Audio8/Audio8-TTS-Preview-0.6b. Requires
6
+ trust_remote_code=True (custom `arktts` modeling/processing code shipped in
7
+ the model repo).
8
+
9
+ ``load()`` runs once at app startup and moves the model to "cuda" eagerly —
10
+ ZeroGPU's CUDA emulation packs the tensors to disk and streams them into VRAM
11
+ on the first real request, so lazy loading inside the decorated handler would
12
+ cost every user instead of only the first.
13
+ """
14
+ import logging
15
+ import os
16
+
17
+ import torch
18
+
19
+ MODEL_REPO = "Audio8/Audio8-TTS-Preview-0.6b"
20
+
21
+ _processor = None
22
+ _model = None
23
+
24
+
25
+ def load():
26
+ """Load the processor + model onto cuda. Idempotent."""
27
+ global _processor, _model
28
+ if _model is not None:
29
+ return
30
+
31
+ from transformers import AutoModel, AutoProcessor
32
+
33
+ logging.info(f"Loading Audio8 TTS ({MODEL_REPO})…")
34
+ token = os.environ.get("HF_TOKEN")
35
+ _processor = AutoProcessor.from_pretrained(
36
+ MODEL_REPO, token=token, trust_remote_code=True
37
+ )
38
+ _model = (
39
+ AutoModel.from_pretrained(
40
+ MODEL_REPO, token=token, trust_remote_code=True, dtype=torch.bfloat16
41
+ )
42
+ .eval()
43
+ .to("cuda")
44
+ )
45
+ logging.info("Audio8 TTS ready.")
46
+
47
+
48
+ def generate(text, voice_ref=None, reference_text=None, temperature=0.8,
49
+ top_p=0.95, top_k=50, max_new_tokens=1024, seed=-1):
50
+ """Generate speech with Audio8 TTS Preview.
51
+
52
+ voice_ref: optional path to a reference clip for zero-shot cloning.
53
+ reference_text: transcript of voice_ref. The processor *requires* a
54
+ non-empty reference_text whenever voice_ref is given — the model
55
+ conditions generation on the text/audio alignment, not just the
56
+ audio. Raises ValueError if missing.
57
+
58
+ Returns (waveform: 1-D numpy array, sample_rate: int).
59
+ """
60
+ if _model is None:
61
+ raise RuntimeError("Audio8 TTS is not loaded — call audio8_backend.load() at startup.")
62
+
63
+ if seed is not None and int(seed) >= 0:
64
+ torch.manual_seed(int(seed))
65
+
66
+ call_kwargs = {}
67
+ if voice_ref:
68
+ if not reference_text or not reference_text.strip():
69
+ raise ValueError(
70
+ "This model needs a transcript of the reference clip to clone it. "
71
+ "Fill in \"Reference transcript\" (auto-transcription may have failed) "
72
+ "or clear the reference audio to generate without cloning."
73
+ )
74
+ call_kwargs["reference_audio"] = [voice_ref]
75
+ call_kwargs["reference_text"] = [reference_text.strip()]
76
+
77
+ inputs = _processor(text=[text], return_tensors="pt", **call_kwargs)
78
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
79
+
80
+ with torch.inference_mode():
81
+ output = _model.generate(
82
+ **inputs,
83
+ max_new_tokens=int(max_new_tokens),
84
+ temperature=float(temperature),
85
+ top_p=float(top_p),
86
+ top_k=int(top_k),
87
+ do_sample=True,
88
+ return_dict_in_generate=True,
89
+ )
90
+ waveforms, waveform_lengths = _model.decode_audio(output.codes)
91
+
92
+ audio = waveforms[0, : int(waveform_lengths[0])].float().cpu().numpy()
93
+ if audio.size == 0:
94
+ raise RuntimeError("Audio8 TTS produced no audio — try again or adjust the text.")
95
+ return audio, _model.config.codec_sample_rate
voices.json ADDED
The diff for this file is too large to render. See raw diff