离线翻译工具

"""
v2.0:增加文本翻译
安装包及打包命令:
pip install pyinstaller pywin32-ctypes
pyinstaller --onefile --windowed --icon=icon.ico --name="离线翻译工具_v2.0" Translate.py 或
pyinstaller Translate.spec
"""
import sys
import os
import openpyxl
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QPushButton, QFileDialog, QProgressBar,
                             QTextEdit, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal, Qt
from PyQt5.QtGui import QIcon
from transformers import MarianMTModel, MarianTokenizer

def resource_path(relative_path):
    """ 
    自动适配开发与打包环境的资源路径 
    
    参数:
        relative_path: 相对路径
        
    返回:
        根据运行环境返回正确的资源路径
        - 打包环境: 返回sys._MEIPASS下的路径
        - 开发环境: 返回当前目录下的路径
    """
    # sys._MEIPASS是PyInstaller打包后创建的临时目录,用于存放应用程序的资源文件。
    if hasattr(sys, '_MEIPASS'):
        # 打包后的资源路径
        return os.path.join(sys._MEIPASS, relative_path)
    # 开发环境的资源路径
    return os.path.join(os.path.abspath("."), relative_path)


class TranslationThread(QThread):
    """
    翻译线程类,用于在后台执行Excel文件的翻译任务
    
    信号:
        update_progress: 更新进度条和日志信息
        finished: 翻译完成信号,包含成功状态和消息
    """
    # 定义信号用于更新进度条和日志信息
    # 参数1: int类型,表示进度值(0-100)
    # 参数2: str类型,表示要显示的日志消息
    update_progress = pyqtSignal(int, str)
    finished = pyqtSignal(bool, str)        # 成功状态和消息

    def __init__(self, input_path, output_path, model_path):
        """
        初始化翻译线程
        
        参数:
            input_path: 输入Excel文件路径
            output_path: 输出Excel文件路径
            model_path: 翻译模型路径
        """
        super().__init__()
        self.input_path = input_path
        self.output_path = output_path
        self.model_path = model_path
        self._is_running = True  # 控制线程运行的标志

    def run(self):
        """
        线程主函数,执行Excel文件的翻译任务
        
        工作流程:
        1. 加载翻译模型
        2. 读取Excel文件
        3. 遍历所有工作表和单元格
        4. 对文本单元格进行翻译
        5. 保存翻译后的Excel文件
        """
        try:
            # 加载翻译模型
            self.update_progress.emit(0, "正在加载翻译模型...")
            tokenizer = MarianTokenizer.from_pretrained(self.model_path)
            model = MarianMTModel.from_pretrained(self.model_path)

            # 加载Excel文件
            self.update_progress.emit(5, "正在读取Excel文件...")
            wb = openpyxl.load_workbook(self.input_path)
            total_sheets = len(wb.sheetnames)
            processed_sheets = 0

            # 遍历所有工作表
            for sheet_name in wb.sheetnames:
                if not self._is_running:
                    break  # 如果线程被停止,则中断处理

                ws = wb[sheet_name]
                total_cells = sum(1 for row in ws.iter_rows() for _ in row)
                processed_cells = 0

                self.update_progress.emit(10, f"正在处理工作表: {sheet_name}...")

                # 遍历工作表中的所有单元格
                for row in ws.iter_rows():
                    for cell in row:
                        if not self._is_running:
                            break  # 如果线程被停止,则中断处理

                        # 只翻译字符串类型的单元格
                        if cell.value and isinstance(cell.value, str):
                            try:
                                translated = self.translate_text(cell.value, tokenizer, model)
                                cell.value = translated
                            except Exception as e:
                                self.update_progress.emit(
                                    -1, f"翻译错误(单元格{cell.coordinate}):{str(e)}")

                        processed_cells += 1
                        # 计算进度百分比:10%用于加载,80%用于翻译,10%用于保存
                        progress = 10 + 80 * (processed_cells / total_cells)
                        self.update_progress.emit(
                            int(progress), f"处理进度: {processed_cells}/{total_cells}")

                processed_sheets += 1

            # 如果线程未被停止,则保存文件
            if self._is_running:
                self.update_progress.emit(95, "正在保存文件...")
                wb.save(self.output_path)
                self.update_progress.emit(95, "翻译完成!")
                self.finished.emit(True, "翻译完成!")
            else:
                self.finished.emit(False, "翻译已取消")

        except Exception as e:
            # 发生异常时发送错误信号
            self.finished.emit(False, f"发生错误:{str(e)}")

    def translate_text(self, text, tokenizer, model):
        """
        使用模型翻译单个文本
        
        参数:
            text: 要翻译的文本
            tokenizer: 分词器
            model: 翻译模型
            
        返回:
            翻译后的文本
        """
        inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
        translated = model.generate(**inputs)
        return tokenizer.decode(translated[0], skip_special_tokens=True)

    def stop(self):
        """
        停止翻译线程
        通过设置标志位实现优雅停止
        """
        self._is_running = False


class TranslationApp(QMainWindow):
    """
    翻译应用程序主窗口类
    
    功能:
    1. 提供Excel文件翻译界面
    2. 提供单文本翻译界面
    3. 管理翻译线程
    4. 处理用户交互
    """
    def __init__(self):
        """
        初始化应用程序窗口
        设置窗口大小、位置和UI组件
        """
        super().__init__()
        self.thread = None  # 翻译线程对象

        # 设置窗口初始尺寸
        self.resize(1200, 800)  # 增大尺寸

        # 计算居中坐标
        screen = QApplication.desktop()
        screen_geometry = screen.screenGeometry()
        center_x = (screen_geometry.width() - self.width()) // 2
        center_y = (screen_geometry.height() - self.height()) // 2

        # 定位到屏幕中心
        self.move(center_x, center_y)

        # 初始化界面
        self.init_ui()
        self.setWindowTitle("离线翻译工具 v2.0")
        #self.setWindowIcon(QIcon(':/icon.ico'))  # 冒号开头表示资源路径
        # 加载图标
        icon_path = resource_path('icon.ico')
        self.setWindowIcon(QIcon(icon_path))

    def init_ui(self):
        """
        初始化用户界面
        创建并布局所有UI组件
        """
        main_widget = QWidget()
        layout = QVBoxLayout()

        # 输入文件选择
        input_layout = QHBoxLayout()
        self.input_label = QLabel("输入文件:")
        self.input_edit = QLineEdit()
        self.input_btn = QPushButton("浏览...")
        self.input_btn.clicked.connect(self.select_input_file)
        input_layout.addWidget(self.input_label)
        input_layout.addWidget(self.input_edit)
        input_layout.addWidget(self.input_btn)

        # 输出文件选择
        output_layout = QHBoxLayout()
        self.output_label = QLabel("输出文件:")
        self.output_edit = QLineEdit()
        self.output_btn = QPushButton("浏览...")
        self.output_btn.clicked.connect(self.select_output_file)
        output_layout.addWidget(self.output_label)
        output_layout.addWidget(self.output_edit)
        output_layout.addWidget(self.output_btn)

        # 模型路径选择
        model_layout = QHBoxLayout()
        self.model_label = QLabel("模型路径:")
        self.model_edit = QLineEdit("./local_model")
        self.model_btn = QPushButton("选择...")
        self.model_btn.clicked.connect(self.select_model_path)
        model_layout.addWidget(self.model_label)
        model_layout.addWidget(self.model_edit)
        model_layout.addWidget(self.model_btn)

        # 控制按钮
        control_layout = QHBoxLayout()
        self.start_btn = QPushButton("开始翻译")
        self.start_btn.clicked.connect(self.start_translation)
        self.stop_btn = QPushButton("停止")
        self.stop_btn.clicked.connect(self.stop_translation)
        self.stop_btn.setEnabled(False)
        control_layout.addWidget(self.start_btn)
        control_layout.addWidget(self.stop_btn)

        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)

        # 日志显示
        self.log_area = QTextEdit()
        self.log_area.setReadOnly(True)

        # 添加分隔线
        separator = QLabel()
        separator.setStyleSheet("border-top: 1px solid #cccccc; margin: 10px 0px;")
        layout.addWidget(separator)

        # 添加文本翻译区域标题
        title_label = QLabel("单文本翻译")
        title_label.setStyleSheet("font-size: 14px; font-weight: bold; margin: 5px 0px;")
        layout.addWidget(title_label)

        # 文本翻译区域
        text_layout = QHBoxLayout()
        
        # 输入区域
        input_group = QVBoxLayout()
        input_group.addWidget(QLabel("输入英文文本:"))
        self.input_text = QTextEdit()
        self.input_text.setPlaceholderText("请在此输入要翻译的英文...")
        self.input_text.setMinimumHeight(100)
        input_group.addWidget(self.input_text)
        
        # 翻译按钮区域
        btn_group = QVBoxLayout()
        btn_group.addStretch()
        self.text_translate_btn = QPushButton("翻译 →")
        self.text_translate_btn.setFixedWidth(100)
        self.text_translate_btn.clicked.connect(self.translate_single_text)
        btn_group.addWidget(self.text_translate_btn)
        btn_group.addStretch()
        
        # 输出区域
        output_group = QVBoxLayout()
        output_group.addWidget(QLabel("翻译结果:"))
        self.output_text = QTextEdit()
        self.output_text.setPlaceholderText("翻译结果将显示在这里...")
        self.output_text.setReadOnly(True)
        self.output_text.setMinimumHeight(100)
        output_group.addWidget(self.output_text)
        
        # 组装文本翻译区域
        text_layout.addLayout(input_group)
        text_layout.addLayout(btn_group)
        text_layout.addLayout(output_group)
        
        # 添加到主布局
        layout.addLayout(text_layout)

        # 添加另一个分隔线
        separator2 = QLabel()
        separator2.setStyleSheet("border-top: 1px solid #cccccc; margin: 10px 0px;")
        layout.addWidget(separator2)

        # 组合布局
        layout.addLayout(input_layout)
        layout.addLayout(output_layout)
        layout.addLayout(model_layout)
        layout.addLayout(control_layout)
        layout.addWidget(self.progress_bar)
        layout.addWidget(self.log_area)

        main_widget.setLayout(layout)
        self.setCentralWidget(main_widget)

    def select_input_file(self):
        """
        选择输入Excel文件
        打开文件对话框并更新输入路径
        """
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择Excel文件", "", "Excel Files (*.xlsx *.xls)")
        if file_path:
            self.input_edit.setText(file_path)

    def select_output_file(self):
        """
        选择输出Excel文件
        打开文件对话框并更新输出路径
        """
        file_path, _ = QFileDialog.getSaveFileName(
            self, "保存翻译文件", "", "Excel Files (*.xlsx)")
        if file_path:
            self.output_edit.setText(file_path)

    def select_model_path(self):
        """
        选择翻译模型目录
        打开目录对话框并更新模型路径
        """
        dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录")
        if dir_path:
            self.model_edit.setText(dir_path)

    def start_translation(self):
        """
        开始Excel文件翻译
        验证输入并启动翻译线程
        """
        if not self.validate_inputs():
            return

        # 更新UI状态
        self.start_btn.setEnabled(False)
        self.stop_btn.setEnabled(True)
        self.log_area.clear()

        # 创建并启动翻译线程
        self.thread = TranslationThread(
            self.input_edit.text(),
            self.output_edit.text(),
            self.model_edit.text()
        )

        # 连接信号
        self.thread.update_progress.connect(self.update_progress)
        self.thread.finished.connect(self.translation_finished)
        self.thread.start()

    def stop_translation(self):
        """
        停止Excel文件翻译
        停止线程并重置UI状态
        """
        if self.thread and self.thread.isRunning():
            self.thread.stop()
            self.thread.wait()
        self.reset_ui()

    def validate_inputs(self):
        """
        验证用户输入是否有效
        
        检查:
        1. 输入文件路径是否已设置
        2. 输出文件路径是否已设置
        3. 模型路径是否已设置
        
        返回:
            如果所有输入有效则返回True,否则显示错误并返回False
        """
        if not self.input_edit.text():
            self.show_error("请选择输入文件")
            return False
        if not self.output_edit.text():
            self.show_error("请指定输出文件路径")
            return False
        if not self.model_edit.text():
            self.show_error("请选择模型目录")
            return False
        return True

    def update_progress(self, value, message):
        """
        更新进度条和日志信息
        
        参数:
            value: 进度值(0-100)
            message: 日志消息
        """
        if value >= 0:
            self.progress_bar.setValue(value)
        self.log_area.append(f"[进度] {message}")

    def translation_finished(self, success, message):
        """
        翻译完成处理
        
        参数:
            success: 是否成功完成
            message: 完成消息
        """
        self.reset_ui()
        if success:
            QMessageBox.information(self, "完成", message)
        else:
            QMessageBox.critical(self, "错误", message)

    def reset_ui(self):
        """
        重置UI状态
        启用开始按钮,禁用停止按钮,重置进度条
        """
        self.start_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        self.progress_bar.setValue(0)

    def show_error(self, message):
        """
        显示错误消息对话框
        
        参数:
            message: 错误消息
        """
        QMessageBox.critical(self, "输入错误", message)

    def validate_model_path(self):
        """
        验证模型路径是否有效
        
        检查:
        1. 模型路径是否已设置
        2. 模型目录是否存在
        
        返回:
            如果模型路径有效则返回True,否则显示错误并返回False
        """
        model_path = self.model_edit.text()
        if not model_path:
            self.show_error("请选择模型目录")
            return False
        if not os.path.exists(model_path):
            self.show_error("模型目录不存在")
            return False
        return True

    def translate_single_text(self):
        """
        处理单条文本翻译
        
        工作流程:
        1. 验证模型路径
        2. 获取输入文本
        3. 加载翻译模型
        4. 执行翻译
        5. 显示翻译结果
        """
        if not self.validate_model_path():
            return
            
        input_text = self.input_text.toPlainText()
        if not input_text.strip():
            self.show_error("请输入要翻译的文本")
            return
            
        try:
            # 更新UI状态
            self.text_translate_btn.setEnabled(False)
            self.text_translate_btn.setText("翻译中...")
            QApplication.processEvents()  # 确保UI更新
            
            # 加载翻译模型
            tokenizer = MarianTokenizer.from_pretrained(self.model_edit.text())
            model = MarianMTModel.from_pretrained(self.model_edit.text())
            
            # 执行翻译
            inputs = tokenizer(input_text, return_tensors="pt", truncation=True, max_length=512)
            translated = model.generate(**inputs)
            result = tokenizer.decode(translated[0], skip_special_tokens=True)
            
            # 显示结果
            self.output_text.setPlainText(result)
            
        except Exception as e:
            self.show_error(f"翻译出错:{str(e)}")
        finally:
            # 恢复UI状态
            self.text_translate_btn.setEnabled(True)
            self.text_translate_btn.setText("翻译 →")


def init_font():
    """
    初始化应用程序字体
    根据屏幕DPI设置合适的字体大小
    """
    font = QApplication.font()
    font.setFamily("Microsoft YaHei")  # 使用系统默认字体
    font.setPointSize(10 if QApplication.primaryScreen().logicalDotsPerInch() < 120 else 12)
    QApplication.setFont(font)


# 在main函数最前面添加
import ctypes
try:
    ctypes.windll.shcore.SetProcessDpiAwareness(2)  # 设置系统DPI感知,确保在高分辨率屏幕上正确显示
except Exception as e:
    print(f"DPI设置失败: {str(e)}")


if __name__ == "__main__":
    # 启用高DPI支持
    QApplication.setAttribute(Qt.AA_EnableHighDpiScaling,True)  # 启用Qt高DPI缩放
    QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps,True)     # 启用高DPI图标
    
    # 创建应用程序实例
    app = QApplication(sys.argv)
    init_font()  # 添加字体初始化
    
    # 创建并显示主窗口
    window = TranslationApp()
    window.show()
    
    # 进入应用程序主循环
    sys.exit(app.exec_())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值