作者 主题: 移动: 一种保留原格式且应用译名表的Ai大模型PDF翻译办法  (阅读 82 次)

副标题:

线上 \星尘/

  • 版主
  • *
  • 帖子数: 1945
  • 苹果币: 6
  • 星尘
这篇主题已经移动到 工具區

http://www.GoddessFantasy.net/bbs/index.php?topic=157541.0

引用
PS:本方法仅供个人尝鲜使用,请尊重人类译者的劳动成果。人工智能是辅助而不是替代。

在本站的ai翻译方向,极夜十四已经写过沉浸式翻译插件的实践,如下链接:【工具/网站安利】机翻不求人终极对策:沉浸式翻译多端插件+神秘海盗航线图

这依然有一些问题,沉浸式翻译无法完美地保留原有排版格式,且不能应该译名表进行专有名词的翻译。

故本文介绍一款基于先识别再翻译的PDF翻译工具PDFMathTranslate,它是目前学术论文翻译的极佳助手之一,原理是先用定位PDF文档中的文字区块,再进行翻译,最后填充进去,可以保留公式、图表、目录和注释等。

通过自定义的中间件,还可以应用译名表功能提升翻译质量。

关于部署方式(目前提供一键安装),下载带模型的版本后解压在build目录中运行exe即可,使用办法可参考文档说明,或是pdf2zh-哔哩哔哩_bilibili的视频教程。
关于大模型选择,可以是量大管饱的deepseekv3(以及后续的v4),每百万token(约三四十万字)输入输出仅需几元钱,可以使用官方api,硅基流动api,或是第三方的更低价api。

为了提供pdf2zh与deepseek官方默认不支持的译名表功能,需要用中间件辅助,原理是pdf2zh不直接向大模型api请求,而是向本地中间件程序请求,该程序可优化prompt,将译名植入其中后再向大模型请求,之后将结果返回给pdf2zh,以下为一个中间件(包括prompt)的示例。
代码: [选择]
from flask import Flask, request, jsonify
import flask
import requests
import pandas as pd
import re
import os
from nltk.stem import WordNetLemmatizer
import nltk

# 下载nltk资源(首次运行时需要)
try:
    nltk.data.find('corpora/wordnet')
except LookupError:
    nltk.download('wordnet')

OPENAI_API_KEY = 'sk-3FUiTSl1111111111111111示例apirArtg4tEwH'

# 优化后的提示词,更清晰地说明翻译要求
TRANSLATION_PROMPTS = {
    "standard": """请将以下markdown文本翻译成流畅、准确、符合中文表达习惯的中文。
文本中某些专业术语后面会标注其对应的中文翻译,格式为"术语(中文翻译)"。
请确保在翻译时:
1. 使用这些已提供的中文翻译来保持术语一致性
2. 输出纯中文,不要在翻译结果中保留英文术语或注释
3. 保持markdown格式不变
4. 避免机器翻译的生硬感,使翻译结果自然流畅
5. 对于专有名词、人名和地名,根据上下文合理翻译

源文本:
""",
    "technical": """请将以下技术文档翻译成专业、准确的中文。
文本中的专业术语已经标注了推荐翻译,格式为"术语(中文翻译)"。
请遵循以下要求:
1. 必须使用已标注的中文术语翻译
2. 保持技术文档的精确性和专业性
3. 保留所有代码示例和格式不变
4. 确保翻译后的文本逻辑连贯,符合中文技术文档的表达习惯
5. 适当调整语序,使翻译更自然

源文本:
"""
}

# 读取Excel文件并建立全局字典
df = pd.read_excel('translation_table1.xlsx')
translation_dict = dict(zip(df.iloc[:, 0], df.iloc[:, 1]))

# 初始化词形还原器
lemmatizer = WordNetLemmatizer()

# 增强的文本替换函数


def text_replace(long_string, translation_dict, case_sensitive=False, use_lemmatization=True):
    if not long_string:
        return long_string

    # 过滤掉非字符串键,并确保所有键都转换为字符串
    valid_pairs = {
        str(k): v for k, v in translation_dict.items() if k is not None}

    # 先对键按长度从大到小排序
    sorted_pairs = sorted(valid_pairs.items(),
                          key=lambda x: len(x[0]), reverse=True)

    # 创建一个映射关系,包含原始形式和词形还原后的形式
    term_mapping = {}
    if use_lemmatization:
        for term, trans in sorted_pairs:
            term_mapping[term.lower()] = (term, trans)
            # 添加单词的词形还原版本
            words = term.split()
            if len(words) == 1:  # 只对单个词进行词形还原
                lemma = lemmatizer.lemmatize(term.lower())
                if lemma != term.lower():
                    term_mapping[lemma] = (term, trans)
    else:
        for term, trans in sorted_pairs:
            term_mapping[term.lower()] = (term, trans)

    # 使用正则表达式查找术语
    def replace_term(match):
        matched_text = match.group(0)
        # 尝试直接匹配
        if case_sensitive and matched_text in translation_dict:
            return f"{matched_text}({translation_dict[matched_text]})"

        # 不区分大小写的匹配
        matched_lower = matched_text.lower()
        if matched_lower in term_mapping:
            original_term, trans = term_mapping[matched_lower]
            return f"{matched_text}({trans})"

        return matched_text

    # 构建正则表达式模式
    if use_lemmatization:
        # 包含所有可能的术语形式
        terms = list(term_mapping.keys())
    else:
        # 只包含原始术语
        terms = [k.lower() for k in translation_dict.keys()
                 ] if not case_sensitive else list(translation_dict.keys())

    # 按长度排序,优先匹配最长的术语
    terms.sort(key=len, reverse=True)

    # 防止特殊字符被当作正则表达式元字符
    escaped_terms = [re.escape(term) for term in terms]

    # 构建使用分词边界的正则表达式
    pattern = r'\b(?:' + '|'.join(escaped_terms) + r')\b'

    # 根据大小写敏感设置编译正则表达式
    if case_sensitive:
        regex = re.compile(pattern)
    else:
        regex = re.compile(pattern, re.IGNORECASE)

    # 执行替换
    return regex.sub(replace_term, long_string)


# 创建Flask应用
app = Flask(__name__)


@app.route('/v1/chat/completions', methods=['POST'])
def chat_completions():
    req_data = request.json
    messages = req_data.get('messages', [])

    # 获取配置选项
    config = req_data.get('config', {})
    prompt_type = config.get('prompt_type', 'standard')
    case_sensitive = config.get('case_sensitive', False)
    use_lemmatization = config.get('use_lemmatization', True)

    # 选择合适的提示词
    selected_prompt = TRANSLATION_PROMPTS.get(
        prompt_type, TRANSLATION_PROMPTS['standard'])

    prompt_added = False  # 标记是否已添加prompt

    for msg in messages:
        if msg.get('role') == 'user':
            content = msg.get('content', [])
            if isinstance(content, str):  # 处理字符串类型的内容
                replaced_text = text_replace(
                    content, translation_dict, case_sensitive, use_lemmatization)
                msg['content'] = selected_prompt + replaced_text
                prompt_added = True
            elif isinstance(content, list):  # 处理列表类型的内容
                for i in range(len(content)):
                    if isinstance(content[i], dict) and content[i].get('type') == 'text':
                        text = content[i].get('text', '')
                        replaced_text = text_replace(
                            text, translation_dict, case_sensitive, use_lemmatization)
                        if not prompt_added:
                            content[i]['text'] = selected_prompt + \
                                replaced_text
                            prompt_added = True
                        else:
                            content[i]['text'] = replaced_text

    # 组装新的请求数据
    new_payload = {
        "model": 'deepseek-v3',
        "messages": messages,
        "stream": req_data.get("stream"),
        "max_tokens": req_data.get("max_tokens"),
        "stop": req_data.get("stop"),
        "temperature": req_data.get("temperature"),
        "top_p": req_data.get("top_p"),
        "top_k": req_data.get("top_k"),
        "frequency_penalty": req_data.get("frequency_penalty"),
        "n": req_data.get("n"),
        "response_format": req_data.get("response_format")
    }
    # 发送请求到真实的API接口
    real_api_url = "https://www.api.com/v1/chat/completions"
    headers = {
        "Authorization": "Bearer " + OPENAI_API_KEY,
        "Content-Type": "application/json"
    }
    response = requests.post(
        real_api_url, json=new_payload, headers=headers, proxies={})
    # 返回结果给原始请求者
    return response.text, response.status_code


@app.route('/health', methods=['GET'])
def health_check():
    return jsonify({"status": "ok", "version": "1.0.0"})


if __name__ == '__main__':
    app.run(debug=True)

这样,就可以得到一个排版较为良好的双语对照PDF了。
另外提供一种目录书签的机翻办法,pdfpatch提供了将书签以xml形式导出的办法,可使用类似的办法对书签进行处理,再重新导入。

以猩红王座诅咒的首页为例子,可以看出它翻译出了“萨西隆符文领主”、“科沃萨”等PF名词。
劇透 -   :
银色的龙儿记录着旅程中收集的故事,细心地收纳于云雾中的巢穴
已经结束的故事在这里沉睡,新的故事正被撰写。

欢迎访问持续维护的枭熊1地图镜像The Trove dnd与coc原文资源镜像站FVTT世界安装包资源站5e不全书在线chm3r全书在线chm3r扩展在线chmPF1大合集在线chm染色器果园角色卡展示生成器MD2BBCODE格式转换器

最新版5e不全书