Arxords commited on
Commit
e4aaf99
·
verified ·
1 Parent(s): 6070859

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +61 -87
app.py CHANGED
@@ -49,82 +49,70 @@ LANGUAGE_MAP = {
49
  # CHUNK TEXT - Chia văn bản thành các đoạn nhỏ theo câu/đoạn
50
  # ============================================================================
51
 
52
- def split_into_chunks(text: str, max_chars: int = 200) -> list:
53
  """
54
- Chia văn bản thành các chunk theo câu/đoạn.
55
- Ưu tiên chia ở dấu câu kết thúc câu, sau đó dấu phẩy, sau đó khoảng trắng.
56
- Không giới hạn số lượng chunk.
 
 
 
57
  """
58
  text = text.strip()
59
  if not text:
60
  return []
61
 
62
- if len(text) <= max_chars:
63
- return [text]
64
-
65
- chunks = []
66
- paragraphs = re.split(r'\n\s*\n', text)
67
 
68
- for para in paragraphs:
 
69
  para = para.strip()
70
  if not para:
71
  continue
 
 
 
 
 
72
 
73
- if len(para) <= max_chars:
74
- chunks.append(para)
75
- continue
76
 
77
- # Chia theo câu: dấu chấm, chấm hỏi, chấm than, dấu ba chấm
78
- sentences = re.split(r'(?<=[.!?…。!?])\s+', para)
79
- current = ""
80
-
81
- for sent in sentences:
82
- sent = sent.strip()
83
- if not sent:
84
- continue
85
-
86
- # Nếu câu đơn lẻ đã vượt max_chars, chia nhỏ hơn theo dấu phẩy
87
- if len(sent) > max_chars:
88
- sub_parts = re.split(r'(?<=[,;])\s+', sent)
89
- for part in sub_parts:
90
- part = part.strip()
91
- if not part:
92
- continue
93
- if len(current) + len(part) + 1 <= max_chars:
94
- current = (current + " " + part).strip()
95
- else:
96
- if current:
97
- chunks.append(current)
98
- # Nếu part vẫn quá dài, chia theo từ
99
- if len(part) > max_chars:
100
- words = part.split()
101
- current = ""
102
- for w in words:
103
- if len(current) + len(w) + 1 <= max_chars:
104
- current = (current + " " + w).strip()
105
- else:
106
- if current:
107
- chunks.append(current)
108
- current = w
109
- else:
110
- current = part
111
  else:
112
- if len(current) + len(sent) + 1 <= max_chars:
113
- current = (current + " " + sent).strip()
114
- else:
115
- if current:
116
- chunks.append(current)
117
- current = sent
118
-
119
- if current:
120
  chunks.append(current)
 
121
 
122
- return [c for c in chunks if c.strip()]
 
123
 
 
124
 
125
- # ============================================================================
126
- # AUDIO UTILS
127
- # ============================================================================
128
 
129
  def _normalize_audio(wav, eps=1e-12, clip=True):
130
  """Chuẩn hóa âm thanh về float32 trong khoảng [-1, 1]."""
@@ -269,11 +257,11 @@ def wrap_chunk_area(inner_html: str) -> str:
269
  # HELPER: Preview chunks trước khi xử lý
270
  # ============================================================================
271
 
272
- def preview_chunks(text: str, chunk_size: int) -> str:
273
  """Hiển thị preview danh sách chunks sẽ được tạo."""
274
  if not text or not text.strip():
275
  return "<p style='color:#9ca3af; font-style:italic; padding:8px;'>Nhập văn bản để xem trước các đoạn...</p>"
276
- chunks = split_into_chunks(text.strip(), max_chars=int(chunk_size))
277
  if not chunks:
278
  return "<p style='color:#9ca3af;'>Không có đoạn nào.</p>"
279
  rows = ""
@@ -355,7 +343,7 @@ def _run_chunked(chunks, generate_fn, total):
355
  yield all_audio, html_blocks, f"✅ Hoàn tất {total} đoạn.", total
356
 
357
 
358
- def generate_voice_design_chunked(text, language, voice_description, chunk_size):
359
  """Tạo giọng nói theo từng chunk - Voice Design (1.7B)."""
360
  if not text or not text.strip():
361
  yield None, "Lỗi: Văn bản là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Văn bản là bắt buộc.</p>")
@@ -364,7 +352,7 @@ def generate_voice_design_chunked(text, language, voice_description, chunk_size)
364
  yield None, "Lỗi: Mô tả giọng nói là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Mô tả giọng nói là bắt buộc.</p>")
365
  return
366
 
367
- chunks = split_into_chunks(text.strip(), max_chars=int(chunk_size))
368
  total = len(chunks)
369
  lang_en = LANGUAGE_MAP.get(language, "Auto")
370
 
@@ -392,7 +380,7 @@ def generate_voice_design_chunked(text, language, voice_description, chunk_size)
392
  yield out_audio, status, wrap_chunk_area("".join(html_blocks))
393
 
394
 
395
- def generate_voice_clone_chunked(ref_audio, ref_text, target_text, language, use_xvector_only, model_size, chunk_size):
396
  """Tạo giọng nói theo từng chunk - Voice Clone (Base)."""
397
  if not target_text or not target_text.strip():
398
  yield None, "Lỗi: Văn bản cần đọc là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Văn bản cần đọc là bắt buộc.</p>")
@@ -407,7 +395,7 @@ def generate_voice_clone_chunked(ref_audio, ref_text, target_text, language, use
407
  yield None, "Lỗi: Văn bản tham chiếu là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Văn bản tham chiếu là bắt buộc.</p>")
408
  return
409
 
410
- chunks = split_into_chunks(target_text.strip(), max_chars=int(chunk_size))
411
  total = len(chunks)
412
  lang_en = LANGUAGE_MAP.get(language, "Auto")
413
 
@@ -437,7 +425,7 @@ def generate_voice_clone_chunked(ref_audio, ref_text, target_text, language, use
437
  yield out_audio, status, wrap_chunk_area("".join(html_blocks))
438
 
439
 
440
- def generate_custom_voice_chunked(text, language, speaker, instruct, model_size, chunk_size):
441
  """Tạo giọng nói theo từng chunk - CustomVoice."""
442
  if not text or not text.strip():
443
  yield None, "Lỗi: Văn bản là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Văn bản là bắt buộc.</p>")
@@ -446,7 +434,7 @@ def generate_custom_voice_chunked(text, language, speaker, instruct, model_size,
446
  yield None, "Lỗi: Giọng đọc là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Giọng đọc là bắt buộc.</p>")
447
  return
448
 
449
- chunks = split_into_chunks(text.strip(), max_chars=int(chunk_size))
450
  total = len(chunks)
451
  lang_en = LANGUAGE_MAP.get(language, "Auto")
452
 
@@ -479,17 +467,6 @@ def generate_custom_voice_chunked(text, language, speaker, instruct, model_size,
479
  # UI
480
  # ============================================================================
481
 
482
- def _chunk_controls():
483
- """Controls cài đặt chia đoạn tái sử dụng."""
484
- with gr.Accordion("⚙️ Cài đặt chia đoạn", open=False):
485
- chunk_size = gr.Slider(
486
- label="Số ký tự tối đa mỗi đoạn",
487
- minimum=50, maximum=500, value=200, step=10,
488
- info="Văn bản tự động chia ở dấu câu gần nhất trước giới hạn này. Số đoạn không giới hạn."
489
- )
490
- return chunk_size
491
-
492
-
493
  def _chunk_output_area(tab_id: str):
494
  """Output area: audio tổng hợp + status + HTML preview từng chunk."""
495
  audio_out = gr.Audio(
@@ -548,7 +525,6 @@ Hỗ trợ văn bản **không giới hạn độ dài** — tự động chia c
548
  placeholder="Ví dụ: Giọng ngạc nhiên, lo lắng, bắt đầu hoảng loạn...",
549
  value="Giọng ngạc nhiên, không tin tưởng, bắt đầu có chút hoảng loạn."
550
  )
551
- d_chunk_size = _chunk_controls()
552
  with gr.Row():
553
  d_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
554
  d_gen_btn = gr.Button("▶ Tạo giọng nói", variant="primary", scale=2)
@@ -558,12 +534,12 @@ Hỗ trợ văn bản **không giới hạn độ dài** — tự động chia c
558
 
559
  d_preview_btn.click(
560
  fn=lambda t, cs: preview_chunks(t, cs),
561
- inputs=[d_text, d_chunk_size],
562
  outputs=[d_chunk_html],
563
  )
564
  d_gen_btn.click(
565
  fn=generate_voice_design_chunked,
566
- inputs=[d_text, d_language, d_instruct, d_chunk_size],
567
  outputs=[d_audio_out, d_status, d_chunk_html],
568
  )
569
 
@@ -599,7 +575,6 @@ Hỗ trợ văn bản **không giới hạn độ dài** — tự động chia c
599
  c_model_size = gr.Dropdown(
600
  label="Kích thước mô hình", choices=MODEL_SIZES, value="0.6B", interactive=True
601
  )
602
- c_chunk_size = _chunk_controls()
603
  with gr.Row():
604
  c_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
605
  c_gen_btn = gr.Button("▶ Nhân bản & Tạo", variant="primary", scale=2)
@@ -609,12 +584,12 @@ Hỗ trợ văn bản **không giới hạn độ dài** — tự động chia c
609
 
610
  c_preview_btn.click(
611
  fn=lambda t, cs: preview_chunks(t, cs),
612
- inputs=[c_target_text, c_chunk_size],
613
  outputs=[c_chunk_html],
614
  )
615
  c_gen_btn.click(
616
  fn=generate_voice_clone_chunked,
617
- inputs=[c_ref_audio, c_ref_text, c_target_text, c_language, c_xvector, c_model_size, c_chunk_size],
618
  outputs=[c_audio_out, c_status, c_chunk_html],
619
  )
620
 
@@ -651,7 +626,6 @@ Hỗ trợ văn bản **không giới hạn độ dài** — tự động chia c
651
  t_model_size = gr.Dropdown(
652
  label="Kích thước mô hình", choices=MODEL_SIZES, value="0.6B", interactive=True
653
  )
654
- t_chunk_size = _chunk_controls()
655
  with gr.Row():
656
  t_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
657
  t_gen_btn = gr.Button("▶ Tạo giọng nói", variant="primary", scale=2)
@@ -661,12 +635,12 @@ Hỗ trợ văn bản **không giới hạn độ dài** — tự động chia c
661
 
662
  t_preview_btn.click(
663
  fn=lambda t, cs: preview_chunks(t, cs),
664
- inputs=[t_text, t_chunk_size],
665
  outputs=[t_chunk_html],
666
  )
667
  t_gen_btn.click(
668
  fn=generate_custom_voice_chunked,
669
- inputs=[t_text, t_language, t_speaker, t_instruct, t_model_size, t_chunk_size],
670
  outputs=[t_audio_out, t_status, t_chunk_html],
671
  )
672
 
 
49
  # CHUNK TEXT - Chia văn bản thành các đoạn nhỏ theo câu/đoạn
50
  # ============================================================================
51
 
52
+ def split_into_chunks(text: str) -> list:
53
  """
54
+ Chia văn bản thành các chunk thông minh theo dấu câu.
55
+ Quy tắc:
56
+ - Tách thành câu tại dấu kết thúc câu (.!?… và tương đương CJK).
57
+ - Gom câu ngắn (<60 ký tự) vào chunk hiện tại.
58
+ - Flush chunk khi đã đủ dài (>= 80 ký tự) và câu tiếp theo cũng tự đứng được (>= 30 ký tự).
59
+ - Tôn trọng ngắt đoạn (dòng trống) — luôn flush trước đoạn mới.
60
  """
61
  text = text.strip()
62
  if not text:
63
  return []
64
 
65
+ SENT_SPLIT = re.compile(r'(?<=[.!?\u2026\u3002\uff01\uff1f])\s+')
66
+ FLUSH_LEN = 100 # flush chunk khi đạt độ dài này
67
+ SHORT_SENT = 40 # câu ngắn hơn ngưỡng này luôn được gom vào chunk trước
68
+ MAX_LEN = 200 # không để chunk vượt quá ngưỡng này dù câu tiếp có ngắn
 
69
 
70
+ raw_sents = []
71
+ for para in re.split(r'\n\s*\n', text):
72
  para = para.strip()
73
  if not para:
74
  continue
75
+ sents = [s.strip() for s in SENT_SPLIT.split(para) if s.strip()]
76
+ if sents:
77
+ # Đánh dấu câu cuối của mỗi đoạn để flush
78
+ raw_sents.extend(sents[:-1])
79
+ raw_sents.append(("PARA_END", sents[-1]))
80
 
81
+ if not raw_sents:
82
+ return [text]
 
83
 
84
+ chunks = []
85
+ current = ""
86
+
87
+ for item in raw_sents:
88
+ is_para_end = isinstance(item, tuple)
89
+ sent = item[1] if is_para_end else item
90
+
91
+ if not current:
92
+ current = sent
93
+ else:
94
+ combined = current + " " + sent
95
+ # Gom nếu: chunk hiện tại còn ngắn HOẶC câu tiếp theo quá ngắn để đứng riêng
96
+ if len(combined) > MAX_LEN:
97
+ # Câu tiếp quá dài để gom — flush ngay
98
+ chunks.append(current)
99
+ current = sent
100
+ elif len(current) < FLUSH_LEN or len(sent) < SHORT_SENT:
101
+ current = combined
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  else:
103
+ chunks.append(current)
104
+ current = sent
105
+
106
+ # Flush tại cuối đoạn
107
+ if is_para_end and current:
 
 
 
108
  chunks.append(current)
109
+ current = ""
110
 
111
+ if current:
112
+ chunks.append(current)
113
 
114
+ return [c for c in chunks if c.strip()]
115
 
 
 
 
116
 
117
  def _normalize_audio(wav, eps=1e-12, clip=True):
118
  """Chuẩn hóa âm thanh về float32 trong khoảng [-1, 1]."""
 
257
  # HELPER: Preview chunks trước khi xử lý
258
  # ============================================================================
259
 
260
+ def preview_chunks(text: str) -> str:
261
  """Hiển thị preview danh sách chunks sẽ được tạo."""
262
  if not text or not text.strip():
263
  return "<p style='color:#9ca3af; font-style:italic; padding:8px;'>Nhập văn bản để xem trước các đoạn...</p>"
264
+ chunks = split_into_chunks(text.strip())
265
  if not chunks:
266
  return "<p style='color:#9ca3af;'>Không có đoạn nào.</p>"
267
  rows = ""
 
343
  yield all_audio, html_blocks, f"✅ Hoàn tất {total} đoạn.", total
344
 
345
 
346
+ def generate_voice_design_chunked(text, language, voice_description):
347
  """Tạo giọng nói theo từng chunk - Voice Design (1.7B)."""
348
  if not text or not text.strip():
349
  yield None, "Lỗi: Văn bản là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Văn bản là bắt buộc.</p>")
 
352
  yield None, "Lỗi: Mô tả giọng nói là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Mô tả giọng nói là bắt buộc.</p>")
353
  return
354
 
355
+ chunks = split_into_chunks(text.strip())
356
  total = len(chunks)
357
  lang_en = LANGUAGE_MAP.get(language, "Auto")
358
 
 
380
  yield out_audio, status, wrap_chunk_area("".join(html_blocks))
381
 
382
 
383
+ def generate_voice_clone_chunked(ref_audio, ref_text, target_text, language, use_xvector_only, model_size):
384
  """Tạo giọng nói theo từng chunk - Voice Clone (Base)."""
385
  if not target_text or not target_text.strip():
386
  yield None, "Lỗi: Văn bản cần đọc là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Văn bản cần đọc là bắt buộc.</p>")
 
395
  yield None, "Lỗi: Văn bản tham chiếu là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Văn bản tham chiếu là bắt buộc.</p>")
396
  return
397
 
398
+ chunks = split_into_chunks(target_text.strip())
399
  total = len(chunks)
400
  lang_en = LANGUAGE_MAP.get(language, "Auto")
401
 
 
425
  yield out_audio, status, wrap_chunk_area("".join(html_blocks))
426
 
427
 
428
+ def generate_custom_voice_chunked(text, language, speaker, instruct, model_size):
429
  """Tạo giọng nói theo từng chunk - CustomVoice."""
430
  if not text or not text.strip():
431
  yield None, "Lỗi: Văn bản là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Văn bản là bắt buộc.</p>")
 
434
  yield None, "Lỗi: Giọng đọc là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Giọng đọc là bắt buộc.</p>")
435
  return
436
 
437
+ chunks = split_into_chunks(text.strip())
438
  total = len(chunks)
439
  lang_en = LANGUAGE_MAP.get(language, "Auto")
440
 
 
467
  # UI
468
  # ============================================================================
469
 
 
 
 
 
 
 
 
 
 
 
 
470
  def _chunk_output_area(tab_id: str):
471
  """Output area: audio tổng hợp + status + HTML preview từng chunk."""
472
  audio_out = gr.Audio(
 
525
  placeholder="Ví dụ: Giọng ngạc nhiên, lo lắng, bắt đầu hoảng loạn...",
526
  value="Giọng ngạc nhiên, không tin tưởng, bắt đầu có chút hoảng loạn."
527
  )
 
528
  with gr.Row():
529
  d_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
530
  d_gen_btn = gr.Button("▶ Tạo giọng nói", variant="primary", scale=2)
 
534
 
535
  d_preview_btn.click(
536
  fn=lambda t, cs: preview_chunks(t, cs),
537
+ inputs=[d_text],
538
  outputs=[d_chunk_html],
539
  )
540
  d_gen_btn.click(
541
  fn=generate_voice_design_chunked,
542
+ inputs=[d_text, d_language, d_instruct],
543
  outputs=[d_audio_out, d_status, d_chunk_html],
544
  )
545
 
 
575
  c_model_size = gr.Dropdown(
576
  label="Kích thước mô hình", choices=MODEL_SIZES, value="0.6B", interactive=True
577
  )
 
578
  with gr.Row():
579
  c_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
580
  c_gen_btn = gr.Button("▶ Nhân bản & Tạo", variant="primary", scale=2)
 
584
 
585
  c_preview_btn.click(
586
  fn=lambda t, cs: preview_chunks(t, cs),
587
+ inputs=[c_target_text],
588
  outputs=[c_chunk_html],
589
  )
590
  c_gen_btn.click(
591
  fn=generate_voice_clone_chunked,
592
+ inputs=[c_ref_audio, c_ref_text, c_target_text, c_language, c_xvector, c_model_size],
593
  outputs=[c_audio_out, c_status, c_chunk_html],
594
  )
595
 
 
626
  t_model_size = gr.Dropdown(
627
  label="Kích thước mô hình", choices=MODEL_SIZES, value="0.6B", interactive=True
628
  )
 
629
  with gr.Row():
630
  t_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
631
  t_gen_btn = gr.Button("▶ Tạo giọng nói", variant="primary", scale=2)
 
635
 
636
  t_preview_btn.click(
637
  fn=lambda t, cs: preview_chunks(t, cs),
638
+ inputs=[t_text],
639
  outputs=[t_chunk_html],
640
  )
641
  t_gen_btn.click(
642
  fn=generate_custom_voice_chunked,
643
+ inputs=[t_text, t_language, t_speaker, t_instruct, t_model_size],
644
  outputs=[t_audio_out, t_status, t_chunk_html],
645
  )
646