Spaces:
Sleeping
Sleeping
| import spaces | |
| import os | |
| os.environ['SPACES_ZERO_GPU'] = '1' | |
| import gradio as gr | |
| import soundfile as sf | |
| import tempfile | |
| import torch | |
| import librosa # Thêm thư viện xử lý âm thanh | |
| from vieneu_tts import VieNeuTTS | |
| import time | |
| # --- 1. SETUP MODEL --- | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| try: | |
| tts = VieNeuTTS( | |
| backbone_repo="pnnbao-ump/VieNeu-TTS", | |
| backbone_device=device, | |
| codec_repo="neuphonic/neucodec", | |
| codec_device=device | |
| ) | |
| except Exception as e: | |
| class MockTTS: | |
| def encode_reference(self, path): return None | |
| def infer(self, text, ref, ref_text): | |
| time.sleep(1.2) | |
| import numpy as np | |
| return np.random.uniform(-0.1, 0.1, 24000*2) | |
| tts = MockTTS() | |
| # --- 2. DATA (Giữ nguyên danh sách giọng mẫu) --- | |
| VOICE_SAMPLES = { | |
| "Tuyên (nam miền Bắc)": {"audio": "./sample/Tuyên (nam miền Bắc).wav", "text": "./sample/Tuyên (nam miền Bắc).txt"}, | |
| "Thiện Tâm": {"audio": "./sample/thientam.mp3", "text": "./sample/thientam.txt"}, | |
| "Vĩnh (nam miền Nam)": {"audio": "./sample/Vĩnh (nam miền Nam).wav", "text": "./sample/Vĩnh (nam miền Nam).txt"}, | |
| "Bình (nam miền Bắc)": {"audio": "./sample/Bình (nam miền Bắc).wav", "text": "./sample/Bình (nam miền Bắc).txt"}, | |
| "Nguyên (nam miền Nam)": {"audio": "./sample/Nguyên (nam miền Nam).wav", "text": "./sample/Nguyên (nam miền Nam).txt"}, | |
| "Sơn (nam miền Nam)": {"audio": "./sample/Sơn (nam miền Nam).wav", "text": "./sample/Sơn (nam miền Nam).txt"}, | |
| "Đoan (nữ miền Nam)": {"audio": "./sample/Đoan (nữ miền Nam).wav", "text": "./sample/Đoan (nữ miền Nam).txt"}, | |
| "Ngọc (nữ miền Bắc)": {"audio": "./sample/Ngọc (nữ miền Bắc).wav", "text": "./sample/Ngọc (nữ miền Bắc).txt"}, | |
| "Ly (nữ miền Bắc)": {"audio": "./sample/Ly (nữ miền Bắc).wav", "text": "./sample/Ly (nữ miền Bắc).txt"}, | |
| "Dung (nữ miền Nam)": {"audio": "./sample/Dung (nữ miền Nam).wav", "text": "./sample/Dung (nữ miền Nam).txt"} | |
| } | |
| # --- 3. HELPER FUNCTIONS --- | |
| def load_reference_info(voice_choice): | |
| if voice_choice in VOICE_SAMPLES: | |
| audio_path = VOICE_SAMPLES[voice_choice]["audio"] | |
| text_path = VOICE_SAMPLES[voice_choice]["text"] | |
| if os.path.exists(text_path): | |
| with open(text_path, "r", encoding="utf-8") as f: | |
| ref_text = f.read() | |
| return audio_path, ref_text | |
| return None, "" | |
| def synthesize_speech(text, voice_choice, custom_audio, custom_text, mode_tab, pause_level, speed_value): | |
| try: | |
| if not text or text.strip() == "": | |
| return None, "⚠️ Vui lòng nhập nội dung!" | |
| # 3.1. Xử lý độ ngắt nghỉ (Pause level) | |
| processed_text = text | |
| if pause_level == "Trung bình": | |
| processed_text = processed_text.replace(",", ", , ").replace(".", ". . ") | |
| elif pause_level == "Dài": | |
| processed_text = processed_text.replace(",", ", , , ").replace(".", ". . . . ") | |
| if len(processed_text) > 400: | |
| processed_text = processed_text[:400] | |
| # 3.2. Lấy dữ liệu Reference | |
| if mode_tab == "custom_mode": | |
| if custom_audio is None or not custom_text: | |
| return None, "⚠️ Thiếu Audio mẫu hoặc Text mẫu." | |
| ref_audio_path = custom_audio | |
| ref_text_raw = custom_text | |
| else: | |
| ref_audio_path = VOICE_SAMPLES[voice_choice]["audio"] | |
| with open(VOICE_SAMPLES[voice_choice]["text"], "r", encoding="utf-8") as f: | |
| ref_text_raw = f.read() | |
| # 3.3. Thực hiện Inference | |
| start_time = time.time() | |
| ref_codes = tts.encode_reference(ref_audio_path) | |
| wav = tts.infer(processed_text, ref_codes, ref_text_raw) | |
| # 3.4. Điều chỉnh Tốc độ (Speed) bằng librosa | |
| if speed_value != 1.0: | |
| # Time stretch giữ nguyên pitch | |
| wav = librosa.effects.time_stretch(wav, rate=float(speed_value)) | |
| process_time = time.time() - start_time | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file: | |
| sf.write(tmp_file.name, wav, 24000) | |
| output_path = tmp_file.name | |
| return output_path, f"⚡ Xử lý: {process_time:.2f}s | Tốc độ: {speed_value}x" | |
| except Exception as e: | |
| return None, f"❌ Lỗi: {str(e)}" | |
| # --- 4. THEME & CSS --- | |
| theme = gr.themes.Default( | |
| primary_hue="indigo", | |
| secondary_hue="blue", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont('Inter'), 'sans-serif'], | |
| ).set( | |
| body_background_fill="#020617", | |
| block_background_fill="#0f172a", | |
| block_border_width="1px", | |
| input_background_fill="#1e293b", | |
| input_border_color="#334155", | |
| button_primary_background_fill="linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)", | |
| ) | |
| css = """ | |
| .main-wrap { max-width: 1200px !important; margin: auto !important; padding: 20px !important; } | |
| .st-card { | |
| border-radius: 16px !important; | |
| border: 1px solid rgba(255,255,255,0.1) !important; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.5) !important; | |
| padding: 15px; | |
| } | |
| .result-card { | |
| background: linear-gradient(180deg, rgba(15, 23, 42, 0.8) 0%, rgba(30, 41, 59, 0.8) 100%) !important; | |
| border: 1px solid rgba(99, 102, 241, 0.2) !important; | |
| margin-top: 15px; | |
| } | |
| audio { filter: invert(90%) hue-rotate(180deg) brightness(1.5); width: 100%; border-radius: 8px; } | |
| .footer { text-align: center; margin-top: 40px; color: #475569; font-size: 0.8rem; font-weight: 500; } | |
| """ | |
| # --- 5. UI CONSTRUCTION --- | |
| with gr.Blocks(title="AI Voice Studio") as demo: | |
| with gr.Column(elem_classes="main-wrap"): | |
| with gr.Row(equal_height=True): | |
| # CỘT TRÁI | |
| with gr.Column(scale=1): | |
| with gr.Group(elem_classes="st-card"): | |
| text_input = gr.Textbox( | |
| label="VĂN BẢN CẦN CHUYỂN ĐỔI", | |
| placeholder="Nhập nội dung vào đây...", | |
| lines=20, # Tăng thêm để cân bằng với các nút mới | |
| show_label=True, | |
| ) | |
| char_count = gr.HTML("<div style='text-align: right; color: #6366f1; font-size: 0.85rem; font-weight: bold; padding: 5px;'>0 / 250</div>") | |
| # CỘT PHẢI | |
| with gr.Column(scale=1): | |
| with gr.Tabs() as tabs: | |
| with gr.TabItem("👤 Nghệ sĩ đọc", id="preset_mode"): | |
| voice_select = gr.Dropdown( | |
| choices=list(VOICE_SAMPLES.keys()), | |
| value="Tuyên (nam miền Bắc)", | |
| label="Lựa chọn giọng đọc mẫu", | |
| ) | |
| with gr.Accordion("Nghe thử giọng mẫu", open=False): | |
| ref_audio_preview = gr.Audio(interactive=False, show_label=False) | |
| ref_text_preview = gr.Markdown("...") | |
| with gr.TabItem("🎙️ Nhân bản (Clone)", id="custom_mode"): | |
| custom_audio = gr.Audio(label="Audio gốc", type="filepath") | |
| custom_text = gr.Textbox( | |
| label="NỘI DUNG AUDIO MẪU", | |
| placeholder="Nhập lời thoại của audio mẫu...", | |
| lines=4, | |
| show_label=True | |
| ) | |
| # --- KHU VỰC ĐIỀU CHỈNH ÂM THANH --- | |
| with gr.Row(): | |
| pause_level = gr.Radio( | |
| choices=["Mặc định", "Trung bình", "Dài"], | |
| value="Mặc định", | |
| label="Độ ngắt nghỉ", | |
| scale=1 | |
| ) | |
| speed_select = gr.Dropdown( | |
| choices=[0.8, 0.9, 1.0, 1.1, 1.2, 1.5], | |
| value=1.0, | |
| label="Tốc độ đọc", | |
| scale=1 | |
| ) | |
| current_mode = gr.State(value="preset_mode") | |
| gr.Markdown("<br>") | |
| btn_generate = gr.Button("BẮT ĐẦU TỔNG HỢP", variant="primary", size="lg") | |
| with gr.Group(elem_classes="st-card result-card"): | |
| audio_output = gr.Audio(label="AUDIO KẾT QUẢ", interactive=False, autoplay=True) | |
| status_output = gr.Markdown("<p style='text-align: center; color: #818cf8; font-weight: 500;'>✨ Sẵn sàng thực hiện</p>") | |
| gr.HTML("<div class='footer'>ENGINE BY VIENEU-TTS • PROFESSIONAL AI SOLUTIONS 2025</div>") | |
| # --- LOGIC --- | |
| def update_count(text): | |
| l = len(text) | |
| color = "#6366f1" if l <= 250 else "#f43f5e" | |
| return f"<div style='text-align: right; color: {color}; font-size: 0.85rem; font-weight: bold; padding: 5px;'>{l} / 250</div>" | |
| text_input.change(update_count, text_input, char_count) | |
| def update_ref_preview(voice): | |
| audio, text = load_reference_info(voice) | |
| return audio, f"**Nội dung mẫu:** *\"{text}\"*" | |
| voice_select.change(update_ref_preview, voice_select, [ref_audio_preview, ref_text_preview]) | |
| demo.load(update_ref_preview, voice_select, [ref_audio_preview, ref_text_preview]) | |
| tabs.children[0].select(fn=lambda: "preset_mode", outputs=current_mode) | |
| tabs.children[1].select(fn=lambda: "custom_mode", outputs=current_mode) | |
| btn_generate.click( | |
| fn=synthesize_speech, | |
| inputs=[text_input, voice_select, custom_audio, custom_text, current_mode, pause_level, speed_select], | |
| outputs=[audio_output, status_output] | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue().launch(theme=theme, css=css, server_name="0.0.0.0", server_port=7860) |