[工具]构建本地离线翻译器

Abstract: 主要采用nllb-600M的蒸馏模型进行部署。目的是为了在无网络链接的时候也能进行翻译,准确度肯定是不如在线的ai翻译,只能说辅助作用,过年期间或者什么时候需要大量翻译内容,节约流量;根据结果去人工修改校验就可以了(反正AI翻译的结果也需要校验)。

一.模型选择

Helsinki-NLP

一开始我找到的是Helsinki-NLP/opus-mt-en-zhHelsinki-NLP/opus-mt-zh-en。看名字也知道,是两个模型,进行中文和英文的翻译,毕竟这也是我的主要目的。这个模型的话,特点是轻量级,很小的内容,翻译的速度很快。

zh-en模型的大小如下, en-zh也差不多:

opus-mt-zh-en (master) $ ls -lh
total 1.2G
-rw-r--r-- 1 uid03619 1049089 3.3K Jan  5 13:34 README.md
-rw-r--r-- 1 uid03619 1049089 1.5K Jan  5 13:34 config.json
-rw-r--r-- 1 uid03619 1049089   69 Jan  5 13:34 configuration.json
-rw-r--r-- 1 uid03619 1049089  309 Jan  5 13:34 generation_config.json
-rw-r--r-- 1 uid03619 1049089 1.5K Jan  5 13:34 metadata.json
-rw-r--r-- 1 uid03619 1049089 298M Jan  5 13:39 pytorch_model.bin
-rw-r--r-- 1 uid03619 1049089 552M Jan  5 13:39 rust_model.ot
-rw-r--r-- 1 uid03619 1049089 786K Jan  5 13:34 source.spm
-rw-r--r-- 1 uid03619 1049089 788K Jan  5 13:34 target.spm
-rw-r--r-- 1 uid03619 1049089 299M Jan  5 13:39 tf_model.h5
-rw-r--r-- 1 uid03619 1049089   44 Jan  5 13:34 tokenizer_config.json
-rw-r--r-- 1 uid03619 1049089 1.6M Jan  5 13:34 vocab.json

也就1.2G

这个模型的翻译质量有点差,需要人工校验的部分过多,所以我就没有使用。我是翻译了一个定语从句大致如下

" sth that do sth through sth "

这个模型直接翻译成(断句):

" 啥, 做啥, 通过啥 "

我无法接受!

NLLB-200-DISTILLED-600M

这个模型的介绍我就不说了,它翻译更准确,语言也更多(200种)。我没有采用py代码的方式下载,它下载的默认地址在外网,访问不方便而且速度很慢,所以我是先把模型下载到本地工作目录的:

git clone https://gitee.com/hf-models/nllb-200-distilled-600M.git

在当前目录下就会有nllb-200-distilled-600M/的文件夹,里面就是要调用的模型文件。


二.安装NLLB

环境配置

由于是模型,所以调用的话,要下载依赖包,pytorch与transformer。

python -m pip install torch torchvision torchaudio
python -m pip install transformers

直接装的cpu版本,带出门用的,是轻薄本,本身是没有nvidia显卡加速的。mac或者linux记得指令用python3

测试模型

在当前目录下有模型的文件夹,创建一个nllb-tanslate.py的测试脚本来检测是否可以正常使用

$ ls -lrth
drwxr-xr-x 1 uid03619 1049089    0 Jan  5 10:55 nllb-200-distilled-600M/
-rw-r--r-- 1 uid03619 1049089  865 Jan  5 11:33 nllb-tanslate.py

脚本内容如下:

from transformers import pipeline

model_path = "./nllb-200-distilled-600M"

translator = pipeline(
    "translation",
    model=model_path,
    device=-1
)

print("✓ 本地模型加载成功!")

def translate(text, src_lang="zho_Hans", tgt_lang="eng_Latn"):
    """
    NLLB 模型必须使用语言代码,例如:
    - 中文: zho_Hans
    - 英文: eng_Latn 
    - 日文: jpn_Jpan
    - 法文: fra_Latn
    """
    result = translator(
        text,
        src_lang=src_lang,
        tgt_lang=tgt_lang,
        max_length=200
    )
    return result[0]['translation_text']

print("测试翻译...")
try:
    result = translate("你好,世界!", src_lang="zho_Hans", tgt_lang="eng_Latn")
    print(f"翻译结果: {result}")
except Exception as e:
    print(f"错误: {e}")

运行脚本,输出如下:

$ python .\nllb-tanslate.py
Device set to use cpu
✓ 本地模型加载成功!
测试翻译...
翻译结果: Hello, the world!

我忘了,在运行的时候可能会有一个错误,是关于numpy版本的,目前我这个numpy版本是1.23.5,原来是1.23.0会报错。


三.GUI设定

我直接让AI写了份代码,自己做了点debug,可以直接运行,运行前记得python -m pip install tkinter

import tkinter as tk
from tkinter import ttk, scrolledtext
import threading
import queue
import time
from transformers import pipeline

class TranslationApp:
    def __init__(self):
        self.model_path = "./nllb-200-distilled-600M"
        self.translator = None
        self.translation_queue = queue.Queue()
        self.result_queue = queue.Queue()
        self.is_loaded = False
        
        # 创建界面
        self.create_gui()
        
        # 在后台线程中加载模型
        self.start_model_loading()
        
        # 启动处理队列的循环
        self.root.after(100, self.process_queue)
        
    def create_gui(self):
        """创建图形界面"""
        self.root = tk.Tk()
        self.root.title("NLLB翻译器")
        self.root.geometry("600x500")
        
        self.root.bind('<Escape>', lambda e: self.quit_app())
        # 设置窗口图标(可选)
        try:
            self.root.iconbitmap(default='icon.ico')
        except:
            pass
        
        # 使窗口最小化到系统托盘
        self.root.protocol('WM_DELETE_WINDOW', self.minimize_to_tray)
        
        # 创建菜单
        self.create_menu()
        
        # 创建主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 状态标签
        self.status_label = ttk.Label(
            main_frame, 
            text="⏳ 正在加载翻译模型...",
            foreground="orange"
        )
        self.status_label.grid(row=0, column=0, columnspan=2, pady=(0, 10))
        
        # 源语言选择
        ttk.Label(main_frame, text="源语言:").grid(row=1, column=0, sticky=tk.W)
        self.src_lang = ttk.Combobox(
            main_frame, 
            values=[
                ("中文", "zho_Hans"),
                ("英文", "eng_Latn"),
                ("日文", "jpn_Jpan"),
                ("法文", "fra_Latn"),
                ("德文", "deu_Latn"),
                ("韩文", "kor_Hang")
            ],
            width=15,
            state="readonly"
        )
        self.src_lang.grid(row=1, column=1, sticky=tk.W, pady=5)
        self.src_lang.set("英文 eng_Latn")
        
        # 目标语言选择
        ttk.Label(main_frame, text="目标语言:").grid(row=2, column=0, sticky=tk.W)
        self.tgt_lang = ttk.Combobox(
            main_frame, 
            values=[
                ("英文", "eng_Latn"),
                ("中文", "zho_Hans"),
                ("日文", "jpn_Jpan"),
                ("法文", "fra_Latn"),
                ("德文", "deu_Latn"),
                ("韩文", "kor_Hang")
            ],
            width=15,
            state="readonly"
        )
        self.tgt_lang.grid(row=2, column=1, sticky=tk.W, pady=5)
        self.tgt_lang.set("中文 zho_Hans")
        
        # 输入文本框
        ttk.Label(main_frame, text="输入文本:").grid(row=3, column=0, sticky=tk.NW, pady=(10, 0))
        self.input_text = scrolledtext.ScrolledText(
            main_frame, 
            width=50, 
            height=8,
            wrap=tk.WORD
        )
        self.input_text.grid(row=4, column=0, columnspan=2, pady=(0, 10))
        
        # 翻译按钮
        self.translate_btn = ttk.Button(
            main_frame,
            text="翻译",
            command=self.start_translation,
            state="disabled"
        )
        self.translate_btn.grid(row=5, column=0, columnspan=2, pady=(0, 10))
        
        # 输出标签
        ttk.Label(main_frame, text="翻译结果:").grid(row=6, column=0, sticky=tk.NW)
        self.output_text = scrolledtext.ScrolledText(
            main_frame, 
            width=50, 
            height=8,
            wrap=tk.WORD,
            state="normal"
        )
        self.output_text.grid(row=7, column=0, columnspan=2, pady=(0, 10))
        
        # 配置网格权重
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        
        # 绑定Ctrl+Enter快捷键翻译
        self.root.bind('<Control-Return>', lambda e: self.start_translation())
        
    def create_menu(self):
        """创建系统托盘菜单"""
        self.menu_bar = tk.Menu(self.root)
        
        # 文件菜单
        file_menu = tk.Menu(self.menu_bar, tearoff=0)
        file_menu.add_command(label="显示窗口", command=self.show_window)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.quit_app)
        self.menu_bar.add_cascade(label="文件", menu=file_menu)
        
        # 语言菜单
        lang_menu = tk.Menu(self.menu_bar, tearoff=0)
        lang_menu.add_command(label="中 → 英", command=lambda: self.set_languages("zho_Hans", "eng_Latn"))
        lang_menu.add_command(label="英 → 中", command=lambda: self.set_languages("eng_Latn", "zho_Hans"))
        lang_menu.add_command(label="中 → 日", command=lambda: self.set_languages("zho_Hans", "jpn_Jpan"))
        lang_menu.add_command(label="日 → 中", command=lambda: self.set_languages("jpn_Jpan", "zho_Hans"))
        self.menu_bar.add_cascade(label="快速设置", menu=lang_menu)
        
        self.root.config(menu=self.menu_bar)
        
    def set_languages(self, src, tgt):
        """设置语言对"""
        lang_map = {
            "zho_Hans": "中文",
            "eng_Latn": "英文",
            "jpn_Jpan": "日文",
            "fra_Latn": "法文"
        }
        
        for i, (name, code) in enumerate(self.src_lang['values']):
            if code == src:
                self.src_lang.current(i)
                break
                
        for i, (name, code) in enumerate(self.tgt_lang['values']):
            if code == tgt:
                self.tgt_lang.current(i)
                break
        
        self.show_window()
        
    def start_model_loading(self):
        """在后台线程中加载模型"""
        def load_model():
            try:
                self.translator = pipeline(
                    "translation",
                    model=self.model_path,
                    device=0
                )
                self.is_loaded = True
                self.result_queue.put(("status", "✅ 模型加载成功!可以开始翻译"))
                self.result_queue.put(("enable_button", True))
            except Exception as e:
                self.result_queue.put(("status", f"❌ 模型加载失败: {str(e)}"))
                self.result_queue.put(("enable_button", False))
        
        thread = threading.Thread(target=load_model, daemon=True)
        thread.start()
        
    def start_translation(self):
        """开始翻译(在后台线程中执行)"""
        if not self.is_loaded:
            self.output_text.delete(1.0, tk.END)
            self.output_text.insert(tk.END, "模型尚未加载完成,请稍候...")
            return
            
        text = self.input_text.get(1.0, tk.END).strip()
        if not text:
            return
            
        # 获取语言代码
        src_code = self.src_lang.get().split(" ")[-1] if " " in self.src_lang.get() else self.get_lang_code(self.src_lang.get())
        tgt_code = self.tgt_lang.get().split(" ")[-1] if " " in self.tgt_lang.get() else self.get_lang_code(self.tgt_lang.get())
        
        # 禁用按钮,显示加载状态
        self.translate_btn.config(state="disabled", text="翻译中...")
        self.output_text.delete(1.0, tk.END)
        self.output_text.insert(tk.END, "正在翻译...")
        
        # 将翻译任务放入队列
        self.translation_queue.put((text, src_code, tgt_code))
        
    def get_lang_code(self, lang_name):
        """从语言名称获取代码"""
        lang_map = {
            "中文": "zho_Hans",
            "英文": "eng_Latn",
            "日文": "jpn_Jpan",
            "法文": "fra_Latn",
            "德文": "deu_Latn",
            "韩文": "kor_Hang"
        }
        return lang_map.get(lang_name, "eng_Latn")
        
    def process_translation(self, text, src_lang, tgt_lang):
        """在后台线程中执行翻译"""
        try:
            result = self.translator(
                text,
                src_lang=src_lang,
                tgt_lang=tgt_lang,
                max_length=200
            )
            self.result_queue.put(("result", result[0]['translation_text']))
        except Exception as e:
            self.result_queue.put(("error", f"翻译错误: {str(e)}"))
            
    def process_queue(self):
        """处理队列中的结果"""
        # 启动翻译线程
        while not self.translation_queue.empty():
            text, src, tgt = self.translation_queue.get()
            thread = threading.Thread(
                target=self.process_translation, 
                args=(text, src, tgt),
                daemon=True
            )
            thread.start()
        
        # 处理结果
        try:
            while True:
                msg_type, content = self.result_queue.get_nowait()
                
                if msg_type == "status":
                    self.status_label.config(text=content, foreground="green")
                elif msg_type == "enable_button":
                    self.translate_btn.config(state="normal" if content else "disabled")
                    if content:
                        self.status_label.config(text="✅ 就绪", foreground="green")
                elif msg_type == "result":
                    self.output_text.delete(1.0, tk.END)
                    self.output_text.insert(tk.END, content)
                    self.translate_btn.config(state="normal", text="翻译")
                elif msg_type == "error":
                    self.output_text.delete(1.0, tk.END)
                    self.output_text.insert(tk.END, content)
                    self.translate_btn.config(state="normal", text="翻译")
                    
        except queue.Empty:
            pass
            
        # 每100毫秒检查一次队列
        self.root.after(100, self.process_queue)
        
    def minimize_to_tray(self):
        """最小化到系统托盘"""
        self.root.withdraw()  # 隐藏窗口
        
    def show_window(self):
        """显示窗口"""
        self.root.deiconify()  # 显示窗口
        self.root.lift()  # 提到最前面
        
    def quit_app(self):
        """退出应用"""
        self.root.quit()
        self.root.destroy()
        
    def run(self):
        """运行应用"""
        self.root.mainloop()

def main():
    app = TranslationApp()
    app.run()

if __name__ == "__main__":
    main()

四.模型加速

NLLB这个模型的翻译速度实在难以恭维,所以转换了一下,进行加速。直接走流程了:

python -m pip install ctranslate2

编写脚本,进行模型转换:

from transformers import AutoModelForSeq2SeqLM
import ctranslate2

def convert_nllb_to_ct2():
    """将 NLLB 模型转换为 CTranslate2 格式"""

    converter = ctranslate2.converters.TransformersConverter("./nllb-200-distilled-600M")
    converter.convert("./nllb-ct2", quantization="int8")

if __name__ == "__main__":
    convert_nllb_to_ct2()

五.修改GUI

主要是将加速后的模型部署到gui上

修改这几个函数还有头文件即可:

from transformers import AutoTokenizer
import ctranslate2

    def __init__(self):
        self.model_path = "./nllb-ct2"  # 改为你的ct2模型路径  
        self.translator = None
        self.tokenizer = None
        self.translation_queue = queue.Queue()
        self.result_queue = queue.Queue()
        self.is_loaded = False
        
        # 创建界面
        self.create_gui()
        
        # 在后台线程中加载模型
        self.start_model_loading()
        
        # 启动处理队列的循环
        self.root.after(100, self.process_queue)

    def start_model_loading(self):
        """在后台线程中加载模型"""
        def load_model():
            try:
                # 加载分词器
                self.tokenizer = AutoTokenizer.from_pretrained(
                    "./nllb-200-distilled-600M"
                )
                
                # 加载CTranslate2模型
                self.translator = ctranslate2.Translator(
                    self.model_path,
                    device="cpu",
                    compute_type="int8"
                )
                
                self.is_loaded = True
                self.result_queue.put(("status", "✅ CT2模型加载成功!"))
                self.result_queue.put(("enable_button", True))
            except Exception as e:
                self.result_queue.put(("status", f"❌ 模型加载失败: {str(e)}"))
                self.result_queue.put(("enable_button", False))
        
        thread = threading.Thread(target=load_model, daemon=True)
        thread.start()

    def process_translation(self, text, src_lang, tgt_lang):
        """在后台线程中执行翻译"""
        try:
            # 设置源语言
            self.tokenizer.src_lang = src_lang
            
            # 分词
            source = self.tokenizer.convert_ids_to_tokens(
                self.tokenizer.encode(text)
            )
            
            # 使用CT2翻译
            results = self.translator.translate_batch(
                [source],
                target_prefix=[[tgt_lang]],
                beam_size=2,
                max_decoding_length=200
            )
            
            # 解码
            target = results[0].hypotheses[0][1:]  # 跳过语言标记
            translation = self.tokenizer.decode(
                self.tokenizer.convert_tokens_to_ids(target)
            )
            
            self.result_queue.put(("result", translation))
        except Exception as e:
            self.result_queue.put(("error", f"翻译错误: {str(e)}"))

然后就可以用了。


Last modified on 2026-01-05