Spaces:
Sleeping
Sleeping
File size: 8,509 Bytes
c441d2c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | import os
import re
from enum import Enum
from datetime import datetime
import socket
from typing import List
from PySide6.QtWidgets import (
QWidget, QHBoxLayout, QPushButton, QLineEdit, QFileDialog, QMessageBox, QComboBox, QTextEdit
)
from PySide6.QtCore import (Qt, Signal, QSettings, Property, QObject, QEvent, QMimeData)
from PySide6.QtGui import QFont
def sanitize_filename(filename: str, replacement: str = '') -> str:
"""
将文本清理为合法的 Windows 文件名。
Args:
filename (str): 原始文件名。
replacement (str): 非法字符的替换字符,默认为空,建议使用 "_"。
Returns:
str: 清理后的文件名。
"""
# 1. 去除非法字符
# \/:*?"<>| 是标准非法字符
# \x00-\x1f 是控制字符 (如换行符、制表符等),Windows 也不允许
cleaned = re.sub(r'[\\/:*?"<>|\x00-\x1f]', replacement, filename)
# 2. 去除首尾的空格和点
# Windows 文件名不能以空格或点结尾,也不能以空格开头(虽然允许但通常不推荐)
cleaned = cleaned.strip().rstrip('.')
# 3. 处理 Windows 保留文件名 (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
# 这些名字不论加什么扩展名都是非法的 (例如 con.txt 也是非法的)
reserved_names = {
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
}
# 如果文件名(全大写)是保留字,或者文件名是保留字+扩展名(如 con.txt),则加下划线前缀
filename_upper = cleaned.upper()
file_stem = filename_upper.split('.')[0] # 获取不带后缀的主文件名
if filename_upper in reserved_names or file_stem in reserved_names:
cleaned = "_" + cleaned
# 4. 处理空文件名 (如果输入全是乱码被删光了)
if not cleaned:
cleaned = "unnamed_file"
# 5. 限制长度 (Windows API 通常限制 255 字符,但在某些路径下更短)
cleaned = truncate_text(cleaned)
return cleaned
def is_port_free(port: int) -> bool:
"""返回端口是否可用"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(('', port))
except OSError:
return False
return True
def find_free_port(preferred: int = 8000) -> int:
# 先尝试 preferred 端口
if is_port_free(preferred):
return preferred
# 否则用系统自动分配
s = socket.socket()
s.bind(('', 0))
port = s.getsockname()[1]
s.close()
return port
def truncate_text(text: str, max_len: int = 30) -> str:
"""
按指定宽度截断文本
"""
result = ""
cur_len = 0
for ch in text:
char_len = 2 if ('\u4e00' <= ch <= '\u9fff') else 1
if cur_len + char_len > max_len:
break
result += ch
cur_len += char_len
return result
def generate_output_filenames(folder: str, original_texts: List[str]) -> List[str]:
"""
批量生成文件名:
输入 original_texts 列表
输出等长 filenames 列表
"""
today = datetime.now().strftime("%Y%m%d")
pattern = re.compile(rf'^\[{today}]\[(\d{{3}})]')
# ① 查找当天现存最大编号
max_n = 0
if os.path.isdir(folder):
for name in os.listdir(folder):
m = pattern.match(name)
if m:
n = int(m.group(1))
max_n = max(max_n, n)
filenames = []
cur_n = max_n
# ② 依次生成新文件名
for text in original_texts:
cur_n += 1
n_str = f"{cur_n:03d}"
cleaned = sanitize_filename(text)
filename = f"[{today}][{n_str}]{cleaned}.wav"
filenames.append(filename)
return filenames
# ==================== 通用组件 ====================
class FileSelectionMode(Enum):
FILE = 0
DIRECTORY = 1
class FileSelectorWidget(QWidget):
"""一个包含行编辑和浏览按钮的复合控件,支持文件和文件夹选择。(原样引用并稍作适配)"""
pathChanged = Signal(str)
def __init__(
self,
setting_key: str,
selection_mode: FileSelectionMode = FileSelectionMode.DIRECTORY,
file_filter: str = "All Files (*)",
parent: QWidget = None,
):
super().__init__(parent)
self.setting_key: str = setting_key
self.selection_mode: FileSelectionMode = selection_mode
self.file_filter: str = file_filter
# 使用 QSettings 模拟 STUDIO_SETTINGS
self.settings = QSettings("MyTTS", "GUI")
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(5)
self.path_edit: QLineEdit = QLineEdit()
self.path_edit.setReadOnly(True)
self.path_edit.setPlaceholderText("未选择路径")
self.browse_button: QPushButton = QPushButton("📁")
self.browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
default_font = QFont()
default_font.setPointSize(10)
self.browse_button.setFont(default_font)
self.browse_button.setFixedSize(30, 30)
self.clear_button: QPushButton = QPushButton("❌")
self.clear_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.clear_button.setFont(default_font)
self.clear_button.setFixedSize(30, 30)
layout.addWidget(self.path_edit)
layout.addWidget(self.browse_button)
layout.addWidget(self.clear_button)
self.browse_button.clicked.connect(self._open_dialog)
self.clear_button.clicked.connect(self._clear_path)
self.path_edit.textChanged.connect(self.pathChanged)
def _open_dialog(self):
path = self.path_edit.text()
if path and os.path.exists(path):
start_path = path
else:
# 【修改点】默认路径改为 Desktop
desktop_path = os.path.join(os.path.expanduser("~"), "Desktop")
start_path = str(self.settings.value(
f"last_path_{self.setting_key}", defaultValue=desktop_path
))
if self.selection_mode == FileSelectionMode.DIRECTORY:
selected_path = QFileDialog.getExistingDirectory(
self, "选择文件夹", start_path
)
else:
selected_path, _ = QFileDialog.getOpenFileName(
self, "选择文件", start_path, self.file_filter
)
if selected_path:
self.set_path(selected_path)
parent_path = os.path.dirname(selected_path)
if parent_path:
self.settings.setValue(
f"last_path_{self.setting_key}", parent_path)
def _clear_path(self):
if not self.path_edit.text():
return
reply = QMessageBox.question(self, '确认', '您确定要清空路径吗?',
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
self.set_path("")
def get_path(self) -> str:
text = self.path_edit.text()
# 即使路径不存在(可能是输入时),也返回文本供逻辑判断,或者严格校验
return text
def set_path(self, path: str, block_signals: bool = False):
if block_signals:
self.path_edit.blockSignals(True)
self.path_edit.setText(path)
if block_signals:
self.path_edit.blockSignals(False)
path = Property(str, fget=get_path, fset=set_path, notify=pathChanged) # type: ignore
class WheelEventFilter(QObject):
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.Wheel and isinstance(obj, QComboBox):
return True # 阻止默认滚轮行为
return super().eventFilter(obj, event)
class MyComboBox(QComboBox):
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self._wheelFilter = WheelEventFilter()
self.installEventFilter(self._wheelFilter)
class MyTextEdit(QTextEdit):
def insertFromMimeData(self, source: QMimeData) -> None:
# 仅取纯文本
if source.hasText():
self.insertPlainText(source.text())
else:
super().insertFromMimeData(source)
|