File size: 8,966 Bytes
6e0ee41
 
f38f765
 
8bb312c
a2462e9
 
 
a16a0e6
8bb312c
 
a2462e9
 
3960baa
8bb312c
b76f6b2
a2462e9
8bb312c
b76f6b2
a2462e9
8bb312c
 
 
3960baa
 
6550a56
8bb312c
 
df31591
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f38f765
8bb312c
 
 
a2462e9
 
f38f765
8bb312c
 
 
a2462e9
8bb312c
 
f38f765
8bb312c
 
 
 
 
f38f765
 
6550a56
 
 
 
8bb312c
 
f38f765
3960baa
 
8bb312c
3960baa
8bb312c
 
b76f6b2
8bb312c
b76f6b2
8bb312c
 
b76f6b2
8bb312c
 
b76f6b2
a2462e9
8bb312c
 
 
 
a2462e9
8bb312c
 
6550a56
 
 
8bb312c
6550a56
8bb312c
6550a56
8bb312c
6550a56
8bb312c
 
6550a56
8bb312c
 
6550a56
a2462e9
8bb312c
6550a56
 
8bb312c
 
 
 
 
 
a2462e9
8bb312c
 
 
 
a2462e9
8bb312c
 
 
 
 
 
 
 
 
a2462e9
 
8bb312c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f38f765
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
import os
import random
import re
import numpy as np
import torch
import spaces
from kokoro import KModel, KPipeline
import gradio as gr

# --- Cấu hình chung ---
CUDA_AVAILABLE = torch.cuda.is_available()
IS_LTTEAM = os.getenv('SPACE_ID', '').startswith('LTTEAM/')
CHAR_LIMIT = None  # Không giới hạn ký tự

# Khởi tạo mô hình trên CPU/GPU
models = {
    use_gpu: KModel().to('cuda' if use_gpu else 'cpu').eval()
    for use_gpu in ( [False, True] if CUDA_AVAILABLE else [False] )
}

# Chuẩn bị pipelines cho ký tự ngữ âm 'a' và 'b'
pipelines = {lang: KPipeline(lang_code=lang, model=False) for lang in ('a', 'b')}
# Ví dụ thêm lexicon tùy chỉnh
pipelines['a'].g2p.lexicon.golds['kokoro'] = 'kˈOkəɹO'
pipelines['b'].g2p.lexicon.golds['kokoro'] = 'kˈQkəɹQ'

# Danh sách giọng nói (cờ + biểu tượng + tên) -> mã nội bộ
LUA_CHON_GIONG = {
'🇺🇸 👩 Heart ❤️ (Mỹ)':    'af_heart',
'🇺🇸 👩 Bella 🔥 (Mỹ)':    'af_bella',
'🇺🇸 👩 Nicole 🎧 (Mỹ)':   'af_nicole',
'🇺🇸 👩 Aoede (Mỹ)':       'af_aoede',
'🇺🇸 👩 Kore (Mỹ)':        'af_kore',
'🇺🇸 👩 Sarah (Mỹ)':       'af_sarah',
'🇺🇸 👩 Nova (Mỹ)':        'af_nova',
'🇺🇸 👩 Sky (Mỹ)':         'af_sky',
'🇺🇸 👩 Alloy (Mỹ)':       'af_alloy',
'🇺🇸 👩 Jessica (Mỹ)':     'af_jessica',
'🇺🇸 👩 River (Mỹ)':       'af_river',

'🇺🇸 👨 Michael (Mỹ)':     'am_michael',
'🇺🇸 👨 Fenrir (Mỹ)':      'am_fenrir',
'🇺🇸 👨 Puck (Mỹ)':        'am_puck',
'🇺🇸 👨 Echo (Mỹ)':        'am_echo',
'🇺🇸 👨 Eric (Mỹ)':        'am_eric',
'🇺🇸 👨 Liam (Mỹ)':        'am_liam',
'🇺🇸 👨 Onyx (Mỹ)':        'am_onyx',
'🇺🇸 👨 Santa (Mỹ)':       'am_santa',
'🇺🇸 👨 Adam (Mỹ)':        'am_adam',

'🇬🇧 👩 Emma (Anh)':        'bf_emma',
'🇬🇧 👩 Isabella (Anh)':    'bf_isabella',
'🇬🇧 👩 Alice (Anh)':       'bf_alice',
'🇬🇧 👩 Lily (Anh)':        'bf_lily',

'🇬🇧 👨 George (Anh)':      'bm_george',
'🇬🇧 👨 Fable (Anh)':       'bm_fable',
'🇬🇧 👨 Lewis (Anh)':       'bm_lewis',
'🇬🇧 👨 Daniel (Anh)':      'bm_daniel',
}
# Tải trước tất cả giọng
for voice_code in LUA_CHON_GIONG.values():
    pipelines[voice_code[0]].load_voice(voice_code)

# --- Hàm tiện ích ---
def split_into_chunks(text, max_chars=2000):
    """Chia văn bản thành các khúc nhỏ không vượt quá max_chars."""
    sentences = re.split(r'(?<=[\.!\?])\s+', text.strip())
    chunks, current = [], ""
    for s in sentences:
        if len(current) + len(s) + 1 <= max_chars:
            current = f"{current} {s}".strip()
        else:
            if current:
                chunks.append(current)
            current = s
    if current:
        chunks.append(current)
    return chunks

@spaces.GPU(duration=30)
def forward_gpu(ps, ref_s, speed):
    return models[True](ps, ref_s, speed)

def generate_unlimited(text, voice, speed, use_gpu, max_chars=2000):
    """Chế độ không giới hạn: chia chunk rồi ghép thanh âm."""
    text = text.strip()
    pipeline = pipelines[voice[0]]
    pack = pipeline.load_voice(voice)
    use_gpu = use_gpu and CUDA_AVAILABLE

    all_audio = []
    for chunk in split_into_chunks(text, max_chars):
        for _, ps, _ in pipeline(chunk, voice, speed):
            ref_s = pack[len(ps) - 1]
            try:
                audio = forward_gpu(ps, ref_s, speed) if use_gpu else models[False](ps, ref_s, speed)
            except gr.Error as e:
                if use_gpu:
                    gr.Warning(f"Lỗi GPU: {e}\nChuyển sang CPU cho khúc này.")
                    audio = models[False](ps, ref_s, speed)
                else:
                    raise
            all_audio.append(audio.numpy())
        # thêm 0.2s im lặng
        all_audio.append(np.zeros(int(0.2 * 24000)))
    return (24000, np.concatenate(all_audio, axis=0))

def generate_stream(text, voice, speed, use_gpu, max_chars=2000):
    """Chế độ streaming: yield từng đoạn audio nhỏ."""
    text = text.strip()
    pipeline = pipelines[voice[0]]
    pack = pipeline.load_voice(voice)
    use_gpu = use_gpu and CUDA_AVAILABLE

    for chunk in split_into_chunks(text, max_chars):
        for _, ps, _ in pipeline(chunk, voice, speed):
            ref_s = pack[len(ps) - 1]
            try:
                audio = forward_gpu(ps, ref_s, speed) if use_gpu else models[False](ps, ref_s, speed)
            except gr.Error as e:
                if use_gpu:
                    gr.Warning(f"Lỗi GPU: {e}\nChuyển sang CPU cho khúc này.")
                    audio = models[False](ps, ref_s, speed)
                else:
                    raise
            yield 24000, audio.numpy()
        yield 24000, np.zeros(int(0.2 * 24000))

def tokenize_first(text, voice):
    for _, ps, _ in pipelines[voice[0]](text, voice):
        return ps
    return ""

# Các văn bản mẫu
with open('en.txt', 'r') as f:
    TRICH_DAN_NGAU_NHIEN = [l.strip() for l in f]
def random_quote(): return random.choice(TRICH_DAN_NGAU_NHIEN)
def load_gatsby(): return open('gatsby5k.md','r').read()
def load_frank(): return open('frankenstein5k.md','r').read()

# --- Giao diện Gradio ---
BANNER = """
# 📣 **TTS-82M**  
Mô hình TTS 82M tham số do LTTEAM mở.  
[Tham gia nhóm FB](https://www.facebook.com/groups/622526090937760)
"""

with gr.Blocks() as app:
    gr.Markdown(BANNER)

    with gr.Tabs():
        # Tab 1: Không giới hạn
        with gr.TabItem("📝 TTS Không Giới Hạn"):
            with gr.Row():
                with gr.Column(scale=6):
                    txt_in = gr.Textbox(label="Văn bản đầu vào", placeholder="Nhập hoặc dán văn bản...", lines=5)
                    with gr.Row():
                        dd_voice = gr.Dropdown(list(LUA_CHON_GIONG.items()), value='af_heart', label="Chọn Giọng")
                        dd_hw = gr.Dropdown([('GPU (Nhanh)', True), ('CPU (Chậm)', False)],
                                            value=CUDA_AVAILABLE, label="Thiết bị xử lý", interactive=CUDA_AVAILABLE)
                    slider_speed = gr.Slider(0.5, 2.0, value=1.0, step=0.1, label="Tốc độ phát âm")
                    with gr.Row():
                        btn_random = gr.Button("🎲 Trích ngẫu nhiên", variant='secondary')
                        btn_gatsby = gr.Button("📖 Gatsby dài", variant='secondary')
                        btn_frank = gr.Button("📖 Frankenstein dài", variant='secondary')

                with gr.Column(scale=6):
                    out_audio = gr.Audio(label="Kết quả âm thanh", interactive=False, autoplay=True)
                    out_tokens = gr.Textbox(label="Tokens đầu ra", interactive=False)
                    btn_generate = gr.Button("▶️ Chuyển đổi", variant='primary')

        # Tab 2: Streaming
        with gr.TabItem("🔴 TTS Streaming"):
            with gr.Row():
                with gr.Column(scale=6):
                    txt_in2 = gr.Textbox(label="Văn bản đầu vào", placeholder="Nhập văn bản để phát trực tiếp...", lines=5)
                    with gr.Row():
                        dd_voice2 = gr.Dropdown(list(LUA_CHON_GIONG.items()), value='af_heart', label="Chọn Giọng")
                        dd_hw2 = gr.Dropdown([('GPU (Nhanh)', True), ('CPU (Chậm)', False)],
                                             value=CUDA_AVAILABLE, label="Thiết bị xử lý", interactive=CUDA_AVAILABLE)
                    slider_speed2 = gr.Slider(0.5, 2.0, value=1.0, step=0.1, label="Tốc độ phát âm")
                    btn_stream = gr.Button("🎙️ Bắt đầu Streaming", variant='primary')
                    btn_stop = gr.Button("⏹️ Dừng lại", variant='stop')
                with gr.Column(scale=6):
                    out_stream = gr.Audio(label="Phát trực tiếp", streaming=True, autoplay=True)

    # Sự kiện nút bấm Tab 1
    btn_random.click(fn=random_quote, inputs=[], outputs=[txt_in])
    btn_gatsby.click(fn=load_gatsby, inputs=[], outputs=[txt_in])
    btn_frank.click(fn=load_frank, inputs=[], outputs=[txt_in])

    btn_generate.click(fn=generate_unlimited,
                       inputs=[txt_in, dd_voice, slider_speed, dd_hw],
                       outputs=[out_audio])
    # nếu cần hiện tokens: .click(fn=tokenize_first, inputs=[txt_in, dd_voice], outputs=[out_tokens])

    # Sự kiện Tab 2
    stream_event = btn_stream.click(fn=generate_stream,
                                    inputs=[txt_in2, dd_voice2, slider_speed2, dd_hw2],
                                    outputs=[out_stream])
    btn_stop.click(fn=None, cancels=[stream_event])

    # Khởi chạy
    app.queue(api_open=not IS_LTTEAM).launch(
        show_api=not IS_LTTEAM,
        ssr_mode=True
    )