File size: 9,646 Bytes
faf23d3
8c2b5ab
 
 
 
 
 
 
 
 
 
 
 
 
6eb7e7c
fccdbc5
faf23d3
6eb7e7c
 
 
 
c4ae071
fa8f957
 
faf23d3
fa8f957
 
 
 
 
6eb7e7c
b4f59a5
6eb7e7c
 
8c2b5ab
 
 
 
 
 
 
 
 
 
3d7bdf6
8c2b5ab
 
 
 
 
 
 
 
 
 
faf23d3
 
 
 
 
 
 
8c2b5ab
956d5cb
 
 
 
3881111
956d5cb
 
 
3881111
 
 
 
 
 
 
 
b4466cb
 
 
 
8c2b5ab
 
b4466cb
 
8c2b5ab
 
b4466cb
 
8c2b5ab
b4466cb
 
 
6eb7e7c
 
b4466cb
 
 
 
 
 
 
3881111
 
 
 
 
 
 
 
 
 
b4466cb
 
 
 
 
 
 
 
6eb7e7c
 
 
8c2b5ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3034bda
 
 
 
 
 
 
 
8c2b5ab
3034bda
 
 
d662d8e
 
 
 
 
3034bda
 
db8290f
956d5cb
3034bda
6eb7e7c
3034bda
 
 
 
 
d662d8e
 
 
 
 
3034bda
 
 
 
 
 
d662d8e
 
3034bda
 
 
 
 
 
d662d8e
 
3034bda
 
3d7bdf6
d662d8e
 
8c2b5ab
3034bda
 
 
 
 
 
 
85e34fd
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import spaces  # imported first, as ZeroGPU requires
import torch
import os
os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
import gradio as gr
import traceback
import gc
import numpy as np
import librosa
from pydub import AudioSegment
from pydub.effects import normalize
from huggingface_hub import snapshot_download
from tts.infer_cli import MegaTTS3DiTInfer, convert_to_wav, cut_wav

from pyharp.core import ModelCard, build_endpoint
from pyharp.media.audio import save_audio
from pyharp.tags import Channels, SampleRate, Subcategory
from audiotools import AudioSignal


model_card = ModelCard(
    name="MegaTTS 3",
    description=(
        "MegaTTS 3 is a text-to-speech model trained by ByteDance with "
        "exceptional voice cloning capabilities, as 24 kHz mono speech. Please use this Space "
        "responsibly and do not abuse it! This demo is for research and "
        "educational purposes only. Repository: "
        "https://github.com/bytedance/MegaTTS3 Paper: "
        "https://arxiv.org/abs/2502.18924"
    ),
    author="Ziyue Jiang et al.",
    tags=[Subcategory.TEXT_TO_SPEECH, SampleRate(24000), Channels(1), 'voice-cloning', 'domain:speech']
)


def download_weights():
    """Download model weights from HuggingFace if not already present."""
    repo_id = "mrfakename/MegaTTS3-VoiceCloning"
    weights_dir = "checkpoints"
    
    if not os.path.exists(weights_dir):
        print("Downloading model weights from HuggingFace...")
        snapshot_download(
            repo_id=repo_id,
            revision="b3a2c297eea319931ee985751f117f866085d0c5",
            local_dir=weights_dir,
            local_dir_use_symlinks=False
        )
        print("Model weights downloaded successfully!")
    else:
        print("Model weights already exist.")
    
    return weights_dir


# pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the
# server, so the weights are downloaded and the model built there alone
if __name__ != "__main__":
    download_weights()
    print("Initializing MegaTTS3 model...")
    infer_pipe = MegaTTS3DiTInfer()
    print("Model loaded successfully!")

# PyHARP stops a job that runs past its timeout_s, 15 minutes by default, so a request whose
# reservation would exceed that is refused rather than started. A shorter one is left to
# ZeroGPU, which admits it when the caller's own allowance covers it.
MAX_GPU_SECONDS = 900

# The text is spoken in one pass the GPU holds whole, so it is limited to the longest measured to
# fit. The cost grows with its length and with the timesteps
MAX_TEXT_CHARACTERS = 2500


def gpu_seconds(wav_path, inp_text, infer_timestep, p_w, t_w):
    # Measured at about 15 s for 70 characters and 35 s for 550, both at 32 timesteps, padded
    return 15 + 0.002 * len(inp_text) * int(infer_timestep)


@spaces.GPU(duration=gpu_seconds)
def generate_speech(wav_path, inp_text, infer_timestep, p_w, t_w):
    with open(wav_path, 'rb') as file:
        file_content = file.read()

    try:
        print(f"Generating speech with: {inp_text}...")
        resource_context = infer_pipe.preprocess(file_content)
        fs, wav = infer_pipe.forward(resource_context, inp_text, time_step=infer_timestep, p_w=p_w, t_w=t_w)
    except Exception as e:
        traceback.print_exc()
        raise gr.Error(f"Speech generation failed: {e}") from e
    finally:
        cleanup_memory()

    return fs, wav


def process_fn(inp_audio, inp_text, infer_timestep, p_w, t_w):
    # Checked and prepared before the GPU is reserved, so a request that cannot run spends none of
    # the caller's ZeroGPU allowance. A failure is raised as gr.Error, since HARP never sees a
    # gr.Warning.
    if not inp_audio:
        raise gr.Error("Provide a recording of the voice to clone.")
    if not (inp_text or "").strip():
        raise gr.Error("Enter the text for the cloned voice to speak.")
    if len(inp_text) > MAX_TEXT_CHARACTERS:
        raise gr.Error(
            f"The text is {len(inp_text)} characters long. Shorten it to {MAX_TEXT_CHARACTERS} "
            "or fewer."
        )
    if gpu_seconds(None, inp_text, infer_timestep, p_w, t_w) > MAX_GPU_SECONDS:
        raise gr.Error(
            "This much text at this many timesteps needs more GPU time than one request may "
            "reserve. Shorten the text, or use fewer timesteps."
        )

    try:
        wav_path = preprocess_audio_robust(inp_audio)
        cut_wav(wav_path, max_len=10)
    except Exception as e:
        raise gr.Error(f"Could not use the reference recording: {e}") from e

    fs, wav = generate_speech(wav_path, inp_text, int(infer_timestep), p_w, t_w)
    sig = AudioSignal(wav, sample_rate=fs)
    return save_audio(sig)


def cleanup_memory():
    """Clean up GPU and system memory."""
    gc.collect()
    if torch.cuda.is_available():
        torch.cuda.empty_cache()
        torch.cuda.synchronize()

def preprocess_audio_robust(audio_path, target_sr=22050, max_duration=30):
    """Robustly preprocess audio to prevent CUDA errors."""
    try:
        # Load with pydub for robust format handling
        audio = AudioSegment.from_file(audio_path)
        
        # Convert to mono if stereo
        if audio.channels > 1:
            audio = audio.set_channels(1)
        
        # Limit duration to prevent memory issues
        if len(audio) > max_duration * 1000:  # pydub uses milliseconds
            audio = audio[:max_duration * 1000]
        
        # Normalize audio to prevent clipping
        audio = normalize(audio)
        
        # Convert to target sample rate
        audio = audio.set_frame_rate(target_sr)
        
        # Export to temporary WAV file with specific parameters
        temp_path = audio_path.replace(os.path.splitext(audio_path)[1], '_processed.wav')
        audio.export(
            temp_path,
            format="wav",
            parameters=["-acodec", "pcm_s16le", "-ac", "1", "-ar", str(target_sr)]
        )
        
        # Validate the audio with librosa
        wav, sr = librosa.load(temp_path, sr=target_sr, mono=True)
        
        # Check for invalid values
        if np.any(np.isnan(wav)) or np.any(np.isinf(wav)):
            raise ValueError("Audio contains NaN or infinite values")
        
        # Ensure reasonable amplitude range
        if np.max(np.abs(wav)) < 1e-6:
            raise ValueError("Audio signal is too quiet")
        
        # Re-save the validated audio
        import soundfile as sf
        sf.write(temp_path, wav, sr)
        
        return temp_path
        
    except Exception as e:
        print(f"Audio preprocessing failed: {e}")
        raise ValueError(f"Failed to process audio: {str(e)}")


if __name__ == "__main__":
    with gr.Blocks(title="MegaTTS3 Voice Cloning") as demo:
        gr.Markdown("# MegaTTS 3 Voice Cloning")
        # gr.Markdown("MegaTTS 3 is a text-to-speech model trained by ByteDance with exceptional voice cloning capabilities. The original authors did not release the WavVAE encoder, so voice cloning was not publicly available; however, thanks to [@ACoderPassBy](https://modelscope.cn/models/ACoderPassBy/MegaTTS-SFT)'s WavVAE encoder, we can now clone voices with MegaTTS 3!")
        # gr.Markdown("This is by no means the best voice cloning solution, but it works pretty well for some specific use-cases. Try out multiple and see which one works best for you.")
        # gr.Markdown("**Please use this Space responsibly and do not abuse it!** This demo is for research and educational purposes only!")
        # gr.Markdown("h/t to MysteryShack on Discord for the info about the unofficial WavVAE encoder!")
        # gr.Markdown("Upload a reference audio clip and enter text to generate speech with the cloned voice.")
    
        reference_audio = gr.Audio(
            label="Reference Audio",
            type="filepath"
        ).set_info((
            "A clean recording of the voice to clone. A few seconds of speech "
            "is enough, and background noise or room tone will be cloned along "
            "with the voice."
        ))
        text_input = gr.Textbox(
            label="Text to Generate",
            placeholder="Enter the text you want to synthesize...",
            info="Text for the cloned voice to speak, up to 2500 characters."
        )
    
        infer_timestep = gr.Slider(
            label="Inference Timesteps",
            value=32,
            minimum=1,
            maximum=100,
            step=1,
            info=(
                "Higher values (e.g., 50+) give clearer audio but take longer to "
                "generate."
            )
        )
        p_w = gr.Slider(
            label="Intelligibility Weight",
            value=1.4,
            minimum=0.1,
            maximum=5.0,
            step=0.1,
            info="Increase this if the speech sounds mumbled or unclear."
        )
        t_w = gr.Slider(
            label="Similarity Weight", 
            value=3.0,
            minimum=0.1,
            maximum=10.0,
            step=0.1,
            info="Increase this for a stronger likeness to the reference voice."
        )

        output_audio = gr.Audio(type="filepath", label="Synthesized Speech").set_info((
            "The text spoken in the reference voice."
        ))
    
        _ = build_endpoint(
            model_card=model_card,
            input_components=[reference_audio, text_input, infer_timestep, p_w, t_w],
            output_components=[output_audio],
            process_fn=process_fn
        )

    demo.queue().launch(pwa=True, show_error=True, server_name='0.0.0.0', server_port=7860)