File size: 5,062 Bytes
ad3a84d
8b96d5e
 
a83d850
8b96d5e
9131697
8b96d5e
9131697
ad3a84d
 
8b96d5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad3a84d
 
8b96d5e
 
 
 
 
 
 
9131697
8b96d5e
 
9131697
8b96d5e
94a096e
9131697
94a096e
ad3a84d
8b96d5e
 
 
 
 
9131697
8b96d5e
 
ad3a84d
 
a83d850
94a096e
 
 
 
a093279
 
 
 
a83d850
a093279
94a096e
a093279
 
a83d850
a093279
 
 
 
 
 
 
a83d850
 
a093279
a83d850
a093279
 
 
 
a83d850
94a096e
8b96d5e
 
 
 
 
 
 
 
 
 
 
 
a83d850
8b96d5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9131697
8b96d5e
cddc8cc
 
8b96d5e
ad3a84d
8b96d5e
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
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

    # VoxCPM2 Core Generation
    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,
    )

    # Safe convert to numpy array
    if hasattr(wav, "detach"):
        wav = wav.detach().cpu().numpy()
    wav = np.asarray(wav, dtype=np.float32).squeeze()

    # 🔥 အစဦးပိုင်း ကလစ်သံ (Click/Pop Artifact) သီးသန့်ကို စစ်ထုတ်ဖျက်ဆီးမည့် Logic အသစ်
    search_limit = int(0.35 * sample_rate)  # ထိပ်ဆုံး 350ms အတွင်း ရှာဖွေပါမည်
    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 ကွက်များဖြင့် စကားသံ အစစ်အမှန်ကို ရှာဖွေခြင်း
        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]
            # တစ်ချက်တည်းထွက်တဲ့ ကလစ်သံမဟုတ်ကြောင်း သေချာစေရန် နောက်ထပ် Frame ကိုပါ ပူးတွဲစစ်ဆေးပါသည်
            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:
            # စကားသံ အစဦးပိုင်း ပြတ်မသွားစေရန် 5ms မျှ Cushion ချန်လှပ်၍ ညှပ်ထုတ်ပါသည်
            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()