| 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.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:")) |
| self.voice_combo = QComboBox() |
| left_layout.addWidget(self.voice_combo) |
|
|
| 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_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) |
|
|
| if __name__ == "__main__": |
| app = QApplication(sys.argv) |
| |
| |
| app.setStyle("Fusion") |
| |
| gui = OmniVoiceGUI() |
| gui.show() |
| sys.exit(app.exec_()) |
|
|