├── .gitignore ├── GAG_tools ├── api_manager_tab.py ├── batch_tts_tab.py ├── config_manager.py ├── images │ ├── GAG-1.png │ ├── GAG-2.png │ └── GAG-3.png └── tts_gui_tab.py ├── LICENSE ├── README.md ├── gsv_api_gui.py ├── requirements.txt ├── resources_rc.py └── translations ├── GAG_en.qm └── GAG_en.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /GAG_tools/api_manager_tab.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | import re 4 | import psutil 5 | import logging 6 | import subprocess 7 | from PyQt5.QtCore import QThread, pyqtSignal 8 | from PyQt5.QtWidgets import ( 9 | QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, 10 | QPushButton, QTextEdit, QCheckBox, QFileDialog, 11 | QGroupBox, QMessageBox 12 | ) 13 | from PyQt5.QtGui import QTextCursor, QTextCharFormat, QColor, QFont 14 | from GAG_tools.config_manager import ConfigManager 15 | 16 | logging.basicConfig(level=logging.INFO) 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class ProcessOutputReader(QThread): 21 | output_received = pyqtSignal(str, bool) # text, is_progress_update 22 | 23 | def __init__(self, process): 24 | super().__init__() 25 | self.process = process 26 | self.is_running = True 27 | 28 | def run(self): 29 | text_stream = io.TextIOWrapper( 30 | self.process.stdout, 31 | encoding='utf-8', 32 | errors='replace', 33 | line_buffering=True 34 | ) 35 | 36 | progress_pattern = re.compile(r'^\r.*it/s\]|.*it/s\]') 37 | 38 | while self.is_running and self.process: 39 | try: 40 | line = text_stream.readline() 41 | if not line: 42 | if self.process.poll() is not None: 43 | break 44 | continue 45 | 46 | line = line.rstrip() 47 | is_progress = bool(progress_pattern.search(line)) 48 | self.output_received.emit(line, is_progress) 49 | 50 | except UnicodeDecodeError: 51 | # If UTF-8 fails, try GBK 52 | text_stream = io.TextIOWrapper( 53 | self.process.stdout, 54 | encoding='gbk', 55 | errors='replace', 56 | line_buffering=True 57 | ) 58 | except Exception as e: 59 | self.output_received.emit(f'Error reading output: {str(e)}', False) 60 | 61 | def stop(self): 62 | self.is_running = False 63 | 64 | 65 | class APIManager(QWidget): 66 | def __init__(self, parent=None): 67 | super().__init__(parent) 68 | self.last_was_progress = False 69 | self.config_manager = ConfigManager() 70 | self.process = None 71 | self.output_reader = None 72 | self.is_running = False 73 | self.initUI() 74 | 75 | # Autostart if configured 76 | if self.config_manager.get_value('autostart_api'): 77 | self.start_api() 78 | 79 | def initUI(self): 80 | self.setGeometry(100, 100, 800, 800) 81 | layout = QVBoxLayout(self) 82 | 83 | # API URL Group 84 | url_group = QGroupBox(self.tr("API 配置")) 85 | url_layout = QVBoxLayout() 86 | 87 | # URL input 88 | url_input_layout = QHBoxLayout() 89 | url_label = QLabel(self.tr("API URL:")) 90 | self.url_input = QLineEdit(self.config_manager.get_value('api_url')) 91 | url_input_layout.addWidget(url_label) 92 | url_input_layout.addWidget(self.url_input) 93 | url_layout.addLayout(url_input_layout) 94 | 95 | # Python path selection 96 | python_path_layout = QHBoxLayout() 97 | python_path_label = QLabel(self.tr("Python 环境:")) 98 | self.python_path_input = QLineEdit(self.config_manager.get_value('python_path')) 99 | self.python_path_input.setReadOnly(True) 100 | python_path_button = QPushButton(self.tr("浏览")) 101 | python_path_button.clicked.connect(self.browse_python_path) 102 | python_path_layout.addWidget(python_path_label) 103 | python_path_layout.addWidget(self.python_path_input) 104 | python_path_layout.addWidget(python_path_button) 105 | url_layout.addLayout(python_path_layout) 106 | 107 | # Autostart checkbox 108 | self.autostart_checkbox = QCheckBox(self.tr("随软件启动API")) 109 | self.autostart_checkbox.setChecked(self.config_manager.get_value('autostart_api')) 110 | url_layout.addWidget(self.autostart_checkbox) 111 | 112 | url_group.setLayout(url_layout) 113 | layout.addWidget(url_group) 114 | 115 | # Control buttons 116 | control_layout = QHBoxLayout() 117 | self.start_button = QPushButton(self.tr("启动 API")) 118 | self.start_button.setStyleSheet("min-height: 30px;") 119 | self.stop_button = QPushButton(self.tr("停止 API")) 120 | self.stop_button.setStyleSheet("min-height: 30px;") 121 | self.start_button.clicked.connect(self.start_api) 122 | self.stop_button.clicked.connect(self.stop_api) 123 | self.stop_button.setEnabled(False) 124 | control_layout.addWidget(self.start_button) 125 | control_layout.addWidget(self.stop_button) 126 | 127 | # Output console 128 | output_label = QLabel(self.tr("控制台输出")) 129 | layout.addWidget(output_label) 130 | self.output_console = QTextEdit() 131 | self.output_console.setReadOnly(True) 132 | self.output_console.setStyleSheet(""" 133 | QTextEdit { 134 | background-color: #1e1e1e; 135 | color: #ffffff; 136 | font-family: "Microsoft YaHei Mono", Consolas, Monaco, monospace; 137 | } 138 | """) 139 | layout.addWidget(self.output_console) 140 | 141 | layout.addLayout(control_layout) 142 | 143 | def process_inference_output(self, text, is_progress_update): 144 | cursor = self.output_console.textCursor() 145 | cursor.movePosition(QTextCursor.End) 146 | 147 | if hasattr(self, 'last_was_progress'): 148 | if not self.last_was_progress and is_progress_update: 149 | pass 150 | elif self.last_was_progress and not is_progress_update: 151 | text = '\n' + text 152 | self.last_was_progress = is_progress_update 153 | 154 | if is_progress_update: 155 | cursor.movePosition(QTextCursor.StartOfLine, QTextCursor.KeepAnchor) 156 | cursor.removeSelectedText() 157 | self.update_output(text, color='#ffa500', auto_newline=False) 158 | else: 159 | if "info:" in text.lower(): 160 | self.update_output(text, color='green') 161 | elif "debug:" in text.lower(): 162 | self.update_output(text, color='#87CEEB') 163 | elif "error:" in text.lower(): 164 | self.update_output(text, color='red') 165 | elif "warning:" in text.lower(): 166 | self.update_output(text, color='yellow') 167 | else: 168 | self.update_output(text) 169 | 170 | def update_output(self, text, color='white', bold=False, italic=False, auto_newline=True): 171 | cursor = self.output_console.textCursor() 172 | format = QTextCharFormat() 173 | format.setForeground(QColor(color)) 174 | if bold: 175 | format.setFontWeight(QFont.Bold) 176 | if italic: 177 | format.setFontItalic(True) 178 | cursor.movePosition(QTextCursor.End) 179 | cursor.insertText(text + ('\n' if auto_newline else ''), format) 180 | self.output_console.setTextCursor(cursor) 181 | self.output_console.ensureCursorVisible() 182 | 183 | def print_separator(self, char='-', count=50): 184 | self.update_output(char * count) 185 | 186 | def browse_python_path(self): 187 | file_path, _ = QFileDialog.getOpenFileName( 188 | self, 189 | self.tr("选择 Python Executable"), 190 | os.path.dirname(self.python_path_input.text()), 191 | "Python Executable (python.exe)" 192 | ) 193 | if file_path: 194 | self.python_path_input.setText(file_path) 195 | self.config_manager.set_value('python_path', file_path) 196 | 197 | def start_api(self): 198 | if self.is_running: 199 | return 200 | 201 | python_path = self.python_path_input.text() 202 | self.output_console.clear() 203 | if not os.path.exists(python_path): 204 | QMessageBox.warning( 205 | self, 206 | self.tr("错误"), 207 | self.tr("指定的Python环境不存在.") 208 | ) 209 | return 210 | 211 | try: 212 | # Add Python directory to PATH 213 | env = os.environ.copy() 214 | python_dir = os.path.dirname(python_path) 215 | env["PATH"] = python_dir + os.pathsep + env.get("PATH", "") 216 | 217 | # Start the API process 218 | startupinfo = None 219 | if os.name == 'nt': 220 | startupinfo = subprocess.STARTUPINFO() 221 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 222 | 223 | # Create process 224 | self.process = subprocess.Popen( 225 | [python_path, "api_v2.py", "-a", "127.0.0.1", "-p", "9880"], 226 | stdout=subprocess.PIPE, 227 | stderr=subprocess.STDOUT, 228 | env={**env, "PYTHONIOENCODING": "utf-8"}, 229 | creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0, 230 | startupinfo=startupinfo 231 | ) 232 | 233 | # Start output reader 234 | self.output_reader = ProcessOutputReader(self.process) 235 | self.output_reader.output_received.connect(self.process_inference_output) 236 | self.output_reader.start() 237 | 238 | self.is_running = True 239 | self.start_button.setEnabled(False) 240 | self.stop_button.setEnabled(True) 241 | 242 | logger.info("API process started") 243 | self.update_output("API process started...", color='#90EE90', bold=True) 244 | 245 | except Exception as e: 246 | QMessageBox.critical( 247 | self, 248 | self.tr("错误"), 249 | self.tr("尝试启动API时出错: ") + str(e) 250 | ) 251 | logger.error(f"Failed to start API process: {e}") 252 | 253 | def stop_api(self): 254 | if not self.is_running: 255 | return 256 | 257 | try: 258 | self.terminate_process() 259 | self.is_running = False 260 | self.start_button.setEnabled(True) 261 | self.stop_button.setEnabled(False) 262 | 263 | if self.output_reader: 264 | self.output_reader.stop() 265 | self.output_reader = None 266 | 267 | logger.info("API process stopped") 268 | self.update_output("API process stopped.", color='#90EE90', bold=True) 269 | 270 | except Exception as e: 271 | QMessageBox.critical( 272 | self, 273 | self.tr("错误"), 274 | self.tr("尝试停止API时出错: ") + str(e) 275 | ) 276 | logger.error(f"Failed to stop API process: {e}") 277 | 278 | def terminate_process(self): 279 | logger.info("Terminating API process") 280 | if self.process: 281 | try: 282 | parent = psutil.Process(self.process.pid) 283 | children = parent.children(recursive=True) 284 | for child in children: 285 | child.terminate() 286 | parent.terminate() 287 | gone, still_alive = psutil.wait_procs(children + [parent], timeout=5) 288 | for p in still_alive: 289 | p.kill() 290 | except psutil.NoSuchProcess: 291 | pass 292 | self.process = None 293 | 294 | def cleanup(self): 295 | # Save current configuration 296 | updates = { 297 | 'api_url': self.url_input.text(), 298 | 'autostart_api': self.autostart_checkbox.isChecked() 299 | } 300 | self.config_manager.update_config(updates) 301 | 302 | # Stop API if running 303 | if self.is_running: 304 | self.stop_api() 305 | 306 | def closeEvent(self, event): 307 | self.cleanup() 308 | super().closeEvent(event) 309 | 310 | 311 | # For testing purposes 312 | if __name__ == '__main__': 313 | from PyQt5.QtWidgets import QApplication 314 | import sys 315 | 316 | app = QApplication(sys.argv) 317 | window = APIManager() 318 | window.show() 319 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /GAG_tools/batch_tts_tab.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import shutil 4 | import requests 5 | import threading 6 | import subprocess 7 | from subprocess import PIPE 8 | from typing import List, Dict, Optional 9 | from dataclasses import dataclass, asdict 10 | from concurrent.futures import ThreadPoolExecutor 11 | from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, 12 | QPushButton, QComboBox, QListWidget, 13 | QLabel, QFileDialog, QMessageBox, QSizePolicy) 14 | from PyQt5.QtCore import pyqtSignal, QUrl 15 | from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QColor, QDesktopServices 16 | from charset_normalizer import from_path 17 | from GAG_tools.config_manager import ConfigManager 18 | 19 | 20 | class EnterableComboBox(QComboBox): 21 | entered = pyqtSignal() 22 | 23 | def enterEvent(self, event): 24 | self.entered.emit() 25 | super().enterEvent(event) 26 | 27 | 28 | @dataclass 29 | class TTSTask: 30 | file_path: str 31 | preset_name: str 32 | segment_count: int = 0 33 | completed_segments: int = 0 34 | status: str = 'pending' 35 | average_time_per_segment: float = 0.0 36 | 37 | @property 38 | def progress(self) -> int: 39 | return int(self.completed_segments * 100 / self.segment_count) if self.segment_count else 0 40 | 41 | @classmethod 42 | def from_dict(cls, data: Dict): 43 | return cls(**{k: v for k, v in data.items() if k in cls.__annotations__}) 44 | 45 | 46 | class BatchTTS(QWidget): 47 | status_updated = pyqtSignal(str, str, int, str, float) # file_path, status, progress, color, remaining_time 48 | 49 | def __init__(self): 50 | super().__init__() 51 | self.tasks = {} 52 | self.current_gpt = '' 53 | self.current_sovits = '' 54 | self.config_manager = ConfigManager() 55 | self.setAcceptDrops(True) 56 | self.processing = False 57 | self.worker_thread = None 58 | self.executor = ThreadPoolExecutor(max_workers=1) 59 | 60 | self.initUI() 61 | self.setup_connections() 62 | self.load_saved_state() 63 | 64 | def initUI(self): 65 | self.setGeometry(100, 100, 800, 800) 66 | layout = QVBoxLayout() 67 | 68 | # API URL 69 | url_layout = QHBoxLayout() 70 | self.url_edit = QLineEdit() 71 | url_layout.addWidget(QLabel(self.tr("API URL:")), 0) 72 | url_layout.addWidget(self.url_edit, 1) 73 | layout.addLayout(url_layout) 74 | 75 | # Preset and format selector 76 | preset_layout = QHBoxLayout() 77 | self.preset_combo = EnterableComboBox() 78 | self.format_combo = QComboBox() 79 | self.format_combo.addItems(['wav', 'mp3', 'flac', 'ogg', 'aac']) 80 | 81 | preset_label = QLabel(self.tr("预设: ")) 82 | format_label = QLabel(self.tr("输出格式:")) 83 | 84 | preset_layout.addWidget(preset_label, 0) 85 | preset_layout.addWidget(self.preset_combo, 4) 86 | preset_layout.addWidget(format_label, 0) 87 | preset_layout.addWidget(self.format_combo, 2) 88 | layout.addLayout(preset_layout) 89 | 90 | # File list with vertical buttons on the right 91 | file_section = QHBoxLayout() 92 | 93 | # File list on the left 94 | self.file_list = QListWidget() 95 | self.file_list.setSelectionMode(QListWidget.ExtendedSelection) 96 | file_section.addWidget(self.file_list) 97 | 98 | # Create a container widget for buttons to match file_list height 99 | btn_container = QWidget() 100 | btn_layout = QVBoxLayout(btn_container) 101 | btn_layout.setSpacing(4) # Remove spacing between buttons 102 | btn_layout.setContentsMargins(0, 1, 0, 1) # Remove margins 103 | 104 | # Create vertical buttons 105 | self.clear_btn = QPushButton('\n'.join(self.tr("清空列表"))) 106 | self.clear_btn.setFixedWidth(30) 107 | self.clear_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) 108 | btn_layout.addWidget(self.clear_btn) 109 | 110 | self.move_up_btn = QPushButton('\n'.join(self.tr("▲‖‖‖"))) 111 | self.move_up_btn.setFixedWidth(30) 112 | self.move_up_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) 113 | btn_layout.addWidget(self.move_up_btn) 114 | 115 | self.move_down_btn = QPushButton('\n'.join(self.tr("‖‖‖▼"))) 116 | self.move_down_btn.setFixedWidth(30) 117 | self.move_down_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) 118 | btn_layout.addWidget(self.move_down_btn) 119 | 120 | self.remove_btn = QPushButton('\n'.join(self.tr("删除选中"))) 121 | self.remove_btn.setFixedWidth(30) 122 | self.remove_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) 123 | btn_layout.addWidget(self.remove_btn) 124 | 125 | file_section.addWidget(btn_container) 126 | layout.addLayout(file_section) 127 | 128 | # Output directory 129 | out_layout = QHBoxLayout() 130 | self.out_dir_edit = QLineEdit() 131 | self.out_dir_edit.setPlaceholderText(self.tr("在这里设置批量合成的保存路径...")) 132 | self.browse_btn = QPushButton(self.tr("浏览")) 133 | self.open_output_button = QPushButton(self.tr("打开")) 134 | out_layout.addWidget(QLabel(self.tr("批量合成保存路径:")), 0) 135 | out_layout.addWidget(self.out_dir_edit, 1) 136 | out_layout.addWidget(self.browse_btn) 137 | out_layout.addWidget(self.open_output_button) 138 | layout.addLayout(out_layout) 139 | 140 | # Synthesis controls 141 | control_layout = QHBoxLayout() 142 | self.synth_btn = QPushButton(self.tr("开始合成")) 143 | self.synth_btn.setStyleSheet("min-height: 30px;") 144 | control_layout.addWidget(self.synth_btn) 145 | layout.addLayout(control_layout) 146 | 147 | self.setLayout(layout) 148 | 149 | 150 | def setup_connections(self): 151 | self.clear_btn.clicked.connect(self.clear_list) 152 | self.move_up_btn.clicked.connect(self.move_up) 153 | self.move_down_btn.clicked.connect(self.move_down) 154 | self.remove_btn.clicked.connect(self.remove_selected) 155 | self.browse_btn.clicked.connect(self.browse_output_dir) 156 | self.open_output_button.clicked.connect(self.open_output_folder) 157 | self.synth_btn.clicked.connect(self.start_synthesis) 158 | self.preset_combo.entered.connect(self._update_preset) 159 | self.status_updated.connect(self._update_item_status) 160 | 161 | def open_output_folder(self): 162 | output_folder = self.out_dir_edit.text() 163 | if os.path.isdir(output_folder): 164 | QDesktopServices.openUrl(QUrl.fromLocalFile(output_folder)) 165 | else: 166 | QMessageBox.warning(self, self.tr('警告'), self.tr('输出文件夹无效')) 167 | 168 | def _update_preset(self): 169 | presets = self.config_manager.get_value('presets', {}) 170 | current = self.preset_combo.currentText() 171 | self.preset_combo.clear() 172 | self.preset_combo.addItems(presets.keys()) 173 | if current in presets: 174 | self.preset_combo.setCurrentText(current) 175 | 176 | def load_saved_state(self): 177 | self.url_edit.setText(self.config_manager.get_value('api_url', '')) 178 | self.out_dir_edit.setText(self.config_manager.get_value('batch_tts_save_directory', '')) 179 | 180 | presets = self.config_manager.get_value('presets', {}) 181 | self.preset_combo.clear() 182 | self.preset_combo.addItems(presets.keys()) 183 | self.preset_combo.setCurrentText(self.config_manager.get_value('current_preset', '')) 184 | 185 | saved_format = self.config_manager.get_value('output_format', 'wav') 186 | self.format_combo.setCurrentText(saved_format) 187 | 188 | saved_tasks = self.config_manager.get_value('batch_tts_tasks', []) 189 | for task_dict in saved_tasks: 190 | file_path = task_dict.get('file_path', '') 191 | if os.path.exists(file_path): 192 | task = TTSTask.from_dict(task_dict) 193 | self.file_list.addItem(file_path) 194 | self.tasks[file_path] = task 195 | 196 | cache_dir = os.path.join(self.out_dir_edit.text(), 'cache', 197 | os.path.splitext(os.path.basename(file_path))[0]) 198 | if os.path.exists(cache_dir): 199 | segments = [f for f in os.listdir(cache_dir) if f.endswith('.txt') and f != 'files.txt'] 200 | wavs = [f for f in os.listdir(cache_dir) if f.endswith('.wav')] 201 | if segments: 202 | task.segment_count = len(segments) 203 | task.completed_segments = len(wavs) 204 | status = 'completed' if task.completed_segments == task.segment_count else 'processing' 205 | self.status_updated.emit( 206 | file_path, 207 | self.tr("已完成") if status == 'completed' else self.tr("已暂停"), 208 | task.progress, 209 | 'green' if status == 'completed' else 'blue', 210 | 0.0 211 | ) 212 | 213 | def save_current_state(self): 214 | self.config_manager.set_value('api_url', self.url_edit.text()) 215 | self.config_manager.set_value('batch_tts_save_directory', self.out_dir_edit.text()) 216 | self.config_manager.set_value('output_format', self.format_combo.currentText()) 217 | 218 | tasks_data = [] 219 | for file_path, task in self.tasks.items(): 220 | if os.path.exists(file_path): 221 | tasks_data.append(asdict(task)) 222 | self.config_manager.set_value('batch_tts_tasks', tasks_data) 223 | 224 | def cleanup(self): 225 | if self.processing: 226 | self.processing = False 227 | 228 | for file_path, task in self.tasks.items(): 229 | if task.status == 'processing': 230 | task.status = 'pending' 231 | 232 | if self.worker_thread: 233 | self.worker_thread.join(timeout=0.5) 234 | 235 | self.save_current_state() 236 | if self.executor: 237 | self.executor.shutdown(wait=False) 238 | 239 | def closeEvent(self, event): 240 | self.cleanup() 241 | super().closeEvent(event) 242 | 243 | def dragEnterEvent(self, event: QDragEnterEvent): 244 | if event.mimeData().hasUrls(): 245 | for url in event.mimeData().urls(): 246 | if url.toLocalFile().endswith('.txt'): 247 | event.acceptProposedAction() 248 | return 249 | 250 | def dragMoveEvent(self, event): 251 | if event.mimeData().hasUrls(): 252 | event.acceptProposedAction() 253 | 254 | def dropEvent(self, event: QDropEvent): 255 | for url in event.mimeData().urls(): 256 | file_path = url.toLocalFile() 257 | if file_path.endswith('.txt') and os.path.exists(file_path): 258 | base_path = file_path.split(" (")[0] 259 | if base_path not in self.tasks: 260 | self.file_list.addItem(base_path) 261 | self.tasks[base_path] = TTSTask( 262 | file_path=base_path, 263 | preset_name=self.preset_combo.currentText() 264 | ) 265 | self.status_updated.emit(base_path, self.tr("待合成"), -1, 'gray', 0.0) 266 | self.save_current_state() 267 | 268 | def clear_list(self): 269 | try: 270 | cache_dir = os.path.join(self.out_dir_edit.text(), 'cache') 271 | if os.path.exists(cache_dir): 272 | shutil.rmtree(cache_dir, ignore_errors=False) 273 | self.file_list.clear() 274 | self.tasks.clear() 275 | self.save_current_state() 276 | except Exception as e: 277 | QMessageBox.warning(self, self.tr("警告"), 278 | self.tr("清空列表时出错: {}").format(str(e))) 279 | 280 | def move_up(self): 281 | current = self.file_list.currentRow() 282 | if current > 0: 283 | item = self.file_list.takeItem(current) 284 | self.file_list.insertItem(current - 1, item) 285 | self.file_list.setCurrentRow(current - 1) 286 | self.save_current_state() 287 | 288 | def move_down(self): 289 | current = self.file_list.currentRow() 290 | if current < self.file_list.count() - 1: 291 | item = self.file_list.takeItem(current) 292 | self.file_list.insertItem(current + 1, item) 293 | self.file_list.setCurrentRow(current + 1) 294 | self.save_current_state() 295 | 296 | def remove_selected(self): 297 | items = self.file_list.selectedItems() 298 | for item in items: 299 | try: 300 | file_path = item.text().split(" (")[0] # Extract file path 301 | self.file_list.takeItem(self.file_list.row(item)) 302 | if file_path in self.tasks: 303 | del self.tasks[file_path] 304 | cache_dir = os.path.join(self.out_dir_edit.text(), 'cache', 305 | os.path.splitext(os.path.basename(file_path))[0]) 306 | if os.path.exists(cache_dir): 307 | shutil.rmtree(cache_dir, ignore_errors=False) 308 | except Exception as e: 309 | QMessageBox.warning(self, self.tr("警告"), 310 | self.tr("删除文件 {} 时出错: {}").format(file_path, str(e))) 311 | continue 312 | self.save_current_state() 313 | 314 | def browse_output_dir(self): 315 | directory = QFileDialog.getExistingDirectory(self, self.tr("选择批量合成保存路径")) 316 | if directory: 317 | self.out_dir_edit.setText(directory) 318 | self.save_current_state() 319 | 320 | def start_synthesis(self): 321 | if hasattr(self, '_last_click_time') and time.time() - self._last_click_time < 1.0: 322 | return 323 | self._last_click_time = time.time() 324 | if self.processing: 325 | self.processing = False 326 | self.clear_btn.setEnabled(True) 327 | self.move_up_btn.setEnabled(True) 328 | self.move_down_btn.setEnabled(True) 329 | self.remove_btn.setEnabled(True) 330 | for file_path, task in self.tasks.items(): 331 | if task.status == 'processing': 332 | self._update_item_status(file_path, self.tr("已暂停"), -1, 'blue', 0) 333 | self.synth_btn.setText(self.tr("开始合成")) 334 | return 335 | 336 | if not self.file_list.count(): 337 | QMessageBox.warning(self, self.tr("警告"), 338 | self.tr("请先添加需要合成的文件!")) 339 | return 340 | 341 | for file_path, task in self.tasks.items(): 342 | if task.status == 'failed': 343 | task.status = 'pending' 344 | task.completed_segments = 0 345 | self._update_item_status(file_path, self.tr("准备合成"), -1, 'blue', 0) 346 | 347 | self.processing = True 348 | self.clear_btn.setEnabled(False) 349 | self.move_up_btn.setEnabled(False) 350 | self.move_down_btn.setEnabled(False) 351 | self.remove_btn.setEnabled(False) 352 | self.synth_btn.setText(self.tr("停止合成")) 353 | self.worker_thread = threading.Thread(target=self._synthesis_worker) 354 | self.worker_thread.start() 355 | 356 | def _synthesis_worker(self): 357 | for i in range(self.file_list.count()): 358 | if not self.processing: 359 | break 360 | 361 | file_path = self.file_list.item(i).text().split(" (")[0] 362 | if file_path not in self.tasks: 363 | continue 364 | 365 | task = self.tasks[file_path] 366 | if task.status == 'completed': 367 | continue 368 | 369 | task.status = 'processing' 370 | 371 | try: 372 | self._process_file(task) 373 | if not self.processing: 374 | break 375 | task.status = 'completed' 376 | except Exception as e: 377 | if self.processing: 378 | self.processing = False 379 | QMessageBox.critical(self, self.tr("错误"), 380 | self.tr("处理文件时出错: {}: {}").format(file_path, str(e))) 381 | task.status = 'failed' 382 | break 383 | 384 | self.processing = False 385 | self.clear_btn.setEnabled(True) 386 | self.move_up_btn.setEnabled(True) 387 | self.move_down_btn.setEnabled(True) 388 | self.remove_btn.setEnabled(True) 389 | self.synth_btn.setText(self.tr("开始合成")) 390 | 391 | def _process_file(self, task: TTSTask): 392 | try: 393 | try: 394 | with open(task.file_path, 'r', encoding='utf-8') as f: 395 | text = f.read() 396 | except UnicodeDecodeError: 397 | text = str(from_path(task.file_path).best()) 398 | if not text: 399 | raise Exception( 400 | self.tr("无法检测文件编码。该文件可能不是文本文件或编码未知: {}").format(task.file_path)) 401 | 402 | self.status_updated.emit(task.file_path, self.tr("正在准备文本..."), -1, 'blue', 0) 403 | base_name = os.path.splitext(os.path.basename(task.file_path))[0] 404 | cache_dir = os.path.join(self.out_dir_edit.text(), 'cache', base_name) 405 | os.makedirs(cache_dir, exist_ok=True) 406 | 407 | segment_size = self.config_manager.get_value('batch_tts_segment_size', 100) 408 | segments = self._split_text(text, segment_size) 409 | task.segment_count = len(segments) 410 | 411 | existing_txt_files = [f for f in os.listdir(cache_dir) if f.endswith('.txt')] 412 | if len(existing_txt_files) != task.segment_count: 413 | for f in os.listdir(cache_dir): 414 | if f.endswith(('.txt', '.wav')): 415 | os.remove(os.path.join(cache_dir, f)) 416 | 417 | for i, segment in enumerate(segments): 418 | segment_txt = os.path.join(cache_dir, f"{i:09d}.txt") 419 | with open(segment_txt, 'w', encoding='utf-8') as f: 420 | f.write(segment) 421 | 422 | existing_wav_files = [f for f in os.listdir(cache_dir) if f.endswith('.wav')] 423 | task.completed_segments = len(existing_wav_files) 424 | 425 | self.status_updated.emit(task.file_path, self.tr("正在合成"), -1, 'blue', 0) 426 | txt_files = sorted([f for f in os.listdir(cache_dir) if f.endswith('.txt')]) 427 | for txt_file in txt_files: 428 | if not self.processing: 429 | return 430 | 431 | segment_index = int(txt_file.split('.')[0]) 432 | segment_wav = os.path.join(cache_dir, f"{segment_index:09d}.wav") 433 | 434 | if not os.path.exists(segment_wav): 435 | segment_start_time = time.time() 436 | segment_txt = os.path.join(cache_dir, txt_file) 437 | with open(segment_txt, 'r', encoding='utf-8') as f: 438 | segment_text = f.read() 439 | 440 | response = self._synthesize_segment(segment_text) 441 | if not response: 442 | raise Exception(self.tr("合成片段 {} 失败,请检查API状态").format(segment_index)) 443 | 444 | with open(segment_wav, 'wb') as f: 445 | f.write(response) 446 | 447 | segment_end_time = time.time() 448 | segment_time = segment_end_time - segment_start_time 449 | task.average_time_per_segment = ((task.average_time_per_segment * task.completed_segments) + segment_time) / (task.completed_segments + 1) 450 | task.completed_segments += 1 451 | 452 | remaining_time = task.average_time_per_segment * (task.segment_count - task.completed_segments) 453 | 454 | self.status_updated.emit(task.file_path, self.tr("正在合成"), task.progress, 'blue', remaining_time) 455 | 456 | if task.completed_segments == task.segment_count: 457 | output_format = self.format_combo.currentText() 458 | output_file = os.path.join( 459 | self.out_dir_edit.text(), 460 | f"{base_name}_{task.preset_name}.{output_format}" 461 | ) 462 | self.status_updated.emit(task.file_path, self.tr("正在压制音频"), -1, 'blue', 0) 463 | self._merge_audio_files(cache_dir, output_file, output_format) 464 | self.status_updated.emit(task.file_path, self.tr("已完成"), 100, 'green', 0) 465 | 466 | except Exception as e: 467 | self.status_updated.emit(task.file_path, self.tr("失败"), -1, 'red', 0) 468 | raise Exception(self.tr("处理文件时出错: {}").format(str(e))) 469 | 470 | def _update_item_status(self, file_path: str, status: str, progress: int, color: str, remaining_time: float): 471 | for i in range(self.file_list.count()): 472 | item = self.file_list.item(i) 473 | if item.text().startswith(file_path): 474 | status_text = (f"{file_path} ({status}" 475 | f"{f':{progress}%' if progress != -1 else ''}" 476 | f"{f' - 剩余时间:{int(remaining_time)}秒' if remaining_time > 0.5 else ''}" 477 | f")") 478 | item.setText(status_text) 479 | item.setForeground(QColor( 480 | {'blue': '#0000FF', 'green': '#008000', 'red': '#FF0000', 'black': '#000000', 'gray': '#555555'}[ 481 | color] 482 | )) 483 | break 484 | 485 | def _split_text(self, text: str, segment_size: int) -> List[str]: 486 | if not text or segment_size <= 0: 487 | return [] 488 | 489 | delimiters = ['。', '!', '?', ';', '\n', '.', '!', '?', ';'] 490 | segments = [] 491 | current_pos = 0 492 | 493 | while current_pos < len(text): 494 | min_end = current_pos + segment_size 495 | 496 | if min_end >= len(text): 497 | segment = text[current_pos:] 498 | if segment.strip(): 499 | segments.append(self._remove_empty_lines(segment)) 500 | break 501 | 502 | delimiter_pos = -1 503 | search_pos = min_end 504 | 505 | while search_pos < len(text): 506 | for delimiter in delimiters: 507 | pos = text.find(delimiter, search_pos, search_pos + 1) 508 | if pos != -1: 509 | delimiter_pos = pos + 1 510 | break 511 | if delimiter_pos != -1: 512 | break 513 | search_pos += 1 514 | 515 | if delimiter_pos == -1: 516 | delimiter_pos = len(text) 517 | 518 | segment = text[current_pos:delimiter_pos] 519 | if segment.strip(): 520 | segments.append(self._remove_empty_lines(segment)) 521 | 522 | current_pos = delimiter_pos 523 | 524 | return segments 525 | 526 | def _remove_empty_lines(self, text: str) -> str: 527 | lines = text.splitlines() 528 | non_empty_lines = [line for line in lines if line.strip()] 529 | return '\n'.join(non_empty_lines) 530 | 531 | def _synthesize_segment(self, text: str) -> Optional[bytes]: 532 | if not self.processing: # Check if stopped 533 | return None 534 | 535 | base_url = self.url_edit.text().rstrip('/') 536 | preset_name = self.preset_combo.currentText() 537 | preset_config = self.config_manager.get_value('presets', {}).get(preset_name, {}) 538 | 539 | def is_prompt_required_model(model_path): 540 | PROMPT_REQUIRED_VERSIONS = {'SoVITS_weights_v3', 'SoVITS_weights_v4'} 541 | if not model_path: 542 | return False 543 | dir_name = os.path.basename(os.path.dirname(model_path)) 544 | return dir_name in PROMPT_REQUIRED_VERSIONS 545 | 546 | # Prepare request parameters 547 | params = { 548 | 'text': text, 549 | 'text_lang': preset_config.get('text_lang', 'all_zh'), 550 | 'ref_audio_path': preset_config.get('ref_audio_path', ''), 551 | 'prompt_lang': preset_config.get('prompt_lang', 'all_zh'), 552 | 'prompt_text': '' if preset_config.get('no_prompt', False) and not is_prompt_required_model(preset_config.get('sovits_model')) else preset_config.get('prompt_text', ''), 553 | 'aux_ref_audio_paths': preset_config.get('aux_ref_audio_paths', []), 554 | 'top_k': preset_config.get('top_k', 5), 555 | 'top_p': preset_config.get('top_p', 1.0), 556 | 'temperature': preset_config.get('temperature', 1.0), 557 | 'text_split_method': preset_config.get('text_split_method', 'cut1'), 558 | 'batch_size': preset_config.get('batch_size', 1), 559 | 'batch_threshold': preset_config.get('batch_threshold', 0.75), 560 | 'split_bucket': preset_config.get('split_bucket', True), 561 | 'speed_factor': preset_config.get('speed_factor', 1.0), 562 | 'streaming_mode': False, # 强制关闭流式传输 563 | 'seed': preset_config.get('seed', -1), 564 | 'parallel_infer': preset_config.get('parallel_infer', True), 565 | 'repetition_penalty': preset_config.get('repetition_penalty', 1.35), 566 | "sample_steps": preset_config.get('sample_steps', 32), 567 | "super_sampling": preset_config.get('super_sampling', False), 568 | } 569 | 570 | gpt_model = preset_config.get('gpt_model') 571 | sovits_model = preset_config.get('sovits_model') 572 | 573 | for _ in range(3): # Retry 3 times 574 | if not self.processing: 575 | return None 576 | try: 577 | if gpt_model != self.current_gpt: 578 | if gpt_model: 579 | requests.get(f"{base_url}/set_gpt_weights", params={'weights_path': gpt_model}) 580 | self.current_gpt = gpt_model 581 | 582 | if sovits_model != self.current_sovits: 583 | if sovits_model: 584 | requests.get(f"{base_url}/set_sovits_weights", params={'weights_path': sovits_model}) 585 | self.current_sovits = sovits_model 586 | 587 | response = requests.post(f"{base_url}/tts", json=params) 588 | 589 | if response.status_code == 200: 590 | return response.content 591 | elif response.status_code == 400: 592 | error = response.json() 593 | raise Exception(self.tr("API错误: {}").format(error)) 594 | 595 | except Exception as e: 596 | if not self.processing: # Check if stopped 597 | return None 598 | print(self.tr("合成尝试失败: {}").format(str(e))) 599 | time.sleep(1) 600 | 601 | return None 602 | 603 | def _merge_audio_files(self, cache_dir: str, output_file: str, format: str): 604 | # Create file list 605 | file_list = os.path.join(cache_dir, 'files.txt') 606 | with open(file_list, 'w', encoding='utf-8') as f: 607 | for audio in sorted(os.listdir(cache_dir)): 608 | if audio.endswith('.wav'): 609 | f.write(f"file '{os.path.join(cache_dir, audio)}'\n") 610 | 611 | # Map formats to FFmpeg encoders 612 | format_encoders = { 613 | 'wav': ['-c', 'copy'], 614 | 'mp3': ['-c:a', 'libmp3lame'], 615 | 'ogg': ['-c:a', 'libvorbis'], 616 | 'aac': ['-c:a', 'aac'], 617 | 'flac': ['-c:a', 'flac'] 618 | } 619 | encoder_args = format_encoders.get(format, ['-c', 'copy']) 620 | 621 | startupinfo = None 622 | if os.name == 'nt': 623 | startupinfo = subprocess.STARTUPINFO() 624 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 625 | 626 | process = subprocess.Popen( 627 | [ 628 | 'ffmpeg', '-f', 'concat', '-safe', '0', 629 | '-i', file_list, *encoder_args, 630 | '-y', output_file 631 | ], 632 | stdout=PIPE, 633 | stderr=PIPE, 634 | startupinfo=startupinfo, 635 | creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 636 | ) 637 | 638 | # Wait for completion and check for errors 639 | stdout, stderr = process.communicate() 640 | if process.returncode != 0: 641 | raise subprocess.CalledProcessError( 642 | process.returncode, 643 | process.args, 644 | stdout, 645 | stderr 646 | ) 647 | -------------------------------------------------------------------------------- /GAG_tools/config_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import threading 4 | from copy import deepcopy 5 | from typing import Dict, Any 6 | 7 | 8 | class ConfigDict: 9 | def __init__(self, config_manager): 10 | self._config_manager = config_manager 11 | 12 | def __getitem__(self, key): 13 | return self._config_manager.get_value(key) 14 | 15 | def __setitem__(self, key, value): 16 | self._config_manager.set_value(key, value) 17 | 18 | def get(self, key, default=None): 19 | return self._config_manager.get_value(key, default) 20 | 21 | def __delitem__(self, key): 22 | self._config_manager.delete_value(key) 23 | 24 | 25 | class ConfigManager: 26 | _instance_lock = threading.Lock() 27 | _instance = None 28 | 29 | def __new__(cls): 30 | if not cls._instance: 31 | with cls._instance_lock: 32 | if not cls._instance: 33 | cls._instance = super(ConfigManager, cls).__new__(cls) 34 | return cls._instance 35 | 36 | def __init__(self): 37 | if not hasattr(self, '_initialized'): 38 | self.config_file = os.path.abspath("GAG_config.json") 39 | self._config = {} 40 | self._config_dict = ConfigDict(self) 41 | self._initialized = True 42 | 43 | self._default_config = { 44 | 'api_url': 'http://127.0.0.1:9880', 45 | 'python_path': os.path.join('runtime', 'python.exe'), 46 | 'autostart_api': True, 47 | 'presets': { 48 | 'Default': { 49 | 'text_lang': 'all_zh', 50 | 'ref_audio_path': '', 51 | 'aux_ref_audio_paths': [], 52 | 'prompt_text': '', 53 | 'prompt_lang': 'all_zh', 54 | 'top_k': 5, 55 | 'top_p': 1.0, 56 | 'temperature': 1.0, 57 | 'text_split_method': 'cut1', 58 | 'batch_size': 1, 59 | 'batch_threshold': 0.75, 60 | 'split_bucket': False, 61 | 'return_fragment': False, 62 | 'speed_factor': 1.0, 63 | 'streaming_mode': False, 64 | 'seed': -1, 65 | 'parallel_infer': False, 66 | 'repetition_penalty': 1.35, 67 | 'gpt_model': '', 68 | 'sovits_model': '', 69 | 'no_prompt': False, 70 | "sample_steps": 32, 71 | "super_sampling": False, 72 | } 73 | }, 74 | 'current_preset': 'Default', 75 | 'save_directory': '', 76 | 'batch_tts_save_directory': '', 77 | 'batch_tts_segment_size': 100, 78 | 'batch_tts_tasks': [], 79 | } 80 | 81 | self.load_config() 82 | 83 | def load_config(self) -> None: 84 | try: 85 | if os.path.exists(self.config_file): 86 | with open(self.config_file, 'r', encoding='utf-8') as f: 87 | loaded_config = json.load(f) 88 | self._config = self._deep_merge(deepcopy(self._default_config), loaded_config) 89 | else: 90 | self._config = deepcopy(self._default_config) 91 | self.save_config() 92 | except Exception as e: 93 | print(f"Error loading config: {e}") 94 | self._config = deepcopy(self._default_config) 95 | self.save_config() 96 | 97 | def _deep_merge(self, base: Dict, update: Dict) -> Dict: 98 | for key, value in update.items(): 99 | if key in base and isinstance(base[key], dict) and isinstance(value, dict): 100 | base[key] = self._deep_merge(base[key], value) 101 | else: 102 | base[key] = value 103 | return base 104 | 105 | def save_config(self) -> None: 106 | try: 107 | with open(self.config_file, 'w', encoding='utf-8') as f: 108 | json.dump(self._config, f, indent=4, ensure_ascii=False) 109 | except Exception as e: 110 | print(f"Error saving config: {e}") 111 | 112 | def update_config(self, updates: Dict[str, Any], save: bool = True) -> None: 113 | self._config = self._deep_merge(self._config, updates) 114 | if save: 115 | self.save_config() 116 | 117 | @property 118 | def config(self) -> Dict: 119 | return self._config_dict 120 | 121 | def get_value(self, key: str, default: Any = None) -> Any: 122 | try: 123 | keys = key.split('.') 124 | value = self._config 125 | for k in keys: 126 | value = value[k] 127 | return deepcopy(value) 128 | except KeyError: 129 | return default 130 | 131 | def set_value(self, key: str, value: Any, save: bool = True) -> None: 132 | keys = key.split('.') 133 | current = self._config 134 | for k in keys[:-1]: 135 | if k not in current: 136 | current[k] = {} 137 | current = current[k] 138 | current[keys[-1]] = deepcopy(value) 139 | if save: 140 | self.save_config() 141 | 142 | def delete_value(self, key: str, save: bool = True) -> bool: 143 | try: 144 | keys = key.split('.') 145 | current = self._config 146 | for k in keys[:-1]: 147 | current = current[k] 148 | if keys[-1] in current: 149 | del current[keys[-1]] 150 | if save: 151 | self.save_config() 152 | return True 153 | return False 154 | except (KeyError, TypeError): 155 | return False -------------------------------------------------------------------------------- /GAG_tools/images/GAG-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AliceNavigator/GPT-SoVITS-Api-GUI/c09416d4c9d900b2ec466439d6ef3013984926f6/GAG_tools/images/GAG-1.png -------------------------------------------------------------------------------- /GAG_tools/images/GAG-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AliceNavigator/GPT-SoVITS-Api-GUI/c09416d4c9d900b2ec466439d6ef3013984926f6/GAG_tools/images/GAG-2.png -------------------------------------------------------------------------------- /GAG_tools/images/GAG-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AliceNavigator/GPT-SoVITS-Api-GUI/c09416d4c9d900b2ec466439d6ef3013984926f6/GAG_tools/images/GAG-3.png -------------------------------------------------------------------------------- /GAG_tools/tts_gui_tab.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import re 4 | import time 5 | import psutil 6 | import shutil 7 | import requests 8 | import sounddevice as sd 9 | import soundfile as sf 10 | from datetime import datetime 11 | from PyQt5.QtGui import QDesktopServices 12 | from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 13 | QHBoxLayout, QLabel, QLineEdit, QPushButton, QComboBox, 14 | QSpinBox, QDoubleSpinBox, QCheckBox, QTextEdit, 15 | QFileDialog, QGroupBox, QMessageBox, QStatusBar, QInputDialog, QGridLayout, 16 | QProgressBar, QFormLayout) 17 | from PyQt5.QtCore import Qt, QThread, pyqtSignal, QUrl, QFileSystemWatcher, QTimer 18 | from GAG_tools.config_manager import ConfigManager 19 | import resources_rc 20 | 21 | 22 | class ModelSwitchThread(QThread): 23 | switch_signal = pyqtSignal(bool, str) 24 | 25 | def __init__(self, url, model_type, weights_path): 26 | super().__init__() 27 | self.url = url 28 | self.model_type = model_type # 'gpt' or 'sovits' 29 | self.weights_path = weights_path 30 | 31 | def run(self): 32 | try: 33 | endpoint = '/set_gpt_weights' if self.model_type == 'gpt' else '/set_sovits_weights' 34 | response = requests.get(f"{self.url}{endpoint}", params={'weights_path': self.weights_path}) 35 | 36 | if response.status_code == 200: 37 | self.switch_signal.emit(True, self.tr("{} 模型切换成功").format(self.model_type.upper())) 38 | else: 39 | error_msg = response.json() if response.headers.get( 40 | 'content-type') == 'application/json' else response.text 41 | self.switch_signal.emit(False, 42 | self.tr("{} 模型切换失败:\n {}").format(self.model_type.upper(), error_msg)) 43 | except Exception as e: 44 | self.switch_signal.emit(False, 45 | self.tr("{} 模型切换时发生错误:\n {}").format(self.model_type.upper(), str(e))) 46 | 47 | 48 | class TTSThread(QThread): 49 | finished = pyqtSignal(str) 50 | error = pyqtSignal(str) 51 | 52 | def __init__(self, url, params, cache_dir): 53 | super().__init__() 54 | self.url = url 55 | self.params = params 56 | self.temp_file = None 57 | self.cache_dir = cache_dir 58 | 59 | def run(self): 60 | try: 61 | print(str(self.params)) 62 | response = requests.post(f"{self.url}/tts", json=self.params) 63 | if response.status_code == 200: 64 | temp_file_path = os.path.join(self.cache_dir, f"tmp_{int(time.time())}.wav") 65 | with open(temp_file_path, 'wb') as f: 66 | f.write(response.content) 67 | self.temp_file = temp_file_path 68 | self.finished.emit(self.temp_file) 69 | else: 70 | self.error.emit(self.tr("执行推理时出现错误:\n {}").format(response.text)) 71 | except Exception as e: 72 | self.error.emit(self.tr("执行推理时出现错误:\n {}").format(str(e))) 73 | 74 | 75 | class APICheckThread(QThread): 76 | status_signal = pyqtSignal(bool, str) 77 | 78 | def __init__(self, url, silent=False, continuous=False, retry_interval=2000): 79 | super().__init__() 80 | self.url = url 81 | self.silent = silent 82 | self.continuous = continuous 83 | self.retry_interval = retry_interval 84 | self.should_continue = True 85 | 86 | def run(self): 87 | while self.should_continue: 88 | try: 89 | response = requests.get(f"{self.url}/control") 90 | if response.status_code == 400: # API is ready 91 | self.status_signal.emit(True, self.tr("API 就绪")) 92 | if not self.continuous: 93 | break 94 | elif not self.silent: 95 | self.status_signal.emit(False, self.tr("API 未就绪")) 96 | except Exception as e: 97 | if not self.silent: 98 | self.status_signal.emit(False, self.tr("API 不可用: {}").format(str(e))) 99 | 100 | # If not continuous or stopped, break the loop 101 | if not self.continuous or not self.should_continue: 102 | break 103 | 104 | # Sleep for retry interval before next check 105 | self.msleep(self.retry_interval) 106 | 107 | def stop(self): 108 | self.should_continue = False 109 | 110 | 111 | class TTSGUI(QMainWindow): 112 | def __init__(self): 113 | super().__init__() 114 | self.param_widgets = None 115 | self.SPLIT_METHODS = { 116 | 'cut0': self.tr('不切'), 117 | 'cut1': self.tr('凑四句一切'), 118 | 'cut2': self.tr('凑50字一切'), 119 | 'cut3': self.tr('按中文句号。切'), 120 | 'cut4': self.tr('按英文句号.切'), 121 | 'cut5': self.tr('按标点符号切') 122 | } 123 | self.LANGUAGES = { 124 | 'all_zh': self.tr('中文'), 125 | 'en': self.tr('英文'), 126 | 'all_ja': self.tr('日文'), 127 | 'all_yue': self.tr('粤语'), 128 | 'all_ko': self.tr('韩文'), 129 | 'zh': self.tr('中英混合'), 130 | 'ja': self.tr('日英混合'), 131 | 'yue': self.tr('粤英混合'), 132 | 'ko': self.tr('韩英混合'), 133 | 'auto': self.tr('多语种混合'), 134 | 'auto_yue': self.tr('多语种混合(粤语)') 135 | } 136 | self.SAMPLE_STEPS = { 137 | 4: '4', 138 | 8: '8', 139 | 16: '16', 140 | 32: '32', 141 | 64: '64', 142 | 128: '128' 143 | } 144 | self.GPT_DIRS = [ 145 | ('GPT_weights', 'v1'), 146 | ('GPT_weights_v2', 'v2'), 147 | ('GPT_weights_v3', 'v3'), 148 | ('GPT_weights_v4', 'v4') 149 | ] 150 | self.SOVITS_DIRS = [ 151 | ('SoVITS_weights', 'v1'), 152 | ('SoVITS_weights_v2', 'v2'), 153 | ('SoVITS_weights_v3', 'v3'), 154 | ('SoVITS_weights_v4', 'v4') 155 | ] 156 | self.MODEL_PARAM_RESTRICTIONS = { 157 | 'sovits': { 158 | 'v1': ['sample_steps', 'super_sampling'], 159 | 'v2': ['sample_steps', 'super_sampling'], 160 | 'v3': ['aux_ref_audio_paths', 'no_prompt'], 161 | 'v4': ['aux_ref_audio_paths', 'no_prompt', 'super_sampling'], 162 | }, 163 | 'gpt': { 164 | 'v1': [], 165 | 'v2': [], 166 | 'v3': [], 167 | 'v4': [], 168 | } 169 | } 170 | self.config_manager = ConfigManager() 171 | self.setup_cache_directory() 172 | self.current_audio_file = None 173 | self.is_playing = False 174 | self.current_gpt_model = None 175 | self.current_sovits_model = None 176 | self.gpt_switching = False 177 | self.sovits_switching = False 178 | self.synthesis_pending = False 179 | self.watcher = QFileSystemWatcher(self) 180 | for dir_info in self.GPT_DIRS + self.SOVITS_DIRS: 181 | dir_name = dir_info[0] 182 | os.makedirs(dir_name, exist_ok=True) 183 | self.watcher.addPath(dir_name) 184 | self.watcher.directoryChanged.connect(self.update_model_lists) 185 | self.setAttribute(Qt.WA_StyledBackground, True) 186 | self.setAutoFillBackground(True) 187 | self.setup_background() 188 | self.initUI() 189 | self.start_system_monitoring() 190 | self.api_check_thread = None 191 | self.autostart_api_check() 192 | 193 | def initUI(self): 194 | self.setWindowTitle(self.tr('TTS GUI')) 195 | self.setGeometry(100, 100, 800, 800) 196 | 197 | # Create central widget 198 | central_widget = QWidget() 199 | central_widget.setObjectName("centralWidget") 200 | self.setCentralWidget(central_widget) 201 | main_layout = QVBoxLayout(central_widget) 202 | 203 | # API settings group 204 | api_group = QGroupBox(self.tr("API 设置")) 205 | api_layout = QHBoxLayout() 206 | self.api_url_input = QLineEdit(self.config_manager.get_value('api_url')) 207 | self.api_status_label = QLabel(self.tr("状态: API 未就绪")) 208 | self.check_api_button = QPushButton(self.tr("检查")) 209 | self.check_api_button.clicked.connect(self.check_api_status) 210 | api_layout.addWidget(QLabel(self.tr("API URL:"))) 211 | api_layout.addWidget(self.api_url_input) 212 | api_layout.addWidget(self.api_status_label) 213 | api_layout.addWidget(self.check_api_button) 214 | api_group.setLayout(api_layout) 215 | main_layout.addWidget(api_group) 216 | 217 | # Parameter settings group 218 | params_group = QGroupBox(self.tr("待合成文本:")) 219 | params_group.setFlat(True) 220 | params_layout = QVBoxLayout() 221 | 222 | # Text input 223 | self.text_input = QTextEdit() 224 | self.text_input.setAcceptRichText(False) 225 | params_layout.addWidget(self.text_input) 226 | self.text_input.setPlaceholderText( 227 | self.tr("在这里输入需要合成的文本..." 228 | "\n\n使用方法:\n" 229 | "1.将本exe放入GPT-SoVITS-v4-20250422fix或更新的官方整合包下,双击启动,支持v1,v2,v3, v4模型。\n" 230 | "2.将读取并使用GPT_weights,_v2,_v3, _v4与SoVITS_weights,_v2,_v3, _v4下的模型,请先完成训练获得模型。\n" 231 | "3.保存预设将保存当前所有合成参数设定,可视为一个说话人,后续可快速切换,亦可用于批量合成页面。\n" 232 | "4.默认使用整合包自带环境来调起并使用API,也可以在API管理页面自定义。\n" 233 | "\n此外,若无可用N卡并使用官方整合包,请在初次启动前修改GPT_SoVITS/configs/tts_infer.yaml中的device为cpu, is_half为false 以避免API启动失败。" 234 | "\n\nGitHub开源地址: https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI by 领航员未鸟\n") 235 | ) 236 | 237 | # Create parameter input widgets 238 | self.create_parameter_inputs(params_layout) 239 | 240 | params_group.setLayout(params_layout) 241 | main_layout.addWidget(params_group) 242 | 243 | # Control button group 244 | control_group = QGroupBox(self.tr("控制")) 245 | control_layout = QVBoxLayout() 246 | 247 | # Save path settings 248 | path_layout = QHBoxLayout() 249 | self.save_path_input = QLineEdit(self.config_manager.get_value('save_directory')) 250 | # self.save_path_input.setReadOnly(True) 251 | self.save_path_input.setPlaceholderText(self.tr("将会把合成的音频保存在该路径下...")) 252 | self.browse_button = QPushButton(self.tr("浏览")) 253 | self.browse_button.clicked.connect(self.set_save_path) 254 | self.open_save_directory_button = QPushButton(self.tr("打开")) 255 | self.open_save_directory_button.clicked.connect(self.open_save_directory) 256 | path_layout.addWidget(QLabel(self.tr("保存路径:"))) 257 | path_layout.addWidget(self.save_path_input) 258 | path_layout.addWidget(self.browse_button) 259 | path_layout.addWidget(self.open_save_directory_button) 260 | control_layout.addLayout(path_layout) 261 | 262 | # Existing control buttons 263 | buttons_layout = QHBoxLayout() 264 | self.synthesize_button = QPushButton(self.tr("开始合成")) 265 | self.synthesize_button.setStyleSheet("min-height: 20px;") 266 | self.synthesize_button.setEnabled(False) 267 | self.synthesize_button.clicked.connect(self.prepare_synthesis) 268 | self.play_button = QPushButton(self.tr("播放")) 269 | self.play_button.setStyleSheet("min-height: 20px;") 270 | self.play_button.clicked.connect(self.play_audio) 271 | self.save_button = QPushButton(self.tr("保存音频")) 272 | self.save_button.setStyleSheet("min-height: 20px;") 273 | self.save_button.clicked.connect(self.save_audio) 274 | buttons_layout.addWidget(self.synthesize_button) 275 | buttons_layout.addWidget(self.play_button) 276 | buttons_layout.addWidget(self.save_button) 277 | control_layout.addLayout(buttons_layout) 278 | 279 | control_group.setLayout(control_layout) 280 | main_layout.addWidget(control_group) 281 | 282 | # Status bar 283 | self.statusBar = QStatusBar() 284 | self.setStatusBar(self.statusBar) 285 | 286 | # Update model lists 287 | self.update_model_lists() 288 | 289 | # Load preset 290 | self.load_preset() 291 | 292 | # Sets the translucent GroupBox style 293 | self.setup_group_box_styles() 294 | 295 | def create_parameter_inputs(self, layout): 296 | # All parameters in param_widgets will be used as request parameters. 297 | self.param_widgets = {} 298 | 299 | # Create main horizontal layout to utilize screen width 300 | main_param_layout = QHBoxLayout() 301 | 302 | # Left side layout (Presets, Models, and Language settings) 303 | left_side = QVBoxLayout() 304 | 305 | # Top row layout for presets and models 306 | top_row = QHBoxLayout() 307 | 308 | # Preset controls 309 | preset_group = QGroupBox(self.tr("预设")) 310 | preset_layout = QVBoxLayout() 311 | preset_layout.setSpacing(8) 312 | 313 | # Preset combo 314 | self.preset_combo = QComboBox() 315 | self.preset_combo.addItems(self.config_manager.get_value('presets').keys()) 316 | self.preset_combo.setCurrentText(self.config_manager.get_value('current_preset')) 317 | self.preset_combo.currentTextChanged.connect(self.load_preset) 318 | preset_layout.addWidget(self.preset_combo) 319 | 320 | # Preset buttons 321 | preset_buttons = QHBoxLayout() 322 | preset_buttons.setSpacing(4) 323 | self.save_preset_button = QPushButton(self.tr("保存")) 324 | self.delete_preset_button = QPushButton(self.tr("删除")) 325 | preset_buttons.addWidget(self.save_preset_button) 326 | self.save_preset_button.clicked.connect(self.save_preset) 327 | preset_buttons.addWidget(self.delete_preset_button) 328 | self.delete_preset_button.clicked.connect(self.delete_preset) 329 | preset_layout.addLayout(preset_buttons) 330 | 331 | preset_group.setLayout(preset_layout) 332 | top_row.addWidget(preset_group) 333 | 334 | # Model Selection 335 | model_group = QGroupBox(self.tr("模型选择")) 336 | model_layout = QFormLayout() 337 | model_layout.setSpacing(8) 338 | 339 | # GPT model 340 | self.gpt_combo = QComboBox() 341 | self.gpt_combo.setMinimumWidth(175) 342 | self.gpt_combo.activated.connect(self.update_model_lists) 343 | self.gpt_combo.currentIndexChanged.connect(self.update_param_restrictions) 344 | model_layout.addRow(self.tr("GPT 模型:"), self.gpt_combo) 345 | 346 | # Sovits model 347 | self.sovits_combo = QComboBox() 348 | self.sovits_combo.setMinimumWidth(175) 349 | self.sovits_combo.activated.connect(self.update_model_lists) 350 | self.sovits_combo.currentIndexChanged.connect(self.update_param_restrictions) 351 | model_layout.addRow(self.tr("SoVITS 模型:"), self.sovits_combo) 352 | 353 | model_group.setLayout(model_layout) 354 | top_row.addWidget(model_group) 355 | 356 | # Add top row to left side 357 | left_side.addLayout(top_row) 358 | 359 | # Language and Text Settings 360 | lang_group = QGroupBox(self.tr("语言与文本设置")) 361 | lang_layout = QVBoxLayout() 362 | lang_layout.setSpacing(8) 363 | 364 | # Language settings in form layout 365 | lang_form = QFormLayout() 366 | lang_form.setSpacing(8) 367 | 368 | # Text language 369 | self.param_widgets['text_lang'] = QComboBox() 370 | for key, value in self.LANGUAGES.items(): 371 | self.param_widgets['text_lang'].addItem(value, key) 372 | lang_form.addRow(self.tr("合成文本语种:"), self.param_widgets['text_lang']) 373 | 374 | # Prompt language 375 | self.param_widgets['prompt_lang'] = QComboBox() 376 | for key, value in self.LANGUAGES.items(): 377 | self.param_widgets['prompt_lang'].addItem(value, key) 378 | lang_form.addRow(self.tr("参考音频语种:"), self.param_widgets['prompt_lang']) 379 | 380 | # Text split method 381 | self.param_widgets['text_split_method'] = QComboBox() 382 | for key, value in self.SPLIT_METHODS.items(): 383 | self.param_widgets['text_split_method'].addItem(value, key) 384 | lang_form.addRow(self.tr("文本分割方式:"), self.param_widgets['text_split_method']) 385 | 386 | # Reference audio settings 387 | ref_audio_widget = QWidget() 388 | ref_audio_layout = QHBoxLayout(ref_audio_widget) 389 | ref_audio_layout.setContentsMargins(0, 0, 0, 0) 390 | ref_audio_layout.setSpacing(4) 391 | self.param_widgets['ref_audio_path'] = QLineEdit() 392 | self.param_widgets['ref_audio_path'].setPlaceholderText(self.tr("3-10秒的参考音频,超过会报错")) 393 | ref_audio_button = QPushButton(self.tr("浏览")) 394 | ref_audio_button.setFixedWidth(60) 395 | ref_audio_button.clicked.connect(lambda: self.browse_file('ref_audio_path')) 396 | ref_audio_layout.addWidget(self.param_widgets['ref_audio_path']) 397 | ref_audio_layout.addWidget(ref_audio_button) 398 | lang_form.addRow(self.tr("参考音频:"), ref_audio_widget) 399 | 400 | # Prompt text 401 | prompt_widget = QWidget() 402 | prompt_layout = QHBoxLayout(prompt_widget) 403 | prompt_layout.setContentsMargins(0, 0, 0, 0) 404 | prompt_layout.setSpacing(4) 405 | self.param_widgets['prompt_text'] = QLineEdit() 406 | self.param_widgets['prompt_text'].setPlaceholderText(self.tr("不填则视为使用无参考文本模式")) 407 | self.param_widgets['no_prompt'] = QCheckBox(self.tr("无参 ")) 408 | prompt_layout.addWidget(self.param_widgets['prompt_text']) 409 | prompt_layout.addWidget(self.param_widgets['no_prompt']) 410 | lang_form.addRow(self.tr("参考音频文本:"), prompt_widget) 411 | 412 | # Auxiliary reference audio 413 | aux_ref_widget = QWidget() 414 | aux_ref_layout = QHBoxLayout(aux_ref_widget) 415 | aux_ref_layout.setContentsMargins(0, 0, 0, 0) 416 | aux_ref_layout.setSpacing(4) 417 | self.param_widgets['aux_ref_audio_paths'] = QLineEdit() 418 | self.param_widgets['aux_ref_audio_paths'].setPlaceholderText(self.tr("(可选)以额外的多个音频平均融合音色")) 419 | aux_ref_button = QPushButton(self.tr("浏览")) 420 | aux_ref_button.setFixedWidth(60) 421 | aux_ref_button.clicked.connect(lambda: self.browse_files('aux_ref_audio_paths')) 422 | aux_ref_layout.addWidget(self.param_widgets['aux_ref_audio_paths']) 423 | aux_ref_layout.addWidget(aux_ref_button) 424 | lang_form.addRow(self.tr("辅助参考音频:"), aux_ref_widget) 425 | 426 | lang_layout.addLayout(lang_form) 427 | lang_group.setLayout(lang_layout) 428 | left_side.addWidget(lang_group) 429 | 430 | # Add left side to main layout 431 | main_param_layout.addLayout(left_side, stretch=1) 432 | 433 | # Right side - Generation Parameters 434 | gen_group = QGroupBox(self.tr("合成参数")) 435 | gen_layout = QVBoxLayout() 436 | 437 | # Parameter grid 438 | param_grid = QGridLayout() 439 | param_grid.setVerticalSpacing(10) 440 | 441 | # Create parameter input widgets with original layout 442 | self.param_widgets['top_k'] = QSpinBox() 443 | self.param_widgets['top_k'].setRange(1, 100) 444 | self.param_widgets['top_p'] = QDoubleSpinBox() 445 | self.param_widgets['top_p'].setRange(0, 1) 446 | self.param_widgets['top_p'].setSingleStep(0.05) 447 | self.param_widgets['temperature'] = QDoubleSpinBox() 448 | self.param_widgets['temperature'].setRange(0, 1) 449 | self.param_widgets['temperature'].setSingleStep(0.05) 450 | 451 | param_grid.addWidget(QLabel(self.tr("Top K:")), 0, 0) 452 | param_grid.addWidget(self.param_widgets['top_k'], 0, 1) 453 | param_grid.addWidget(QLabel(self.tr("Top P:")), 1, 0) 454 | param_grid.addWidget(self.param_widgets['top_p'], 1, 1) 455 | param_grid.addWidget(QLabel(self.tr("Temperature:")), 2, 0) 456 | param_grid.addWidget(self.param_widgets['temperature'], 2, 1) 457 | 458 | self.param_widgets['speed_factor'] = QDoubleSpinBox() 459 | self.param_widgets['speed_factor'].setRange(0.1, 3) 460 | self.param_widgets['speed_factor'].setSingleStep(0.1) 461 | self.param_widgets['repetition_penalty'] = QDoubleSpinBox() 462 | self.param_widgets['repetition_penalty'].setRange(0, 5) 463 | self.param_widgets['repetition_penalty'].setSingleStep(0.05) 464 | self.param_widgets['seed'] = QSpinBox() 465 | self.param_widgets['seed'].setRange(-1, 1000000) 466 | 467 | param_grid.addWidget(QLabel(self.tr("语速:")), 0, 2) 468 | param_grid.addWidget(self.param_widgets['speed_factor'], 0, 3) 469 | param_grid.addWidget(QLabel(self.tr("重复惩罚:")), 1, 2) 470 | param_grid.addWidget(self.param_widgets['repetition_penalty'], 1, 3) 471 | param_grid.addWidget(QLabel(self.tr("种子:")), 2, 2) 472 | param_grid.addWidget(self.param_widgets['seed'], 2, 3) 473 | 474 | gen_layout.addLayout(param_grid) 475 | 476 | # Checkbox options 477 | options_grid = QGridLayout() 478 | self.param_widgets['parallel_infer'] = QCheckBox(self.tr("并行推理")) 479 | self.param_widgets['split_bucket'] = QCheckBox(self.tr("数据分桶")) 480 | self.param_widgets['super_sampling'] = QCheckBox(self.tr("音频超分")) 481 | self.param_widgets['sample_steps'] = QComboBox() 482 | for key, value in self.SAMPLE_STEPS.items(): 483 | self.param_widgets['sample_steps'].addItem(value, key) 484 | self.param_widgets['sample_steps'].setCurrentIndex(3) 485 | 486 | sample_steps_widget = QWidget() 487 | sample_steps_layout = QHBoxLayout(sample_steps_widget) 488 | sample_steps_layout.setContentsMargins(0, 0, 0, 0) 489 | sample_steps_layout.addWidget(QLabel(self.tr("采样步数:"))) 490 | sample_steps_layout.addWidget(self.param_widgets['sample_steps']) 491 | 492 | options_grid.addWidget(self.param_widgets['parallel_infer'], 0, 0) 493 | options_grid.addWidget(sample_steps_widget, 0, 1) 494 | options_grid.addWidget(self.param_widgets['split_bucket'], 1, 0) 495 | options_grid.addWidget(self.param_widgets['super_sampling'], 1, 1) 496 | 497 | gen_layout.addLayout(options_grid) 498 | 499 | # Batch settings 500 | batch_layout = QGridLayout() 501 | self.param_widgets['batch_size'] = QSpinBox() 502 | self.param_widgets['batch_size'].setRange(1, 10) 503 | self.param_widgets['batch_threshold'] = QDoubleSpinBox() 504 | self.param_widgets['batch_threshold'].setRange(0, 1) 505 | self.param_widgets['batch_threshold'].setSingleStep(0.05) 506 | 507 | batch_layout.addWidget(QLabel(self.tr("批次大小:")), 0, 0) 508 | batch_layout.addWidget(self.param_widgets['batch_size'], 0, 1) 509 | batch_layout.addWidget(QLabel(self.tr("分批阈值:")), 0, 2) 510 | batch_layout.addWidget(self.param_widgets['batch_threshold'], 0, 3) 511 | 512 | gen_layout.addLayout(batch_layout) 513 | 514 | # System monitoring 515 | monitor_layout = QVBoxLayout() 516 | 517 | # CPU usage 518 | cpu_layout = QGridLayout() 519 | cpu_layout.addWidget(QLabel(self.tr("CPU 占用:")), 0, 0) 520 | self.cpu_progress = QProgressBar() 521 | self.cpu_progress.setRange(0, 100) 522 | cpu_layout.addWidget(self.cpu_progress, 0, 1) 523 | monitor_layout.addLayout(cpu_layout) 524 | 525 | # Memory usage 526 | memory_layout = QGridLayout() 527 | memory_layout.addWidget(QLabel(self.tr("内存占用:")), 1, 0) 528 | self.memory_progress = QProgressBar() 529 | self.memory_progress.setRange(0, 100) 530 | self.memory_progress.setTextVisible(False) 531 | memory_layout.addWidget(self.memory_progress, 1, 1) 532 | self.memory_label = QLabel() 533 | self.memory_label.setAlignment(Qt.AlignCenter) 534 | memory_layout.addWidget(self.memory_label, 1, 1) 535 | monitor_layout.addLayout(memory_layout) 536 | 537 | gen_layout.addLayout(monitor_layout) 538 | gen_group.setLayout(gen_layout) 539 | 540 | # Add right side to main layout 541 | main_param_layout.addWidget(gen_group, stretch=1) 542 | 543 | # Add the main layout to the parent layout 544 | layout.addLayout(main_param_layout) 545 | 546 | def open_save_directory(self): 547 | if os.path.exists(self.config_manager.get_value('save_directory')): 548 | QDesktopServices.openUrl(QUrl.fromLocalFile(self.config_manager.get_value('save_directory'))) 549 | else: 550 | QMessageBox.warning(self, self.tr("错误"), self.tr("指定的保存文件夹不存在")) 551 | 552 | def browse_file(self, param_name): 553 | file_name, _ = QFileDialog.getOpenFileName(self, self.tr("选择音频文件"), "", self.tr("Audio Files (*.wav)")) 554 | if file_name: 555 | self.param_widgets[param_name].setText(file_name) 556 | 557 | def browse_files(self, param_name): 558 | files, _ = QFileDialog.getOpenFileNames(self, self.tr("选择音频文件"), "", self.tr("Audio Files (*.wav)")) 559 | if files: 560 | self.param_widgets[param_name].setText(';'.join(files)) 561 | 562 | def autostart_api_check(self): 563 | autostart = self.config_manager.get_value('autostart_api', False) 564 | if not autostart: 565 | return 566 | 567 | self.check_api_button.setEnabled(False) 568 | self.api_status_label.setText(self.tr("状态: 检查中...")) 569 | self.api_check_thread = APICheckThread( 570 | self.api_url_input.text(), 571 | silent=True, 572 | continuous=True 573 | ) 574 | self.api_check_thread.status_signal.connect(self.handle_autostart_check) 575 | self.api_check_thread.start() 576 | 577 | def handle_autostart_check(self, is_available, message): 578 | if is_available: 579 | self.api_status_label.setText(self.tr("状态: API 就绪")) 580 | self.check_api_button.setEnabled(True) 581 | self.synthesize_button.setEnabled(True) 582 | if self.api_check_thread: 583 | self.api_check_thread.stop() 584 | self.api_check_thread = None 585 | 586 | def check_api_status(self): 587 | self.api_status_label.setText(self.tr("状态: 检查中...")) 588 | self.check_api_button.setEnabled(False) 589 | self.synthesize_button.setEnabled(False) 590 | 591 | self.api_check_thread_once = APICheckThread( 592 | self.api_url_input.text(), 593 | silent=False, 594 | continuous=False 595 | ) 596 | self.api_check_thread_once.status_signal.connect(self.update_api_status) 597 | self.api_check_thread_once.start() 598 | 599 | def update_api_status(self, is_available, message): 600 | if is_available: 601 | self.api_status_label.setText(self.tr("状态: API 就绪")) 602 | self.check_api_button.setEnabled(True) 603 | self.synthesize_button.setEnabled(True) 604 | else: 605 | self.api_status_label.setText(self.tr("状态: API 未就绪")) 606 | self.check_api_button.setEnabled(True) 607 | self.synthesize_button.setEnabled(True) 608 | QMessageBox.warning(self, self.tr("API 错误"), message) 609 | 610 | def start_system_monitoring(self): 611 | self.monitor_timer = QTimer() 612 | self.monitor_timer.timeout.connect(self.update_system_monitoring) 613 | self.monitor_timer.start(1000) # 1000ms 614 | 615 | def update_system_monitoring(self): 616 | cpu_percent = psutil.cpu_percent() 617 | memory_info = psutil.virtual_memory() 618 | memory_percent = memory_info.percent 619 | 620 | self.cpu_progress.setValue(int(cpu_percent)) 621 | 622 | memory_used_gb = memory_info.used / (1024 ** 3) 623 | memory_total_gb = memory_info.total / (1024 ** 3) 624 | self.memory_label.setText( 625 | self.tr("{:.2f} GB / {:.2f} GB").format(memory_used_gb, memory_total_gb)) 626 | self.memory_progress.setValue(int(memory_percent)) 627 | 628 | def get_model_version(self, model_type): 629 | combo = self.gpt_combo if model_type == 'gpt' else self.sovits_combo 630 | return combo.currentText().split('/')[0] 631 | 632 | def update_param_restrictions(self): 633 | sovits_ver = self.get_model_version('sovits') 634 | gpt_ver = self.get_model_version('gpt') 635 | 636 | restricted_params = set( 637 | self.MODEL_PARAM_RESTRICTIONS['sovits'].get(sovits_ver, []) + 638 | self.MODEL_PARAM_RESTRICTIONS['gpt'].get(gpt_ver, []) 639 | ) 640 | 641 | for param, widget in self.param_widgets.items(): 642 | widget.setEnabled(param not in restricted_params) 643 | 644 | @staticmethod 645 | def get_model_files(directory, extension): 646 | if not os.path.exists(directory): 647 | return [] 648 | return [f for f in os.listdir(directory) if f.endswith(extension)] 649 | 650 | def update_model_lists(self): 651 | current_gpt = self.gpt_combo.currentData() 652 | current_sovits = self.sovits_combo.currentData() 653 | 654 | self.gpt_combo.clear() 655 | gpt_models = [] 656 | for dir_name, version in self.GPT_DIRS: 657 | models = self.get_model_files(dir_name, '.ckpt') 658 | for model in models: 659 | display_name = f"{version}/{model}" 660 | full_path = os.path.join(dir_name, model) 661 | gpt_models.append((display_name, full_path)) 662 | 663 | seen = set() 664 | unique_models = [] 665 | for display, path in gpt_models: 666 | if path not in seen: 667 | seen.add(path) 668 | unique_models.append((display, path)) 669 | unique_models.sort(key=lambda x: (x[0].split('/')[0], x[0].split('/')[1])) 670 | 671 | for display, path in unique_models: 672 | self.gpt_combo.addItem(display, path) 673 | 674 | if current_gpt in seen: 675 | index = self.gpt_combo.findData(current_gpt) 676 | if index >= 0: 677 | self.gpt_combo.setCurrentIndex(index) 678 | 679 | self.sovits_combo.clear() 680 | sovits_models = [] 681 | for dir_name, version in self.SOVITS_DIRS: 682 | models = self.get_model_files(dir_name, '.pth') 683 | for model in models: 684 | display_name = f"{version}/{model}" 685 | full_path = os.path.join(dir_name, model) 686 | sovits_models.append((display_name, full_path)) 687 | 688 | seen_sovits = set() 689 | unique_sovits = [] 690 | for display, path in sovits_models: 691 | if path not in seen_sovits: 692 | seen_sovits.add(path) 693 | unique_sovits.append((display, path)) 694 | unique_sovits.sort(key=lambda x: (x[0].split('/')[0], x[0].split('/')[1])) 695 | 696 | for display, path in unique_sovits: 697 | self.sovits_combo.addItem(display, path) 698 | 699 | if current_sovits in seen_sovits: 700 | index = self.sovits_combo.findData(current_sovits) 701 | if index >= 0: 702 | self.sovits_combo.setCurrentIndex(index) 703 | 704 | def switch_models_and_synthesize(self): 705 | gpt_model = self.gpt_combo.currentData() 706 | sovits_model = self.sovits_combo.currentData() 707 | 708 | self.gpt_switching = False 709 | self.sovits_switching = False 710 | 711 | self.synthesize_button.setEnabled(False) 712 | 713 | if gpt_model != self.current_gpt_model: 714 | self.gpt_switching = True 715 | self.gpt_switch_thread = ModelSwitchThread(self.api_url_input.text(), 'gpt', gpt_model) 716 | self.gpt_switch_thread.switch_signal.connect(self.handle_switch_result) 717 | self.gpt_switch_thread.start() 718 | 719 | if sovits_model != self.current_sovits_model: 720 | self.sovits_switching = True 721 | self.sovits_switch_thread = ModelSwitchThread(self.api_url_input.text(), 'sovits', sovits_model) 722 | self.sovits_switch_thread.switch_signal.connect(self.handle_switch_result) 723 | self.sovits_switch_thread.start() 724 | 725 | # If no models need to be switched 726 | if not self.gpt_switching and not self.sovits_switching: 727 | self.statusBar.showMessage(self.tr("未检测到模型变更"), 5000) 728 | self.execute_synthesis() 729 | 730 | def handle_switch_result(self, success, message): 731 | if not success: 732 | self.synthesis_pending = False 733 | self.synthesize_button.setEnabled(True) 734 | QMessageBox.warning(self, self.tr("模型切换时出错"), message) 735 | 736 | if "GPT" in message: 737 | self.gpt_switching = False 738 | elif "SOVITS" in message: 739 | self.sovits_switching = False 740 | else: 741 | self.statusBar.showMessage(message, 5000) 742 | if "GPT" in message: 743 | self.current_gpt_model = self.gpt_combo.currentData() 744 | self.gpt_switching = False 745 | elif "SOVITS" in message: 746 | self.current_sovits_model = self.sovits_combo.currentData() 747 | self.sovits_switching = False 748 | 749 | # If all requested switches are complete and there is a pending synthesis task, execute synthesis 750 | if not self.gpt_switching and not self.sovits_switching and self.synthesis_pending: 751 | self.execute_synthesis() 752 | 753 | def prepare_synthesis(self): 754 | if not self.text_input.toPlainText(): 755 | QMessageBox.warning(self, self.tr("错误"), self.tr("请先输入需要合成的文本!")) 756 | return 757 | 758 | self.synthesis_pending = True 759 | self.synthesize_button.setEnabled(False) 760 | self.switch_models_and_synthesize() 761 | 762 | def execute_synthesis(self): 763 | self.synthesis_pending = False 764 | self.statusBar.showMessage(self.tr("合成中...")) 765 | 766 | params = self.get_current_parameters() 767 | api_url = self.api_url_input.text() 768 | 769 | self.tts_thread = TTSThread(api_url, params, self.cache_dir) 770 | self.tts_thread.finished.connect(self.synthesis_finished) 771 | self.tts_thread.error.connect(self.synthesis_error) 772 | self.tts_thread.start() 773 | 774 | def synthesis_finished(self, audio_file): 775 | self.current_audio_file = audio_file 776 | self.synthesize_button.setEnabled(True) 777 | self.statusBar.showMessage(self.tr("合成成功!")) 778 | self.play_button.setEnabled(True) 779 | self.save_button.setEnabled(True) 780 | 781 | def synthesis_error(self, error_message): 782 | self.synthesize_button.setEnabled(True) 783 | self.statusBar.showMessage(self.tr("合成失败!")) 784 | QMessageBox.critical(self, self.tr("合成语音失败"), error_message) 785 | 786 | def play_audio(self): 787 | if not self.current_audio_file: 788 | return 789 | 790 | if self.is_playing: 791 | # Stop playback 792 | if hasattr(self, 'stream') and self.stream: 793 | self.stream.stop() 794 | self.stream.close() 795 | delattr(self, 'stream') 796 | self.is_playing = False 797 | self.play_button.setText(self.tr("播放")) 798 | return 799 | 800 | try: 801 | data, samplerate = sf.read(self.current_audio_file) 802 | 803 | # Ensure data is 2D (samples, channels) 804 | if len(data.shape) == 1: 805 | data = data.reshape(-1, 1) 806 | 807 | # Define callback function 808 | def callback(outdata, frames, time, status): 809 | if status: 810 | print('Status:', status) 811 | # When playback is complete 812 | if len(data) <= self.play_position + frames: 813 | self.is_playing = False 814 | self.play_button.setText(self.tr("播放")) 815 | self.play_position = 0 816 | raise sd.CallbackStop() 817 | # Continue playback 818 | current_chunk = data[self.play_position:self.play_position + frames] 819 | # Ensure dimensions match 820 | if len(current_chunk.shape) == 1: 821 | current_chunk = current_chunk.reshape(-1, 1) 822 | outdata[:] = current_chunk 823 | self.play_position += frames 824 | 825 | # Reset playback position 826 | self.play_position = 0 827 | 828 | # Create and save output stream 829 | self.stream = sd.OutputStream( 830 | samplerate=samplerate, 831 | channels=data.shape[1] if len(data.shape) > 1 else 1, 832 | callback=callback 833 | ) 834 | 835 | # Start playback 836 | self.stream.start() 837 | self.is_playing = True 838 | self.play_button.setText(self.tr("停止")) 839 | 840 | except Exception as e: 841 | QMessageBox.critical(self, self.tr("错误"), self.tr("尝试播放时出错: {}").format(str(e))) 842 | self.is_playing = False 843 | self.play_button.setText(self.tr("播放")) 844 | if hasattr(self, 'stream') and self.stream: 845 | self.stream.stop() 846 | self.stream.close() 847 | delattr(self, 'stream') 848 | 849 | def __del__(self): 850 | # Clean up resources 851 | if hasattr(self, 'stream') and self.stream: 852 | self.stream.stop() 853 | self.stream.close() 854 | 855 | def save_audio(self): 856 | if not self.current_audio_file: 857 | return 858 | 859 | save_directory = self.save_path_input.text() 860 | 861 | if not save_directory: 862 | QMessageBox.warning( 863 | self, 864 | self.tr("警告"), 865 | self.tr("请先设置音频保存的路径。") 866 | ) 867 | return 868 | 869 | if not os.path.exists(save_directory): 870 | reply = QMessageBox.question(self, self.tr("目录不存在"), 871 | self.tr(" {} 不存在, 是否尝试新建该文件夹?").format(save_directory), 872 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No) 873 | if reply == QMessageBox.Yes: 874 | os.makedirs(save_directory) 875 | else: 876 | return 877 | 878 | try: 879 | filename = self.generate_filename() 880 | file_path = os.path.join(save_directory, filename) 881 | shutil.copy2(self.current_audio_file, file_path) 882 | 883 | self.statusBar.showMessage(self.tr("音频保存至 {}").format(file_path)) 884 | except Exception as e: 885 | QMessageBox.critical(self, self.tr("错误"), self.tr("保存音频时出错: {}").format(str(e))) 886 | 887 | def set_save_path(self): 888 | directory = QFileDialog.getExistingDirectory( 889 | self, 890 | self.tr("选择保存目录"), 891 | self.config_manager.get_value('save_directory', '') 892 | ) 893 | if directory: 894 | self.save_path_input.setText(directory) 895 | 896 | def generate_filename(self): 897 | # Get first 10 characters of text, remove special characters 898 | text = self.text_input.toPlainText().strip() 899 | clean_text = re.sub(r'[^\w\s]', '', text) 900 | clean_text = clean_text.replace(' ', '_') 901 | prefix = clean_text[:10] 902 | 903 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') 904 | 905 | return f"{prefix}_{timestamp}.wav" 906 | 907 | def load_preset(self): 908 | preset_name = self.preset_combo.currentText() 909 | presets = self.config_manager.get_value('presets', {}) 910 | preset_data = presets.get(preset_name, {}) 911 | 912 | if 'gpt_model' in preset_data and preset_data['gpt_model']: 913 | index = self.gpt_combo.findData(preset_data['gpt_model']) 914 | if index >= 0: 915 | self.gpt_combo.setCurrentIndex(index) 916 | 917 | if 'sovits_model' in preset_data and preset_data['sovits_model']: 918 | index = self.sovits_combo.findData(preset_data['sovits_model']) 919 | if index >= 0: 920 | self.sovits_combo.setCurrentIndex(index) 921 | 922 | for param, value in preset_data.items(): 923 | if param in self.param_widgets: 924 | widget = self.param_widgets[param] 925 | if isinstance(widget, QLineEdit): 926 | if isinstance(value, list): 927 | widget.setText(';'.join(value)) 928 | else: 929 | widget.setText(str(value)) 930 | elif isinstance(widget, QComboBox): 931 | index = widget.findData(value) 932 | if index >= 0: 933 | widget.setCurrentIndex(index) 934 | elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): 935 | widget.setValue(value) 936 | elif isinstance(widget, QCheckBox): 937 | widget.setChecked(value) 938 | 939 | def save_preset(self): 940 | preset_name, ok = QInputDialog.getText(self, self.tr("保存预设"), self.tr("输入预设名:")) 941 | if ok and preset_name: 942 | preset_data = { 943 | 'gpt_model': self.gpt_combo.currentData(), 944 | 'sovits_model': self.sovits_combo.currentData(), 945 | } 946 | 947 | for param, widget in self.param_widgets.items(): 948 | if isinstance(widget, QLineEdit): 949 | if param == 'aux_ref_audio_paths': 950 | value = widget.text().split(';') if widget.text() else [] 951 | else: 952 | value = widget.text() 953 | elif isinstance(widget, QComboBox): 954 | value = widget.currentData() 955 | elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): 956 | value = widget.value() 957 | elif isinstance(widget, QCheckBox): 958 | value = widget.isChecked() 959 | preset_data[param] = value 960 | 961 | update_data = { 962 | 'presets': { 963 | preset_name: preset_data 964 | } 965 | } 966 | self.config_manager.update_config(update_data) 967 | 968 | current_items = [self.preset_combo.itemText(i) for i in range(self.preset_combo.count())] 969 | if preset_name not in current_items: 970 | self.preset_combo.addItem(preset_name) 971 | self.preset_combo.setCurrentText(preset_name) 972 | 973 | QMessageBox.information(self, self.tr("成功"), 974 | self.tr("已成功将当前参数保存为 '{}' !").format(preset_name)) 975 | 976 | def delete_preset(self): 977 | current_preset = self.preset_combo.currentText() 978 | if current_preset == 'Default': 979 | QMessageBox.warning(self, self.tr('警告'), 980 | self.tr('默认预设不能被删除!')) 981 | return 982 | 983 | reply = QMessageBox.question(self, self.tr('删除预设'), 984 | self.tr('确认删除当前预设 "{}"?').format(current_preset), 985 | QMessageBox.Yes | QMessageBox.No, QMessageBox.No) 986 | 987 | if reply == QMessageBox.Yes: 988 | if self.config_manager.delete_value(f'presets.{current_preset}'): 989 | # 更新UI 990 | self.preset_combo.removeItem(self.preset_combo.currentIndex()) 991 | QMessageBox.information(self, self.tr("成功"), 992 | self.tr("预设 '{}' 已被成功删除!").format(current_preset)) 993 | else: 994 | QMessageBox.warning(self, self.tr("错误"), 995 | self.tr("删除预设 '{}' 失败!").format(current_preset)) 996 | 997 | def get_current_parameters(self): 998 | params = {} 999 | params['text'] = self.text_input.toPlainText() 1000 | 1001 | for param, widget in self.param_widgets.items(): 1002 | if isinstance(widget, QLineEdit): 1003 | if param == 'aux_ref_audio_paths': 1004 | value = widget.text().split(';') if widget.text() else [] 1005 | elif param == 'prompt_text' and self.param_widgets['no_prompt'].isChecked() and self.get_model_version('sovits') not in {'v3', 'v4'}: 1006 | value = "" 1007 | else: 1008 | value = widget.text() 1009 | elif isinstance(widget, QComboBox): 1010 | value = widget.currentData() # Get current item's data (key) 1011 | elif isinstance(widget, (QSpinBox, QDoubleSpinBox)): 1012 | value = widget.value() 1013 | elif isinstance(widget, QCheckBox): 1014 | if param == 'no_prompt': 1015 | continue 1016 | else: 1017 | value = widget.isChecked() 1018 | params[param] = value 1019 | 1020 | return params 1021 | 1022 | def setup_cache_directory(self): 1023 | self.cache_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cache') 1024 | if not os.path.exists(self.cache_dir): 1025 | os.makedirs(self.cache_dir) 1026 | else: 1027 | for file in os.listdir(self.cache_dir): 1028 | try: 1029 | file_path = os.path.join(self.cache_dir, file) 1030 | if os.path.isfile(file_path): 1031 | os.remove(file_path) 1032 | except Exception as e: 1033 | print(self.tr("清理缓存时出错 {}: {}").format(file, str(e))) 1034 | 1035 | def setup_background(self): 1036 | background_path = ":/GAG_tools/images/GAG_background.png" 1037 | style = f""" 1038 | QMainWindow {{ 1039 | background-image: url({background_path}); 1040 | }} 1041 | QWidget#centralWidget {{ 1042 | background-color: rgba(255, 255, 255, 180); 1043 | }} 1044 | QGroupBox {{ 1045 | background-color: rgba(255, 255, 255, 200); 1046 | }} 1047 | QGroupBox::title {{ 1048 | background-color: rgba(255, 255, 255, 200); 1049 | }} 1050 | """ 1051 | self.setStyleSheet(style) 1052 | 1053 | def setup_group_box_styles(self): 1054 | style = """ 1055 | QGroupBox { 1056 | background-color: rgba(255, 255, 255, 150); 1057 | } 1058 | QGroupBox::title { 1059 | background-color: rgba(255, 255, 255, 200); 1060 | } 1061 | QTextEdit{ 1062 | background-color: rgba(255, 255, 255, 150); 1063 | } 1064 | 1065 | """ 1066 | for widget in self.findChildren(QGroupBox): 1067 | widget.setStyleSheet(style) 1068 | 1069 | def cleanup(self): 1070 | # Save current configuration 1071 | updates = { 1072 | 'api_url': self.api_url_input.text(), 1073 | 'current_preset': self.preset_combo.currentText(), 1074 | 'save_directory': self.save_path_input.text() 1075 | } 1076 | self.config_manager.update_config(updates) 1077 | 1078 | # Clean up temporary files 1079 | if self.current_audio_file and os.path.exists(self.current_audio_file): 1080 | try: 1081 | os.remove(self.current_audio_file) 1082 | except: 1083 | pass 1084 | 1085 | # Stop API check thread if running 1086 | if self.api_check_thread: 1087 | self.api_check_thread.stop() 1088 | 1089 | def closeEvent(self, event): 1090 | self.cleanup() 1091 | super().closeEvent(event) 1092 | 1093 | 1094 | def main(): 1095 | app = QApplication(sys.argv) 1096 | gui = TTSGUI() 1097 | gui.show() 1098 | sys.exit(app.exec_()) 1099 | 1100 | 1101 | if __name__ == '__main__': 1102 | main() 1103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 AliceNavigator 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPT-SoVITS-Api-GUI 2 | 3 | GAG is an inference GUI that can be directly used with the official GPT-SoVITS integration package. Simply place it into the integration package and run it to enjoy a more convenient inference experience. 4 | 5 | ## Introduction 6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 | **注意: 如果你的系统语言为中文,那么界面语言应该会自动切换为中文** 14 | 15 | 16 | ## Usage 17 | 18 | You can download the latest release directly from [here](https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI/releases/latest). Place this exe in the GPT-SoVITS official integrated package directory and double-click to start. 19 | 20 | ## Installation 21 | 22 | 1. Install FFmpeg and ensure it's in your system PATH. 23 | 2. Clone this repository: 24 | ``` 25 | git clone https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI.git 26 | ``` 27 | 3. Install dependencies: 28 | ``` 29 | pip install -r requirements.txt 30 | ``` 31 | 4. Execute: 32 | ``` 33 | python gsv_api_gui.py 34 | ``` 35 | -------------------------------------------------------------------------------- /gsv_api_gui.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | from PyQt5.QtGui import QPixmap, QPainter, QColor, QIcon 5 | from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, 6 | QTabWidget, QLabel) 7 | from PyQt5.QtCore import Qt, QTranslator, QLocale 8 | import tempfile 9 | import qdarktheme 10 | from GAG_tools.tts_gui_tab import TTSGUI 11 | from GAG_tools.api_manager_tab import APIManager 12 | from GAG_tools.batch_tts_tab import BatchTTS 13 | import resources_rc 14 | 15 | 16 | def get_language(): 17 | locale = QLocale.system().name() 18 | if locale.startswith('zh'): 19 | return 'zh' # Chinese (Simplified and Traditional) 20 | else: 21 | return 'en' # English (default for all other languages) 22 | 23 | 24 | def get_base_path(): 25 | if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): 26 | # PyInstaller 27 | print('at PyInstaller') 28 | return sys._MEIPASS 29 | elif hasattr(get_base_path, '__compiled__'): 30 | # Nuitka 31 | print('at Nuitka') 32 | return os.path.dirname(os.path.abspath(__file__)) 33 | else: 34 | # dev 35 | print('at dev') 36 | return '' 37 | 38 | def get_translator_path(): 39 | language = get_language() 40 | base_path = get_base_path() 41 | 42 | possible_paths = [ 43 | os.path.join(base_path, "translations", f"GAG_{language}.qm"), 44 | os.path.join(base_path, f"GAG_{language}.qm"), 45 | ] 46 | 47 | for translations_path in possible_paths: 48 | print(f"Attempting to load translations from: {translations_path}") 49 | if os.path.exists(translations_path): 50 | print("translations found!") 51 | return translations_path 52 | 53 | print("Failed to load translator") 54 | 55 | def remove_screen_splash(): 56 | if "NUITKA_ONEFILE_PARENT" in os.environ: 57 | splash_filename = os.path.join( 58 | tempfile.gettempdir(), 59 | "onefile_%d_splash_feedback.tmp" % int(os.environ["NUITKA_ONEFILE_PARENT"]), 60 | ) 61 | 62 | if os.path.exists(splash_filename): 63 | os.unlink(splash_filename) 64 | 65 | 66 | class GSVApiGUI(QWidget): 67 | def __init__(self): 68 | super().__init__() 69 | self.initUI() 70 | self.setWindowIcon(QIcon(":/GAG_tools/images/GAG_icon.ico")) 71 | self.setStyleSheet(""" 72 | QPushButton { 73 | background-color: #42A5F5; 74 | min-height: 15px; 75 | color: white; 76 | } 77 | 78 | QPushButton:hover { 79 | background-color: #1E88E5; 80 | } 81 | 82 | QPushButton:pressed { 83 | background-color: #1565C0; 84 | } 85 | 86 | QPushButton:disabled { 87 | background-color: #BDBDBD; 88 | color: #757575; 89 | } 90 | 91 | QTabWidget::tab-bar { 92 | alignment: left; 93 | } 94 | 95 | QTabBar::tab { 96 | background-color: #D3DDE6; 97 | color: #424242; 98 | padding: 8px 16px; 99 | margin-right: 2px; 100 | border-top-left-radius: 4px; 101 | border-top-right-radius: 4px; 102 | } 103 | 104 | QTabBar::tab:selected { 105 | background-color: #2196F3; 106 | color: white; 107 | } 108 | 109 | QTabBar::tab:hover:!selected { 110 | background-color: #90CAF9; 111 | } 112 | 113 | QGroupBox {background-color: rgba(255, 255, 255, 220);font-weight: Normal;} 114 | QTextEdit{background-color: rgba(255, 255, 255, 220);} 115 | QHBoxLayout{background-color: rgba(255, 255, 255, 220);} 116 | QLineEdit{background-color: rgba(255, 255, 255, 220);} 117 | QListWidget{background-color: rgba(255, 255, 255, 220);} 118 | QHBoxLayout{background-color: rgba(255, 255, 255, 220);} 119 | QVBoxLayout{background-color: rgba(255, 255, 255, 220);} 120 | QTabWidget{background-color: rgba(255, 255, 255, 220);} 121 | """) 122 | 123 | def initUI(self): 124 | # Get the primary screen scaling_factor 125 | screen = QApplication.primaryScreen() 126 | dpi = screen.logicalDotsPerInch() 127 | scaling_factor = max(1.0, dpi / 96.0) # assuming 96 DPI as the base 128 | 129 | # Scale the window size 130 | base_width, base_height = 800, 800 131 | scaled_width = int(base_width * scaling_factor) 132 | scaled_height = int(base_height * scaling_factor) 133 | self.setWindowTitle(self.tr('GSV Api GUI v0.4.0 by 领航员未鸟')) 134 | self.setGeometry(100, 100, scaled_width, scaled_height) 135 | 136 | # Scale the font, in theory this is redundant but there are strange special cases 137 | font = QApplication.font() 138 | font.setPointSize(int(font.pointSize())) 139 | QApplication.setFont(font) 140 | 141 | main_layout = QVBoxLayout() 142 | 143 | # Create a tab 144 | self.tab_widget = QTabWidget() 145 | main_layout.addWidget(self.tab_widget) 146 | 147 | # Main function tab 148 | self.tts_gui_tab = TTSGUI() 149 | self.tab_widget.addTab(self.tts_gui_tab, self.tr("语音合成")) 150 | 151 | # API management tab 152 | self.api_manager_tab = APIManager() 153 | self.tab_widget.addTab(self.api_manager_tab, self.tr("API管理")) 154 | self.set_tab_background(self.api_manager_tab, ":/GAG_tools/images/GAG_background.png") 155 | 156 | # Batch TTS tab 157 | self.batch_tts_tab = BatchTTS() 158 | self.tab_widget.addTab(self.batch_tts_tab, self.tr("批量合成")) 159 | self.set_tab_background(self.batch_tts_tab, ":/GAG_tools/images/GAG_background.png") 160 | 161 | self.setLayout(main_layout) 162 | 163 | def set_tab_background(self, tab_widget, image_path): 164 | background = QPixmap(image_path) 165 | if background.isNull(): 166 | print(f"Failed to load background image: {image_path}") 167 | return 168 | 169 | overlay = QPixmap(background.size()) 170 | overlay.fill(QColor(255, 255, 255, 128)) 171 | 172 | painter = QPainter(background) 173 | painter.setCompositionMode(QPainter.CompositionMode_SourceOver) 174 | painter.drawPixmap(0, 0, overlay) 175 | painter.end() 176 | 177 | background_label = QLabel(tab_widget) 178 | background_label.setPixmap(background) 179 | background_label.setScaledContents(True) 180 | background_label.resize(tab_widget.size()) 181 | background_label.lower() 182 | 183 | background_label.setAttribute(Qt.WA_TransparentForMouseEvents) 184 | background_label.setStyleSheet("background-color: transparent;") 185 | 186 | background_label.lower() 187 | 188 | def closeEvent(self, event): 189 | if hasattr(self, 'tts_gui_tab'): 190 | self.tts_gui_tab.cleanup() 191 | if hasattr(self, 'batch_tts_tab'): 192 | self.batch_tts_tab.cleanup() 193 | if hasattr(self, 'api_manager_tab'): 194 | self.api_manager_tab.cleanup() 195 | super().closeEvent(event) 196 | 197 | if __name__ == '__main__': 198 | qdarktheme.enable_hi_dpi() 199 | app = QApplication(sys.argv) 200 | 201 | translator = QTranslator() 202 | if translator.load(get_translator_path()): 203 | app.installTranslator(translator) 204 | else: 205 | print('Use default') 206 | 207 | ex = GSVApiGUI() 208 | qdarktheme.setup_theme("light") 209 | remove_screen_splash() 210 | ex.show() 211 | sys.exit(app.exec_()) 212 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5 2 | pyqtdarktheme 3 | psutil 4 | requests 5 | sounddevice 6 | soundfile 7 | -------------------------------------------------------------------------------- /translations/GAG_en.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AliceNavigator/GPT-SoVITS-Api-GUI/c09416d4c9d900b2ec466439d6ef3013984926f6/translations/GAG_en.qm -------------------------------------------------------------------------------- /translations/GAG_en.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | APICheckThread 6 | 7 | 8 | API 就绪 9 | API Ready 10 | 11 | 12 | 13 | API 未就绪 14 | API Not Ready 15 | 16 | 17 | 18 | API 不可用: {} 19 | API Unavailable: {} 20 | 21 | 22 | 23 | APIManager 24 | 25 | 26 | API 配置 27 | API Configuration 28 | 29 | 30 | 31 | API URL: 32 | API URL: 33 | 34 | 35 | 36 | Python 环境: 37 | Python Environment: 38 | 39 | 40 | 41 | 浏览 42 | Browse 43 | 44 | 45 | 46 | 随软件启动API 47 | Start API with Application 48 | 49 | 50 | 51 | 启动 API 52 | Start API 53 | 54 | 55 | 56 | 停止 API 57 | Stop API 58 | 59 | 60 | 61 | 控制台输出 62 | Console Output 63 | 64 | 65 | 66 | 选择 Python Executable 67 | Select Python Executable 68 | 69 | 70 | 71 | 错误 72 | Error 73 | 74 | 75 | 76 | 指定的Python环境不存在. 77 | Specified Python environment does not exist. 78 | 79 | 80 | 81 | 尝试启动API时出错: 82 | Error when attempting to start API: 83 | 84 | 85 | 86 | 尝试停止API时出错: 87 | Error when attempting to stop API: 88 | 89 | 90 | 91 | BatchTTS 92 | 93 | 94 | API URL: 95 | API URL: 96 | 97 | 98 | 99 | 预设: 100 | Preset: 101 | 102 | 103 | 104 | 输出格式: 105 | Output Format: 106 | 107 | 108 | 109 | 清空列表 110 | Clear 111 | 112 | 113 | 114 | ▲‖‖‖ 115 | ▲‖‖‖ 116 | 117 | 118 | 119 | ‖‖‖▼ 120 | ‖‖‖▼ 121 | 122 | 123 | 124 | 删除选中 125 | Remove 126 | 127 | 128 | 129 | 在这里设置批量合成的保存路径... 130 | Set batch synthesis save path here... 131 | 132 | 133 | 134 | 浏览 135 | Browse 136 | 137 | 138 | 139 | 打开 140 | Open 141 | 142 | 143 | 144 | 批量合成保存路径: 145 | Batch Synthesis Save Path: 146 | 147 | 148 | 149 | 开始合成 150 | Start Synthesis 151 | 152 | 153 | 154 | 警告 155 | Warning 156 | 157 | 158 | 159 | 输出文件夹无效 160 | Invalid output folder 161 | 162 | 163 | 164 | 已完成 165 | Completed 166 | 167 | 168 | 169 | 已暂停 170 | Paused 171 | 172 | 173 | 174 | 待合成 175 | Pending 176 | 177 | 178 | 179 | 清空列表时出错: {} 180 | Error clearing list: {} 181 | 182 | 183 | 184 | 删除文件 {} 时出错: {} 185 | Error deleting file {}: {} 186 | 187 | 188 | 189 | 选择批量合成保存路径 190 | Select Batch Synthesis Save Path 191 | 192 | 193 | 194 | 请先添加需要合成的文件! 195 | Please add files for synthesis first! 196 | 197 | 198 | 199 | 准备合成 200 | Preparing Synthesis 201 | 202 | 203 | 204 | 停止合成 205 | Stop Synthesis 206 | 207 | 208 | 209 | 错误 210 | Error 211 | 212 | 213 | 214 | 处理文件时出错: {}: {} 215 | Error processing file {}: {} 216 | 217 | 218 | 219 | 无法检测文件编码。该文件可能不是文本文件或编码未知: {} 220 | Unable to detect file encoding. The file may not be text or has unknown encoding: {} 221 | 222 | 223 | 224 | 正在准备文本... 225 | Preparing text... 226 | 227 | 228 | 229 | 正在合成 230 | Synthesizing 231 | 232 | 233 | 234 | 合成片段 {} 失败,请检查API状态 235 | Failed to synthesize segment {}. Please check API status 236 | 237 | 238 | 239 | 正在压制音频 240 | Compressing Audio 241 | 242 | 243 | 244 | 失败 245 | Failed 246 | 247 | 248 | 249 | 处理文件时出错: {} 250 | Error processing file: {} 251 | 252 | 253 | 254 | API错误: {} 255 | API Error: {} 256 | 257 | 258 | 259 | 合成尝试失败: {} 260 | Synthesis attempt failed: {} 261 | 262 | 263 | 264 | GSVApiGUI 265 | 266 | 267 | GSV Api GUI v0.1.0 by 领航员未鸟 268 | GSV Api GUI v0.1.0 by 领航员未鸟 269 | 270 | 271 | 272 | 语音合成 273 | TTS 274 | 275 | 276 | 277 | API管理 278 | API Management 279 | 280 | 281 | 282 | 批量合成 283 | Batch TTS 284 | 285 | 286 | 287 | GSV Api GUI v0.3.1 by 领航员未鸟 288 | GSV Api GUI v0.3.1 by 领航员未鸟 289 | 290 | 291 | 292 | GSV Api GUI v0.4.0 by 领航员未鸟 293 | GSV Api GUI v0.4.0 by 领航员未鸟 294 | 295 | 296 | 297 | ModelSwitchThread 298 | 299 | 300 | {} 模型切换成功 301 | {} Model Switch Successful 302 | 303 | 304 | 305 | {} 模型切换失败: 306 | {} 307 | {} Model Switch Failed:\n {} 308 | 309 | 310 | 311 | {} 模型切换时发生错误: 312 | {} 313 | Error Occurred During {} Model Switch:\n {} 314 | 315 | 316 | 317 | TTSGUI 318 | 319 | 320 | 不切 321 | No Split 322 | 323 | 324 | 325 | 凑四句一切 326 | Split Every Four Sentences 327 | 328 | 329 | 330 | 凑50字一切 331 | Split Every 50 Characters 332 | 333 | 334 | 335 | 按中文句号。切 336 | Split by Chinese Period 。 337 | 338 | 339 | 340 | 按英文句号.切 341 | Split by English Period . 342 | 343 | 344 | 345 | 按标点符号切 346 | Split by Punctuation 347 | 348 | 349 | 350 | 中文 351 | Chinese 352 | 353 | 354 | 355 | 英文 356 | English 357 | 358 | 359 | 360 | 日文 361 | Japanese 362 | 363 | 364 | 365 | 粤语 366 | Cantonese 367 | 368 | 369 | 370 | 韩文 371 | Korean 372 | 373 | 374 | 375 | 中英混合 376 | Chinese-English Mixed 377 | 378 | 379 | 380 | 日英混合 381 | Japanese-English Mixed 382 | 383 | 384 | 385 | 粤英混合 386 | Cantonese-English Mixed 387 | 388 | 389 | 390 | 韩英混合 391 | Korean-English Mixed 392 | 393 | 394 | 395 | 多语种混合 396 | Multi-Language Mixed 397 | 398 | 399 | 400 | 多语种混合(粤语) 401 | Multi-Language Mixed (Cantonese) 402 | 403 | 404 | 405 | TTS GUI 406 | TTS GUI 407 | 408 | 409 | 410 | API 设置 411 | API Settings 412 | 413 | 414 | 415 | 状态: API 未就绪 416 | Status: API Not Ready 417 | 418 | 419 | 420 | 检查 421 | Check 422 | 423 | 424 | 425 | API URL: 426 | API URL: 427 | 428 | 429 | 430 | 待合成文本: 431 | Text to Synthesize: 432 | 433 | 434 | 435 | 在这里输入需要合成的文本... 436 | 437 | 使用方法: 438 | 1.将本exe放入GPT-SoVITS官方整合包下,双击启动。 439 | 2.将读取并使用GPT_weights_v2与SoVITS_weights_v2下的模型,请先完成训练获得模型。 440 | 3.保存预设将保存当前所有合成参数设定,可视为一个说话人,后续可快速切换,亦可用于批量合成页面 441 | 4.默认使用整合包自带环境来调起并使用API,也可以在API管理页面自定义。 442 | 443 | 此外,如果你使用官方v2-240821整合包,你可能会遇见粤语等语种合成空白音频的问题,这是GPT-SoVITS的一个已知并已解决的问题,更新整合包代码到仓库最新即可。 444 | 445 | GitHub开源地址: https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI by 领航员未鸟 446 | 447 | Enter text to synthesize here... 448 | 449 | Usage: 450 | 1. Place this exe in the GPT-SoVITS official integrated package directory and double-click to start. 451 | 2. It will read and use models from GPT_weights_v2 and SoVITS_weights_v2 folders. Please complete training to obtain models first. 452 | 3. Saving presets will store all current synthesis parameter settings, which can be treated as a speaker profile for quick switching and batch synthesis. 453 | 4. By default, it uses the integrated package's built-in environment to call and use the API, but you can customize this in the API Management page. 454 | 455 | Additionally, if you're using the official v2-240821 package, you may encounter issues with blank audio synthesis for languages like Cantonese. This is a known and resolved issue in GPT-SoVITS - update the package code to the latest repository version to fix it. 456 | 457 | GitHub: https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI by 领航员未鸟 458 | 459 | 460 | 461 | 462 | 控制 463 | Control 464 | 465 | 466 | 467 | 将会把合成的音频保存在该路径下... 468 | Synthesized audio will be saved to this path... 469 | 470 | 471 | 472 | 浏览 473 | Browse 474 | 475 | 476 | 477 | 打开 478 | Open 479 | 480 | 481 | 482 | 保存路径: 483 | Save Path: 484 | 485 | 486 | 487 | 开始合成 488 | Start Synthesis 489 | 490 | 491 | 492 | 播放 493 | Play 494 | 495 | 496 | 497 | 保存音频 498 | Save Audio 499 | 500 | 501 | 502 | 预设 503 | Preset 504 | 505 | 506 | 507 | 保存 508 | Save 509 | 510 | 511 | 512 | 删除 513 | Delete 514 | 515 | 516 | 517 | 模型选择 518 | Model Selection 519 | 520 | 521 | 522 | GPT 模型: 523 | GPT Model: 524 | 525 | 526 | 527 | SoVITS 模型: 528 | SoVITS Model: 529 | 530 | 531 | 532 | 语言与文本设置 533 | Language and Text Settings 534 | 535 | 536 | 537 | 合成文本语种: 538 | Text Language: 539 | 540 | 541 | 542 | 参考音频语种: 543 | Reference Audio Language: 544 | 545 | 546 | 547 | 文本分割方式: 548 | Text Split Method: 549 | 550 | 551 | 552 | 3-10秒的参考音频,超过会报错 553 | 3-10 second reference audio, longer will cause error 554 | 555 | 556 | 557 | 参考音频: 558 | Reference Audio: 559 | 560 | 561 | 562 | 不填则视为使用无参考文本模式 563 | Empty means using no-reference text mode 564 | 565 | 566 | 567 | 无参 568 | No Reference 569 | 570 | 571 | 572 | 参考音频文本: 573 | Reference Audio Text: 574 | 575 | 576 | 577 | (可选)以额外的多个音频平均融合音色 578 | (Optional) Average multiple audio files to blend voice characteristics 579 | 580 | 581 | 582 | 辅助参考音频: 583 | Auxiliary Reference Audio: 584 | 585 | 586 | 587 | 合成参数 588 | Synthesis Parameters 589 | 590 | 591 | 592 | Top K: 593 | Top K: 594 | 595 | 596 | 597 | Top P: 598 | Top P: 599 | 600 | 601 | 602 | Temperature: 603 | Temperature: 604 | 605 | 606 | 607 | 语速: 608 | Speech Factor: 609 | 610 | 611 | 612 | 重复惩罚: 613 | Repetition Penalty: 614 | 615 | 616 | 617 | 种子: 618 | Seed: 619 | 620 | 621 | 622 | 并行推理 623 | Parallel Infer 624 | 625 | 626 | 627 | 数据分桶 628 | Split Bucket 629 | 630 | 631 | 632 | 返回片段 633 | Return Fragment 634 | 635 | 636 | 637 | 流式模式 638 | Streaming Mode 639 | 640 | 641 | 642 | 批次大小: 643 | Batch Size: 644 | 645 | 646 | 647 | 分批阈值: 648 | Batch Threshold: 649 | 650 | 651 | 652 | CPU 占用: 653 | CPU Usage : 654 | 655 | 656 | 657 | 内存占用: 658 | Memory Usage: 659 | 660 | 661 | 662 | 错误 663 | Error 664 | 665 | 666 | 667 | 指定的保存文件夹不存在 668 | Specified save folder does not exist 669 | 670 | 671 | 672 | 选择音频文件 673 | Select Audio File 674 | 675 | 676 | 677 | Audio Files (*.wav) 678 | Audio Files (*.wav) 679 | 680 | 681 | 682 | 状态: 检查中... 683 | Status: Checking... 684 | 685 | 686 | 687 | 状态: API 就绪 688 | Status: API Ready 689 | 690 | 691 | 692 | API 错误 693 | API Error 694 | 695 | 696 | 697 | {:.2f} GB / {:.2f} GB 698 | {:.2f} GB / {:.2f} GB 699 | 700 | 701 | 702 | 未检测到模型变更 703 | No model changes detected 704 | 705 | 706 | 707 | 模型切换时出错 708 | Error during model switch 709 | 710 | 711 | 712 | 请先输入需要合成的文本! 713 | Please enter text for synthesis first! 714 | 715 | 716 | 717 | 合成中... 718 | Synthesizing... 719 | 720 | 721 | 722 | 合成成功! 723 | Synthesis Successful! 724 | 725 | 726 | 727 | 合成失败! 728 | Synthesis Failed! 729 | 730 | 731 | 732 | 合成语音失败 733 | Synthesis Failed 734 | 735 | 736 | 737 | 停止 738 | Stop 739 | 740 | 741 | 742 | 尝试播放时出错: {} 743 | Error while attempting playback: {} 744 | 745 | 746 | 747 | 警告 748 | Warning 749 | 750 | 751 | 752 | 请先设置音频保存的路径。 753 | Please set the audio save path first. 754 | 755 | 756 | 757 | 目录不存在 758 | Directory does not exist 759 | 760 | 761 | 762 | {} 不存在, 是否尝试新建该文件夹? 763 | {} does not exist. Attempt to create new folder? 764 | 765 | 766 | 767 | 音频保存至 {} 768 | Audio saved to {} 769 | 770 | 771 | 772 | 保存音频时出错: {} 773 | Error saving audio: {} 774 | 775 | 776 | 777 | 选择保存目录 778 | Select Save Directory 779 | 780 | 781 | 782 | 保存预设 783 | Save Preset 784 | 785 | 786 | 787 | 输入预设名: 788 | Enter Preset Name: 789 | 790 | 791 | 792 | 成功 793 | Success 794 | 795 | 796 | 797 | 已成功将当前参数保存为 '{}' ! 798 | Current parameters successfully saved as '{}'! 799 | 800 | 801 | 802 | 默认预设不能被删除! 803 | Default preset cannot be deleted! 804 | 805 | 806 | 807 | 删除预设 808 | Delete Preset 809 | 810 | 811 | 812 | 确认删除当前预设 "{}"? 813 | Confirm deletion of current preset "{}"? 814 | 815 | 816 | 817 | 预设 '{}' 已被成功删除! 818 | Preset '{}' successfully deleted! 819 | 820 | 821 | 822 | 删除预设 '{}' 失败! 823 | Failed to delete preset '{}'! 824 | 825 | 826 | 827 | 清理缓存时出错 {}: {} 828 | Error clearing cache {}: {} 829 | 830 | 831 | 832 | 在这里输入需要合成的文本... 833 | 834 | 使用方法: 835 | 1.将本exe放入GPT-SoVITS-v3lora-20250401或更新的官方整合包下,双击启动,支持v1,v2,v3模型。 836 | 2.将读取并使用GPT_weights,_v2,_v3与SoVITS_weights,_v2,_v3下的模型,请先完成训练获得模型。 837 | 3.保存预设将保存当前所有合成参数设定,可视为一个说话人,后续可快速切换,亦可用于批量合成页面。 838 | 4.默认使用整合包自带环境来调起并使用API,也可以在API管理页面自定义。 839 | 840 | 此外,若无可用N卡并使用官方整合包,请在初次启动前修改GPT_SoVITS/configs/tts_infer.yaml中的device为cpu, is_half为false 以避免API启动失败。 841 | 842 | GitHub开源地址: https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI by 领航员未鸟 843 | 844 | Enter text to synthesize here... 845 | 846 | Usage: 847 | 1. Place this exe in the v3lora-20250401 official integrated package(or a newer version) directory and double-click to start. 848 | 2. It will load models from GPT_weights,v2,v3 and SoVITS_weights,v2,v3 folders. Please complete training to obtain models first. 849 | 3. Saving presets will store all current synthesis parameter settings, which can be treated as a speaker profile for quick switching and batch synthesis. 850 | 4. By default, it uses the integrated package's built-in environment to call and use the API, but you can customize this in the API Management page. 851 | 852 | Additionally, if no NVIDIA GPU is available and the official integrated package is used, modify the device to "cpu" and set is_half to "false" in GPT_SoVITS/configs/tts_infer.yaml before the initial launch to prevent API startup failures. 853 | 854 | GitHub: https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI by 领航员未鸟 855 | 856 | 857 | 858 | 音频超分 859 | Super Sampling 860 | 861 | 862 | 863 | 采样步数: 864 | Sample Steps: 865 | 866 | 867 | 868 | 在这里输入需要合成的文本... 869 | 870 | 使用方法: 871 | 1.将本exe放入GPT-SoVITS-v4-20250422fix或更新的官方整合包下,双击启动,支持v1,v2,v3, v4模型。 872 | 2.将读取并使用GPT_weights,_v2,_v3, _v4与SoVITS_weights,_v2,_v3, _v4下的模型,请先完成训练获得模型。 873 | 3.保存预设将保存当前所有合成参数设定,可视为一个说话人,后续可快速切换,亦可用于批量合成页面。 874 | 4.默认使用整合包自带环境来调起并使用API,也可以在API管理页面自定义。 875 | 876 | 此外,若无可用N卡并使用官方整合包,请在初次启动前修改GPT_SoVITS/configs/tts_infer.yaml中的device为cpu, is_half为false 以避免API启动失败。 877 | 878 | GitHub开源地址: https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI by 领航员未鸟 879 | 880 | Enter text to synthesize here... 881 | 882 | Usage: 883 | 1. Place this exe in the v4-20250422fix official integrated package(or a newer version) directory and double-click to start. 884 | 2. Loads models from GPT_weights,v2/v3/v4 and SoVITS_weights,v2/v3/v4 folders. Please complete training to obtain models first. 885 | 3. Saving presets will store all current synthesis settings, which can be treated as a speaker profile for quick switching and batch synthesis. 886 | 4. By default, it uses the integrated package's built-in environment to call and use the API, but you can customize this in the API Management page. 887 | 888 | Additionally, if no NVIDIA GPU is available and the official integrated package is used, modify the device to "cpu" and set is_half to "false" in GPT_SoVITS/configs/tts_infer.yaml before the initial launch to prevent API startup failures. 889 | 890 | GitHub: https://github.com/AliceNavigator/GPT-SoVITS-Api-GUI by 领航员未鸟 891 | 892 | 893 | 894 | TTSThread 895 | 896 | 897 | 执行推理时出现错误: 898 | {} 899 | Error during inference:\n {} 900 | 901 | 902 | 903 | --------------------------------------------------------------------------------