LTTEAM commited on
Commit
a2462e9
·
verified ·
1 Parent(s): df31591

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +111 -149
app.py CHANGED
@@ -1,34 +1,30 @@
1
- import spaces
2
- from kokoro import KModel, KPipeline
3
- import gradio as gr
4
  import os
5
  import random
6
- import torch
7
  import re
 
8
  import numpy as np
 
 
 
9
 
10
- # Nếu chạy dưới namespace của LTTEAM thì cho phép không giới hạn ký tự
11
- IS_DUPLICATE = not os.getenv('SPACE_ID', '').startswith('LTTEAM/')
12
- CUDA_AVAILABLE = torch.cuda.is_available()
13
- CHAR_LIMIT = None # bỏ hoàn toàn giới hạn
14
-
15
- if not IS_DUPLICATE:
16
- import kokoro
17
- import misaki
18
- print('DEBUG', kokoro.__version__, CUDA_AVAILABLE, misaki.__version__)
19
 
20
- # Khởi tạo model trên CPU/GPU
21
  models = {
22
- gpu: KModel().to('cuda' if gpu else 'cpu').eval()
23
- for gpu in [False] + ([True] if CUDA_AVAILABLE else [])
24
  }
25
- # Chuẩn bị pipeline cho hai nhóm ngữ âm 'a' và 'b'
26
- pipelines = {lang: KPipeline(lang_code=lang, model=False) for lang in 'ab'}
 
27
  pipelines['a'].g2p.lexicon.golds['kokoro'] = 'kˈOkəɹO'
28
  pipelines['b'].g2p.lexicon.golds['kokoro'] = 'kˈQkəɹQ'
29
 
30
- # Load trước tất cả voice choices sau này
31
- CHOICES = {
32
  '🇺🇸 👩 Heart ❤️ (Mỹ)': 'af_heart',
33
  '🇺🇸 👩 Bella 🔥 (Mỹ)': 'af_bella',
34
  '🇺🇸 👩 Nicole 🎧 (Mỹ)': 'af_nicole',
@@ -61,174 +57,140 @@ CHOICES = {
61
  '🇬🇧 👨 Lewis (Anh)': 'bm_lewis',
62
  '🇬🇧 👨 Daniel (Anh)': 'bm_daniel',
63
  }
64
- for v in CHOICES.values():
65
- pipelines[v[0]].load_voice(v)
66
 
67
- # Hàm chia văn bản thành các chunk nhỏ ~max_chars ký tự
 
 
 
 
68
  def split_into_chunks(text, max_chars=2000):
69
- sentences = re.split(r'(?<=[\.!\?])\s+', text)
70
- chunks, current = [], ""
71
- for sent in sentences:
72
- if len(current) + len(sent) + 1 <= max_chars:
73
- current = current + (" " if current else "") + sent
 
74
  else:
75
- if current:
76
- chunks.append(current)
77
- current = sent
78
- if current:
79
- chunks.append(current)
80
  return chunks
81
 
82
- # Wrapper để chạy trên GPU với decorator của Spaces
83
  @spaces.GPU(duration=30)
84
  def forward_gpu(ps, ref_s, speed):
85
  return models[True](ps, ref_s, speed)
86
 
87
- # Tạo toàn bộ audio nối từ nhiều chunk
88
- def generate_unlimited(text, voice='af_heart', speed=1, use_gpu=CUDA_AVAILABLE, max_chars=2000):
89
  text = text.strip()
90
  pipeline = pipelines[voice[0]]
91
  pack = pipeline.load_voice(voice)
92
- use_gpu = use_gpu and CUDA_AVAILABLE
93
-
94
- chunks = split_into_chunks(text, max_chars=max_chars)
95
- all_audio = []
96
 
 
 
97
  for chunk in chunks:
98
  for _, ps, _ in pipeline(chunk, voice, speed):
99
  ref_s = pack[len(ps)-1]
100
  try:
101
- audio = forward_gpu(ps, ref_s, speed) if use_gpu else models[False](ps, ref_s, speed)
102
- except gr.exceptions.Error as e:
103
  if use_gpu:
104
- gr.Warning(str(e))
105
- gr.Info('Chuyển sang CPU cho chunk này.')
106
- audio = models[False](ps, ref_s, speed)
107
  else:
108
- raise gr.Error(e)
109
- all_audio.append(audio.numpy())
110
- # Thêm 0.2s im lặng giữa các chunk
111
- all_audio.append(np.zeros(int(0.2 * 24000)))
112
-
113
- # Ghép nối và trả về
114
- full_audio = np.concatenate(all_audio, axis=0)
115
- return (24000, full_audio)
116
-
117
- # Phiên bản streaming: yield từng segment
118
- def generate_unlimited_stream(text, voice='af_heart', speed=1, use_gpu=CUDA_AVAILABLE, max_chars=2000):
119
  text = text.strip()
120
  pipeline = pipelines[voice[0]]
121
  pack = pipeline.load_voice(voice)
122
- use_gpu = use_gpu and CUDA_AVAILABLE
123
-
124
- chunks = split_into_chunks(text, max_chars=max_chars)
125
- first = True
126
 
127
- for chunk in chunks:
128
  for _, ps, _ in pipeline(chunk, voice, speed):
129
  ref_s = pack[len(ps)-1]
130
  try:
131
- audio = forward_gpu(ps, ref_s, speed) if use_gpu else models[False](ps, ref_s, speed)
132
- except gr.exceptions.Error as e:
133
  if use_gpu:
134
- gr.Warning(str(e))
135
- gr.Info('Chuyển sang CPU cho chunk này.')
136
- audio = models[False](ps, ref_s, speed)
137
  else:
138
- raise gr.Error(e)
139
- yield 24000, audio.numpy()
140
- # Giữa các chunk, gửi 0.2s im lặng để tránh dính âm
141
  yield 24000, np.zeros(int(0.2 * 24000))
142
 
143
- # Arena API (không giới hạn)
144
- def predict(text, voice='af_heart', speed=1):
145
- return generate_unlimited(text, voice, speed, use_gpu=False)
146
-
147
- def tokenize_first(text, voice='af_heart'):
148
- pipeline = pipelines[voice[0]]
149
- for _, ps, _ in pipeline(text, voice):
150
- return ps
151
- return ''
152
-
153
- # Các nút lấy văn bản mẫu
154
- with open('en.txt', 'r') as r:
155
- random_quotes = [line.strip() for line in r]
156
-
157
- def get_random_quote():
158
- return random.choice(random_quotes)
159
 
160
- def get_gatsby():
161
- return open('gatsby5k.md', 'r').read().strip()
 
162
 
163
- def get_frankenstein():
164
- return open('frankenstein5k.md', 'r').read().strip()
 
 
 
165
 
166
- # Giao diện Gradio
167
- TOKEN_NOTE = '''
168
- 💡 Tùy chỉnh cách phát âm bằng cú pháp liên kết Markdown và gạch chéo...
169
- ⬇️ Hạ mức độ...
170
- ⬆️ Nâng mức độ...
171
- '''
172
 
173
- STREAM_NOTE = '''
174
- ⚠️ Có một lỗi Gradio có thể khiến âm thanh không phát khi lần đầu Direct.
175
- 🚀 Nhập không giới hạn, đã chia chunk tự động.
176
- '''
177
-
178
- BANNER_TEXT = '''
179
- [***TTS-82M*** **là mô hình TTS 82M tham số mở bởi LTTEAM.**](https://www.facebook.com/groups/622526090937760)
180
- '''
181
-
182
- API_OPEN = os.getenv('SPACE_ID') != 'LTTEAM/TTS-82M'
183
- API_NAME = None if API_OPEN else False
184
-
185
- with gr.Blocks() as app:
186
- gr.Markdown(BANNER_TEXT)
187
  with gr.Row():
188
- with gr.Column():
189
- text = gr.Textbox(label='Văn bản đầu vào', info="Không giới hạn độ dài, sẽ tự chia nhỏ")
 
 
 
 
 
190
  with gr.Row():
191
- voice = gr.Dropdown(list(CHOICES.items()), value='af_heart', label='Giọng')
192
- use_gpu = gr.Dropdown(
193
- [('GPU (Nhanh)', True), ('CPU (Chậm)', False)],
194
- value=CUDA_AVAILABLE,
195
- label='Phần cứng',
196
- interactive=CUDA_AVAILABLE
 
 
 
 
 
197
  )
198
- speed = gr.Slider(minimum=0.5, maximum=2, value=1, step=0.1, label='Tốc độ')
199
- random_btn = gr.Button('🎲 Trích dẫn ngẫu nhiên', variant='secondary')
200
- gatsby_btn = gr.Button('🎲 Văn bản dài (Gatsby)', variant='secondary')
201
- frankenstein_btn = gr.Button('🎲 Văn bản dài (Frankenstein)', variant='secondary')
202
- with gr.Column():
203
- out_audio = gr.Audio(label='Đầu ra âm thanh', interactive=False, streaming=False, autoplay=True)
204
- out_ps = gr.Textbox(label='Tokens đầu ra', interactive=False, show_label=False)
205
- generate_btn = gr.Button('Chuyển đổi', variant='primary')
206
- # Streaming tab
207
- with gr.Blocks():
208
- out_stream = gr.Audio(label='Đầu ra Streaming', interactive=False, streaming=True, autoplay=True)
209
- stream_btn = gr.Button('Trực Tiếp', variant='primary')
210
- stop_btn = gr.Button('Dừng', variant='stop')
211
- gr.Markdown(STREAM_NOTE)
212
-
213
- # Sự kiện
214
- random_btn.click(fn=get_random_quote, inputs=[], outputs=[text], api_name=API_NAME)
215
- gatsby_btn.click(fn=get_gatsby, inputs=[], outputs=[text], api_name=API_NAME)
216
- frankenstein_btn.click(fn=get_frankenstein, inputs=[], outputs=[text], api_name=API_NAME)
217
-
218
- generate_btn.click(
219
- fn=generate_unlimited,
220
- inputs=[text, voice, speed, use_gpu],
221
  outputs=[out_audio]
222
  )
223
- # Nếu muốn thêm hiển thị tokens lần đầu:
224
- # generate_btn.click(fn=tokenize_first, inputs=[text, voice], outputs=[out_ps])
225
-
226
- stream_event = stream_btn.click(
227
- fn=generate_unlimited_stream,
228
- inputs=[text, voice, speed, use_gpu],
229
  outputs=[out_stream]
 
 
230
  )
231
- stop_btn.click(fn=None, cancels=stream_event)
232
 
233
- if __name__ == '__main__':
234
- app.queue(api_open=API_OPEN).launch(show_api=API_OPEN, ssr_mode=True)
 
 
 
 
1
  import os
2
  import random
 
3
  import re
4
+ import torch
5
  import numpy as np
6
+ import spaces
7
+ from kokoro import KModel, KPipeline
8
+ import gradio as gr
9
 
10
+ # --- Cấu hình bản ---
11
+ IS_LTTEAM = os.getenv('SPACE_ID', '').startswith('LTTEAM/')
12
+ CUDA = torch.cuda.is_available()
13
+ CHAR_LIMIT = None # Không giới hạn tự
 
 
 
 
 
14
 
15
+ # Khởi tạo model CPU/GPU
16
  models = {
17
+ use_gpu: KModel().to('cuda' if use_gpu else 'cpu').eval()
18
+ for use_gpu in ([False] + ([True] if CUDA else []))
19
  }
20
+
21
+ # Chuẩn bị pipeline cho phoneme groups 'a' 'b'
22
+ pipelines = {g: KPipeline(lang_code=g, model=False) for g in ['a', 'b']}
23
  pipelines['a'].g2p.lexicon.golds['kokoro'] = 'kˈOkəɹO'
24
  pipelines['b'].g2p.lexicon.golds['kokoro'] = 'kˈQkəɹQ'
25
 
26
+ # Đăng toàn bộ giọng đọc
27
+ VOICE_CHOICES = {
28
  '🇺🇸 👩 Heart ❤️ (Mỹ)': 'af_heart',
29
  '🇺🇸 👩 Bella 🔥 (Mỹ)': 'af_bella',
30
  '🇺🇸 👩 Nicole 🎧 (Mỹ)': 'af_nicole',
 
57
  '🇬🇧 👨 Lewis (Anh)': 'bm_lewis',
58
  '🇬🇧 👨 Daniel (Anh)': 'bm_daniel',
59
  }
 
 
60
 
61
+ for group in VOICE_CHOICES.values():
62
+ for _, voice_id in group:
63
+ pipelines[voice_id[0]].load_voice(voice_id)
64
+
65
+ # --- Hàm tiện ích ---
66
  def split_into_chunks(text, max_chars=2000):
67
+ """Chia văn bản thành các đoạn không vượt quá max_chars."""
68
+ sentences = re.split(r'(?<=[.!?])\s+', text)
69
+ chunks, cur = [], ""
70
+ for s in sentences:
71
+ if len(cur) + len(s) + 1 <= max_chars:
72
+ cur = f"{cur} {s}".strip()
73
  else:
74
+ chunks.append(cur)
75
+ cur = s
76
+ if cur:
77
+ chunks.append(cur)
 
78
  return chunks
79
 
 
80
  @spaces.GPU(duration=30)
81
  def forward_gpu(ps, ref_s, speed):
82
  return models[True](ps, ref_s, speed)
83
 
84
+ def generate_audio(text, voice, speed, use_gpu):
85
+ """Tạo file audio ghép từ nhiều chunk."""
86
  text = text.strip()
87
  pipeline = pipelines[voice[0]]
88
  pack = pipeline.load_voice(voice)
89
+ use_gpu = use_gpu and CUDA
 
 
 
90
 
91
+ chunks = split_into_chunks(text)
92
+ audios = []
93
  for chunk in chunks:
94
  for _, ps, _ in pipeline(chunk, voice, speed):
95
  ref_s = pack[len(ps)-1]
96
  try:
97
+ out = forward_gpu(ps, ref_s, speed) if use_gpu else models[False](ps, ref_s, speed)
98
+ except Exception as e:
99
  if use_gpu:
100
+ gr.Warning(f"Lỗi GPU, chuyển sang CPU: {e}")
101
+ out = models[False](ps, ref_s, speed)
 
102
  else:
103
+ raise
104
+ audios.append(out.numpy())
105
+ # Thêm 0.2s im lặng giữa đoạn
106
+ audios.append(np.zeros(int(0.2 * 24000)))
107
+ return (24000, np.concatenate(audios, axis=0))
108
+
109
+ def generate_stream(text, voice, speed, use_gpu):
110
+ """Phát trực tiếp từng đoạn audio."""
 
 
 
111
  text = text.strip()
112
  pipeline = pipelines[voice[0]]
113
  pack = pipeline.load_voice(voice)
114
+ use_gpu = use_gpu and CUDA
 
 
 
115
 
116
+ for chunk in split_into_chunks(text):
117
  for _, ps, _ in pipeline(chunk, voice, speed):
118
  ref_s = pack[len(ps)-1]
119
  try:
120
+ out = forward_gpu(ps, ref_s, speed) if use_gpu else models[False](ps, ref_s, speed)
121
+ except Exception as e:
122
  if use_gpu:
123
+ gr.Warning(f"Lỗi GPU, chuyển sang CPU: {e}")
124
+ out = models[False](ps, ref_s, speed)
 
125
  else:
126
+ raise
127
+ yield 24000, out.numpy()
 
128
  yield 24000, np.zeros(int(0.2 * 24000))
129
 
130
+ # --- Tải dụ văn bản ---
131
+ with open('en.txt', 'r') as f:
132
+ QUOTES = [l.strip() for l in f]
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
+ def random_quote(): return random.choice(QUOTES)
135
+ def load_gatsby(): return open('gatsby5k.md','r').read().strip()
136
+ def load_frank(): return open('frankenstein5k.md','r').read().strip()
137
 
138
+ # --- Xây dựng giao diện Gradio ---
139
+ with gr.Blocks(css="""
140
+ .block { max-width: 900px; margin: auto; }
141
+ .voice-group { margin-bottom: 10px; }
142
+ """) as app:
143
 
144
+ gr.Markdown("""
145
+ **TTS-82M** Mô hình chuyển văn bản thành giọng nói 82M tham số, mở bởi LTTEAM.
146
+ """)
 
 
 
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  with gr.Row():
149
+ with gr.Column(scale=2):
150
+ inp_txt = gr.Textbox(label="Văn bản đầu vào", placeholder="Nhập hoặc dán văn bản...", lines=4)
151
+ with gr.Accordion("Ví dụ nhanh", open=False):
152
+ gr.Button("📜 Trích dẫn ngẫu nhiên").click(random_quote, [], inp_txt)
153
+ gr.Button("📗 Gatsby dài").click(load_gatsby, [], inp_txt)
154
+ gr.Button("📕 Frankenstein").click(load_frank, [], inp_txt)
155
+
156
  with gr.Row():
157
+ voice_dd = gr.Dropdown(
158
+ label="Chọn giọng đọc",
159
+ choices=[(f"{grp} {name}", vid)
160
+ for grp, lst in VOICE_CHOICES.items()
161
+ for name, vid in lst],
162
+ value='af_heart'
163
+ )
164
+ hw_dd = gr.Radio(
165
+ ["GPU (Nhanh)", "CPU (Chậm)"],
166
+ label="Phần cứng",
167
+ value="GPU (Nhanh)" if CUDA else "CPU (Chậm)"
168
  )
169
+ speed_sl = gr.Slider(0.5, 2.0, 1.0, step=0.1, label="Tốc độ phát")
170
+
171
+ with gr.Column(scale=1):
172
+ out_audio = gr.Audio(label="Kết quả âm thanh", interactive=False, streaming=False, autoplay=True)
173
+ btn_gen = gr.Button("▶️ Tạo âm thanh", variant="primary")
174
+
175
+ with gr.Tabs():
176
+ with gr.TabItem("Phát trực tiếp"):
177
+ out_stream = gr.Audio(label="Phát trực tiếp", interactive=False, streaming=True, autoplay=True)
178
+ btn_stream = gr.Button("🔴 Bắt đầu", variant="primary")
179
+ btn_stop = gr.Button("⏹ Dừng", variant="stop")
180
+
181
+ # Liên kết sự kiện
182
+ btn_gen.click(
183
+ fn=generate_audio,
184
+ inputs=[inp_txt, voice_dd, speed_sl, hw_dd.map({"GPU (Nhanh)": True, "CPU (Chậm)": False})],
 
 
 
 
 
 
 
185
  outputs=[out_audio]
186
  )
187
+ btn_stream.click(
188
+ fn=generate_stream,
189
+ inputs=[inp_txt, voice_dd, speed_sl, hw_dd.map({"GPU (Nhanh)": True, "CPU (Chậm)": False})],
 
 
 
190
  outputs=[out_stream]
191
+ ).then(
192
+ None, [], [], cancels=btn_stream
193
  )
 
194
 
195
+ if __name__ == "__main__":
196
+ app.queue(api_open=not IS_LTTEAM).launch(show_api=not IS_LTTEAM, ssr_mode=True)