| import sys
|
| import json
|
| import re
|
| import numpy as np
|
| import os
|
| import torch
|
| import soundfile as sf
|
| from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
| QLabel, QComboBox, QSlider, QTextEdit, QPushButton, QMessageBox, QProgressBar)
|
| from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
| from omnivoice import OmniVoice, OmniVoiceGenerationConfig
|
|
|
| BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
| class WorkerThread(QThread):
|
| progress = pyqtSignal(int, str)
|
| finished = pyqtSignal(str)
|
| error = pyqtSignal(str)
|
|
|
| def __init__(self, model, voice_prompts_cache, text, voice_name, num_step, guidance_scale, speed):
|
| super().__init__()
|
| self.model = model
|
| self.voice_prompts_cache = voice_prompts_cache
|
| self.text = text
|
| self.voice_name = voice_name
|
| self.num_step = num_step
|
| self.guidance_scale = guidance_scale
|
| self.speed = speed
|
|
|
| def run(self):
|
| try:
|
| if not self.text.strip():
|
| self.error.emit("Vui lòng nhập văn bản cần đọc!")
|
| return
|
|
|
| prompt = self.voice_prompts_cache.get(self.voice_name)
|
| if not prompt:
|
| self.error.emit(f"Không tìm thấy dữ liệu đã cache cho giọng: {self.voice_name}")
|
| return
|
|
|
| config = OmniVoiceGenerationConfig(num_step=self.num_step, guidance_scale=self.guidance_scale)
|
|
|
| sentences = [s.strip() for s in re.split(r'(?<=[.!?\n])\s+', self.text) if s.strip()]
|
| audio_chunks = []
|
|
|
| for i, sentence in enumerate(sentences):
|
| self.progress.emit(int((i / len(sentences)) * 100), f"Đang đọc câu {i+1}/{len(sentences)}...")
|
| audio = self.model.generate(
|
| text=sentence,
|
| language="vietnamese",
|
| voice_clone_prompt=prompt,
|
| generation_config=config,
|
| speed=self.speed,
|
| )
|
| audio_chunks.append(audio[0])
|
| silence = np.zeros(int(24000 * 0.1), dtype=np.float32)
|
| audio_chunks.append(silence)
|
|
|
| self.progress.emit(100, "Đang ghép file audio...")
|
| final_audio = np.concatenate(audio_chunks)
|
| output_path = os.path.join(BASE_DIR, "output_gui.wav")
|
| sf.write(output_path, final_audio, 24000)
|
|
|
| self.finished.emit(output_path)
|
| except Exception as e:
|
| self.error.emit(str(e))
|
|
|
|
|
| class OmniVoiceGUI(QMainWindow):
|
| def __init__(self):
|
| super().__init__()
|
| self.setWindowTitle("OmniVoice Vietnamese - PyQt5 GUI")
|
| self.resize(800, 600)
|
|
|
| self.model = None
|
| self.voice_prompts_cache = {}
|
| self.voice_audio_paths = {}
|
|
|
| self.init_ui()
|
| self.load_model_and_voices()
|
|
|
| def init_ui(self):
|
| central_widget = QWidget()
|
| self.setCentralWidget(central_widget)
|
| main_layout = QHBoxLayout()
|
|
|
|
|
| left_layout = QVBoxLayout()
|
|
|
| left_layout.addWidget(QLabel("🧑🎤 Chọn giọng đọc:"))
|
|
|
| voice_layout = QHBoxLayout()
|
| self.voice_combo = QComboBox()
|
| voice_layout.addWidget(self.voice_combo)
|
|
|
| self.play_voice_btn = QPushButton("🔊 Nghe thử")
|
| self.play_voice_btn.clicked.connect(self.play_selected_voice)
|
| voice_layout.addWidget(self.play_voice_btn)
|
|
|
| left_layout.addLayout(voice_layout)
|
|
|
| left_layout.addSpacing(20)
|
| left_layout.addWidget(QLabel("⚙️ Num Steps (Độ mượt):"))
|
| self.step_label = QLabel("32")
|
| left_layout.addWidget(self.step_label)
|
| self.step_slider = QSlider(Qt.Horizontal)
|
| self.step_slider.setMinimum(8)
|
| self.step_slider.setMaximum(50)
|
| self.step_slider.setValue(32)
|
| self.step_slider.valueChanged.connect(lambda v: self.step_label.setText(str(v)))
|
| left_layout.addWidget(self.step_slider)
|
|
|
| left_layout.addSpacing(10)
|
| left_layout.addWidget(QLabel("⚙️ Guidance Scale (Chuẩn xác/Robot):"))
|
| self.cfg_label = QLabel("5.0")
|
| left_layout.addWidget(self.cfg_label)
|
| self.cfg_slider = QSlider(Qt.Horizontal)
|
| self.cfg_slider.setMinimum(10)
|
| self.cfg_slider.setMaximum(100)
|
| self.cfg_slider.setValue(50)
|
| self.cfg_slider.valueChanged.connect(lambda v: self.cfg_label.setText(str(v/10.0)))
|
| left_layout.addWidget(self.cfg_slider)
|
|
|
| left_layout.addSpacing(10)
|
| left_layout.addWidget(QLabel("⚙️ Tốc độ đọc (Speed):"))
|
| self.speed_label = QLabel("1.0")
|
| left_layout.addWidget(self.speed_label)
|
| self.speed_slider = QSlider(Qt.Horizontal)
|
| self.speed_slider.setMinimum(5)
|
| self.speed_slider.setMaximum(20)
|
| self.speed_slider.setValue(10)
|
| self.speed_slider.valueChanged.connect(lambda v: self.speed_label.setText(str(v/10.0)))
|
| left_layout.addWidget(self.speed_slider)
|
|
|
| left_layout.addStretch()
|
|
|
|
|
| right_layout = QVBoxLayout()
|
|
|
| right_layout.addWidget(QLabel("📝 Nhập nội dung cần đọc:"))
|
| self.text_input = QTextEdit()
|
| self.text_input.setPlaceholderText("Nhập văn bản dài thoải mái, hệ thống sẽ tự chia nhỏ theo câu...")
|
| right_layout.addWidget(self.text_input)
|
|
|
| self.progress_bar = QProgressBar()
|
| self.progress_bar.setValue(0)
|
| right_layout.addWidget(self.progress_bar)
|
|
|
| self.status_label = QLabel("Trạng thái: Sẵn sàng")
|
| right_layout.addWidget(self.status_label)
|
|
|
| self.generate_btn = QPushButton("🚀 TẠO GIỌNG NÓI")
|
| self.generate_btn.setMinimumHeight(50)
|
| self.generate_btn.clicked.connect(self.start_generation)
|
| right_layout.addWidget(self.generate_btn)
|
|
|
| main_layout.addLayout(left_layout, 1)
|
| main_layout.addLayout(right_layout, 2)
|
| central_widget.setLayout(main_layout)
|
|
|
| def load_model_and_voices(self):
|
| self.status_label.setText("Trạng thái: Đang load OmniVoice Model... (Vui lòng đợi vài chục giây)")
|
| QApplication.processEvents()
|
|
|
| try:
|
| self.model = OmniVoice.from_pretrained(
|
| BASE_DIR,
|
| device_map="cuda:0",
|
| dtype=torch.float16,
|
| )
|
|
|
| VOICES_FILE = os.path.join(BASE_DIR, "voice", "voices.json")
|
| if os.path.exists(VOICES_FILE):
|
| with open(VOICES_FILE, "r", encoding="utf-8") as f:
|
| voices_config = json.load(f)
|
|
|
| self.status_label.setText("Trạng thái: Đang cache các giọng mẫu...")
|
| QApplication.processEvents()
|
|
|
| for name, data in voices_config.items():
|
| audio_rel_path = data.get("audio")
|
| audio_path = os.path.join(BASE_DIR, "voice", audio_rel_path) if audio_rel_path else None
|
| ref_text = data.get("text")
|
| if audio_path and os.path.exists(audio_path) and ref_text:
|
| self.voice_audio_paths[name] = audio_path
|
| self.voice_prompts_cache[name] = self.model.create_voice_clone_prompt(
|
| ref_audio=audio_path,
|
| ref_text=ref_text,
|
| )
|
| self.voice_combo.addItem(name)
|
|
|
| self.status_label.setText("Trạng thái: Load model thành công. Sẵn sàng!")
|
| self.progress_bar.setValue(100)
|
| except Exception as e:
|
| QMessageBox.critical(self, "Lỗi", f"Không thể load model: {e}")
|
| self.status_label.setText("Trạng thái: Lỗi Load Model!")
|
|
|
| def start_generation(self):
|
| if not self.model:
|
| QMessageBox.warning(self, "Cảnh báo", "Model chưa được load thành công!")
|
| return
|
|
|
| text = self.text_input.toPlainText()
|
| voice_name = self.voice_combo.currentText()
|
| num_step = self.step_slider.value()
|
| guidance_scale = self.cfg_slider.value() / 10.0
|
| speed = self.speed_slider.value() / 10.0
|
|
|
| self.generate_btn.setEnabled(False)
|
| self.progress_bar.setValue(0)
|
| self.status_label.setText("Trạng thái: Bắt đầu xử lý...")
|
|
|
| self.worker = WorkerThread(self.model, self.voice_prompts_cache, text, voice_name, num_step, guidance_scale, speed)
|
| self.worker.progress.connect(self.update_progress)
|
| self.worker.finished.connect(self.on_finished)
|
| self.worker.error.connect(self.on_error)
|
| self.worker.start()
|
|
|
| def update_progress(self, percent, msg):
|
| self.progress_bar.setValue(percent)
|
| self.status_label.setText(f"Trạng thái: {msg}")
|
|
|
| def on_finished(self, output_path):
|
| self.progress_bar.setValue(100)
|
| self.status_label.setText(f"Trạng thái: Đã lưu thành công tại {output_path}")
|
| self.generate_btn.setEnabled(True)
|
|
|
| if sys.platform == "win32":
|
| os.startfile(output_path)
|
| elif sys.platform == "darwin":
|
| os.system(f"open {output_path}")
|
| else:
|
| os.system(f"xdg-open {output_path} &")
|
|
|
| def on_error(self, err_msg):
|
| QMessageBox.critical(self, "Lỗi", err_msg)
|
| self.status_label.setText("Trạng thái: Lỗi khi xử lý!")
|
| self.generate_btn.setEnabled(True)
|
|
|
| def play_selected_voice(self):
|
| voice_name = self.voice_combo.currentText()
|
| if not voice_name:
|
| return
|
|
|
| audio_path = self.voice_audio_paths.get(voice_name)
|
| if audio_path and os.path.exists(audio_path):
|
| if sys.platform == "win32":
|
| import winsound
|
|
|
| winsound.PlaySound(audio_path, winsound.SND_FILENAME | winsound.SND_ASYNC)
|
| elif sys.platform == "darwin":
|
| os.system(f"afplay '{audio_path}' &")
|
| else:
|
| os.system(f"aplay '{audio_path}' &")
|
| else:
|
| QMessageBox.warning(self, "Lỗi", "Không tìm thấy file audio mẫu!")
|
|
|
| if __name__ == "__main__":
|
| app = QApplication(sys.argv)
|
|
|
|
|
| app.setStyle("Fusion")
|
|
|
| gui = OmniVoiceGUI()
|
| gui.show()
|
| sys.exit(app.exec_())
|
|
|