386 lines
15 KiB
Python
386 lines
15 KiB
Python
from PySide6.QtWidgets import (
|
||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton,
|
||
QLineEdit, QLabel, QGroupBox, QScrollArea, QToolButton, QStatusBar,
|
||
QFileDialog, QMessageBox, QListWidget, QListWidgetItem, QFrame, QSplitter
|
||
)
|
||
from PySide6.QtCore import Qt, QTimer, QSize
|
||
from PySide6.QtGui import QFont, QPalette, QColor
|
||
from utils.word_handler import WordHandler
|
||
from utils.system_monitor import SystemMonitor
|
||
from translator import Translator
|
||
from utils.logger import logger
|
||
import os
|
||
|
||
class MainWindow(QMainWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setWindowTitle("PrivaTrans")
|
||
self.setMinimumSize(800, 600)
|
||
|
||
# 初始化组件
|
||
self.translator = Translator()
|
||
self.system_monitor = SystemMonitor()
|
||
self.word_handler = WordHandler()
|
||
|
||
# 默认模型路径
|
||
self.default_model_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "models", "HY-MT1.5-1.8B_bf16_Q4_K_M.gguf")
|
||
|
||
# 术语列表
|
||
self.terms = []
|
||
|
||
# 初始化UI
|
||
self.init_ui()
|
||
|
||
# 初始化状态栏定时器
|
||
self.init_status_bar_timer()
|
||
|
||
# 加载默认模型
|
||
self.load_default_model()
|
||
|
||
def init_ui(self):
|
||
"""初始化UI界面"""
|
||
# 主布局
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
main_layout = QVBoxLayout(central_widget)
|
||
main_layout.setSpacing(20)
|
||
main_layout.setContentsMargins(20, 20, 20, 10)
|
||
|
||
# 顶部标题
|
||
title_label = QLabel("PrivaTrans")
|
||
title_font = QFont()
|
||
title_font.setPointSize(24)
|
||
title_font.setBold(True)
|
||
title_label.setFont(title_font)
|
||
title_label.setStyleSheet("color: #1a365d;")
|
||
|
||
# 模型信息和更换按钮
|
||
model_layout = QHBoxLayout()
|
||
|
||
self.model_status_label = QLabel()
|
||
self.model_status_label.setStyleSheet("color: #2d3748;")
|
||
|
||
self.model_info_label = QLabel()
|
||
self.model_info_label.setStyleSheet("color: #2d3748; font-weight: bold;")
|
||
|
||
self.change_model_btn = QPushButton("更换")
|
||
self.change_model_btn.setFixedSize(80, 32)
|
||
self.change_model_btn.setStyleSheet(
|
||
"background-color: #e2e8f0; color: #2d3748; border: none; border-radius: 6px;"
|
||
)
|
||
self.change_model_btn.clicked.connect(self.change_model)
|
||
|
||
model_layout.addWidget(self.model_status_label)
|
||
model_layout.addWidget(self.model_info_label)
|
||
model_layout.addStretch()
|
||
model_layout.addWidget(self.change_model_btn)
|
||
|
||
# 高级辅助面板(可折叠)
|
||
self.advanced_group = QGroupBox("高级辅助 (背景与术语)")
|
||
self.advanced_group.setCheckable(True)
|
||
self.advanced_group.setChecked(False)
|
||
self.advanced_group.setStyleSheet(
|
||
"QGroupBox { border: 1px solid #e2e8f0; border-radius: 8px; margin-top: 10px; }"
|
||
"QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; }"
|
||
)
|
||
|
||
advanced_layout = QVBoxLayout(self.advanced_group)
|
||
advanced_layout.setSpacing(15)
|
||
advanced_layout.setContentsMargins(15, 15, 15, 15)
|
||
|
||
# 文本背景/场景介绍
|
||
context_label = QLabel("文本背景 / 场景介绍")
|
||
context_label.setStyleSheet("color: #4a5568; font-weight: bold;")
|
||
|
||
self.context_edit = QTextEdit()
|
||
self.context_edit.setPlaceholderText("例如:这是一份关于建筑工程的合同...")
|
||
self.context_edit.setFixedHeight(80)
|
||
self.context_edit.setStyleSheet(
|
||
"border: 1px solid #e2e8f0; border-radius: 6px; padding: 8px;"
|
||
)
|
||
|
||
# 术语定义
|
||
terms_label = QLabel("术语簿 (定义 A=B)")
|
||
terms_label.setStyleSheet("color: #4a5568; font-weight: bold;")
|
||
|
||
self.terms_list = QListWidget()
|
||
self.terms_list.setStyleSheet(
|
||
"border: 1px solid #e2e8f0; border-radius: 6px;"
|
||
)
|
||
|
||
terms_input_layout = QHBoxLayout()
|
||
self.term_input = QLineEdit()
|
||
self.term_input.setPlaceholderText("你好 = what's up")
|
||
self.term_input.setStyleSheet(
|
||
"border: 1px solid #e2e8f0; border-radius: 6px; padding: 8px; flex: 1;"
|
||
)
|
||
|
||
add_term_btn = QPushButton("+")
|
||
add_term_btn.setFixedSize(32, 32)
|
||
add_term_btn.setStyleSheet(
|
||
"background-color: #3182ce; color: white; border: none; border-radius: 6px;"
|
||
)
|
||
add_term_btn.clicked.connect(self.add_term)
|
||
|
||
terms_input_layout.addWidget(self.term_input)
|
||
terms_input_layout.addWidget(add_term_btn)
|
||
|
||
# 术语列表项右键菜单
|
||
self.terms_list.setContextMenuPolicy(Qt.CustomContextMenu)
|
||
self.terms_list.customContextMenuRequested.connect(self.show_term_context_menu)
|
||
|
||
advanced_layout.addWidget(context_label)
|
||
advanced_layout.addWidget(self.context_edit)
|
||
advanced_layout.addWidget(terms_label)
|
||
advanced_layout.addWidget(self.terms_list)
|
||
advanced_layout.addLayout(terms_input_layout)
|
||
|
||
# 原文输入区域
|
||
input_layout = QVBoxLayout()
|
||
|
||
input_header_layout = QHBoxLayout()
|
||
input_label = QLabel("原文内容")
|
||
input_label.setStyleSheet("color: #4a5568; font-weight: bold;")
|
||
|
||
import_word_btn = QPushButton("导入 Word")
|
||
import_word_btn.setFixedSize(120, 32)
|
||
import_word_btn.setStyleSheet(
|
||
"background-color: #e2e8f0; color: #2d3748; border: none; border-radius: 6px;"
|
||
)
|
||
import_word_btn.clicked.connect(self.import_word)
|
||
|
||
input_header_layout.addWidget(input_label)
|
||
input_header_layout.addStretch()
|
||
input_header_layout.addWidget(import_word_btn)
|
||
|
||
self.source_text = QTextEdit()
|
||
self.source_text.setStyleSheet(
|
||
"border: 1px solid #e2e8f0; border-radius: 6px; padding: 10px;"
|
||
)
|
||
|
||
input_layout.addLayout(input_header_layout)
|
||
input_layout.addWidget(self.source_text)
|
||
|
||
# 翻译按钮
|
||
self.translate_btn = QPushButton("开始翻译")
|
||
self.translate_btn.setFixedHeight(50)
|
||
self.translate_btn.setStyleSheet(
|
||
"background-color: #3182ce; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold;"
|
||
)
|
||
self.translate_btn.clicked.connect(self.start_translation)
|
||
|
||
# 译文结果区域
|
||
output_layout = QVBoxLayout()
|
||
|
||
output_header_layout = QHBoxLayout()
|
||
output_label = QLabel("译文结果")
|
||
output_label.setStyleSheet("color: #4a5568; font-weight: bold;")
|
||
|
||
output_buttons_layout = QHBoxLayout()
|
||
export_word_btn = QPushButton("导出 Word")
|
||
export_word_btn.setFixedSize(120, 32)
|
||
export_word_btn.setStyleSheet(
|
||
"background-color: #e2e8f0; color: #2d3748; border: none; border-radius: 6px; margin-right: 10px;"
|
||
)
|
||
export_word_btn.clicked.connect(self.export_word)
|
||
|
||
copy_btn = QPushButton("复制内容")
|
||
copy_btn.setFixedSize(120, 32)
|
||
copy_btn.setStyleSheet(
|
||
"background-color: #e2e8f0; color: #2d3748; border: none; border-radius: 6px;"
|
||
)
|
||
copy_btn.clicked.connect(self.copy_result)
|
||
|
||
output_buttons_layout.addWidget(export_word_btn)
|
||
output_buttons_layout.addWidget(copy_btn)
|
||
|
||
output_header_layout.addWidget(output_label)
|
||
output_header_layout.addStretch()
|
||
output_header_layout.addLayout(output_buttons_layout)
|
||
|
||
self.result_text = QTextEdit()
|
||
self.result_text.setReadOnly(True)
|
||
self.result_text.setStyleSheet(
|
||
"border: 1px solid #e2e8f0; border-radius: 6px; padding: 10px;"
|
||
)
|
||
|
||
output_layout.addLayout(output_header_layout)
|
||
output_layout.addWidget(self.result_text)
|
||
|
||
# 将所有组件添加到主布局
|
||
main_layout.addWidget(title_label)
|
||
main_layout.addLayout(model_layout)
|
||
main_layout.addWidget(self.advanced_group)
|
||
main_layout.addLayout(input_layout)
|
||
main_layout.addWidget(self.translate_btn)
|
||
main_layout.addLayout(output_layout)
|
||
|
||
# 状态栏
|
||
self.status_bar = QStatusBar()
|
||
self.setStatusBar(self.status_bar)
|
||
self.status_label = QLabel()
|
||
self.status_bar.addWidget(self.status_label)
|
||
|
||
def init_status_bar_timer(self):
|
||
"""初始化状态栏定时器"""
|
||
self.status_timer = QTimer()
|
||
self.status_timer.setInterval(1000) # 每秒更新一次
|
||
self.status_timer.timeout.connect(self.update_status_bar)
|
||
self.status_timer.start()
|
||
|
||
def update_status_bar(self):
|
||
"""更新状态栏显示"""
|
||
status_text = self.system_monitor.get_status_text()
|
||
self.status_label.setText(status_text)
|
||
|
||
def load_default_model(self):
|
||
"""加载默认模型"""
|
||
if os.path.exists(self.default_model_path):
|
||
self.model_status_label.setText("模型加载中...")
|
||
if self.translator.load_model(self.default_model_path):
|
||
self.model_status_label.setText("● 模型就绪")
|
||
self.model_status_label.setStyleSheet("color: #38a169;")
|
||
self.model_info_label.setText(f"{self.translator.get_model_info()}")
|
||
else:
|
||
self.model_status_label.setText("● 模型加载失败")
|
||
self.model_status_label.setStyleSheet("color: #e53e3e;")
|
||
else:
|
||
self.model_status_label.setText("● 模型文件不存在")
|
||
self.model_status_label.setStyleSheet("color: #e53e3e;")
|
||
|
||
def change_model(self):
|
||
"""更换模型"""
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "选择模型文件", "", "GGUF模型文件 (*.gguf)"
|
||
)
|
||
if file_path:
|
||
self.model_status_label.setText("模型加载中...")
|
||
if self.translator.load_model(file_path):
|
||
self.model_status_label.setText("● 模型就绪")
|
||
self.model_status_label.setStyleSheet("color: #38a169;")
|
||
self.model_info_label.setText(f"{self.translator.get_model_info()}")
|
||
QMessageBox.information(self, "成功", "模型加载成功")
|
||
else:
|
||
self.model_status_label.setText("● 模型加载失败")
|
||
self.model_status_label.setStyleSheet("color: #e53e3e;")
|
||
QMessageBox.critical(self, "错误", "模型加载失败")
|
||
|
||
def add_term(self):
|
||
"""添加术语定义"""
|
||
term_text = self.term_input.text().strip()
|
||
if term_text:
|
||
if "=" in term_text:
|
||
self.terms.append(term_text)
|
||
self.terms_list.addItem(term_text)
|
||
self.term_input.clear()
|
||
else:
|
||
QMessageBox.warning(self, "警告", "术语格式不正确,请使用 'A = B' 格式")
|
||
|
||
def show_term_context_menu(self, pos):
|
||
"""显示术语项右键菜单"""
|
||
from PySide6.QtWidgets import QMenu
|
||
menu = QMenu()
|
||
delete_action = menu.addAction("删除")
|
||
action = menu.exec_(self.terms_list.mapToGlobal(pos))
|
||
if action == delete_action:
|
||
selected_items = self.terms_list.selectedItems()
|
||
for item in selected_items:
|
||
self.terms.remove(item.text())
|
||
self.terms_list.takeItem(self.terms_list.row(item))
|
||
|
||
def import_word(self):
|
||
"""导入Word文件"""
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "导入Word文件", "", "Word文件 (*.docx)"
|
||
)
|
||
if file_path:
|
||
try:
|
||
text = self.word_handler.import_docx(file_path)
|
||
self.source_text.setText(text)
|
||
QMessageBox.information(self, "成功", "Word文件导入成功")
|
||
except Exception as e:
|
||
logger.error(f"导入Word文件失败: {e}")
|
||
QMessageBox.critical(self, "错误", f"Word文件导入失败: {str(e)}")
|
||
|
||
def export_word(self):
|
||
"""导出Word文件"""
|
||
result_text = self.result_text.toPlainText()
|
||
if not result_text.strip():
|
||
QMessageBox.warning(self, "警告", "译文结果为空,无法导出")
|
||
return
|
||
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "导出Word文件", "", "Word文件 (*.docx)"
|
||
)
|
||
if file_path:
|
||
try:
|
||
self.word_handler.export_docx(file_path, result_text)
|
||
QMessageBox.information(self, "成功", "Word文件导出成功")
|
||
except Exception as e:
|
||
logger.error(f"导出Word文件失败: {e}")
|
||
QMessageBox.critical(self, "错误", f"Word文件导出失败: {str(e)}")
|
||
|
||
def copy_result(self):
|
||
"""复制译文结果"""
|
||
result_text = self.result_text.toPlainText()
|
||
if result_text.strip():
|
||
from PySide6.QtGui import QClipboard
|
||
clipboard = QApplication.clipboard()
|
||
clipboard.setText(result_text)
|
||
QMessageBox.information(self, "成功", "译文已复制到剪贴板")
|
||
else:
|
||
QMessageBox.warning(self, "警告", "译文结果为空,无法复制")
|
||
|
||
def start_translation(self):
|
||
"""开始翻译"""
|
||
source_text = self.source_text.toPlainText().strip()
|
||
if not source_text:
|
||
QMessageBox.warning(self, "警告", "原文内容为空,无法翻译")
|
||
return
|
||
|
||
if not self.translator.is_ready:
|
||
QMessageBox.warning(self, "警告", "模型未就绪,无法翻译")
|
||
return
|
||
|
||
# 禁用翻译按钮
|
||
self.translate_btn.setEnabled(False)
|
||
self.translate_btn.setText("翻译中...")
|
||
|
||
# 获取上下文和术语
|
||
context = self.context_edit.toPlainText().strip()
|
||
terms = self.terms if self.terms else None
|
||
|
||
# 执行翻译(在后台线程中执行,避免UI卡顿)
|
||
from PySide6.QtCore import QThread, Signal
|
||
|
||
class TranslationThread(QThread):
|
||
finished = Signal(str)
|
||
|
||
def __init__(self, translator, text, context, terms):
|
||
super().__init__()
|
||
self.translator = translator
|
||
self.text = text
|
||
self.context = context
|
||
self.terms = terms
|
||
|
||
def run(self):
|
||
result = self.translator.translate(self.text, self.context, self.terms)
|
||
self.finished.emit(result)
|
||
|
||
self.translation_thread = TranslationThread(self.translator, source_text, context, terms)
|
||
self.translation_thread.finished.connect(self.on_translation_finished)
|
||
self.translation_thread.start()
|
||
|
||
def on_translation_finished(self, result):
|
||
"""翻译完成回调"""
|
||
# 启用翻译按钮
|
||
self.translate_btn.setEnabled(True)
|
||
self.translate_btn.setText("开始翻译")
|
||
|
||
if result:
|
||
self.result_text.setText(result)
|
||
else:
|
||
QMessageBox.critical(self, "错误", "翻译失败,请检查日志")
|
||
|
||
# 修复导入问题
|
||
from PySide6.QtWidgets import QApplication |