""" F5-TTS voice cloning engine. Generates speech in a target speaker's voice given a short reference WAV. Falls back to None gracefully if f5-tts is not installed or the GPU is unavailable — the caller then falls back to MMS-TTS. Install: pip install f5-tts>=1.0.0 Reference: SWivid/F5-TTS (HuggingFace / GitHub) Model: ~750 MB, downloaded on first use to HF cache. """ from __future__ import annotations import logging import threading from pathlib import Path from typing import Optional, Tuple import numpy as np logger = logging.getLogger(__name__) _lock = threading.Lock() _model = None # F5TTS instance, loaded lazily def _load_model(): global _model if _model is not None: return _model with _lock: if _model is None: from f5_tts.api import F5TTS # type: ignore _model = F5TTS(model_type="F5TTS") logger.info("F5-TTS model loaded.") return _model def synthesize( text: str, ref_wav_path: str, ref_text: str = "", speed: float = 1.0, device: str = "cuda", ) -> Optional[Tuple[np.ndarray, int]]: """ Generate speech for `text` using `ref_wav_path` as the speaker reference. Args: text: Text to synthesize (Bambara, Fula, French, or English). ref_wav_path: Path to reference audio (WAV, 5–30 s of the target speaker). ref_text: Transcript of the reference audio. If empty the model uses in-context inference (slightly lower quality but still good for voice matching). speed: Speaking rate multiplier. 1.0 = normal. device: "cuda" or "cpu". CPU is 30-60 s/sentence — use GPU. Returns: (waveform_float32, sample_rate) or None on failure. """ if not text.strip(): return None try: import torch model = _load_model() wav, sr, _ = model.infer( ref_file=ref_wav_path, ref_text=ref_text.strip(), gen_text=text.strip(), speed=speed, target_rms=0.1, cross_fade_duration=0.15, nfe_step=32, cfg_strength=2.0, show_info=False, progress=None, ) if isinstance(wav, torch.Tensor): wav = wav.cpu().float().numpy() else: wav = np.asarray(wav, dtype=np.float32) return wav, int(sr) except ImportError: logger.warning( "f5-tts not installed — voice cloning disabled. " "Add 'f5-tts>=1.0.0' to requirements.txt." ) return None except Exception as exc: logger.error("F5-TTS synthesis failed: %s", exc) return None def to_wav_24k(audio_path: str) -> str: """ Resample any audio file to 24 kHz mono WAV (F5-TTS preferred sample rate). Returns the path to the converted file (same stem, .wav extension). Modifies in-place if the input is already a WAV — otherwise writes a new file. """ import librosa import soundfile as sf out_path = str(Path(audio_path).with_suffix(".f5ref.wav")) audio, _ = librosa.load(audio_path, sr=24_000, mono=True) sf.write(out_path, audio, 24_000) return out_path