返回 F5-TTS
utils_infer.py
根目录 / src / f5_tts / infer / utils_infer.py
1 # A unified script for inference process
2 # Make adjustments inside functions, and consider both gradio and cli scripts if need to change func output format
3 import os
4 import sys
5 from concurrent.futures import ThreadPoolExecutor
6
7
8 os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1" # for MPS device compatibility
9 sys.path.append(f"{os.path.dirname(os.path.abspath(__file__))}/../../third_party/BigVGAN/")
10
11 import hashlib
12 import re
13 import tempfile
14 from importlib.resources import files
15
16 import matplotlib
17
18
19 matplotlib.use("Agg")
20
21 import matplotlib.pylab as plt
22 import numpy as np
23 import torch
24 import torchaudio
25 import tqdm
26 from huggingface_hub import hf_hub_download
27 from pydub import AudioSegment, silence
28 from transformers import pipeline
29 from vocos import Vocos
30
31 from f5_tts.model import CFM
32 from f5_tts.model.utils import convert_char_to_pinyin, get_tokenizer
33
34
35 _ref_audio_cache = {}
36 _ref_text_cache = {}
37
38 device = (
39 "cuda"
40 if torch.cuda.is_available()
41 else "xpu"
42 if torch.xpu.is_available()
43 else "mps"
44 if torch.backends.mps.is_available()
45 else "cpu"
46 )
47
48 tempfile_kwargs = {"delete_on_close": False} if sys.version_info >= (3, 12) else {"delete": False}
49
50 # -----------------------------------------
51
52 target_sample_rate = 24000
53 n_mel_channels = 100
54 hop_length = 256
55 win_length = 1024
56 n_fft = 1024
57 mel_spec_type = "vocos"
58 target_rms = 0.1
59 cross_fade_duration = 0.15
60 ode_method = "euler"
61 nfe_step = 32 # 16, 32
62 cfg_strength = 2.0
63 sway_sampling_coef = -1.0
64 speed = 1.0
65 fix_duration = None
66
67 # -----------------------------------------
68
69
70 # chunk text into smaller pieces
71
72
73 def chunk_text(text, max_chars=135):
74 """
75 Splits the input text into chunks, each with a maximum number of characters.
76
77 Args:
78 text (str): The text to be split.
79 max_chars (int): The maximum number of characters per chunk.
80
81 Returns:
82 List[str]: A list of text chunks.
83 """
84 chunks = []
85 current_chunk = ""
86 # Split the text into sentences based on punctuation followed by whitespace
87 sentences = re.split(r"(?<=[;:,.!?])\s+|(?<=[;:,。!?])", text)
88
89 for sentence in sentences:
90 if not sentence:
91 continue
92 if len(current_chunk.encode("utf-8")) + len(sentence.encode("utf-8")) <= max_chars:
93 current_chunk += sentence + " " if sentence and len(sentence[-1].encode("utf-8")) == 1 else sentence
94 else:
95 if current_chunk:
96 chunks.append(current_chunk.strip())
97 current_chunk = sentence + " " if sentence and len(sentence[-1].encode("utf-8")) == 1 else sentence
98
99 if current_chunk:
100 chunks.append(current_chunk.strip())
101
102 return chunks
103
104
105 # load vocoder
106 def load_vocoder(vocoder_name="vocos", is_local=False, local_path="", device=device, hf_cache_dir=None):
107 if vocoder_name == "vocos":
108 # vocoder = Vocos.from_pretrained("charactr/vocos-mel-24khz").to(device)
109 if is_local:
110 print(f"Load vocos from local path {local_path}")
111 config_path = f"{local_path}/config.yaml"
112 model_path = f"{local_path}/pytorch_model.bin"
113 else:
114 print("Download Vocos from huggingface charactr/vocos-mel-24khz")
115 repo_id = "charactr/vocos-mel-24khz"
116 config_path = hf_hub_download(repo_id=repo_id, cache_dir=hf_cache_dir, filename="config.yaml")
117 model_path = hf_hub_download(repo_id=repo_id, cache_dir=hf_cache_dir, filename="pytorch_model.bin")
118 vocoder = Vocos.from_hparams(config_path)
119 state_dict = torch.load(model_path, map_location="cpu", weights_only=True)
120 from vocos.feature_extractors import EncodecFeatures
121
122 if isinstance(vocoder.feature_extractor, EncodecFeatures):
123 encodec_parameters = {
124 "feature_extractor.encodec." + key: value
125 for key, value in vocoder.feature_extractor.encodec.state_dict().items()
126 }
127 state_dict.update(encodec_parameters)
128 vocoder.load_state_dict(state_dict)
129 vocoder = vocoder.eval().to(device)
130 elif vocoder_name == "bigvgan":
131 try:
132 from third_party.BigVGAN import bigvgan
133 except ImportError:
134 print("You need to follow the README to init submodule and change the BigVGAN source code.")
135 if is_local:
136 # download generator from https://huggingface.co/nvidia/bigvgan_v2_24khz_100band_256x/tree/main
137 vocoder = bigvgan.BigVGAN.from_pretrained(local_path, use_cuda_kernel=False)
138 else:
139 vocoder = bigvgan.BigVGAN.from_pretrained(
140 "nvidia/bigvgan_v2_24khz_100band_256x", use_cuda_kernel=False, cache_dir=hf_cache_dir
141 )
142
143 vocoder.remove_weight_norm()
144 vocoder = vocoder.eval().to(device)
145 return vocoder
146
147
148 # load asr pipeline
149
150 asr_pipe = None
151
152
153 def initialize_asr_pipeline(device: str = device, dtype=None):
154 if dtype is None:
155 dtype = (
156 torch.float16
157 if "cuda" in device
158 and torch.cuda.get_device_properties(device).major >= 7
159 and not torch.cuda.get_device_name().endswith("[ZLUDA]")
160 else torch.float32
161 )
162 global asr_pipe
163 asr_pipe = pipeline(
164 "automatic-speech-recognition",
165 model="openai/whisper-large-v3-turbo",
166 torch_dtype=dtype,
167 device=device,
168 )
169
170
171 # transcribe
172
173
174 def transcribe(ref_audio, language=None):
175 global asr_pipe
176 if asr_pipe is None:
177 initialize_asr_pipeline(device=device)
178 return asr_pipe(
179 ref_audio,
180 chunk_length_s=30,
181 batch_size=128,
182 generate_kwargs={"task": "transcribe", "language": language} if language else {"task": "transcribe"},
183 return_timestamps=False,
184 )["text"].strip()
185
186
187 # load model checkpoint for inference
188
189
190 def load_checkpoint(model, ckpt_path, device: str, dtype=None, use_ema=True):
191 if dtype is None:
192 dtype = (
193 torch.float16
194 if "cuda" in device
195 and torch.cuda.get_device_properties(device).major >= 7
196 and not torch.cuda.get_device_name().endswith("[ZLUDA]")
197 else torch.float32
198 )
199 model = model.to(dtype)
200
201 ckpt_type = ckpt_path.split(".")[-1]
202 if ckpt_type == "safetensors":
203 from safetensors.torch import load_file
204
205 checkpoint = load_file(ckpt_path, device=device)
206 else:
207 checkpoint = torch.load(ckpt_path, map_location=device, weights_only=True)
208
209 if use_ema:
210 if ckpt_type == "safetensors":
211 checkpoint = {"ema_model_state_dict": checkpoint}
212 checkpoint["model_state_dict"] = {
213 k.replace("ema_model.", ""): v
214 for k, v in checkpoint["ema_model_state_dict"].items()
215 if k not in ["initted", "step"]
216 }
217
218 # patch for backward compatibility, 305e3ea
219 for key in ["mel_spec.mel_stft.mel_scale.fb", "mel_spec.mel_stft.spectrogram.window"]:
220 if key in checkpoint["model_state_dict"]:
221 del checkpoint["model_state_dict"][key]
222
223 model.load_state_dict(checkpoint["model_state_dict"])
224 else:
225 if ckpt_type == "safetensors":
226 checkpoint = {"model_state_dict": checkpoint}
227 model.load_state_dict(checkpoint["model_state_dict"])
228
229 del checkpoint
230 torch.cuda.empty_cache()
231
232 return model.to(device)
233
234
235 # load model for inference
236
237
238 def load_model(
239 model_cls,
240 model_cfg,
241 ckpt_path,
242 mel_spec_type=mel_spec_type,
243 vocab_file="",
244 ode_method=ode_method,
245 use_ema=True,
246 device=device,
247 ):
248 if vocab_file == "":
249 vocab_file = str(files("f5_tts").joinpath("infer/examples/vocab.txt"))
250 tokenizer = "custom"
251
252 print("\nvocab : ", vocab_file)
253 print("token : ", tokenizer)
254 print("model : ", ckpt_path, "\n")
255
256 vocab_char_map, vocab_size = get_tokenizer(vocab_file, tokenizer)
257 model = CFM(
258 transformer=model_cls(**model_cfg, text_num_embeds=vocab_size, mel_dim=n_mel_channels),
259 mel_spec_kwargs=dict(
260 n_fft=n_fft,
261 hop_length=hop_length,
262 win_length=win_length,
263 n_mel_channels=n_mel_channels,
264 target_sample_rate=target_sample_rate,
265 mel_spec_type=mel_spec_type,
266 ),
267 odeint_kwargs=dict(
268 method=ode_method,
269 ),
270 vocab_char_map=vocab_char_map,
271 ).to(device)
272
273 dtype = torch.float32 if mel_spec_type == "bigvgan" else None
274 model = load_checkpoint(model, ckpt_path, device, dtype=dtype, use_ema=use_ema)
275
276 return model
277
278
279 def remove_silence_edges(audio, silence_threshold=-42):
280 # Remove silence from the start
281 non_silent_start_idx = silence.detect_leading_silence(audio, silence_threshold=silence_threshold)
282 audio = audio[non_silent_start_idx:]
283
284 # Remove silence from the end
285 reversed_audio = audio.reverse()
286 non_silent_end_idx = silence.detect_leading_silence(reversed_audio, silence_threshold=silence_threshold)
287 if non_silent_end_idx > 0:
288 trimmed_audio = audio[: len(audio) - non_silent_end_idx]
289 else:
290 trimmed_audio = audio
291
292 return trimmed_audio
293
294
295 # preprocess reference audio and text
296
297
298 def preprocess_ref_audio_text(ref_audio_orig, ref_text, show_info=print):
299 show_info("Converting audio...")
300
301 # Compute a hash of the reference audio file
302 with open(ref_audio_orig, "rb") as audio_file:
303 audio_data = audio_file.read()
304 audio_hash = hashlib.md5(audio_data).hexdigest()
305
306 global _ref_audio_cache
307
308 if audio_hash in _ref_audio_cache:
309 show_info("Using cached preprocessed reference audio...")
310 ref_audio = _ref_audio_cache[audio_hash]
311
312 else: # first pass, do preprocess
313 with tempfile.NamedTemporaryFile(suffix=".wav", **tempfile_kwargs) as f:
314 temp_path = f.name
315
316 aseg = AudioSegment.from_file(ref_audio_orig)
317
318 # 1. try to find long silence for clipping
319 non_silent_segs = silence.split_on_silence(
320 aseg, min_silence_len=1000, silence_thresh=-50, keep_silence=1000, seek_step=10
321 )
322 non_silent_wave = AudioSegment.silent(duration=0)
323 for non_silent_seg in non_silent_segs:
324 if len(non_silent_wave) > 6000 and len(non_silent_wave + non_silent_seg) > 12000:
325 show_info("Audio is over 12s, clipping short. (1)")
326 break
327 non_silent_wave += non_silent_seg
328
329 # 2. try to find short silence for clipping if 1. failed
330 if len(non_silent_wave) > 12000:
331 non_silent_segs = silence.split_on_silence(
332 aseg, min_silence_len=100, silence_thresh=-40, keep_silence=1000, seek_step=10
333 )
334 non_silent_wave = AudioSegment.silent(duration=0)
335 for non_silent_seg in non_silent_segs:
336 if len(non_silent_wave) > 6000 and len(non_silent_wave + non_silent_seg) > 12000:
337 show_info("Audio is over 12s, clipping short. (2)")
338 break
339 non_silent_wave += non_silent_seg
340
341 aseg = non_silent_wave
342
343 # 3. if no proper silence found for clipping
344 if len(aseg) > 12000:
345 aseg = aseg[:12000]
346 show_info("Audio is over 12s, clipping short. (3)")
347
348 aseg = remove_silence_edges(aseg) + AudioSegment.silent(duration=50)
349 aseg.export(temp_path, format="wav")
350 ref_audio = temp_path
351
352 # Cache the processed reference audio
353 _ref_audio_cache[audio_hash] = ref_audio
354
355 if not ref_text.strip():
356 global _ref_text_cache
357 if audio_hash in _ref_text_cache:
358 # Use cached asr transcription
359 show_info("Using cached reference text...")
360 ref_text = _ref_text_cache[audio_hash]
361 else:
362 show_info("No reference text provided, transcribing reference audio...")
363 ref_text = transcribe(ref_audio)
364 # Cache the transcribed text (not caching custom ref_text, enabling users to do manual tweak)
365 _ref_text_cache[audio_hash] = ref_text
366 else:
367 show_info("Using custom reference text...")
368
369 # Ensure ref_text ends with a proper sentence-ending punctuation
370 if not ref_text.endswith(". ") and not ref_text.endswith("。"):
371 if ref_text.endswith("."):
372 ref_text += " "
373 else:
374 ref_text += ". "
375
376 print("\nref_text ", ref_text)
377
378 return ref_audio, ref_text
379
380
381 # infer process: chunk text -> infer batches [i.e. infer_batch_process()]
382
383
384 def infer_process(
385 ref_audio,
386 ref_text,
387 gen_text,
388 model_obj,
389 vocoder,
390 mel_spec_type=mel_spec_type,
391 show_info=print,
392 progress=tqdm,
393 target_rms=target_rms,
394 cross_fade_duration=cross_fade_duration,
395 nfe_step=nfe_step,
396 cfg_strength=cfg_strength,
397 sway_sampling_coef=sway_sampling_coef,
398 speed=speed,
399 fix_duration=fix_duration,
400 device=device,
401 ):
402 # Split the input text into batches
403 audio, sr = torchaudio.load(ref_audio)
404 max_chars = int(len(ref_text.encode("utf-8")) / (audio.shape[-1] / sr) * (22 - audio.shape[-1] / sr) * speed)
405 gen_text_batches = chunk_text(gen_text, max_chars=max_chars)
406 for i, gen_text_i in enumerate(gen_text_batches):
407 print(f"gen_text {i}", gen_text_i)
408 print("\n")
409
410 show_info(f"Generating audio in {len(gen_text_batches)} batches...")
411
412 if not gen_text_batches:
413 show_info("No text batches to generate.")
414 return None, target_sample_rate, None
415
416 return next(
417 infer_batch_process(
418 (audio, sr),
419 ref_text,
420 gen_text_batches,
421 model_obj,
422 vocoder,
423 mel_spec_type=mel_spec_type,
424 progress=progress,
425 target_rms=target_rms,
426 cross_fade_duration=cross_fade_duration,
427 nfe_step=nfe_step,
428 cfg_strength=cfg_strength,
429 sway_sampling_coef=sway_sampling_coef,
430 speed=speed,
431 fix_duration=fix_duration,
432 device=device,
433 )
434 )
435
436
437 # infer batches
438
439
440 def infer_batch_process(
441 ref_audio,
442 ref_text,
443 gen_text_batches,
444 model_obj,
445 vocoder,
446 mel_spec_type="vocos",
447 progress=tqdm,
448 target_rms=0.1,
449 cross_fade_duration=0.15,
450 nfe_step=32,
451 cfg_strength=2.0,
452 sway_sampling_coef=-1,
453 speed=1,
454 fix_duration=None,
455 device=None,
456 streaming=False,
457 chunk_size=2048,
458 ):
459 audio, sr = ref_audio
460 if audio.shape[0] > 1:
461 audio = torch.mean(audio, dim=0, keepdim=True)
462
463 rms = torch.sqrt(torch.mean(torch.square(audio)))
464 if rms < target_rms:
465 audio = audio * target_rms / rms
466 if sr != target_sample_rate:
467 resampler = torchaudio.transforms.Resample(sr, target_sample_rate)
468 audio = resampler(audio)
469 audio = audio.to(device)
470
471 generated_waves = []
472 spectrograms = []
473
474 if len(ref_text[-1].encode("utf-8")) == 1:
475 ref_text = ref_text + " "
476
477 # Allocate fix_duration across chunks to avoid each chunk using the full-sentence duration (N× blow-up)
478 if fix_duration is not None and len(gen_text_batches) > 1:
479 ref_audio_len_frames = audio.shape[-1] // hop_length
480 ref_sec = ref_audio_len_frames * hop_length / target_sample_rate
481 target_total = fix_duration - ref_sec
482 weights = [len(c.encode("utf-8")) for c in gen_text_batches]
483 total_w = sum(weights)
484 allocatable = target_total + cross_fade_duration * (len(gen_text_batches) - 1)
485 fix_durations = [ref_sec + allocatable * w / total_w for w in weights]
486 else:
487 fix_durations = [fix_duration] * len(gen_text_batches)
488
489 def _infer_basic(gen_text, fix_dur):
490 local_speed = speed
491 if len(gen_text.encode("utf-8")) < 10:
492 local_speed = 0.3
493
494 # Prepare the text
495 text_list = [ref_text + gen_text]
496 final_text_list = convert_char_to_pinyin(text_list)
497
498 ref_audio_len = audio.shape[-1] // hop_length
499 if fix_dur is not None:
500 duration = int(fix_dur * target_sample_rate / hop_length)
501 else:
502 # Calculate duration
503 ref_text_len = len(ref_text.encode("utf-8"))
504 gen_text_len = len(gen_text.encode("utf-8"))
505 duration = ref_audio_len + int(ref_audio_len / ref_text_len * gen_text_len / local_speed)
506
507 # inference
508 with torch.inference_mode():
509 generated, _ = model_obj.sample(
510 cond=audio,
511 text=final_text_list,
512 duration=duration,
513 steps=nfe_step,
514 cfg_strength=cfg_strength,
515 sway_sampling_coef=sway_sampling_coef,
516 )
517 del _
518
519 generated = generated.to(torch.float32) # generated mel spectrogram
520 generated = generated[:, ref_audio_len:, :]
521 generated = generated.permute(0, 2, 1)
522 if mel_spec_type == "vocos":
523 generated_wave = vocoder.decode(generated)
524 elif mel_spec_type == "bigvgan":
525 generated_wave = vocoder(generated)
526 if rms < target_rms:
527 generated_wave = generated_wave * rms / target_rms
528
529 # wav -> numpy
530 generated_wave = generated_wave.squeeze().cpu().numpy()
531
532 return generated_wave, generated
533
534 def infer_single_process(gen_text, fix_dur):
535 generated_wave, generated = _infer_basic(gen_text, fix_dur)
536 generated_cpu = generated[0].cpu().numpy()
537 del generated
538 return generated_wave, generated_cpu
539
540 def infer_single_process_streaming(gen_text, fix_dur):
541 # for src/f5_tts/socket_server.py
542 generated_wave, generated = _infer_basic(gen_text, fix_dur)
543 del generated
544 for j in range(0, len(generated_wave), chunk_size):
545 yield generated_wave[j : j + chunk_size], target_sample_rate
546
547 if streaming:
548 batches_iter = progress.tqdm(gen_text_batches) if progress is not None else gen_text_batches
549 for gen_text, fix_dur in zip(batches_iter, fix_durations):
550 for chunk in infer_single_process_streaming(gen_text, fix_dur):
551 yield chunk
552 else:
553 with ThreadPoolExecutor() as executor:
554 futures = [executor.submit(infer_single_process, gt, fd) for gt, fd in zip(gen_text_batches, fix_durations)]
555 for future in progress.tqdm(futures) if progress is not None else futures:
556 result = future.result()
557 if result:
558 generated_wave, generated_mel_spec = result
559 generated_waves.append(generated_wave)
560 spectrograms.append(generated_mel_spec)
561
562 if generated_waves:
563 if cross_fade_duration <= 0:
564 # Simply concatenate
565 final_wave = np.concatenate(generated_waves)
566 else:
567 # Combine all generated waves with cross-fading
568 final_wave = generated_waves[0]
569 for i in range(1, len(generated_waves)):
570 prev_wave = final_wave
571 next_wave = generated_waves[i]
572
573 # Calculate cross-fade samples, ensuring it does not exceed wave lengths
574 cross_fade_samples = int(cross_fade_duration * target_sample_rate)
575 cross_fade_samples = min(cross_fade_samples, len(prev_wave), len(next_wave))
576
577 if cross_fade_samples <= 0:
578 # No overlap possible, concatenate
579 final_wave = np.concatenate([prev_wave, next_wave])
580 continue
581
582 # Overlapping parts
583 prev_overlap = prev_wave[-cross_fade_samples:]
584 next_overlap = next_wave[:cross_fade_samples]
585
586 # Fade out and fade in
587 fade_out = np.linspace(1, 0, cross_fade_samples)
588 fade_in = np.linspace(0, 1, cross_fade_samples)
589
590 # Cross-faded overlap
591 cross_faded_overlap = prev_overlap * fade_out + next_overlap * fade_in
592
593 # Combine
594 new_wave = np.concatenate(
595 [prev_wave[:-cross_fade_samples], cross_faded_overlap, next_wave[cross_fade_samples:]]
596 )
597
598 final_wave = new_wave
599
600 # Create a combined spectrogram
601 combined_spectrogram = np.concatenate(spectrograms, axis=1)
602
603 yield final_wave, target_sample_rate, combined_spectrogram
604
605 else:
606 yield None, target_sample_rate, None
607
608
609 # remove silence from generated wav
610
611
612 def remove_silence_for_generated_wav(filename):
613 aseg = AudioSegment.from_file(filename)
614 non_silent_segs = silence.split_on_silence(
615 aseg, min_silence_len=1000, silence_thresh=-50, keep_silence=500, seek_step=10
616 )
617 non_silent_wave = AudioSegment.silent(duration=0)
618 for non_silent_seg in non_silent_segs:
619 non_silent_wave += non_silent_seg
620 aseg = non_silent_wave
621 aseg.export(filename, format="wav")
622
623
624 # save spectrogram
625
626
627 def save_spectrogram(spectrogram, path):
628 plt.figure(figsize=(12, 4))
629 plt.imshow(spectrogram, origin="lower", aspect="auto")
630 plt.colorbar()
631 plt.savefig(path)
632 plt.close()
633
633 lines PYTHON