import os import sys import gradio as gr from tools.DocReader.text_extractor import extract_text_from_file # Heuristic offline language detection def detect_language(text: str) -> str: if not text: return "vi" vi_chars = set("àáảãạăằắẳẵặâầấẩẫậèéẻẽẹêềếểễệìíỉĩịòóỏõọôồốổỗộơờớởỡợùúủũụưừứửữựỳýỷỹỵđ" "ÀÁẢÃẠĂẰẮẰẶÂẦẤẨẪẬÈÉẺẼẸÊỀẾỂỄỆÌÍỈĨỊÒÓỎÕỌÔỒỐỔỖỘƠỜỚỞỠỢÙÚỦŨỤƯỪỨỬỮỰỲÝỶỸỲĐ") vi_count = sum(1 for c in text if c in vi_chars) vi_words = {"và", "là", "của", "được", "người", "có", "không", "trong", "một", "cho", "đến", "với", "những", "này", "theo", "đã", "để"} text_words = set(text.lower().split()) vi_word_count = len(text_words.intersection(vi_words)) en_words = {"the", "and", "of", "to", "in", "is", "that", "it", "he", "was", "for", "on", "are", "as", "with", "his", "they", "at"} en_word_count = len(text_words.intersection(en_words)) if vi_count > 0 or vi_word_count > en_word_count: return "vi" else: return "en" def get_synthesis_language(text, language_choice): if language_choice == "Tự động phát hiện": lang_code = detect_language(text) return lang_code, f"Tự động phát hiện ({'Tiếng Việt' if lang_code == 'vi' else 'Tiếng Anh'})" else: lang_code = "vi" if language_choice == "Tiếng Việt" else "en" return lang_code, language_choice def handle_file_upload(file): if file is None: return "", gr.update(visible=False) try: text = extract_text_from_file(file.name) lang_code = detect_language(text) lang_name = "Tiếng Việt" if lang_code == "vi" else "Tiếng Anh" detect_msg = f"ℹ️ **Đã tự động phát hiện ngôn ngữ:** {lang_name}" return text, gr.update(value=detect_msg, visible=True) except Exception as e: return f"Lỗi đọc file: {str(e)}", gr.update(value=f"❌ Lỗi: {str(e)}", visible=True) def handle_single_read(text, voice, language, session_id): from tools.TextToSpeech.app import synthesize_speech, model_loaded if not model_loaded: yield None, "⚠️ Vui lòng quay lại tab 'Text to Speech' để nạp Model trước khi đọc tài liệu!" return lang_code, lang_name = get_synthesis_language(text, language) yield None, f"⏳ Đang chuẩn bị đọc bằng {lang_name}..." for audio_path, status in synthesize_speech( text, voice, None, None, "preset_mode", "Standard (Một lần)", True, 32, 0.8, 250, session_id ): yield audio_path, status def handle_conversation_read(text, language, silence_dur, session_id, *speaker_args): from tools.TextToSpeech.app import synthesize_conversation, model_loaded if not model_loaded: yield None, "⚠️ Vui lòng quay lại tab 'Text to Speech' để nạp Model trước khi đọc tài liệu!" return # First 8 args are speaker names, next 8 are voices speaker_names = list(speaker_args[:8]) speaker_voices = list(speaker_args[8:16]) yield None, "⏳ Đang chuẩn bị đọc hội thoại..." for audio_path, status in synthesize_conversation( text, *speaker_names, *speaker_voices, silence_dur, 0.8, 250, session_id ): yield audio_path, status def handle_read(mode, text, voice, language, silence_dur, session_id, *speaker_args): if not text or not text.strip(): yield None, "⚠️ Vui lòng nhập hoặc tải lên văn bản!" return if mode == "Đọc truyện (Đơn ca)": yield from handle_single_read(text, voice, language, session_id) else: yield from handle_conversation_read(text, language, silence_dur, session_id, *speaker_args) def get_voices_update_func(): from tools.TextToSpeech.app import PRESET_VOICES_CACHE, CONV_VOICES_CACHE if not PRESET_VOICES_CACHE: return [gr.update()] * (1 + 8) default_v = PRESET_VOICES_CACHE[0][1] if isinstance(PRESET_VOICES_CACHE[0], tuple) else PRESET_VOICES_CACHE[0] conv_default = CONV_VOICES_CACHE[0][1] if (CONV_VOICES_CACHE and isinstance(CONV_VOICES_CACHE[0], tuple)) else (CONV_VOICES_CACHE[0] if CONV_VOICES_CACHE else None) return [ gr.update(choices=PRESET_VOICES_CACHE, value=default_v), *[gr.update(choices=CONV_VOICES_CACHE, value=conv_default) for _ in range(8)] ] def on_mode_change(mode): is_conv = mode == "Hội thoại (Đa ca)" return gr.update(visible=not is_conv), gr.update(visible=is_conv) def request_stop(): from tools.TextToSpeech.app import _STOP_EVENT print("🛑 STOP REQUESTED via DocReader button click.") _STOP_EVENT.set() return None, "⏹️ Đã dừng đọc tài liệu.", gr.update(interactive=False) # Setup layout MAX_SPEAKERS = 8 with gr.Blocks() as demo: session_id_state = gr.State("") gr.HTML("

📖 Đọc tài liệu & Kịch bản hội thoại

Trích xuất tài liệu .txt, .docx để đọc hoặc phát kịch bản hội thoại đa nhân vật sử dụng mô hình VieNeu-TTS chạy cục bộ.

") with gr.Row(): with gr.Column(scale=3): file_input = gr.File( label="Tải lên tài liệu (.txt, .docx)", file_types=[".txt", ".docx"] ) detected_lang_label = gr.Markdown(value="", visible=False) text_input = gr.Textbox( lines=12, label="Nội dung văn bản (Có thể chỉnh sửa)", placeholder="Nội dung tài liệu sẽ hiển thị ở đây sau khi tải lên, hoặc bạn tự nhập nội dung..." ) with gr.Row(): language_select = gr.Dropdown( choices=["Tự động phát hiện", "Tiếng Việt", "Tiếng Anh"], value="Tự động phát hiện", label="Ngôn ngữ đọc" ) mode_select = gr.Radio( choices=["Đọc truyện (Đơn ca)", "Hội thoại (Đa ca)"], value="Đọc truyện (Đơn ca)", label="Chế độ đọc" ) # Single Speaker Voice settings with gr.Group(visible=True) as single_voice_group: voice_select = gr.Dropdown( choices=[], value=None, label="🎤 Giọng đọc truyện", interactive=True, allow_custom_value=True ) # Conversation Settings with gr.Group(visible=False) as multi_voice_group: with gr.Row(): btn_detect_speakers = gr.Button("🔍 Quét nhân vật", variant="secondary") silence_slider = gr.Slider(minimum=0, maximum=3, value=0.5, step=0.1, label="⏱️ Khoảng lặng giữa các câu (giây)") gr.Markdown("### 🎭 Cấu hình giọng nhân vật") speaker_name_boxes = [] speaker_voice_dds = [] speaker_slot_rows = [] for i in range(MAX_SPEAKERS): row_visible = i < 3 default_name = "" if i == 0: default_name = "Phương" elif i == 1: default_name = "Dũng" elif i == 2: default_name = "Hùng" with gr.Row(visible=row_visible) as row: name = gr.Textbox( value=default_name, label="👤 Nhân vật", interactive=False, scale=1, min_width=120 ) dd = gr.Dropdown( choices=[], value=None, label="🎤 Giọng đọc", interactive=True, scale=3, allow_custom_value=True ) speaker_slot_rows.append(row) speaker_name_boxes.append(name) speaker_voice_dds.append(dd) btn_generate = gr.Button("🚀 Bắt đầu đọc tài liệu", variant="primary") btn_stop = gr.Button("🛑 Dừng", variant="stop", interactive=False) with gr.Column(scale=2): status_output = gr.Textbox( label="Trạng thái", value="⏳ Chờ tải tài liệu hoặc nhập văn bản...", interactive=False ) audio_output = gr.Audio( label="Âm thanh đầu ra", interactive=False ) file_input.change( fn=handle_file_upload, inputs=[file_input], outputs=[text_input, detected_lang_label] ) mode_select.change( fn=on_mode_change, inputs=[mode_select], outputs=[single_voice_group, multi_voice_group] ) from tools.TextToSpeech.app import extract_speakers_from_script btn_detect_speakers.click( fn=extract_speakers_from_script, inputs=[text_input], outputs=speaker_name_boxes + speaker_voice_dds + speaker_slot_rows ) # Read generation gen_inputs = [ mode_select, text_input, voice_select, language_select, silence_slider, session_id_state, *speaker_name_boxes, *speaker_voice_dds ] gen_event = btn_generate.click( fn=handle_read, inputs=gen_inputs, outputs=[audio_output, status_output] ) btn_generate.click(lambda: gr.update(interactive=True), outputs=btn_stop) gen_event.then(lambda: gr.update(interactive=False), outputs=btn_stop) btn_stop.click(fn=request_stop, outputs=[audio_output, status_output, btn_stop]) # Expose components and update function voice_components = [voice_select] + speaker_voice_dds update_voices_fn = get_voices_update_func