File size: 3,284 Bytes
8952fff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""
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