antigravity
sync all fixes: prompt leakage, cross-lang, ref_cache update, and file wait logic
c441d2c
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)