| |
| |
| |
|
|
| import os |
| import gradio as gr |
| import numpy as np |
| import torch |
| from huggingface_hub import snapshot_download, login |
|
|
| |
| 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.") |
|
|
| |
| MODEL_SIZES = ["0.6B", "1.7B"] |
|
|
| |
| 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", |
| } |
|
|
|
|
| def get_model_path(model_type: str, model_size: str) -> str: |
| """Lấy đường dẫn mô hình dựa trên loại và kích thước.""" |
| return snapshot_download( |
| f"Qwen/Qwen3-TTS-12Hz-{model_size}-{model_type}", |
| token=HF_TOKEN if HF_TOKEN else None |
| ) |
|
|
|
|
| |
| |
| |
| _loaded_models = {} |
|
|
|
|
| def get_model(model_key: str, model_type: str, model_size: str): |
| """Tải mô hình theo yêu cầu trên CPU với các tối ưu hóa.""" |
| 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] |
|
|
|
|
| |
|
|
|
|
| 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).""" |
| if audio is None: |
| return None |
|
|
| if isinstance(audio, tuple) and len(audio) == 2 and isinstance(audio[0], int): |
| sr, wav = audio |
| wav = _normalize_audio(wav) |
| return wav, int(sr) |
|
|
| 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 generate_voice_design(text, language, voice_description, progress=gr.Progress(track_tqdm=True)): |
| """Tạo giọng nói bằng mô hình Voice Design (chỉ dùng 1.7B).""" |
| if not text or not text.strip(): |
| return None, "Lỗi: Văn bản là bắt buộc." |
| if not voice_description or not voice_description.strip(): |
| return None, "Lỗi: Mô tả giọng nói là bắt buộc." |
|
|
| lang_en = LANGUAGE_MAP.get(language, "Auto") |
|
|
| try: |
| model = get_model("voice_design_1.7B", "VoiceDesign", "1.7B") |
|
|
| wavs, sr = model.generate_voice_design( |
| text=text.strip(), |
| language=lang_en, |
| instruct=voice_description.strip(), |
| non_streaming_mode=True, |
| max_new_tokens=2048, |
| ) |
| return (sr, wavs[0]), "Tạo giọng nói theo thiết kế thành công!" |
| except Exception as e: |
| return None, f"Lỗi: {type(e).__name__}: {e}" |
|
|
|
|
| def generate_voice_clone(ref_audio, ref_text, target_text, language, use_xvector_only, model_size, progress=gr.Progress(track_tqdm=True)): |
| """Tạo giọng nói bằng mô hình Base (Nhân bản giọng nói).""" |
| if not target_text or not target_text.strip(): |
| return None, "Lỗi: Văn bản cần đọc là bắt buộc." |
|
|
| audio_tuple = _audio_to_tuple(ref_audio) |
| if audio_tuple is None: |
| return None, "Lỗi: Âm thanh tham chiếu là bắt buộc." |
|
|
| if not use_xvector_only and (not ref_text or not ref_text.strip()): |
| return None, "Lỗi: Văn bản tham chiếu là bắt buộc khi không bật 'Chỉ dùng x-vector'." |
|
|
| lang_en = LANGUAGE_MAP.get(language, "Auto") |
|
|
| try: |
| model = get_model(f"base_{model_size}", "Base", model_size) |
|
|
| wavs, sr = model.generate_voice_clone( |
| text=target_text.strip(), |
| 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 (sr, wavs[0]), "Nhân bản giọng nói thành công!" |
| except Exception as e: |
| return None, f"Lỗi: {type(e).__name__}: {e}" |
|
|
|
|
| def generate_custom_voice(text, language, speaker, instruct, model_size, progress=gr.Progress(track_tqdm=True)): |
| """Tạo giọng nói bằng mô hình CustomVoice.""" |
| if not text or not text.strip(): |
| return None, "Lỗi: Văn bản là bắt buộc." |
| if not speaker: |
| return None, "Lỗi: Giọng đọc là bắt buộc." |
|
|
| lang_en = LANGUAGE_MAP.get(language, "Auto") |
|
|
| try: |
| model = get_model(f"custom_voice_{model_size}", "CustomVoice", model_size) |
|
|
| wavs, sr = model.generate_custom_voice( |
| text=text.strip(), |
| language=lang_en, |
| speaker=speaker.lower().replace(" ", "_"), |
| instruct=instruct.strip() if instruct else None, |
| non_streaming_mode=True, |
| max_new_tokens=2048, |
| ) |
| return (sr, wavs[0]), "Tạo giọng nói thành công!" |
| except Exception as e: |
| return None, f"Lỗi: {type(e).__name__}: {e}" |
|
|
|
|
| |
| def build_ui(): |
| theme = gr.themes.Soft( |
| font=[gr.themes.GoogleFont("Source Sans Pro"), "Arial", "sans-serif"], |
| ) |
|
|
| css = """ |
| .gradio-container {max-width: none !important;} |
| .tab-content {padding: 20px;} |
| """ |
|
|
| 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) |
| Ba chế độ tổng hợp giọng nói: |
| - **Thiết kế giọng nói**: Tạo giọng nói tùy chỉnh bằng mô tả ngôn ngữ tự nhiên |
| - **Nhân bản giọng nói**: Sao chép giọng nói từ file âm thanh tham chiếu |
| - **TTS (CustomVoice)**: Tổng hợp giọng nói với các giọng đọc có sẵn và tùy chỉnh phong cách |
| |
| ⚠️ **Chế độ CPU**: Phiên bản này chạy trên CPU. Quá trình tạo có thể chậm hơn phiên bản GPU. Vui lòng kiên nhẫn chờ. |
| """ |
| ) |
|
|
| with gr.Tabs(): |
| |
| with gr.Tab("Thiết kế giọng nói"): |
| gr.Markdown("### Tạo giọng nói tùy chỉnh bằng ngôn ngữ tự nhiên") |
| with gr.Row(): |
| with gr.Column(scale=2): |
| design_text = gr.Textbox( |
| label="Văn bản cần đọc", |
| lines=4, |
| placeholder="Nhập văn bản bạn muốn chuyển thành giọng nói...", |
| 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à!" |
| ) |
| design_language = gr.Dropdown( |
| label="Ngôn ngữ", |
| choices=LANGUAGES, |
| value="Tự động", |
| interactive=True, |
| ) |
| design_instruct = gr.Textbox( |
| label="Mô tả giọng nói", |
| lines=3, |
| placeholder="Mô tả đặc điểm giọng nói bạn muốn...", |
| value="Giọng ngạc nhiên, không tin tưởng, bắt đầu có chút hoảng loạn." |
| ) |
| design_btn = gr.Button("Tạo giọng nói", variant="primary") |
|
|
| with gr.Column(scale=2): |
| design_audio_out = gr.Audio(label="Âm thanh đầu ra", type="numpy") |
| design_status = gr.Textbox(label="Trạng thái", lines=2, interactive=False) |
|
|
| design_btn.click( |
| generate_voice_design, |
| inputs=[design_text, design_language, design_instruct], |
| outputs=[design_audio_out, design_status], |
| ) |
|
|
| |
| 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): |
| clone_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="numpy", |
| ) |
| clone_ref_text = gr.Textbox( |
| label="Văn bản tham chiếu (Nội dung được đọc trong file âm thanh tham chiếu)", |
| lines=2, |
| placeholder="Nhập chính xác văn bản được đọc trong file âm thanh tham chiếu...", |
| ) |
| clone_xvector = gr.Checkbox( |
| label="Chỉ dùng x-vector (Không cần văn bản tham chiếu, nhưng chất lượng thấp hơn)", |
| value=False, |
| ) |
|
|
| with gr.Column(scale=2): |
| clone_target_text = gr.Textbox( |
| label="Văn bản cần đọc (Văn bản sẽ được đọc bằng giọng đã nhân bản)", |
| lines=4, |
| placeholder="Nhập văn bản bạn muốn giọng nói đã nhân bản đọc...", |
| ) |
| with gr.Row(): |
| clone_language = gr.Dropdown( |
| label="Ngôn ngữ", |
| choices=LANGUAGES, |
| value="Tự động", |
| interactive=True, |
| ) |
| clone_model_size = gr.Dropdown( |
| label="Kích thước mô hình", |
| choices=MODEL_SIZES, |
| value="0.6B", |
| interactive=True, |
| ) |
| clone_btn = gr.Button("Nhân bản & Tạo giọng nói", variant="primary") |
|
|
| with gr.Row(): |
| clone_audio_out = gr.Audio(label="Âm thanh đầu ra", type="numpy") |
| clone_status = gr.Textbox(label="Trạng thái", lines=2, interactive=False) |
|
|
| clone_btn.click( |
| generate_voice_clone, |
| inputs=[clone_ref_audio, clone_ref_text, clone_target_text, clone_language, clone_xvector, clone_model_size], |
| outputs=[clone_audio_out, clone_status], |
| ) |
|
|
| |
| 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=2): |
| tts_text = gr.Textbox( |
| label="Văn bản cần đọc", |
| lines=4, |
| placeholder="Nhập văn bản bạn muốn chuyển thành giọng nói...", |
| 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 của chúng tôi." |
| ) |
| with gr.Row(): |
| tts_language = gr.Dropdown( |
| label="Ngôn ngữ", |
| choices=LANGUAGES, |
| value="Tiếng Anh", |
| interactive=True, |
| ) |
| tts_speaker = gr.Dropdown( |
| label="Giọng đọc", |
| choices=SPEAKERS, |
| value="Ryan", |
| interactive=True, |
| ) |
| with gr.Row(): |
| tts_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", |
| ) |
| tts_model_size = gr.Dropdown( |
| label="Kích thước mô hình", |
| choices=MODEL_SIZES, |
| value="0.6B", |
| interactive=True, |
| ) |
| tts_btn = gr.Button("Tạo giọng nói", variant="primary") |
|
|
| with gr.Column(scale=2): |
| tts_audio_out = gr.Audio(label="Âm thanh đầu ra", type="numpy") |
| tts_status = gr.Textbox(label="Trạng thái", lines=2, interactive=False) |
|
|
| tts_btn.click( |
| generate_custom_voice, |
| inputs=[tts_text, tts_language, tts_speaker, tts_instruct, tts_model_size], |
| outputs=[tts_audio_out, tts_status], |
| ) |
|
|
| gr.Markdown( |
| """ |
| --- |
| **Lưu ý**: Phiên bản này chạy trên CPU, việc tải mô hình và suy luận sẽ chậm hơn phiên bản GPU. |
| Với văn bản dài, hãy chia thành các đoạn nhỏ hơn. Nên dùng mô hình 0.6B để tạo âm thanh nhanh hơn. |
| """ |
| ) |
|
|
| 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) |