0___0 / app (12).py
Arxords's picture
Upload 2 files
b8a499e verified
# coding=utf-8
# Qwen3-TTS Gradio Demo - Phiên bản tương thích CPU
# Hỗ trợ: Thiết kế giọng nói, Nhân bản giọng nói (Base), TTS (CustomVoice)
# Tính năng: Tự động chia chunk văn bản dài, preview từng chunk, ghép audio
import os
import re
import io
import base64
import gradio as gr
import numpy as np
import torch
from huggingface_hub import snapshot_download, login
# Xử lý HuggingFace Token
HF_TOKEN = os.environ.get('HF_TOKEN')
if HF_TOKEN:
login(token=HF_TOKEN, add_to_git_credential=False)
else:
print("Cảnh báo: Không tìm thấy HF_TOKEN trong môi trường. Chỉ sử dụng các mô hình công khai.")
# Tùy chọn kích thước mô hình
MODEL_SIZES = ["0.6B", "1.7B"]
# Lựa chọn giọng đọc và ngôn ngữ cho mô hình CustomVoice
SPEAKERS = [
"Aiden", "Dylan", "Eric", "Ono_anna", "Ryan", "Serena", "Sohee", "Uncle_fu", "Vivian"
]
LANGUAGES = [
"Tự động", "Tiếng Trung", "Tiếng Anh", "Tiếng Nhật", "Tiếng Hàn",
"Tiếng Pháp", "Tiếng Đức", "Tiếng Tây Ban Nha", "Tiếng Bồ Đào Nha", "Tiếng Nga"
]
LANGUAGE_MAP = {
"Tự động": "Auto",
"Tiếng Trung": "Chinese",
"Tiếng Anh": "English",
"Tiếng Nhật": "Japanese",
"Tiếng Hàn": "Korean",
"Tiếng Pháp": "French",
"Tiếng Đức": "German",
"Tiếng Tây Ban Nha": "Spanish",
"Tiếng Bồ Đào Nha": "Portuguese",
"Tiếng Nga": "Russian",
}
# ============================================================================
# CHUNK TEXT - Chia văn bản thành các đoạn nhỏ theo câu/đoạn
# ============================================================================
def split_into_chunks(text: str, max_chars: int = 200) -> list:
"""
Chia văn bản thành các chunk theo câu/đoạn.
Ưu tiên chia ở dấu câu kết thúc câu, sau đó dấu phẩy, sau đó khoảng trắng.
Không giới hạn số lượng chunk.
"""
text = text.strip()
if not text:
return []
if len(text) <= max_chars:
return [text]
chunks = []
paragraphs = re.split(r'\n\s*\n', text)
for para in paragraphs:
para = para.strip()
if not para:
continue
if len(para) <= max_chars:
chunks.append(para)
continue
# Chia theo câu: dấu chấm, chấm hỏi, chấm than, dấu ba chấm
sentences = re.split(r'(?<=[.!?…。!?])\s+', para)
current = ""
for sent in sentences:
sent = sent.strip()
if not sent:
continue
# Nếu câu đơn lẻ đã vượt max_chars, chia nhỏ hơn theo dấu phẩy
if len(sent) > max_chars:
sub_parts = re.split(r'(?<=[,;])\s+', sent)
for part in sub_parts:
part = part.strip()
if not part:
continue
if len(current) + len(part) + 1 <= max_chars:
current = (current + " " + part).strip()
else:
if current:
chunks.append(current)
# Nếu part vẫn quá dài, chia theo từ
if len(part) > max_chars:
words = part.split()
current = ""
for w in words:
if len(current) + len(w) + 1 <= max_chars:
current = (current + " " + w).strip()
else:
if current:
chunks.append(current)
current = w
else:
current = part
else:
if len(current) + len(sent) + 1 <= max_chars:
current = (current + " " + sent).strip()
else:
if current:
chunks.append(current)
current = sent
if current:
chunks.append(current)
return [c for c in chunks if c.strip()]
# ============================================================================
# AUDIO UTILS
# ============================================================================
def _normalize_audio(wav, eps=1e-12, clip=True):
"""Chuẩn hóa âm thanh về float32 trong khoảng [-1, 1]."""
x = np.asarray(wav)
if np.issubdtype(x.dtype, np.integer):
info = np.iinfo(x.dtype)
if info.min < 0:
y = x.astype(np.float32) / max(abs(info.min), info.max)
else:
mid = (info.max + 1) / 2.0
y = (x.astype(np.float32) - mid) / mid
elif np.issubdtype(x.dtype, np.floating):
y = x.astype(np.float32)
m = np.max(np.abs(y)) if y.size else 0.0
if m > 1.0 + 1e-6:
y = y / (m + eps)
else:
raise TypeError(f"Kiểu dữ liệu không được hỗ trợ: {x.dtype}")
if clip:
y = np.clip(y, -1.0, 1.0)
if y.ndim > 1:
y = np.mean(y, axis=-1).astype(np.float32)
return y
def _audio_to_tuple(audio):
"""
Chuyển đổi đầu vào âm thanh từ Gradio thành tuple (wav, sr).
Hỗ trợ tất cả format Gradio có thể trả về:
- filepath string (Gradio 4.x+ với type='filepath')
- (sr, wav) tuple (Gradio cũ với type='numpy')
- dict {"sampling_rate": sr, "data": wav}
"""
if audio is None:
return None
# Gradio 4.x+: filepath string
if isinstance(audio, str):
if not os.path.exists(audio):
return None
try:
import soundfile as sf
wav, sr = sf.read(audio, dtype="float32")
wav = _normalize_audio(wav)
return wav, int(sr)
except Exception:
pass
try:
from scipy.io import wavfile
sr, wav = wavfile.read(audio)
wav = _normalize_audio(wav)
return wav, int(sr)
except Exception:
pass
return None
# Gradio cũ: (sr, numpy_array)
if isinstance(audio, tuple) and len(audio) == 2:
sr, wav = audio
if isinstance(sr, int) and isinstance(wav, np.ndarray):
wav = _normalize_audio(wav)
return wav, int(sr)
# Dict format
if isinstance(audio, dict) and "sampling_rate" in audio and "data" in audio:
sr = int(audio["sampling_rate"])
wav = _normalize_audio(audio["data"])
return wav, sr
return None
def wav_to_base64(wav: np.ndarray, sr: int) -> str:
"""Chuyển numpy audio array sang base64 WAV string để nhúng vào HTML."""
import wave
buf = io.BytesIO()
wav_int16 = (np.clip(wav, -1.0, 1.0) * 32767).astype(np.int16)
with wave.open(buf, 'wb') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sr)
wf.writeframes(wav_int16.tobytes())
return base64.b64encode(buf.getvalue()).decode('utf-8')
def concat_audio(audio_list: list) -> tuple:
"""Ghép nhiều đoạn audio (wav, sr) thành một đoạn liên tục."""
if not audio_list:
return None
sr = audio_list[0][1]
combined = np.concatenate([a[0] for a in audio_list])
return combined, sr
def make_chunk_done_html(chunk_idx: int, total: int, text: str, wav: np.ndarray, sr: int) -> str:
"""Tạo HTML block cho một chunk đã xử lý xong, có audio player."""
b64 = wav_to_base64(wav, sr)
short_text = text[:100] + "..." if len(text) > 100 else text
escaped = short_text.replace('"', '&quot;').replace('<', '&lt;').replace('>', '&gt;')
return f"""<div style="border:1px solid #d1d5db; border-radius:10px; padding:14px; margin-bottom:10px; background:#ffffff; box-shadow:0 1px 3px rgba(0,0,0,0.06);">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<span style="font-weight:700; color:#4f46e5; font-size:14px;">Đoạn {chunk_idx}<span style="font-weight:400; color:#9ca3af;"> / {total}</span></span>
<span style="font-size:12px; background:#d1fae5; color:#065f46; padding:2px 8px; border-radius:999px;">✅ Hoàn tất</span>
</div>
<p style="font-size:13px; color:#4b5563; margin:0 0 10px 0; font-style:italic; line-height:1.5;">"{escaped}"</p>
<audio controls style="width:100%; height:34px; border-radius:6px;">
<source src="data:audio/wav;base64,{b64}" type="audio/wav">
</audio>
</div>"""
def make_chunk_processing_html(chunk_idx: int, total: int, text: str) -> str:
"""Tạo HTML placeholder khi một chunk đang xử lý."""
short_text = text[:100] + "..." if len(text) > 100 else text
escaped = short_text.replace('"', '&quot;').replace('<', '&lt;').replace('>', '&gt;')
return f"""<div style="border:1px dashed #a5b4fc; border-radius:10px; padding:14px; margin-bottom:10px; background:#f5f3ff;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<span style="font-weight:700; color:#4f46e5; font-size:14px;">Đoạn {chunk_idx}<span style="font-weight:400; color:#9ca3af;"> / {total}</span></span>
<span style="font-size:12px; background:#fef3c7; color:#92400e; padding:2px 8px; border-radius:999px;">⏳ Đang xử lý...</span>
</div>
<p style="font-size:13px; color:#6b7280; margin:0; font-style:italic; line-height:1.5;">"{escaped}"</p>
</div>"""
def make_chunk_error_html(chunk_idx: int, total: int, error: str) -> str:
"""Tạo HTML block khi một chunk gặp lỗi."""
return f"""<div style="border:1px solid #fca5a5; border-radius:10px; padding:12px; margin-bottom:10px; background:#fef2f2;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<span style="font-weight:700; color:#dc2626; font-size:14px;">Đoạn {chunk_idx} / {total}</span>
<span style="font-size:12px; background:#fee2e2; color:#991b1b; padding:2px 8px; border-radius:999px;">❌ Lỗi</span>
</div>
<p style="font-size:13px; color:#7f1d1d; margin:6px 0 0 0;">{error}</p>
</div>"""
def wrap_chunk_area(inner_html: str) -> str:
"""Bọc danh sách chunk trong container có scroll."""
return f"""<div style="max-height:520px; overflow-y:auto; padding-right:4px;">{inner_html}</div>"""
# ============================================================================
# HELPER: Preview chunks trước khi xử lý
# ============================================================================
def preview_chunks(text: str, chunk_size: int) -> str:
"""Hiển thị preview danh sách chunks sẽ được tạo."""
if not text or not text.strip():
return "<p style='color:#9ca3af; font-style:italic; padding:8px;'>Nhập văn bản để xem trước các đoạn...</p>"
chunks = split_into_chunks(text.strip(), max_chars=int(chunk_size))
if not chunks:
return "<p style='color:#9ca3af;'>Không có đoạn nào.</p>"
rows = ""
for i, c in enumerate(chunks, 1):
short = c[:120] + "..." if len(c) > 120 else c
escaped = short.replace('<', '&lt;').replace('>', '&gt;')
rows += f"""<div style="display:flex; gap:10px; padding:9px 4px; border-bottom:1px solid #f3f4f6; align-items:baseline;">
<span style="min-width:56px; font-weight:700; color:#6366f1; font-size:13px;">Đoạn {i}</span>
<span style="color:#374151; font-size:13px; flex:1; line-height:1.4;">{escaped}</span>
<span style="color:#9ca3af; font-size:11px; white-space:nowrap;">{len(c)} ký tự</span>
</div>"""
header = f"""<div style="font-weight:600; color:#374151; margin-bottom:10px; font-size:14px;">
📋 Sẽ chia thành <b style="color:#4f46e5;">{len(chunks)}</b> đoạn
<span style="font-weight:400; color:#9ca3af; font-size:12px;"> · Tổng {len(text.strip())} ký tự</span>
</div>"""
return wrap_chunk_area(f"""<div style="border:1px solid #e0e7ff; border-radius:12px; padding:14px; background:#fafafa;">{header}{rows}</div>""")
# ============================================================================
# MODEL LOADING
# ============================================================================
_loaded_models = {}
def get_model_path(model_type: str, model_size: str) -> str:
return snapshot_download(
f"Qwen/Qwen3-TTS-12Hz-{model_size}-{model_type}",
token=HF_TOKEN if HF_TOKEN else None
)
def get_model(model_key: str, model_type: str, model_size: str):
global _loaded_models
if model_key not in _loaded_models:
print(f"Đang tải mô hình {model_type} {model_size} lên CPU...")
try:
from qwen_tts import Qwen3TTSModel
model_path = get_model_path(model_type, model_size)
_loaded_models[model_key] = Qwen3TTSModel.from_pretrained(
model_path,
device_map="cpu",
dtype=torch.float32,
token=HF_TOKEN if HF_TOKEN else None,
attn_implementation=None,
)
print(f"Đã tải thành công mô hình {model_type} {model_size} trên CPU!")
except Exception as e:
print(f"Lỗi khi tải mô hình: {e}")
raise
return _loaded_models[model_key]
# ============================================================================
# GENERATE FUNCTIONS - Chunked streaming với yield
# ============================================================================
def _run_chunked(chunks, generate_fn, total):
"""
Chạy generate_fn cho từng chunk, yield (all_audio, html_blocks, status, i).
generate_fn(chunk) -> (wav_array, sr)
"""
all_audio = []
html_blocks = []
for i, chunk in enumerate(chunks, 1):
# Hiển thị placeholder "đang xử lý"
processing = make_chunk_processing_html(i, total, chunk)
yield all_audio, html_blocks + [processing], f"Đang xử lý đoạn {i}/{total}...", i
try:
wav, sr = generate_fn(chunk)
all_audio.append((wav, sr))
html_blocks.append(make_chunk_done_html(i, total, chunk, wav, sr))
yield all_audio, html_blocks, f"Xong đoạn {i}/{total}", i
except Exception as e:
html_blocks.append(make_chunk_error_html(i, total, str(e)))
yield all_audio, html_blocks, f"Lỗi đoạn {i}: {e}", i
yield all_audio, html_blocks, f"✅ Hoàn tất {total} đoạn.", total
def generate_voice_design_chunked(text, language, voice_description, chunk_size):
"""Tạo giọng nói theo từng chunk - Voice Design (1.7B)."""
if not text or not text.strip():
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>")
return
if not voice_description or not voice_description.strip():
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>")
return
chunks = split_into_chunks(text.strip(), max_chars=int(chunk_size))
total = len(chunks)
lang_en = LANGUAGE_MAP.get(language, "Auto")
yield None, f"Đang tải mô hình... ({total} đoạn)", wrap_chunk_area("<p style='color:#6b7280; padding:8px;'>Đang tải mô hình...</p>")
try:
model = get_model("voice_design_1.7B", "VoiceDesign", "1.7B")
except Exception as e:
yield None, f"Lỗi tải mô hình: {e}", wrap_chunk_area(f"<p style='color:red'>Lỗi tải mô hình: {e}</p>")
return
def gen(chunk):
wavs, sr = model.generate_voice_design(
text=chunk, language=lang_en,
instruct=voice_description.strip(),
non_streaming_mode=True, max_new_tokens=2048,
)
return wavs[0], sr
for all_audio, html_blocks, status, _ in _run_chunked(chunks, gen, total):
out_audio = None
if all_audio:
combined_wav, combined_sr = concat_audio(all_audio)
out_audio = (combined_sr, combined_wav)
yield out_audio, status, wrap_chunk_area("".join(html_blocks))
def generate_voice_clone_chunked(ref_audio, ref_text, target_text, language, use_xvector_only, model_size, chunk_size):
"""Tạo giọng nói theo từng chunk - Voice Clone (Base)."""
if not target_text or not target_text.strip():
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>")
return
audio_tuple = _audio_to_tuple(ref_audio)
if audio_tuple is None:
yield None, "Lỗi: Âm thanh tham chiếu là bắt buộc.", wrap_chunk_area("<p style='color:red'>Lỗi: Âm thanh tham chiếu là bắt buộc.</p>")
return
if not use_xvector_only and (not ref_text or not ref_text.strip()):
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>")
return
chunks = split_into_chunks(target_text.strip(), max_chars=int(chunk_size))
total = len(chunks)
lang_en = LANGUAGE_MAP.get(language, "Auto")
yield None, f"Đang tải mô hình... ({total} đoạn)", wrap_chunk_area("<p style='color:#6b7280; padding:8px;'>Đang tải mô hình...</p>")
try:
model = get_model(f"base_{model_size}", "Base", model_size)
except Exception as e:
yield None, f"Lỗi tải mô hình: {e}", wrap_chunk_area(f"<p style='color:red'>Lỗi tải mô hình: {e}</p>")
return
def gen(chunk):
wavs, sr = model.generate_voice_clone(
text=chunk, language=lang_en,
ref_audio=audio_tuple,
ref_text=ref_text.strip() if ref_text else None,
x_vector_only_mode=use_xvector_only,
max_new_tokens=2048,
)
return wavs[0], sr
for all_audio, html_blocks, status, _ in _run_chunked(chunks, gen, total):
out_audio = None
if all_audio:
combined_wav, combined_sr = concat_audio(all_audio)
out_audio = (combined_sr, combined_wav)
yield out_audio, status, wrap_chunk_area("".join(html_blocks))
def generate_custom_voice_chunked(text, language, speaker, instruct, model_size, chunk_size):
"""Tạo giọng nói theo từng chunk - CustomVoice."""
if not text or not text.strip():
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>")
return
if not speaker:
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>")
return
chunks = split_into_chunks(text.strip(), max_chars=int(chunk_size))
total = len(chunks)
lang_en = LANGUAGE_MAP.get(language, "Auto")
yield None, f"Đang tải mô hình... ({total} đoạn)", wrap_chunk_area("<p style='color:#6b7280; padding:8px;'>Đang tải mô hình...</p>")
try:
model = get_model(f"custom_voice_{model_size}", "CustomVoice", model_size)
except Exception as e:
yield None, f"Lỗi tải mô hình: {e}", wrap_chunk_area(f"<p style='color:red'>Lỗi tải mô hình: {e}</p>")
return
def gen(chunk):
wavs, sr = model.generate_custom_voice(
text=chunk, language=lang_en,
speaker=speaker.lower().replace(" ", "_"),
instruct=instruct.strip() if instruct else None,
non_streaming_mode=True, max_new_tokens=2048,
)
return wavs[0], sr
for all_audio, html_blocks, status, _ in _run_chunked(chunks, gen, total):
out_audio = None
if all_audio:
combined_wav, combined_sr = concat_audio(all_audio)
out_audio = (combined_sr, combined_wav)
yield out_audio, status, wrap_chunk_area("".join(html_blocks))
# ============================================================================
# UI
# ============================================================================
def _chunk_controls():
"""Controls cài đặt chia đoạn tái sử dụng."""
with gr.Accordion("⚙️ Cài đặt chia đoạn", open=False):
chunk_size = gr.Slider(
label="Số ký tự tối đa mỗi đoạn",
minimum=50, maximum=500, value=200, step=10,
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."
)
return chunk_size
def _chunk_output_area(tab_id: str):
"""Output area: audio tổng hợp + status + HTML preview từng chunk."""
audio_out = gr.Audio(
label="🔊 Audio tổng hợp — cập nhật liên tục sau mỗi đoạn",
type="numpy"
)
status = gr.Textbox(label="Trạng thái", lines=1, interactive=False)
gr.Markdown("#### 📦 Tiến trình & Preview từng đoạn")
chunk_html = gr.HTML(
value="<p style='color:#9ca3af; font-style:italic; padding:8px;'>Nhấn ▶ để bắt đầu...</p>"
)
return audio_out, status, chunk_html
def build_ui():
theme = gr.themes.Soft(
font=[gr.themes.GoogleFont("Source Sans Pro"), "Arial", "sans-serif"],
)
css = ".gradio-container {max-width: none !important;}"
with gr.Blocks(theme=theme, css=css, title="Qwen3-TTS Demo (CPU)") as demo:
gr.Markdown("""
# 🎙️ Demo Chuyển Văn Bản Thành Giọng Nói (CPU)
Hỗ trợ văn bản **không giới hạn độ dài** — tự động chia chunk theo câu/đoạn, xử lý tuần tự, ghép audio và hiển thị preview từng chunk ngay khi hoàn tất.
⚠️ **Chế độ CPU**: Quá trình tạo có thể chậm. Audio tổng hợp sẽ được cập nhật dần sau mỗi đoạn — bạn có thể nghe ngay trong khi các đoạn sau vẫn đang xử lý.
""")
with gr.Tabs():
# ----------------------------------------------------------------
# Tab 1: Thiết kế giọng nói
# ----------------------------------------------------------------
with gr.Tab("🎨 Thiết kế giọng nói"):
gr.Markdown("### Tạo giọng nói tùy chỉnh bằng mô tả ngôn ngữ tự nhiên *(mô hình 1.7B)*")
with gr.Row():
with gr.Column(scale=3):
d_text = gr.Textbox(
label="Văn bản cần đọc (không giới hạn độ dài)",
lines=7,
placeholder="Nhập văn bản dài tùy ý. Hệ thống sẽ tự động chia thành các đoạn nhỏ...",
value=(
"Trong ngăn kéo trên... khoan đã, trống rỗng? Không thể nào, "
"tôi chắc chắn đã để ở đó mà! Đây không phải lần đầu tiên điều này xảy ra. "
"Hôm qua tôi cũng không tìm thấy chìa khóa của mình. "
"Thứ Hai tuần trước thì cái bút lại biến mất. "
"Có gì đó không ổn trong căn phòng này."
)
)
d_language = gr.Dropdown(
label="Ngôn ngữ", choices=LANGUAGES, value="Tự động", interactive=True
)
d_instruct = gr.Textbox(
label="Mô tả giọng nói",
lines=2,
placeholder="Ví dụ: Giọng ngạc nhiên, lo lắng, bắt đầu hoảng loạn...",
value="Giọng ngạc nhiên, không tin tưởng, bắt đầu có chút hoảng loạn."
)
d_chunk_size = _chunk_controls()
with gr.Row():
d_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
d_gen_btn = gr.Button("▶ Tạo giọng nói", variant="primary", scale=2)
with gr.Column(scale=2):
d_audio_out, d_status, d_chunk_html = _chunk_output_area("design")
d_preview_btn.click(
fn=lambda t, cs: preview_chunks(t, cs),
inputs=[d_text, d_chunk_size],
outputs=[d_chunk_html],
)
d_gen_btn.click(
fn=generate_voice_design_chunked,
inputs=[d_text, d_language, d_instruct, d_chunk_size],
outputs=[d_audio_out, d_status, d_chunk_html],
)
# ----------------------------------------------------------------
# Tab 2: Nhân bản giọng nói
# ----------------------------------------------------------------
with gr.Tab("🎤 Nhân bản giọng nói"):
gr.Markdown("### Sao chép giọng nói từ file âm thanh tham chiếu")
with gr.Row():
with gr.Column(scale=2):
c_ref_audio = gr.Audio(
label="Âm thanh tham chiếu (tải lên mẫu giọng nói cần sao chép)",
type="filepath"
)
c_ref_text = gr.Textbox(
label="Văn bản tham chiếu (nội dung đọc trong audio trên)",
lines=2,
placeholder="Nhập chính xác nội dung được đọc trong file âm thanh tham chiếu...",
)
c_xvector = gr.Checkbox(
label="Chỉ dùng x-vector (không cần văn bản tham chiếu, chất lượng thấp hơn)",
value=False,
)
c_target_text = gr.Textbox(
label="Văn bản cần đọc (không giới hạn độ dài)",
lines=6,
placeholder="Nhập văn bản dài tùy ý. Hệ thống sẽ tự động chia thành các đoạn nhỏ...",
)
with gr.Row():
c_language = gr.Dropdown(
label="Ngôn ngữ", choices=LANGUAGES, value="Tự động", interactive=True
)
c_model_size = gr.Dropdown(
label="Kích thước mô hình", choices=MODEL_SIZES, value="0.6B", interactive=True
)
c_chunk_size = _chunk_controls()
with gr.Row():
c_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
c_gen_btn = gr.Button("▶ Nhân bản & Tạo", variant="primary", scale=2)
with gr.Column(scale=2):
c_audio_out, c_status, c_chunk_html = _chunk_output_area("clone")
c_preview_btn.click(
fn=lambda t, cs: preview_chunks(t, cs),
inputs=[c_target_text, c_chunk_size],
outputs=[c_chunk_html],
)
c_gen_btn.click(
fn=generate_voice_clone_chunked,
inputs=[c_ref_audio, c_ref_text, c_target_text, c_language, c_xvector, c_model_size, c_chunk_size],
outputs=[c_audio_out, c_status, c_chunk_html],
)
# ----------------------------------------------------------------
# Tab 3: TTS CustomVoice
# ----------------------------------------------------------------
with gr.Tab("🗣️ TTS (CustomVoice)"):
gr.Markdown("### Chuyển văn bản thành giọng nói với các giọng đọc có sẵn")
with gr.Row():
with gr.Column(scale=3):
t_text = gr.Textbox(
label="Văn bản cần đọc (không giới hạn độ dài)",
lines=7,
placeholder="Nhập văn bản dài tùy ý. Hệ thống sẽ tự động chia thành các đoạn nhỏ...",
value=(
"Xin chào! Chào mừng bạn đến với hệ thống chuyển văn bản thành giọng nói. "
"Đây là bản demo các tính năng TTS. "
"Bạn có thể nhập văn bản dài tùy ý và hệ thống sẽ tự động chia thành các đoạn nhỏ để xử lý tuần tự. "
"Mỗi đoạn hoàn tất sẽ xuất hiện ngay bên dưới với player riêng để bạn nghe thử."
)
)
with gr.Row():
t_language = gr.Dropdown(
label="Ngôn ngữ", choices=LANGUAGES, value="Tiếng Anh", interactive=True
)
t_speaker = gr.Dropdown(
label="Giọng đọc", choices=SPEAKERS, value="Ryan", interactive=True
)
t_instruct = gr.Textbox(
label="Hướng dẫn phong cách (tùy chọn)",
lines=2,
placeholder="Ví dụ: Đọc với giọng vui vẻ và năng động",
)
t_model_size = gr.Dropdown(
label="Kích thước mô hình", choices=MODEL_SIZES, value="0.6B", interactive=True
)
t_chunk_size = _chunk_controls()
with gr.Row():
t_preview_btn = gr.Button("🔍 Xem trước các đoạn", variant="secondary")
t_gen_btn = gr.Button("▶ Tạo giọng nói", variant="primary", scale=2)
with gr.Column(scale=2):
t_audio_out, t_status, t_chunk_html = _chunk_output_area("tts")
t_preview_btn.click(
fn=lambda t, cs: preview_chunks(t, cs),
inputs=[t_text, t_chunk_size],
outputs=[t_chunk_html],
)
t_gen_btn.click(
fn=generate_custom_voice_chunked,
inputs=[t_text, t_language, t_speaker, t_instruct, t_model_size, t_chunk_size],
outputs=[t_audio_out, t_status, t_chunk_html],
)
gr.Markdown("""
---
**Lưu ý**: Mỗi đoạn xử lý xong sẽ tự động xuất hiện bên dưới với player riêng.
Audio tổng hợp (trên cùng) được ghép và cập nhật liên tục — bạn có thể nghe trong khi các đoạn sau vẫn đang chạy.
Nên dùng mô hình 0.6B và đoạn ≤200 ký tự để tốc độ tối ưu.
""")
return demo
if __name__ == "__main__":
print("Đang khởi động Demo TTS (Phiên bản CPU)...")
print("Các mô hình sẽ được tải theo yêu cầu để tiết kiệm bộ nhớ.")
demo = build_ui()
demo.launch(server_name="0.0.0.0", server_port=7860)