496 lines
13 KiB
Markdown
496 lines
13 KiB
Markdown
---
|
||
name: openakita/skills@translate-pdf
|
||
description: Translate PDF documents while preserving original layout, styling, tables, images, and formatting. Supports Simplified Chinese, Traditional Chinese, English, Japanese, Korean, and more. Page-by-page translation with structure preservation.
|
||
license: MIT
|
||
metadata:
|
||
author: openakita
|
||
version: "1.0.0"
|
||
---
|
||
|
||
# Translate PDF — PDF 文档翻译
|
||
|
||
## When to Use
|
||
|
||
- 用户需要将英文 PDF 翻译为中文(或其他语言)
|
||
- 需要保留 PDF 的原始排版、表格、图片和格式
|
||
- 需要翻译学术论文、技术文档、商业报告
|
||
- 需要双语对照的 PDF 输出
|
||
- 需要批量翻译多个 PDF 文件
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
### 必需依赖
|
||
|
||
| 依赖 | 用途 | 安装方式 |
|
||
|------|------|---------|
|
||
| Python ≥ 3.10 | 运行翻译脚本 | 系统预装 |
|
||
| `PyMuPDF` (fitz) | PDF 解析与重建 | `pip install PyMuPDF` |
|
||
| `httpx` | HTTP API 调用 | `pip install httpx` |
|
||
|
||
### 可选依赖
|
||
|
||
| 依赖 | 用途 | 安装方式 |
|
||
|------|------|---------|
|
||
| `pdf2image` | PDF 转图片(OCR 场景) | `pip install pdf2image` |
|
||
| `pytesseract` | OCR 文字识别 | `pip install pytesseract` |
|
||
| `pdfplumber` | 表格提取 | `pip install pdfplumber` |
|
||
| `reportlab` | PDF 生成 | `pip install reportlab` |
|
||
| `deep-translator` | 多引擎翻译 | `pip install deep-translator` |
|
||
| `openai` | GPT 翻译 | `pip install openai` |
|
||
|
||
### 系统级依赖
|
||
|
||
| 工具 | 用途 | 说明 |
|
||
|------|------|------|
|
||
| Poppler | pdf2image 的后端 | Windows: 下载 poppler-utils; macOS: `brew install poppler` |
|
||
| Tesseract | OCR 引擎 | Windows: 下载安装包; macOS: `brew install tesseract` |
|
||
| 中文字体 | PDF 中文渲染 | 系统需安装中文字体(微软雅黑、思源黑体等) |
|
||
|
||
### 验证安装
|
||
|
||
```bash
|
||
python -c "import fitz; print('PyMuPDF', fitz.version)"
|
||
python -c "import pdfplumber; print('pdfplumber OK')"
|
||
```
|
||
|
||
### LLM API 配置
|
||
|
||
翻译引擎首选 LLM(GPT-4 / Claude),在 `.env` 中配置:
|
||
|
||
```
|
||
OPENAI_API_KEY=sk-xxxxx
|
||
```
|
||
|
||
如果未配置 LLM API,将回退到 `deep-translator`(Google Translate / DeepL)。
|
||
|
||
---
|
||
|
||
## Instructions
|
||
|
||
### 支持的语言
|
||
|
||
| 语言代码 | 语言 | 翻译质量 |
|
||
|---------|------|---------|
|
||
| `zh-CN` | 简体中文 | ★★★★★ |
|
||
| `zh-TW` | 繁体中文 | ★★★★★ |
|
||
| `en` | 英文 | ★★★★★ |
|
||
| `ja` | 日文 | ★★★★ |
|
||
| `ko` | 韩文 | ★★★★ |
|
||
| `fr` | 法文 | ★★★★ |
|
||
| `de` | 德文 | ★★★★ |
|
||
| `es` | 西班牙文 | ★★★★ |
|
||
| `ru` | 俄文 | ★★★ |
|
||
| `ar` | 阿拉伯文 | ★★★ |
|
||
|
||
### 翻译引擎优先级
|
||
|
||
| 优先级 | 引擎 | 特点 |
|
||
|--------|------|------|
|
||
| 1 | LLM (GPT-4/Claude) | 最高质量,理解上下文,术语一致 |
|
||
| 2 | DeepL API | 高质量机器翻译 |
|
||
| 3 | Google Translate | 免费,覆盖语种广 |
|
||
|
||
Agent 按照优先级自动选择可用的翻译引擎。用户可以指定使用特定引擎。
|
||
|
||
### PDF 元素处理策略
|
||
|
||
| 元素 | 处理方式 |
|
||
|------|---------|
|
||
| 正文文本 | 翻译并保留字体大小、颜色、粗体/斜体 |
|
||
| 标题 | 翻译并保留层级和样式 |
|
||
| 表格 | 翻译单元格内容,保留表格结构 |
|
||
| 图片 | 保留原图不动 |
|
||
| 图片中的文字 | 可选 OCR 识别后翻译 |
|
||
| 页眉/页脚 | 翻译并保持位置 |
|
||
| 页码 | 保持不变 |
|
||
| 脚注/尾注 | 翻译内容,保留编号 |
|
||
| 目录 | 翻译条目,页码不变 |
|
||
| 书签 | 翻译标题 |
|
||
| 链接/URL | 保持不变 |
|
||
| 数学公式 | 保持不变 |
|
||
| 代码块 | 保持不变(仅翻译注释) |
|
||
| 水印 | 保留原样 |
|
||
|
||
---
|
||
|
||
## Workflows
|
||
|
||
### Workflow 1: 标准 PDF 翻译
|
||
|
||
**步骤 1 — 解析 PDF 结构**
|
||
|
||
```python
|
||
import fitz
|
||
|
||
doc = fitz.open("input.pdf")
|
||
print(f"总页数: {doc.page_count}")
|
||
print(f"元数据: {doc.metadata}")
|
||
|
||
for page_num in range(min(3, doc.page_count)):
|
||
page = doc[page_num]
|
||
text = page.get_text("dict")
|
||
print(f"第 {page_num + 1} 页: {len(text['blocks'])} 个文本块")
|
||
```
|
||
|
||
**步骤 2 — 逐页提取文本块**
|
||
|
||
```python
|
||
def extract_text_blocks(page):
|
||
"""提取页面中所有文本块及其位置和样式"""
|
||
blocks = []
|
||
text_dict = page.get_text("dict")
|
||
|
||
for block in text_dict["blocks"]:
|
||
if block["type"] == 0: # text block
|
||
for line in block["lines"]:
|
||
for span in line["spans"]:
|
||
blocks.append({
|
||
"text": span["text"],
|
||
"bbox": span["bbox"],
|
||
"font": span["font"],
|
||
"size": span["size"],
|
||
"color": span["color"],
|
||
"flags": span["flags"],
|
||
})
|
||
return blocks
|
||
```
|
||
|
||
**步骤 3 — 批量翻译**
|
||
|
||
将提取的文本按段落分组,批量发送给翻译引擎:
|
||
|
||
```python
|
||
async def translate_blocks(blocks, target_lang="zh-CN"):
|
||
paragraphs = merge_spans_to_paragraphs(blocks)
|
||
|
||
translated = []
|
||
for batch in chunk_list(paragraphs, batch_size=20):
|
||
texts = [p["text"] for p in batch]
|
||
results = await batch_translate(texts, target_lang)
|
||
translated.extend(results)
|
||
|
||
return translated
|
||
```
|
||
|
||
**LLM 翻译 Prompt**
|
||
|
||
```
|
||
你是一位专业的文档翻译师。请将以下文本从{source_lang}翻译为{target_lang}。
|
||
|
||
要求:
|
||
1. 保持专业术语的准确性和一致性
|
||
2. 保持段落结构不变
|
||
3. 对于技术术语,首次出现时附上原文:如"卷积神经网络(CNN)"
|
||
4. 不翻译代码、公式、URL、人名(除非有通用中文译名)
|
||
5. 保持原文的语气和风格
|
||
|
||
待翻译文本:
|
||
---
|
||
{text}
|
||
---
|
||
```
|
||
|
||
**步骤 4 — 重建 PDF**
|
||
|
||
```python
|
||
def rebuild_pdf(original_doc, translated_blocks, output_path):
|
||
"""用翻译后的文本替换原文,保留排版"""
|
||
new_doc = fitz.open()
|
||
|
||
for page_num in range(original_doc.page_count):
|
||
orig_page = original_doc[page_num]
|
||
new_page = new_doc.new_page(
|
||
width=orig_page.rect.width,
|
||
height=orig_page.rect.height
|
||
)
|
||
|
||
# 复制图片和非文本元素
|
||
new_page.show_pdf_page(new_page.rect, original_doc, page_num)
|
||
|
||
# 覆盖原文区域并写入译文
|
||
for block in translated_blocks[page_num]:
|
||
rect = fitz.Rect(block["bbox"])
|
||
new_page.draw_rect(rect, color=None, fill=(1, 1, 1))
|
||
new_page.insert_textbox(
|
||
rect,
|
||
block["translated_text"],
|
||
fontsize=block["size"] * 0.85,
|
||
fontname="china-ss",
|
||
align=fitz.TEXT_ALIGN_LEFT
|
||
)
|
||
|
||
new_doc.save(output_path)
|
||
```
|
||
|
||
**步骤 5 — 质量检查**
|
||
|
||
翻译完成后执行自动检查:
|
||
- 页数与原文一致
|
||
- 无空白页面
|
||
- 翻译覆盖率(已翻译文本 / 总文本 ≥ 95%)
|
||
- 字体渲染正常
|
||
|
||
---
|
||
|
||
### Workflow 2: 双语对照 PDF
|
||
|
||
生成左右/上下对照的双语 PDF:
|
||
|
||
**布局选项**
|
||
|
||
| 布局 | 说明 | 适用场景 |
|
||
|------|------|---------|
|
||
| 左右对照 | 左页原文、右页译文 | 学术论文、对比审阅 |
|
||
| 上下对照 | 段落级交替显示 | 学习材料 |
|
||
| 注释模式 | 译文作为侧边注释 | 保留原文为主 |
|
||
|
||
```python
|
||
def create_bilingual_pdf(original_doc, translated_blocks, output_path, layout="side-by-side"):
|
||
new_doc = fitz.open()
|
||
|
||
for page_num in range(original_doc.page_count):
|
||
orig_page = original_doc[page_num]
|
||
|
||
if layout == "side-by-side":
|
||
new_width = orig_page.rect.width * 2
|
||
new_page = new_doc.new_page(
|
||
width=new_width,
|
||
height=orig_page.rect.height
|
||
)
|
||
# 左侧放原文
|
||
new_page.show_pdf_page(
|
||
fitz.Rect(0, 0, orig_page.rect.width, orig_page.rect.height),
|
||
original_doc, page_num
|
||
)
|
||
# 右侧放译文
|
||
insert_translated_page(
|
||
new_page,
|
||
translated_blocks[page_num],
|
||
offset_x=orig_page.rect.width
|
||
)
|
||
|
||
new_doc.save(output_path)
|
||
```
|
||
|
||
---
|
||
|
||
### Workflow 3: 扫描版 PDF 翻译(OCR)
|
||
|
||
处理扫描件或图片型 PDF:
|
||
|
||
**步骤 1 — 检测 PDF 类型**
|
||
|
||
```python
|
||
def is_scanned_pdf(doc):
|
||
"""检测 PDF 是否为扫描件"""
|
||
for page_num in range(min(3, doc.page_count)):
|
||
page = doc[page_num]
|
||
text = page.get_text().strip()
|
||
images = page.get_images()
|
||
if not text and images:
|
||
return True
|
||
return False
|
||
```
|
||
|
||
**步骤 2 — OCR 识别**
|
||
|
||
```python
|
||
from pdf2image import convert_from_path
|
||
import pytesseract
|
||
|
||
images = convert_from_path("scanned.pdf", dpi=300)
|
||
for i, img in enumerate(images):
|
||
text = pytesseract.image_to_string(img, lang='eng')
|
||
# 使用 image_to_data 获取文字位置信息
|
||
data = pytesseract.image_to_data(img, lang='eng', output_type=pytesseract.Output.DICT)
|
||
```
|
||
|
||
**步骤 3** — 对 OCR 结果执行 Workflow 1 的翻译和重建流程
|
||
|
||
---
|
||
|
||
### Workflow 4: 批量 PDF 翻译
|
||
|
||
```python
|
||
import glob
|
||
import asyncio
|
||
|
||
async def batch_translate_pdfs(input_dir, output_dir, target_lang="zh-CN"):
|
||
pdf_files = glob.glob(f"{input_dir}/*.pdf")
|
||
print(f"发现 {len(pdf_files)} 个 PDF 文件")
|
||
|
||
for pdf_path in pdf_files:
|
||
output_path = os.path.join(
|
||
output_dir,
|
||
os.path.basename(pdf_path).replace('.pdf', f'_{target_lang}.pdf')
|
||
)
|
||
print(f"翻译: {pdf_path} -> {output_path}")
|
||
await translate_single_pdf(pdf_path, output_path, target_lang)
|
||
```
|
||
|
||
---
|
||
|
||
## Output Format
|
||
|
||
### 文件命名
|
||
|
||
```
|
||
{原文件名}_{目标语言}.pdf
|
||
```
|
||
|
||
示例:
|
||
- `research_paper_zh-CN.pdf`(翻译版)
|
||
- `research_paper_bilingual.pdf`(双语版)
|
||
|
||
### 输出报告
|
||
|
||
```
|
||
📄 PDF 翻译完成
|
||
- 原文件:research_paper.pdf (25 页)
|
||
- 译文件:research_paper_zh-CN.pdf (25 页)
|
||
- 源语言:English → 目标语言:简体中文
|
||
- 翻译引擎:GPT-4
|
||
- 翻译覆盖率:98.5%
|
||
- 表格数量:12 个(已翻译)
|
||
- 图片数量:8 张(已保留)
|
||
- 耗时:3 分 42 秒
|
||
- 费用估算:$0.85
|
||
```
|
||
|
||
---
|
||
|
||
## Common Pitfalls
|
||
|
||
### 1. 中文字体缺失导致乱码
|
||
|
||
**症状**:翻译后的 PDF 中文显示为方框或乱码
|
||
**解决**:确保系统安装了中文字体,并在 PyMuPDF 中注册:
|
||
|
||
```python
|
||
import fitz
|
||
|
||
# PyMuPDF 支持的中文字体
|
||
# "china-ss" = 思源宋体 (简体)
|
||
# "china-ts" = 思源宋体 (繁体)
|
||
# 或使用自定义字体
|
||
page.insert_font(fontname="custom-zh", fontfile="/path/to/NotoSansCJK-Regular.ttf")
|
||
```
|
||
|
||
### 2. 表格翻译后错位
|
||
|
||
**症状**:表格内容溢出单元格
|
||
**原因**:中文译文通常比英文短,但某些情况下可能更长
|
||
**解决**:
|
||
- 动态调整字体大小以适应单元格
|
||
- 允许文本自动换行
|
||
- 对于复杂表格,使用 pdfplumber 提取后单独翻译
|
||
|
||
### 3. 数学公式被错误翻译
|
||
|
||
**症状**:公式被当作文本翻译
|
||
**解决**:在翻译前识别并标记数学公式区域,跳过翻译:
|
||
|
||
```python
|
||
import re
|
||
|
||
def should_skip_translation(text):
|
||
"""判断文本是否应跳过翻译"""
|
||
# 数学公式模式
|
||
if re.match(r'^[\s\d\+\-\*\/\=\(\)\[\]\{\}\^\_\\\$]+$', text):
|
||
return True
|
||
# LaTeX 公式
|
||
if text.strip().startswith('\\') and not text.strip().startswith('\\text'):
|
||
return True
|
||
# 代码块
|
||
if re.match(r'^(def |class |import |from |const |let |var |function )', text.strip()):
|
||
return True
|
||
return False
|
||
```
|
||
|
||
### 4. 大文件内存溢出
|
||
|
||
**症状**:处理超过 100 页的大型 PDF 时内存不足
|
||
**解决**:逐页处理而非一次性加载:
|
||
|
||
```python
|
||
for page_num in range(doc.page_count):
|
||
page = doc[page_num]
|
||
# 处理当前页
|
||
process_page(page)
|
||
# 释放内存
|
||
page = None
|
||
```
|
||
|
||
### 5. OCR 识别率低
|
||
|
||
**症状**:扫描版 PDF 的文字识别错误多
|
||
**解决**:
|
||
- 提高扫描 DPI(≥ 300)
|
||
- 预处理图片(二值化、去噪、倾斜校正)
|
||
- 使用语言包:`pytesseract.image_to_string(img, lang='eng+chi_sim')`
|
||
|
||
### 6. 翻译术语不一致
|
||
|
||
**症状**:同一术语在不同页面有不同翻译
|
||
**解决**:
|
||
- 第一遍扫描时建立术语表
|
||
- 将术语表作为上下文传递给 LLM
|
||
- 翻译后用脚本检查术语一致性
|
||
|
||
```python
|
||
glossary = {
|
||
"machine learning": "机器学习",
|
||
"neural network": "神经网络",
|
||
"gradient descent": "梯度下降",
|
||
"backpropagation": "反向传播",
|
||
}
|
||
```
|
||
|
||
### 7. 页眉页脚重复翻译
|
||
|
||
**症状**:每页的页眉页脚翻译结果微有差异
|
||
**解决**:先识别页眉页脚模式,统一翻译一次后应用到所有页面
|
||
|
||
---
|
||
|
||
## 高级配置
|
||
|
||
### 翻译质量等级
|
||
|
||
| 等级 | 方式 | 速度 | 质量 | 费用 |
|
||
|------|------|------|------|------|
|
||
| 快速 | Google Translate | ★★★★★ | ★★★ | 免费 |
|
||
| 标准 | DeepL | ★★★★ | ★★★★ | $$ |
|
||
| 专业 | GPT-4 | ★★★ | ★★★★★ | $$$ |
|
||
| 人机协作 | GPT-4 + 人工审校 | ★★ | ★★★★★+ | $$$$ |
|
||
|
||
### 自定义术语表
|
||
|
||
用户可提供术语表文件(CSV/JSON)确保特定术语的翻译一致:
|
||
|
||
```json
|
||
{
|
||
"source_lang": "en",
|
||
"target_lang": "zh-CN",
|
||
"terms": {
|
||
"OpenAkita": "OpenAkita",
|
||
"Agent": "智能体",
|
||
"fine-tuning": "微调",
|
||
"prompt engineering": "提示词工程"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## EXTEND.md 扩展
|
||
|
||
用户可在技能同目录下创建 `EXTEND.md` 添加:
|
||
- 行业专用术语表
|
||
- 首选翻译引擎和质量等级
|
||
- 自定义字体路径
|
||
- PDF 模板和样式覆盖规则
|
||
- 特定文档类型的预处理规则
|