ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Codex 100个真实案例 - 用AI做中英双语对照文档生成器(论文翻译利器)

Codex 100个真实案例 - 用AI做中英双语对照文档生成器(论文翻译利器) 1. 为什么我要用 Codex 做一个中英双语对照文档生成器读英文论文最难受的地方不是看不懂单词而是看完一段英文脑子里还得再翻译一遍才能理解。尤其是几十页的 PDF来回切换翻译软件段落对不上、术语前后不一致读到后面已经忘了前面在讲什么。我试过直接丢给翻译工具整篇翻结果更糟公式被翻乱、代码块被当成正文、专业术语一会儿神经网络一会儿类神经网络最后还得人工逐段校对比自己读还累。所以这次我用 Codex 从零搭了一个中英双语对照文档生成器目标很明确上传英文文档.txt / .md / .docx / .pdf自动逐段翻译输出左英右中的对照 PDF段落一一对齐术语表可自定义支持批量处理。它适合科研人员读论文、留学生看教材、技术同学啃英文文档这几类场景。整篇我会把可复制的 Codex 配置骨架、TaoToken 统一 Key/API 通道接入步骤、双语对照输出的验证动作以及我踩过的报错排查清单都写清楚。你跟着做能跑出一个真正能用的工具而不是一个玩具 demo。2. TaoToken 前置准备统一 Key 与 API 通道在写翻译服务之前先把模型通道打通。这个生成器的翻译质量完全取决于背后的大模型所以我用 TaoToken 作为统一入口一个 Key 就能调用多种模型省得在代码里维护一堆不同的 API 地址和鉴权方式。TaoToken 的定位是给开发者和 AI 应用提供统一的模型调用通道兼容 OpenAI 接口格式所以我们的translator.js里可以直接用chat/completions的标准写法不用为每家模型单独适配。你需要做三件事第一注册并登录后进入控制台创建一个 API Key。地址是https://taotoken.net/api-keys创建后复制保存后面写进.env。第二确认你要用的模型名。TaoToken 支持多种主流模型翻译这种任务我建议用中英能力均衡、上下文较长的模型术语一致性会更好。第三把 API 基地址记下来https://taotoken.net/api。注意这个地址不带任何查询参数直接作为base_url使用。注意API Key 只放在服务端.env里绝对不要写进前端代码或提交到 Git 仓库。前端只调用你自己的后端接口由后端去请求模型。如果你还没决定用哪个模型可以先到模型对话页面手动试几段论文摘要对比一下翻译风格再定下来写进配置。地址是https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite。3. 可复制的 Codex 配置骨架含 config.tomlCodex 的强项是用自然语言驱动工程但前提是项目骨架要清晰。我先把项目结构定下来再让 Codex 逐个模块实现避免一次性生成太多代码导致质量下降。3.1 项目初始化与目录结构在终端里让 Codex 创建项目请帮我创建一个中英双语对照文档生成器项目名为 bilingual-doc-generator。 后端用 Node.js Express前端用 Vue 3 Element Plus。 后端放在 server 目录前端放在 client 目录。 后端需要支持文件上传、翻译处理、PDF 生成。 前端需要支持拖拽上传、实时预览、进度展示。Codex 会执行类似下面的初始化命令mkdir bilingual-doc-generator cd bilingual-doc-generator mkdir -p server/src/{routes,services,utils} mkdir -p server/uploads server/output server/glossaries cd server pnpm init pnpm add express multer cors ws dotenv pnpm add pdfkit mammoth pdf-parse marked archiver pnpm add -D nodemon cd .. pnpm create vuelatest client -- --template vue cd client pnpm add element-plus element-plus/icons-vue axios生成的目录结构大致是这样bilingual-doc-generator/ ├── server/ │ ├── src/ │ │ ├── routes/ # upload / translate / export 路由 │ │ ├── services/ # parser / translator / pdfGenerator / glossary │ │ ├── utils/ # textSplitter 等工具 │ │ └── app.js # 应用入口 │ ├── uploads/ # 上传文件 │ ├── output/ # 输出 PDF │ ├── glossaries/ # 术语表 │ └── package.json ├── client/ │ └── src/ │ ├── views/Home.vue │ └── components/ └── README.md3.2 Codex 的 config.toml 配置Codex 的配置文件放在用户目录下的.codex/config.toml。我这份配置的核心思路是把模型通道指向 TaoToken让 Codex 在生成代码和后续调试时都走同一条通道。# ~/.codex/config.toml model gpt-4o model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat然后在 shell 里导出 KeyWindows 用set或系统环境变量export TAOTOKEN_API_KEYsk-你的TaoToken密钥这样 Codex 在会话里就能直接调用模型。如果你打算长期用 Codex 做编码和 Agent 任务建议单独开一个 Coding Plan额度更划算地址是https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。3.3 后端 .env 配置后端服务自己也要调模型所以单独写一份.env# server/.env LLM_BASE_URLhttps://taotoken.net/api LLM_API_KEYsk-你的TaoToken密钥 LLM_MODELgpt-4o PORT3200 MAX_FILE_SIZE20971520注意这里LLM_BASE_URL用的是https://taotoken.net/api代码里再拼/v1/chat/completions这样切换模型时只改.env不动业务代码。4. 核心模块实现解析、翻译、术语表、PDF骨架搭好后逐个模块让 Codex 实现。这里我把关键代码贴出来你可以直接对照。4.1 文档解析服务 parser.js翻译的第一步是把各种格式的文档正确解析成纯文本段落。让 Codex 实现时我特别强调要保留段落结构、过滤空段、标记段落类型// server/src/services/parser.js const fs require(fs); const path require(path); const mammoth require(mammoth); const pdfParse require(pdf-parse); class DocumentParser { async parse(filePath) { const ext path.extname(filePath).toLowerCase(); let rawText ; switch (ext) { case .txt: rawText fs.readFileSync(filePath, utf-8); break; case .md: rawText this.stripMarkdown(fs.readFileSync(filePath, utf-8)); break; case .docx: rawText (await mammoth.extractRawText({ path: filePath })).value; break; case .pdf: rawText (await pdfParse(fs.readFileSync(filePath))).text; break; default: throw new Error(不支持的文件格式: ${ext}); } return this.splitIntoParagraphs(rawText); } stripMarkdown(content) { return content .replace(/^#{1,6}\s(.)$/gm, $1) .replace(/\*\*(.?)\*\*/g, $1) .replace(/(.?)/g, $1) .replace(/[\s\S]*?/g, ) .replace(/!\[.*?\]\(.*?\)/g, ) .replace(/\[(.?)\]\(.*?\)/g, $1); } splitIntoParagraphs(text) { const raw text.split(/\n{2,}/); const paragraphs []; let index 0; for (const para of raw) { const trimmed para.trim(); if (!trimmed || trimmed.length 2) continue; paragraphs.push({ index: index, text: trimmed, type: this.detectType(trimmed), }); } return paragraphs; } detectType(text) { if (text.length 100 !text.endsWith(.)) return heading; if (/^[\d]\.|^[-*]/.test(text)) return list; if (/^(function|const|let|var|import|class|def)\s/.test(text)) return code; return paragraph; } } module.exports new DocumentParser();给段落打类型标记很关键代码段不翻译、标题保持简洁后续差异化处理全靠这个字段。4.2 翻译服务 translator.js翻译是核心。这里对接 TaoToken 的 OpenAI 兼容接口同时做并发控制和重试// server/src/services/translator.js const https require(https); const http require(http); class TranslatorService { constructor() { this.baseUrl process.env.LLM_BASE_URL || https://taotoken.net/api; this.apiKey process.env.LLM_API_KEY; this.model process.env.LLM_MODEL || gpt-4o; this.maxConcurrency 5; this.maxRetries 3; } async translateParagraphs(paragraphs, glossary {}, onProgress null) { const results new Array(paragraphs.length); let completed 0; const tasks paragraphs.map((para, i) async () { if (para.type code) { results[i] { ...para, translation: para.text, skipped: true }; } else { results[i] { ...para, translation: await this.translateWithRetry(para.text, glossary), skipped: false, }; } completed; if (onProgress) { onProgress({ total: paragraphs.length, completed, percent: Math.round((completed / paragraphs.length) * 100), }); } }); await this.runWithConcurrency(tasks); return results; } async translateWithRetry(text, glossary, attempt 1) { try { return await this.callAPI(text, glossary); } catch (err) { if (attempt this.maxRetries) { await this.sleep(1000 * attempt); return this.translateWithRetry(text, glossary, attempt 1); } return [翻译失败] ${text.substring(0, 50)}...; } } async callAPI(text, glossary) { let glossaryPrompt ; const entries Object.entries(glossary); if (entries.length 0) { const terms entries.map(([en, zh]) ${en} → ${zh}).join(\n); glossaryPrompt \n\n请严格遵守以下术语表翻译\n${terms}; } const systemPrompt 你是一个专业的英中翻译专家。请将以下英文文本翻译为中文。 要求 1. 翻译准确、流畅、自然 2. 保留原文的段落结构和格式 3. 专业术语翻译精准 4. 不要添加任何解释或注释只输出翻译结果 5. 数字、公式、代码保持原样不翻译${glossaryPrompt}; const body JSON.stringify({ model: this.model, messages: [ { role: system, content: systemPrompt }, { role: user, content: text }, ], temperature: 0.3, max_tokens: 4096, }); const url new URL(${this.baseUrl}/v1/chat/completions); const client url.protocol https: ? https : http; return new Promise((resolve, reject) { const req client.request( { hostname: url.hostname, port: url.port || (url.protocol https: ? 443 : 80), path: url.pathname, method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${this.apiKey}, Content-Length: Buffer.byteLength(body), }, }, (res) { let data ; res.on(data, (chunk) (data chunk)); res.on(end, () { try { const json JSON.parse(data); if (json.choices json.choices[0]) { resolve(json.choices[0].message.content.trim()); } else { reject(new Error(API 返回格式异常)); } } catch (e) { reject(new Error(JSON 解析失败: ${e.message})); } }); } ); req.on(error, reject); req.write(body); req.end(); }); } async runWithConcurrency(tasks) { const executing new Set(); for (const task of tasks) { const p task().then(() executing.delete(p)); executing.add(p); if (executing.size this.maxConcurrency) { await Promise.race(executing); } } await Promise.all(executing); } sleep(ms) { return new Promise((r) setTimeout(r, ms)); } } module.exports new TranslatorService();几个设计点值得说明temperature: 0.3保证翻译一致性术语表直接注入 system prompt比事后替换更自然并发上限 5 避免触发限流。4.3 术语表服务 glossary.js专业文档翻译最头疼的就是术语不统一。术语表服务支持多领域、增删改查、CSV 导入// server/src/services/glossary.js const fs require(fs); const path require(path); class GlossaryService { constructor() { this.dir path.join(__dirname, ../../glossaries); if (!fs.existsSync(this.dir)) fs.mkdirSync(this.dir, { recursive: true }); this.cache new Map(); this.loadBuiltin(); } loadBuiltin() { const builtin { computer-science: { Machine Learning: 机器学习, Deep Learning: 深度学习, Neural Network: 神经网络, Natural Language Processing: 自然语言处理, Reinforcement Learning: 强化学习, Gradient Descent: 梯度下降, Overfitting: 过拟合, Attention Mechanism: 注意力机制, Fine-tuning: 微调, Embedding: 嵌入, }, medical: { Randomized Controlled Trial: 随机对照试验, Placebo: 安慰剂, Biomarker: 生物标志物, Efficacy: 疗效, Prognosis: 预后, }, }; for (const [domain, terms] of Object.entries(builtin)) { this.cache.set(domain, terms); this.save(domain, terms); } } get(domain) { if (this.cache.has(domain)) return { ...this.cache.get(domain) }; const file path.join(this.dir, ${domain}.json); if (fs.existsSync(file)) { const terms JSON.parse(fs.readFileSync(file, utf-8)); this.cache.set(domain, terms); return { ...terms }; } return {}; } merge(domains) { const merged {}; for (const d of domains) Object.assign(merged, this.get(d)); return merged; } addTerm(domain, en, zh) { const g this.get(domain); g[en] zh; this.cache.set(domain, g); this.save(domain, g); return g; } importCSV(domain, csv) { const g this.get(domain); for (const line of csv.split(\n)) { const t line.trim(); if (!t || t.startsWith(#)) continue; const [en, zh] t.split(,).map((s) s.trim().replace(/^(.*)$/, $1)); if (en zh) g[en] zh; } this.cache.set(domain, g); this.save(domain, g); return g; } save(domain, glossary) { fs.writeFileSync( path.join(this.dir, ${domain}.json), JSON.stringify(glossary, null, 2), utf-8 ); } } module.exports new GlossaryService();4.4 PDF 双语对照生成 pdfGenerator.js这是最有成就感的部分——左英右中中间一条分割线页眉页脚齐全// server/src/services/pdfGenerator.js const PDFDocument require(pdfkit); const fs require(fs); class PDFGeneratorService { constructor() { this.pageWidth 595.28; this.pageHeight 841.89; this.margin 40; this.columnGap 20; this.headerHeight 50; this.footerHeight 40; const usable this.pageWidth - 2 * this.margin - this.columnGap; this.columnWidth usable / 2; this.chineseFont /System/Library/Fonts/PingFang.ttc; this.fontSize 10; this.headingSize 14; } async generate(paragraphs, title, outputPath) { return new Promise((resolve, reject) { const doc new PDFDocument({ size: A4, margins: { top: this.margin this.headerHeight, bottom: this.margin this.footerHeight, left: this.margin, right: this.margin, }, bufferPages: true, }); const stream fs.createWriteStream(outputPath); doc.pipe(stream); if (fs.existsSync(this.chineseFont)) { doc.registerFont(Chinese, this.chineseFont); } let y this.margin this.headerHeight; let pageNum 1; this.drawHeader(doc, title); for (const para of paragraphs) { if (para.skipped) continue; const isHeading para.type heading; const size isHeading ? this.headingSize : this.fontSize; const leftH this.measure(doc, para.text, size, Helvetica); const rightH this.measure(doc, para.translation, size, Chinese); const rowH Math.max(leftH, rightH) 15; const maxY this.pageHeight - this.margin - this.footerHeight; if (y rowH maxY) { this.drawFooter(doc, pageNum); doc.addPage(); pageNum; y this.margin this.headerHeight; this.drawHeader(doc, title); } doc.font(isHeading ? Helvetica-Bold : Helvetica).fontSize(size); doc.text(para.text, this.margin, y, { width: this.columnWidth, lineGap: 3 }); const dividerX this.margin this.columnWidth this.columnGap / 2; doc .save() .moveTo(dividerX, y) .lineTo(dividerX, y rowH - 10) .strokeColor(#cccccc) .lineWidth(0.5) .stroke() .restore(); const rightX this.margin this.columnWidth this.columnGap; doc.font(Chinese).fontSize(size); doc.text(para.translation, rightX, y, { width: this.columnWidth, lineGap: 3 }); y rowH; } this.drawFooter(doc, pageNum); doc.end(); stream.on(finish, () resolve(outputPath)); stream.on(error, reject); }); } drawHeader(doc, title) { doc .save() .font(Chinese) .fontSize(9) .fillColor(#888888) .text(title, this.margin, this.margin, { width: this.pageWidth - 2 * this.margin, align: center, }); const lineY this.margin this.headerHeight - 10; doc .moveTo(this.margin, lineY) .lineTo(this.pageWidth - this.margin, lineY) .strokeColor(#dddddd) .lineWidth(0.5) .stroke() .restore(); } drawFooter(doc, pageNum) { const y this.pageHeight - this.margin - 15; doc .save() .font(Helvetica) .fontSize(8) .fillColor(#888888) .text(- ${pageNum} -, this.margin, y, { width: this.pageWidth - 2 * this.margin, align: center, }) .restore(); } measure(doc, text, size, font) { doc.font(font).fontSize(size); return doc.heightOfString(text, { width: this.columnWidth, lineGap: 3 }); } } module.exports new PDFGeneratorService();5. 验证请求与成功结果代码写完后必须验证整条链路能跑通。我分三步验证。5.1 启动服务# 启动后端 cd server pnpm dev # 输出 双语文档生成器服务已启动: http://localhost:3200 # 新终端启动前端 cd client pnpm dev5.2 用 curl 验证翻译接口先单独验证 TaoToken 通道是否通curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: gpt-4o, messages: [ {role: system, content: 你是英中翻译专家只输出译文。}, {role: user, content: Attention is all you need.} ], temperature: 0.3 }正常返回里choices[0].message.content应该是类似注意力机制就是你所需要的一切这样的中文。如果这里就报错先排查 Key 和模型名别急着往下走。5.3 验证完整翻译流程上传一个测试文档调用翻译接口curl -X POST http://localhost:3200/api/translate \ -H Content-Type: application/json \ -d { filePath: /绝对路径/server/uploads/test.pdf, fileName: test.pdf, glossaryDomains: [computer-science] }返回{ success: true, data: { taskId: xxx, status: processing } }说明任务已创建。等几秒后查询状态curl http://localhost:3200/api/translate/status/你的taskId成功时data.status为completeddata.paragraphs里每个段落都有text英文和translation中文两个字段一一对应。最后下载 PDFcurl -O http://localhost:3200/api/export/你的taskId打开 PDF应该看到左英右中、段落对齐、页眉有标题、页脚有页码。到这一步整个生成器就跑通了。6. 本篇常见报错排查清单下面这些是我实际踩过的坑按出现频率排序。报错一401 Unauthorized或invalid api key原因基本是 Key 没读到或写错。检查.env里LLM_API_KEY是否和 TaoToken 控制台里的一致注意不要有多余空格或引号。Node 里process.env读不到时确认dotenv在app.js顶部就require了。报错二404 Not Found请求模型接口多半是 base_url 拼错了。正确写法是https://taotoken.net/api加上/v1/chat/completions。如果你在.env里把/v1也写进 base_url就会变成/v1/v1/...。统一约定base_url 只到/api。报错三PDF 中文显示成方块或乱码PDFKit 默认字体不含中文。确认registerFont(Chinese, ...)的字体路径存在。macOS 用/System/Library/Fonts/PingFang.ttcLinux 服务器上要换成系统里实际有的中文字体比如/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc。路径不存在时fs.existsSync会返回 false字体就没注册上。报错四翻译到一半卡住或超时通常是并发太高触发限流。把maxConcurrency从 5 降到 2 或 3同时确认重试逻辑生效。另外长段落要先用textSplitter切分单段超过 500 字符就按句号切避免单次请求 token 超限。报错五Cannot find module archiver批量打包用到的archiver没装。在server目录执行pnpm add archiver。同理mammoth、pdf-parse、pdfkit任何一个缺失都会在对应格式解析时报模块找不到。报错六WebSocket 连不上进度条不动检查前端ws://localhost:3200/ws?taskIdxxx的端口和后端一致且后端WebSocketServer的path是/ws。如果前端跑在 Vite 的 5173 端口跨端口连接 WebSocket 一般没问题但要注意别被浏览器混合内容策略拦http 页面连 ws 可以https 页面必须连 wss。报错七docx 解析出来全是空mammoth.extractRawText对某些复杂排版的 docx 支持有限。如果文档里有大量文本框或表格解析结果可能为空。这种情况建议先另存为纯文本或 PDF 再上传。排查时记住一个原则先单独验证 TaoToken 通道第 5.2 节那条 curl通道通了再查业务代码。大部分翻译失败其实卡在通道层而不是解析或 PDF 层。7. 继续完善与接入建议跑通基础版后这个工具还能继续长。比如加 OCR 支持扫描版 PDF、扩展多语言、加翻译记忆复用相似段落、多人协同校对。每一个扩展都可以通过给 Codex 一个清晰的提示词来实现。如果你打算把它做成长期使用的工具建议把模型调用统一收敛到 TaoToken 的 API Key 上这样切换模型、调整额度都在一个地方管理。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有完整的接口说明和参数示例。需要管理多个 Key 或查看用量去控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite。最后提醒一句术语表是这个工具的灵魂。论文翻译质量的高低八成取决于你有没有把领域术语喂对。先把你要读的那篇论文里的核心术语整理成 CSV 导进去再跑翻译效果会明显不一样。
返回列表