| import os |
| import uuid |
| from pathlib import Path |
| import numpy as np |
|
|
| import gradio as gr |
| import soundfile as sf |
| import spaces |
| from voxcpm import VoxCPM |
|
|
| MODEL_ID = "openbmb/VoxCPM2" |
| OUTPUT_DIR = Path("outputs") |
| OUTPUT_DIR.mkdir(exist_ok=True) |
|
|
| _model = None |
|
|
|
|
| def get_model(): |
| global _model |
| if _model is None: |
| try: |
| _model = VoxCPM.from_pretrained( |
| MODEL_ID, |
| load_denoiser=False, |
| optimize=False, |
| ) |
| except TypeError: |
| _model = VoxCPM.from_pretrained( |
| MODEL_ID, |
| load_denoiser=False, |
| ) |
| return _model |
|
|
|
|
| def clean_text(text: str) -> str: |
| return ( |
| (text or "") |
| .replace("\u200b", "") |
| .replace("\u200c", "") |
| .replace("\u200d", "") |
| .replace("\ufeff", "") |
| .replace("\r\n", "\n") |
| .replace("\r", "\n") |
| .strip() |
| ) |
|
|
|
|
| @spaces.GPU(duration=120) |
| def synthesize(target_text: str, reference_audio: str, reference_text: str): |
| target_text = clean_text(target_text) |
| reference_text = clean_text(reference_text) |
|
|
| if not target_text: |
| raise gr.Error("Target text is required.") |
| if not reference_audio: |
| raise gr.Error("Reference audio is required.") |
| if not reference_text: |
| raise gr.Error("Reference transcript is required.") |
|
|
| model = get_model() |
| sample_rate = model.tts_model.sample_rate |
|
|
| |
| wav = model.generate( |
| text=target_text, |
| prompt_wav_path=reference_audio, |
| prompt_text=reference_text, |
| reference_wav_path=reference_audio, |
| cfg_value=2.0, |
| inference_timesteps=10, |
| normalize=True, |
| denoise=False, |
| ) |
|
|
| |
| if hasattr(wav, "detach"): |
| wav = wav.detach().cpu().numpy() |
| wav = np.asarray(wav, dtype=np.float32).squeeze() |
|
|
| |
| search_limit = int(0.35 * sample_rate) |
| if len(wav) > search_limit: |
| abs_wav = np.abs(wav[:search_limit]) |
| max_val = np.max(abs_wav) if len(abs_wav) > 0 else 1.0 |
| threshold = max(0.012, max_val * 0.05) |
| |
| |
| frame_len = int(0.01 * sample_rate) |
| start_idx = 0 |
| |
| for i in range(0, search_limit - (frame_len * 2), frame_len): |
| current_frame = abs_wav[i : i + frame_len] |
| |
| next_frame = abs_wav[i + frame_len : i + (frame_len * 2)] |
| |
| if np.mean(current_frame) > threshold and np.mean(next_frame) > threshold: |
| start_idx = i |
| break |
| |
| if start_idx > 0: |
| |
| cushion = int(0.005 * sample_rate) |
| start_idx = max(0, start_idx - cushion) |
| print(f"[Info] Successfully stripped initial pop/click artifact at index: {start_idx}") |
| wav = wav[start_idx:] |
|
|
| out_path = OUTPUT_DIR / f"voxcpm2_{uuid.uuid4().hex}.wav" |
| sf.write(str(out_path), wav, sample_rate) |
|
|
| return str(out_path) |
|
|
|
|
| with gr.Blocks(title="Myanmar VoxCPM2 Voice Clone") as demo: |
| gr.Markdown("## Myanmar VoxCPM2 Voice Clone") |
|
|
| target_text = gr.Textbox( |
| label="Target text", |
| lines=5, |
| placeholder="ဒီမှာ ပြောစေချင်တဲ့ မြန်မာစာသားကို ရိုက်ထည့်ပါ။", |
| ) |
|
|
| reference_audio = gr.Audio( |
| label="Reference audio", |
| sources=["upload", "microphone"], |
| type="filepath", |
| ) |
|
|
| reference_text = gr.Textbox( |
| label="Reference transcript", |
| lines=3, |
| placeholder="Reference audio ထဲမှာ ပြောထားတဲ့ စကားကို တိတိကျကျ ရိုက်ထည့်ပါ။", |
| ) |
|
|
| generate_btn = gr.Button("Generate", variant="primary") |
|
|
| output_audio = gr.Audio( |
| label="Generated voice", |
| type="filepath", |
| ) |
|
|
| generate_btn.click( |
| fn=synthesize, |
| inputs=[target_text, reference_audio, reference_text], |
| outputs=output_audio, |
| api_name="synthesize", |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.queue(max_size=10).launch() |
|
|