返回 F5-TTS
infer_gradio.py
根目录 / src / f5_tts / infer / infer_gradio.py
1 # ruff: noqa: E402
2 # Above allows ruff to ignore E402: module level import not at top of file
3
4 import gc
5 import json
6 import os
7 import re
8 import tempfile
9 from collections import OrderedDict
10 from functools import lru_cache
11 from importlib.resources import files
12
13 import click
14 import gradio as gr
15 import numpy as np
16 import soundfile as sf
17 import torch
18 import torchaudio
19 from cached_path import cached_path
20 from transformers import AutoModelForCausalLM, AutoTokenizer
21
22
23 try:
24 import spaces
25
26 USING_SPACES = True
27 except ImportError:
28 USING_SPACES = False
29
30
31 def gpu_decorator(func):
32 if USING_SPACES:
33 return spaces.GPU(func)
34 else:
35 return func
36
37
38 from f5_tts.infer.utils_infer import (
39 infer_process,
40 load_model,
41 load_vocoder,
42 preprocess_ref_audio_text,
43 remove_silence_for_generated_wav,
44 save_spectrogram,
45 tempfile_kwargs,
46 )
47 from f5_tts.model import DiT, UNetT
48
49
50 DEFAULT_TTS_MODEL = "F5-TTS_v1"
51 tts_model_choice = DEFAULT_TTS_MODEL
52
53 DEFAULT_TTS_MODEL_CFG = [
54 "hf://SWivid/F5-TTS/F5TTS_v1_Base/model_1250000.safetensors",
55 "hf://SWivid/F5-TTS/F5TTS_v1_Base/vocab.txt",
56 json.dumps(dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4)),
57 ]
58
59
60 # load models
61
62 vocoder = load_vocoder()
63
64
65 def load_f5tts():
66 ckpt_path = str(cached_path(DEFAULT_TTS_MODEL_CFG[0]))
67 F5TTS_model_cfg = json.loads(DEFAULT_TTS_MODEL_CFG[2])
68 return load_model(DiT, F5TTS_model_cfg, ckpt_path)
69
70
71 def load_e2tts():
72 ckpt_path = str(cached_path("hf://SWivid/E2-TTS/E2TTS_Base/model_1200000.safetensors"))
73 E2TTS_model_cfg = dict(dim=1024, depth=24, heads=16, ff_mult=4, text_mask_padding=False, pe_attn_head=1)
74 return load_model(UNetT, E2TTS_model_cfg, ckpt_path)
75
76
77 def load_custom(ckpt_path: str, vocab_path="", model_cfg=None):
78 ckpt_path, vocab_path = ckpt_path.strip(), vocab_path.strip()
79 if ckpt_path.startswith("hf://"):
80 ckpt_path = str(cached_path(ckpt_path))
81 if vocab_path.startswith("hf://"):
82 vocab_path = str(cached_path(vocab_path))
83 if model_cfg is None:
84 model_cfg = json.loads(DEFAULT_TTS_MODEL_CFG[2])
85 elif isinstance(model_cfg, str):
86 model_cfg = json.loads(model_cfg)
87 return load_model(DiT, model_cfg, ckpt_path, vocab_file=vocab_path)
88
89
90 F5TTS_ema_model = load_f5tts()
91 E2TTS_ema_model = load_e2tts() if USING_SPACES else None
92 custom_ema_model, pre_custom_path = None, ""
93
94 chat_model_state = None
95 chat_tokenizer_state = None
96
97
98 @gpu_decorator
99 def chat_model_inference(messages, model, tokenizer):
100 """Generate response using Qwen"""
101 text = tokenizer.apply_chat_template(
102 messages,
103 tokenize=False,
104 add_generation_prompt=True,
105 )
106
107 model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
108 generated_ids = model.generate(
109 **model_inputs,
110 max_new_tokens=512,
111 temperature=0.7,
112 top_p=0.95,
113 )
114
115 generated_ids = [
116 output_ids[len(input_ids) :] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
117 ]
118 return tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
119
120
121 @gpu_decorator
122 def load_text_from_file(file):
123 if file:
124 with open(file, "r", encoding="utf-8") as f:
125 text = f.read().strip()
126 else:
127 text = ""
128 return gr.update(value=text)
129
130
131 @lru_cache(maxsize=1000) # NOTE. need to ensure params of infer() hashable
132 @gpu_decorator
133 def infer(
134 ref_audio_orig,
135 ref_text,
136 gen_text,
137 model,
138 remove_silence,
139 seed,
140 cross_fade_duration=0.15,
141 nfe_step=32,
142 speed=1,
143 show_info=gr.Info,
144 ):
145 if not ref_audio_orig:
146 gr.Warning("Please provide reference audio.")
147 return gr.update(), gr.update(), ref_text
148
149 # Set inference seed
150 if seed < 0 or seed > 2**31 - 1:
151 gr.Warning("Seed must in range 0 ~ 2147483647. Using random seed instead.")
152 seed = np.random.randint(0, 2**31 - 1)
153 torch.manual_seed(seed)
154 used_seed = seed
155
156 if not gen_text.strip():
157 gr.Warning("Please enter text to generate or upload a text file.")
158 return gr.update(), gr.update(), ref_text
159
160 ref_audio, ref_text = preprocess_ref_audio_text(ref_audio_orig, ref_text, show_info=show_info)
161
162 if model == DEFAULT_TTS_MODEL:
163 ema_model = F5TTS_ema_model
164 elif model == "E2-TTS":
165 global E2TTS_ema_model
166 if E2TTS_ema_model is None:
167 show_info("Loading E2-TTS model...")
168 E2TTS_ema_model = load_e2tts()
169 ema_model = E2TTS_ema_model
170 elif isinstance(model, tuple) and model[0] == "Custom":
171 assert not USING_SPACES, "Only official checkpoints allowed in Spaces."
172 global custom_ema_model, pre_custom_path
173 if pre_custom_path != model[1]:
174 show_info("Loading Custom TTS model...")
175 custom_ema_model = load_custom(model[1], vocab_path=model[2], model_cfg=model[3])
176 pre_custom_path = model[1]
177 ema_model = custom_ema_model
178
179 final_wave, final_sample_rate, combined_spectrogram = infer_process(
180 ref_audio,
181 ref_text,
182 gen_text,
183 ema_model,
184 vocoder,
185 cross_fade_duration=cross_fade_duration,
186 nfe_step=nfe_step,
187 speed=speed,
188 show_info=show_info,
189 progress=gr.Progress(),
190 )
191
192 # Remove silence
193 if remove_silence:
194 with tempfile.NamedTemporaryFile(suffix=".wav", **tempfile_kwargs) as f:
195 temp_path = f.name
196 try:
197 sf.write(temp_path, final_wave, final_sample_rate)
198 remove_silence_for_generated_wav(f.name)
199 final_wave, _ = torchaudio.load(f.name)
200 finally:
201 os.unlink(temp_path)
202 final_wave = final_wave.squeeze().cpu().numpy()
203
204 # Save the spectrogram
205 with tempfile.NamedTemporaryFile(suffix=".png", **tempfile_kwargs) as tmp_spectrogram:
206 spectrogram_path = tmp_spectrogram.name
207 save_spectrogram(combined_spectrogram, spectrogram_path)
208
209 return (final_sample_rate, final_wave), spectrogram_path, ref_text, used_seed
210
211
212 with gr.Blocks() as app_tts:
213 gr.Markdown("# Batched TTS")
214 ref_audio_input = gr.Audio(label="Reference Audio", type="filepath")
215 with gr.Row():
216 gen_text_input = gr.Textbox(
217 label="Text to Generate",
218 lines=10,
219 max_lines=40,
220 scale=4,
221 )
222 gen_text_file = gr.File(label="Load Text to Generate from File (.txt)", file_types=[".txt"], scale=1)
223 generate_btn = gr.Button("Synthesize", variant="primary")
224 with gr.Accordion("Advanced Settings", open=True) as adv_settn:
225 with gr.Row():
226 ref_text_input = gr.Textbox(
227 label="Reference Text",
228 info="Leave blank to automatically transcribe the reference audio. If you enter text or upload a file, it will override automatic transcription.",
229 lines=2,
230 scale=4,
231 )
232 ref_text_file = gr.File(label="Load Reference Text from File (.txt)", file_types=[".txt"], scale=1)
233 with gr.Row():
234 randomize_seed = gr.Checkbox(
235 label="Randomize Seed",
236 info="Check to use a random seed for each generation. Uncheck to use the seed specified.",
237 value=True,
238 scale=3,
239 )
240 seed_input = gr.Number(show_label=False, value=0, precision=0, scale=1)
241 with gr.Column(scale=4):
242 remove_silence = gr.Checkbox(
243 label="Remove Silences",
244 info="If undesired long silence(s) produced, turn on to automatically detect and crop.",
245 value=False,
246 )
247 speed_slider = gr.Slider(
248 label="Speed",
249 minimum=0.3,
250 maximum=2.0,
251 value=1.0,
252 step=0.1,
253 info="Adjust the speed of the audio.",
254 )
255 nfe_slider = gr.Slider(
256 label="NFE Steps",
257 minimum=4,
258 maximum=64,
259 value=32,
260 step=2,
261 info="Set the number of denoising steps.",
262 )
263 cross_fade_duration_slider = gr.Slider(
264 label="Cross-Fade Duration (s)",
265 minimum=0.0,
266 maximum=1.0,
267 value=0.15,
268 step=0.01,
269 info="Set the duration of the cross-fade between audio clips.",
270 )
271
272 def collapse_accordion():
273 return gr.Accordion(open=False)
274
275 # Workaround for https://github.com/SWivid/F5-TTS/issues/1239#issuecomment-3677987413
276 # i.e. to set gr.Accordion(open=True) by default, then collapse manually Blocks loaded
277 app_tts.load(
278 fn=collapse_accordion,
279 inputs=None,
280 outputs=adv_settn,
281 )
282
283 audio_output = gr.Audio(label="Synthesized Audio")
284 spectrogram_output = gr.Image(label="Spectrogram")
285
286 @gpu_decorator
287 def basic_tts(
288 ref_audio_input,
289 ref_text_input,
290 gen_text_input,
291 remove_silence,
292 randomize_seed,
293 seed_input,
294 cross_fade_duration_slider,
295 nfe_slider,
296 speed_slider,
297 ):
298 if randomize_seed:
299 seed_input = np.random.randint(0, 2**31 - 1)
300
301 audio_out, spectrogram_path, ref_text_out, used_seed = infer(
302 ref_audio_input,
303 ref_text_input,
304 gen_text_input,
305 tts_model_choice,
306 remove_silence,
307 seed=seed_input,
308 cross_fade_duration=cross_fade_duration_slider,
309 nfe_step=nfe_slider,
310 speed=speed_slider,
311 )
312 return audio_out, spectrogram_path, ref_text_out, used_seed
313
314 gen_text_file.upload(
315 load_text_from_file,
316 inputs=[gen_text_file],
317 outputs=[gen_text_input],
318 )
319
320 ref_text_file.upload(
321 load_text_from_file,
322 inputs=[ref_text_file],
323 outputs=[ref_text_input],
324 )
325
326 ref_audio_input.clear(
327 lambda: [None, None],
328 None,
329 [ref_text_input, ref_text_file],
330 )
331
332 generate_btn.click(
333 basic_tts,
334 inputs=[
335 ref_audio_input,
336 ref_text_input,
337 gen_text_input,
338 remove_silence,
339 randomize_seed,
340 seed_input,
341 cross_fade_duration_slider,
342 nfe_slider,
343 speed_slider,
344 ],
345 outputs=[audio_output, spectrogram_output, ref_text_input, seed_input],
346 )
347
348
349 def parse_speechtypes_text(gen_text):
350 # Pattern to find {str} or {"name": str, "seed": int, "speed": float}
351 pattern = r"(\{.*?\})"
352
353 # Split the text by the pattern
354 tokens = re.split(pattern, gen_text)
355
356 segments = []
357
358 current_type_dict = {
359 "name": "Regular",
360 "seed": -1,
361 "speed": 1.0,
362 }
363
364 for i in range(len(tokens)):
365 if i % 2 == 0:
366 # This is text
367 text = tokens[i].strip()
368 if text:
369 current_type_dict["text"] = text
370 segments.append(current_type_dict)
371 else:
372 # This is type
373 type_str = tokens[i].strip()
374 try: # if type dict
375 current_type_dict = json.loads(type_str)
376 except json.decoder.JSONDecodeError:
377 type_str = type_str[1:-1] # remove brace {}
378 current_type_dict = {"name": type_str, "seed": -1, "speed": 1.0}
379
380 return segments
381
382
383 with gr.Blocks() as app_multistyle:
384 # New section for multistyle generation
385 gr.Markdown(
386 """
387 # Multiple Speech-Type Generation
388
389 This section allows you to generate multiple speech types or multiple people's voices. Enter your text in the format shown below, or upload a .txt file with the same format. The system will generate speech using the appropriate type. If unspecified, the model will use the regular speech type. The current speech type will be used until the next speech type is specified.
390 """
391 )
392
393 with gr.Row():
394 gr.Markdown(
395 """
396 **Example Input:** <br>
397 {Regular} Hello, I'd like to order a sandwich please. <br>
398 {Surprised} What do you mean you're out of bread? <br>
399 {Sad} I really wanted a sandwich though... <br>
400 {Angry} You know what, darn you and your little shop! <br>
401 {Whisper} I'll just go back home and cry now. <br>
402 {Shouting} Why me?!
403 """
404 )
405
406 gr.Markdown(
407 """
408 **Example Input 2:** <br>
409 {"name": "Speaker1_Happy", "seed": -1, "speed": 1} Hello, I'd like to order a sandwich please. <br>
410 {"name": "Speaker2_Regular", "seed": -1, "speed": 1} Sorry, we're out of bread. <br>
411 {"name": "Speaker1_Sad", "seed": -1, "speed": 1} I really wanted a sandwich though... <br>
412 {"name": "Speaker2_Whisper", "seed": -1, "speed": 1} I'll give you the last one I was hiding.
413 """
414 )
415
416 gr.Markdown(
417 'Upload different audio clips for each speech type. The first speech type is mandatory. You can add additional speech types by clicking the "Add Speech Type" button.'
418 )
419
420 # Regular speech type (mandatory)
421 with gr.Row(variant="compact") as regular_row:
422 with gr.Column(scale=1, min_width=160):
423 regular_name = gr.Textbox(value="Regular", label="Speech Type Name")
424 regular_insert = gr.Button("Insert Label", variant="secondary")
425 with gr.Column(scale=3):
426 regular_audio = gr.Audio(label="Regular Reference Audio", type="filepath")
427 with gr.Column(scale=3):
428 regular_ref_text = gr.Textbox(label="Reference Text (Regular)", lines=4)
429 with gr.Row():
430 regular_seed_slider = gr.Slider(
431 show_label=False, minimum=-1, maximum=999, value=-1, step=1, info="Seed, -1 for random"
432 )
433 regular_speed_slider = gr.Slider(
434 show_label=False, minimum=0.3, maximum=2.0, value=1.0, step=0.1, info="Adjust the speed"
435 )
436 with gr.Column(scale=1, min_width=160):
437 regular_ref_text_file = gr.File(label="Load Reference Text from File (.txt)", file_types=[".txt"])
438
439 # Regular speech type (max 100)
440 max_speech_types = 100
441 speech_type_rows = [regular_row]
442 speech_type_names = [regular_name]
443 speech_type_audios = [regular_audio]
444 speech_type_ref_texts = [regular_ref_text]
445 speech_type_ref_text_files = [regular_ref_text_file]
446 speech_type_seeds = [regular_seed_slider]
447 speech_type_speeds = [regular_speed_slider]
448 speech_type_delete_btns = [None]
449 speech_type_insert_btns = [regular_insert]
450
451 # Additional speech types (99 more)
452 for i in range(max_speech_types - 1):
453 with gr.Row(variant="compact", visible=False) as row:
454 with gr.Column(scale=1, min_width=160):
455 name_input = gr.Textbox(label="Speech Type Name")
456 insert_btn = gr.Button("Insert Label", variant="secondary")
457 delete_btn = gr.Button("Delete Type", variant="stop")
458 with gr.Column(scale=3):
459 audio_input = gr.Audio(label="Reference Audio", type="filepath")
460 with gr.Column(scale=3):
461 ref_text_input = gr.Textbox(label="Reference Text", lines=4)
462 with gr.Row():
463 seed_input = gr.Slider(
464 show_label=False, minimum=-1, maximum=999, value=-1, step=1, info="Seed. -1 for random"
465 )
466 speed_input = gr.Slider(
467 show_label=False, minimum=0.3, maximum=2.0, value=1.0, step=0.1, info="Adjust the speed"
468 )
469 with gr.Column(scale=1, min_width=160):
470 ref_text_file_input = gr.File(label="Load Reference Text from File (.txt)", file_types=[".txt"])
471 speech_type_rows.append(row)
472 speech_type_names.append(name_input)
473 speech_type_audios.append(audio_input)
474 speech_type_ref_texts.append(ref_text_input)
475 speech_type_ref_text_files.append(ref_text_file_input)
476 speech_type_seeds.append(seed_input)
477 speech_type_speeds.append(speed_input)
478 speech_type_delete_btns.append(delete_btn)
479 speech_type_insert_btns.append(insert_btn)
480
481 # Global logic for all speech types
482 for i in range(max_speech_types):
483 speech_type_audios[i].clear(
484 lambda: [None, None],
485 None,
486 [speech_type_ref_texts[i], speech_type_ref_text_files[i]],
487 )
488 speech_type_ref_text_files[i].upload(
489 load_text_from_file,
490 inputs=[speech_type_ref_text_files[i]],
491 outputs=[speech_type_ref_texts[i]],
492 )
493
494 # Button to add speech type
495 add_speech_type_btn = gr.Button("Add Speech Type")
496
497 # Keep track of autoincrement of speech types, no roll back
498 speech_type_count = 1
499
500 # Function to add a speech type
501 def add_speech_type_fn():
502 row_updates = [gr.update() for _ in range(max_speech_types)]
503 global speech_type_count
504 if speech_type_count < max_speech_types:
505 row_updates[speech_type_count] = gr.update(visible=True)
506 speech_type_count += 1
507 else:
508 gr.Warning("Exhausted maximum number of speech types. Consider restart the app.")
509 return row_updates
510
511 add_speech_type_btn.click(add_speech_type_fn, outputs=speech_type_rows)
512
513 # Function to delete a speech type
514 def delete_speech_type_fn():
515 return gr.update(visible=False), None, None, None, None
516
517 # Update delete button clicks and ref text file changes
518 for i in range(1, len(speech_type_delete_btns)):
519 speech_type_delete_btns[i].click(
520 delete_speech_type_fn,
521 outputs=[
522 speech_type_rows[i],
523 speech_type_names[i],
524 speech_type_audios[i],
525 speech_type_ref_texts[i],
526 speech_type_ref_text_files[i],
527 ],
528 )
529
530 # Text input for the prompt
531 with gr.Row():
532 gen_text_input_multistyle = gr.Textbox(
533 label="Text to Generate",
534 lines=10,
535 max_lines=40,
536 scale=4,
537 placeholder="Enter the script with speaker names (or emotion types) at the start of each block, e.g.:\n\n{Regular} Hello, I'd like to order a sandwich please.\n{Surprised} What do you mean you're out of bread?\n{Sad} I really wanted a sandwich though...\n{Angry} You know what, darn you and your little shop!\n{Whisper} I'll just go back home and cry now.\n{Shouting} Why me?!",
538 )
539 gen_text_file_multistyle = gr.File(label="Load Text to Generate from File (.txt)", file_types=[".txt"], scale=1)
540
541 def make_insert_speech_type_fn(index):
542 def insert_speech_type_fn(current_text, speech_type_name, speech_type_seed, speech_type_speed):
543 current_text = current_text or ""
544 if not speech_type_name:
545 gr.Warning("Please enter speech type name before insert.")
546 return current_text
547 speech_type_dict = {
548 "name": speech_type_name,
549 "seed": speech_type_seed,
550 "speed": speech_type_speed,
551 }
552 updated_text = current_text + json.dumps(speech_type_dict) + " "
553 return updated_text
554
555 return insert_speech_type_fn
556
557 for i, insert_btn in enumerate(speech_type_insert_btns):
558 insert_fn = make_insert_speech_type_fn(i)
559 insert_btn.click(
560 insert_fn,
561 inputs=[gen_text_input_multistyle, speech_type_names[i], speech_type_seeds[i], speech_type_speeds[i]],
562 outputs=gen_text_input_multistyle,
563 )
564
565 with gr.Accordion("Advanced Settings", open=True):
566 with gr.Row():
567 with gr.Column():
568 show_cherrypick_multistyle = gr.Checkbox(
569 label="Show Cherry-pick Interface",
570 info="Turn on to show interface, picking seeds from previous generations.",
571 value=False,
572 )
573 with gr.Column():
574 remove_silence_multistyle = gr.Checkbox(
575 label="Remove Silences",
576 info="Turn on to automatically detect and crop long silences.",
577 value=True,
578 )
579
580 # Generate button
581 generate_multistyle_btn = gr.Button("Generate Multi-Style Speech", variant="primary")
582
583 # Output audio
584 audio_output_multistyle = gr.Audio(label="Synthesized Audio")
585
586 # Used seed gallery
587 cherrypick_interface_multistyle = gr.Textbox(
588 label="Cherry-pick Interface",
589 lines=10,
590 max_lines=40,
591 buttons=["copy"], # show_copy_button=True if gradio<6.0
592 interactive=False,
593 visible=False,
594 )
595
596 # Logic control to show/hide the cherrypick interface
597 show_cherrypick_multistyle.change(
598 lambda is_visible: gr.update(visible=is_visible),
599 show_cherrypick_multistyle,
600 cherrypick_interface_multistyle,
601 )
602
603 # Function to load text to generate from file
604 gen_text_file_multistyle.upload(
605 load_text_from_file,
606 inputs=[gen_text_file_multistyle],
607 outputs=[gen_text_input_multistyle],
608 )
609
610 @gpu_decorator
611 def generate_multistyle_speech(
612 gen_text,
613 *args,
614 ):
615 speech_type_names_list = args[:max_speech_types]
616 speech_type_audios_list = args[max_speech_types : 2 * max_speech_types]
617 speech_type_ref_texts_list = args[2 * max_speech_types : 3 * max_speech_types]
618 remove_silence = args[3 * max_speech_types]
619 # Collect the speech types and their audios into a dict
620 speech_types = OrderedDict()
621
622 ref_text_idx = 0
623 for name_input, audio_input, ref_text_input in zip(
624 speech_type_names_list, speech_type_audios_list, speech_type_ref_texts_list
625 ):
626 if name_input and audio_input:
627 speech_types[name_input] = {"audio": audio_input, "ref_text": ref_text_input}
628 else:
629 speech_types[f"@{ref_text_idx}@"] = {"audio": "", "ref_text": ""}
630 ref_text_idx += 1
631
632 # Parse the gen_text into segments
633 segments = parse_speechtypes_text(gen_text)
634
635 # For each segment, generate speech
636 generated_audio_segments = []
637 current_type_name = "Regular"
638 inference_meta_data = ""
639
640 for segment in segments:
641 name = segment["name"]
642 seed_input = segment["seed"]
643 speed = segment["speed"]
644 text = segment["text"]
645
646 if name in speech_types:
647 current_type_name = name
648 else:
649 gr.Warning(f"Type {name} is not available, will use Regular as default.")
650 current_type_name = "Regular"
651
652 try:
653 ref_audio = speech_types[current_type_name]["audio"]
654 except KeyError:
655 gr.Warning(f"Please provide reference audio for type {current_type_name}.")
656 return [None] + [speech_types[name]["ref_text"] for name in speech_types] + [None]
657 ref_text = speech_types[current_type_name].get("ref_text", "")
658
659 if seed_input == -1:
660 seed_input = np.random.randint(0, 2**31 - 1)
661
662 # Generate or retrieve speech for this segment
663 audio_out, _, ref_text_out, used_seed = infer(
664 ref_audio,
665 ref_text,
666 text,
667 tts_model_choice,
668 remove_silence,
669 seed=seed_input,
670 cross_fade_duration=0,
671 speed=speed,
672 show_info=print, # no pull to top when generating
673 )
674 sr, audio_data = audio_out
675
676 generated_audio_segments.append(audio_data)
677 speech_types[current_type_name]["ref_text"] = ref_text_out
678 inference_meta_data += json.dumps(dict(name=name, seed=used_seed, speed=speed)) + f" {text}\n"
679
680 # Concatenate all audio segments
681 if generated_audio_segments:
682 final_audio_data = np.concatenate(generated_audio_segments)
683 return (
684 [(sr, final_audio_data)]
685 + [speech_types[name]["ref_text"] for name in speech_types]
686 + [inference_meta_data]
687 )
688 else:
689 gr.Warning("No audio generated.")
690 return [None] + [speech_types[name]["ref_text"] for name in speech_types] + [None]
691
692 generate_multistyle_btn.click(
693 generate_multistyle_speech,
694 inputs=[
695 gen_text_input_multistyle,
696 ]
697 + speech_type_names
698 + speech_type_audios
699 + speech_type_ref_texts
700 + [
701 remove_silence_multistyle,
702 ],
703 outputs=[audio_output_multistyle] + speech_type_ref_texts + [cherrypick_interface_multistyle],
704 )
705
706 # Validation function to disable Generate button if speech types are missing
707 def validate_speech_types(gen_text, regular_name, *args):
708 speech_type_names_list = args
709
710 # Collect the speech types names
711 speech_types_available = set()
712 if regular_name:
713 speech_types_available.add(regular_name)
714 for name_input in speech_type_names_list:
715 if name_input:
716 speech_types_available.add(name_input)
717
718 # Parse the gen_text to get the speech types used
719 segments = parse_speechtypes_text(gen_text)
720 speech_types_in_text = set(segment["name"] for segment in segments)
721
722 # Check if all speech types in text are available
723 missing_speech_types = speech_types_in_text - speech_types_available
724
725 if missing_speech_types:
726 # Disable the generate button
727 return gr.update(interactive=False)
728 else:
729 # Enable the generate button
730 return gr.update(interactive=True)
731
732 gen_text_input_multistyle.change(
733 validate_speech_types,
734 inputs=[gen_text_input_multistyle, regular_name] + speech_type_names,
735 outputs=generate_multistyle_btn,
736 )
737
738
739 with gr.Blocks() as app_chat:
740 gr.Markdown(
741 """
742 # Voice Chat
743 Have a conversation with an AI using your reference voice!
744 1. Upload a reference audio clip and optionally its transcript (via text or .txt file).
745 2. Load the chat model.
746 3. Record your message through your microphone or type it.
747 4. The AI will respond using the reference voice.
748 """
749 )
750
751 chat_model_name_list = [
752 "Qwen/Qwen2.5-3B-Instruct",
753 "microsoft/Phi-4-mini-instruct",
754 ]
755
756 @gpu_decorator
757 def load_chat_model(chat_model_name):
758 show_info = gr.Info
759 global chat_model_state, chat_tokenizer_state
760 if chat_model_state is not None:
761 chat_model_state = None
762 chat_tokenizer_state = None
763 gc.collect()
764 torch.cuda.empty_cache()
765
766 show_info(f"Loading chat model: {chat_model_name}")
767 chat_model_state = AutoModelForCausalLM.from_pretrained(chat_model_name, torch_dtype="auto", device_map="auto")
768 chat_tokenizer_state = AutoTokenizer.from_pretrained(chat_model_name)
769 show_info(f"Chat model {chat_model_name} loaded successfully!")
770
771 return gr.update(visible=False), gr.update(visible=True)
772
773 if USING_SPACES:
774 load_chat_model(chat_model_name_list[0])
775
776 chat_model_name_input = gr.Dropdown(
777 choices=chat_model_name_list,
778 value=chat_model_name_list[0],
779 label="Chat Model Name",
780 info="Enter the name of a HuggingFace chat model",
781 allow_custom_value=not USING_SPACES,
782 )
783 load_chat_model_btn = gr.Button("Load Chat Model", variant="primary", visible=not USING_SPACES)
784 chat_interface_container = gr.Column(visible=USING_SPACES)
785
786 chat_model_name_input.change(
787 lambda: gr.update(visible=True),
788 None,
789 load_chat_model_btn,
790 show_progress="hidden",
791 )
792 load_chat_model_btn.click(
793 load_chat_model, inputs=[chat_model_name_input], outputs=[load_chat_model_btn, chat_interface_container]
794 )
795
796 with chat_interface_container:
797 with gr.Row():
798 with gr.Column():
799 ref_audio_chat = gr.Audio(label="Reference Audio", type="filepath")
800 with gr.Column():
801 with gr.Accordion("Advanced Settings", open=False):
802 with gr.Row():
803 ref_text_chat = gr.Textbox(
804 label="Reference Text",
805 info="Optional: Leave blank to auto-transcribe",
806 lines=2,
807 scale=3,
808 )
809 ref_text_file_chat = gr.File(
810 label="Load Reference Text from File (.txt)", file_types=[".txt"], scale=1
811 )
812 with gr.Row():
813 randomize_seed_chat = gr.Checkbox(
814 label="Randomize Seed",
815 value=True,
816 info="Uncheck to use the seed specified.",
817 scale=3,
818 )
819 seed_input_chat = gr.Number(show_label=False, value=0, precision=0, scale=1)
820 remove_silence_chat = gr.Checkbox(
821 label="Remove Silences",
822 value=True,
823 )
824 system_prompt_chat = gr.Textbox(
825 label="System Prompt",
826 value="You are not an AI assistant, you are whoever the user says you are. You must stay in character. Keep your responses concise since they will be spoken out loud.",
827 lines=2,
828 )
829
830 chatbot_interface = gr.Chatbot(
831 label="Conversation"
832 ) # type="messages" hard-coded and no need to pass in since gradio 6.0
833
834 with gr.Row():
835 with gr.Column():
836 audio_input_chat = gr.Microphone(
837 label="Speak your message",
838 type="filepath",
839 )
840 audio_output_chat = gr.Audio(autoplay=True)
841 with gr.Column():
842 text_input_chat = gr.Textbox(
843 label="Type your message",
844 lines=1,
845 )
846 send_btn_chat = gr.Button("Send Message")
847 clear_btn_chat = gr.Button("Clear Conversation")
848
849 # Modify process_audio_input to generate user input
850 @gpu_decorator
851 def process_audio_input(conv_state, audio_path, text):
852 """Handle audio or text input from user"""
853
854 if not audio_path and not text.strip():
855 return conv_state
856
857 if audio_path:
858 text = preprocess_ref_audio_text(audio_path, text)[1]
859 if not text.strip():
860 return conv_state
861
862 conv_state.append({"role": "user", "content": text})
863 return conv_state
864
865 # Use model and tokenizer from state to get text response
866 @gpu_decorator
867 def generate_text_response(conv_state, system_prompt):
868 """Generate text response from AI"""
869 for single_state in conv_state:
870 if isinstance(single_state["content"], list):
871 assert len(single_state["content"]) == 1 and single_state["content"][0]["type"] == "text"
872 single_state["content"] = single_state["content"][0]["text"]
873
874 system_prompt_state = [{"role": "system", "content": system_prompt}]
875 response = chat_model_inference(system_prompt_state + conv_state, chat_model_state, chat_tokenizer_state)
876
877 conv_state.append({"role": "assistant", "content": response})
878 return conv_state
879
880 @gpu_decorator
881 def generate_audio_response(conv_state, ref_audio, ref_text, remove_silence, randomize_seed, seed_input):
882 """Generate TTS audio for AI response"""
883 if not conv_state or not ref_audio:
884 return None, ref_text, seed_input
885
886 last_ai_response = conv_state[-1]["content"][0]["text"]
887 if not last_ai_response or conv_state[-1]["role"] != "assistant":
888 return None, ref_text, seed_input
889
890 if randomize_seed:
891 seed_input = np.random.randint(0, 2**31 - 1)
892
893 audio_result, _, ref_text_out, used_seed = infer(
894 ref_audio,
895 ref_text,
896 last_ai_response,
897 tts_model_choice,
898 remove_silence,
899 seed=seed_input,
900 cross_fade_duration=0.15,
901 speed=1.0,
902 show_info=print, # show_info=print no pull to top when generating
903 )
904 return audio_result, ref_text_out, used_seed
905
906 def clear_conversation():
907 """Reset the conversation"""
908 return [], None
909
910 ref_text_file_chat.upload(
911 load_text_from_file,
912 inputs=[ref_text_file_chat],
913 outputs=[ref_text_chat],
914 )
915
916 for user_operation in [audio_input_chat.stop_recording, text_input_chat.submit, send_btn_chat.click]:
917 user_operation(
918 process_audio_input,
919 inputs=[chatbot_interface, audio_input_chat, text_input_chat],
920 outputs=[chatbot_interface],
921 ).then(
922 generate_text_response,
923 inputs=[chatbot_interface, system_prompt_chat],
924 outputs=[chatbot_interface],
925 ).then(
926 generate_audio_response,
927 inputs=[
928 chatbot_interface,
929 ref_audio_chat,
930 ref_text_chat,
931 remove_silence_chat,
932 randomize_seed_chat,
933 seed_input_chat,
934 ],
935 outputs=[audio_output_chat, ref_text_chat, seed_input_chat],
936 ).then(
937 lambda: [None, None],
938 None,
939 [audio_input_chat, text_input_chat],
940 )
941
942 # Handle clear button or system prompt change and reset conversation
943 for user_operation in [clear_btn_chat.click, system_prompt_chat.change, chatbot_interface.clear]:
944 user_operation(
945 clear_conversation,
946 outputs=[chatbot_interface, audio_output_chat],
947 )
948
949
950 with gr.Blocks() as app_credits:
951 gr.Markdown("""
952 # Credits
953
954 * [mrfakename](https://github.com/fakerybakery) for the original [online demo](https://huggingface.co/spaces/mrfakename/E2-F5-TTS)
955 * [RootingInLoad](https://github.com/RootingInLoad) for initial chunk generation and podcast app exploration
956 * [jpgallegoar](https://github.com/jpgallegoar) for multiple speech-type generation & voice chat
957 """)
958
959
960 with gr.Blocks() as app:
961 gr.Markdown(
962 f"""
963 # F5-TTS Demo Space
964
965 This is {"a local web UI for [F5-TTS](https://github.com/SWivid/F5-TTS)" if not USING_SPACES else "an online demo for [F5-TTS](https://github.com/SWivid/F5-TTS)"} with advanced batch processing support. This app supports the following TTS models:
966
967 * [F5-TTS](https://arxiv.org/abs/2410.06885) (A Fairytaler that Fakes Fluent and Faithful Speech with Flow Matching)
968 * [E2 TTS](https://arxiv.org/abs/2406.18009) (Embarrassingly Easy Fully Non-Autoregressive Zero-Shot TTS)
969
970 The checkpoints currently support English and Chinese.
971
972 If you're having issues, try converting your reference audio to WAV or MP3, clipping it to 12s with ✂ in the bottom right corner (otherwise might have non-optimal auto-trimmed result).
973
974 **NOTE: Reference text will be automatically transcribed with Whisper if not provided. For best results, keep your reference clips short (<12s). Ensure the audio is fully uploaded before generating.**
975 """
976 )
977
978 last_used_custom = files("f5_tts").joinpath("infer/.cache/last_used_custom_model_info_v1.txt")
979
980 def load_last_used_custom():
981 try:
982 custom = []
983 with open(last_used_custom, "r", encoding="utf-8") as f:
984 for line in f:
985 custom.append(line.strip())
986 return custom
987 except FileNotFoundError:
988 last_used_custom.parent.mkdir(parents=True, exist_ok=True)
989 return DEFAULT_TTS_MODEL_CFG
990
991 def switch_tts_model(new_choice):
992 global tts_model_choice
993 if new_choice == "Custom": # override in case webpage is refreshed
994 custom_ckpt_path, custom_vocab_path, custom_model_cfg = load_last_used_custom()
995 tts_model_choice = ("Custom", custom_ckpt_path, custom_vocab_path, custom_model_cfg)
996 return (
997 gr.update(visible=True, value=custom_ckpt_path),
998 gr.update(visible=True, value=custom_vocab_path),
999 gr.update(visible=True, value=custom_model_cfg),
1000 )
1001 else:
1002 tts_model_choice = new_choice
1003 return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
1004
1005 def set_custom_model(custom_ckpt_path, custom_vocab_path, custom_model_cfg):
1006 global tts_model_choice
1007 tts_model_choice = ("Custom", custom_ckpt_path, custom_vocab_path, custom_model_cfg)
1008 with open(last_used_custom, "w", encoding="utf-8") as f:
1009 f.write(custom_ckpt_path + "\n" + custom_vocab_path + "\n" + custom_model_cfg + "\n")
1010
1011 with gr.Row():
1012 if not USING_SPACES:
1013 choose_tts_model = gr.Radio(
1014 choices=[DEFAULT_TTS_MODEL, "E2-TTS", "Custom"], label="Choose TTS Model", value=DEFAULT_TTS_MODEL
1015 )
1016 else:
1017 choose_tts_model = gr.Radio(
1018 choices=[DEFAULT_TTS_MODEL, "E2-TTS"], label="Choose TTS Model", value=DEFAULT_TTS_MODEL
1019 )
1020 custom_ckpt_path = gr.Dropdown(
1021 choices=[DEFAULT_TTS_MODEL_CFG[0]],
1022 value=load_last_used_custom()[0],
1023 allow_custom_value=True,
1024 label="Model: local_path | hf://user_id/repo_id/model_ckpt",
1025 visible=False,
1026 )
1027 custom_vocab_path = gr.Dropdown(
1028 choices=[DEFAULT_TTS_MODEL_CFG[1]],
1029 value=load_last_used_custom()[1],
1030 allow_custom_value=True,
1031 label="Vocab: local_path | hf://user_id/repo_id/vocab_file",
1032 visible=False,
1033 )
1034 custom_model_cfg = gr.Dropdown(
1035 choices=[
1036 DEFAULT_TTS_MODEL_CFG[2],
1037 json.dumps(
1038 dict(
1039 dim=1024,
1040 depth=22,
1041 heads=16,
1042 ff_mult=2,
1043 text_dim=512,
1044 text_mask_padding=False,
1045 conv_layers=4,
1046 pe_attn_head=1,
1047 )
1048 ),
1049 json.dumps(
1050 dict(
1051 dim=768,
1052 depth=18,
1053 heads=12,
1054 ff_mult=2,
1055 text_dim=512,
1056 text_mask_padding=False,
1057 conv_layers=4,
1058 pe_attn_head=1,
1059 )
1060 ),
1061 ],
1062 value=load_last_used_custom()[2],
1063 allow_custom_value=True,
1064 label="Config: in a dictionary form",
1065 visible=False,
1066 )
1067
1068 choose_tts_model.change(
1069 switch_tts_model,
1070 inputs=[choose_tts_model],
1071 outputs=[custom_ckpt_path, custom_vocab_path, custom_model_cfg],
1072 show_progress="hidden",
1073 )
1074 custom_ckpt_path.change(
1075 set_custom_model,
1076 inputs=[custom_ckpt_path, custom_vocab_path, custom_model_cfg],
1077 show_progress="hidden",
1078 )
1079 custom_vocab_path.change(
1080 set_custom_model,
1081 inputs=[custom_ckpt_path, custom_vocab_path, custom_model_cfg],
1082 show_progress="hidden",
1083 )
1084 custom_model_cfg.change(
1085 set_custom_model,
1086 inputs=[custom_ckpt_path, custom_vocab_path, custom_model_cfg],
1087 show_progress="hidden",
1088 )
1089
1090 gr.TabbedInterface(
1091 [app_tts, app_multistyle, app_chat, app_credits],
1092 ["Basic-TTS", "Multi-Speech", "Voice-Chat", "Credits"],
1093 )
1094
1095
1096 @click.command()
1097 @click.option("--port", "-p", default=None, type=int, help="Port to run the app on")
1098 @click.option("--host", "-H", default=None, help="Host to run the app on")
1099 @click.option(
1100 "--share",
1101 "-s",
1102 default=False,
1103 is_flag=True,
1104 help="Share the app via Gradio share link",
1105 )
1106 @click.option("--api", "-a", default=True, is_flag=True, help="Allow API access")
1107 @click.option(
1108 "--root_path",
1109 "-r",
1110 default=None,
1111 type=str,
1112 help='The root path (or "mount point") of the application, if it\'s not served from the root ("/") of the domain. Often used when the application is behind a reverse proxy that forwards requests to the application, e.g. set "/myapp" or full URL for application served at "https://example.com/myapp".',
1113 )
1114 @click.option(
1115 "--inbrowser",
1116 "-i",
1117 is_flag=True,
1118 default=False,
1119 help="Automatically launch the interface in the default web browser",
1120 )
1121 def main(port, host, share, api, root_path, inbrowser):
1122 global app
1123 print("Starting app...")
1124 app.queue(api_open=api).launch(
1125 server_name=host,
1126 server_port=port,
1127 share=share,
1128 root_path=root_path,
1129 inbrowser=inbrowser,
1130 )
1131
1132
1133 if __name__ == "__main__":
1134 if not USING_SPACES:
1135 main()
1136 else:
1137 app.queue().launch()
1138
1138 lines PYTHON