将 PDF 论文翻译为中文并写入飞书知识库,保持原文格式,图片放在原位置
将一篇 PDF 论文(通常为英文学术论文)翻译为中文,并写入用户指定的飞书知识库页面下,保持原论文格式,图片插入原位置。
用户需要提供以下信息(如果未提供则主动询问):
pip3 install PyMuPDF -qcurl -s -X POST 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' \
-H 'Content-Type: application/json' \
-d '{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}'
从 URL 中提取 node_token(URL 路径中 /wiki/ 后面的部分),然后:
curl -s -X GET 'https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=<NODE_TOKEN>' \
-H 'Authorization: Bearer <TOKEN>'
从返回结果获取 space_id。
curl -s -X POST 'https://open.feishu.cn/open-apis/wiki/v2/spaces/<SPACE_ID>/nodes' \
-H 'Authorization: Bearer <TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"obj_type":"docx","node_type":"origin","parent_node_token":"<PARENT_TOKEN>","title":"<TITLE>"}'
必须传 node_type: "origin",否则报 field validation failed。
返回的 obj_token 即为 document_id,同时也是根 block_id。
采用混合策略——照片用 pdfimages 提取原始素材,图表用 PyMuPDF 渲染裁剪:
| 图片类型 | 工具 | 原因 |
|---|---|---|
| 照片(机器人、任务场景) | pdfimages | 提取 PDF 嵌入的原始高清照片,无需裁剪 |
| 图表(柱状图、折线图) | PyMuPDF 渲染 + 裁剪 | 矢量绘制,pdfimages 无法提取 |
| 架构图(流程图、框图) | PyMuPDF 渲染 + 裁剪 | 包含矢量元素和文字标签 |
6a. 用 pdfimages 提取照片类图片:
# 列出 PDF 中所有嵌入图片的信息(页码、尺寸、格式)
pdfimages -list paper.pdf
# 提取所有图片为 PNG(-p 在文件名中包含页码)
pdfimages -png -p paper.pdf output_dir/img
提取后会得到大量碎片图片(一篇论文可能有 300+ 张)。需要:
ls -lS),大文件通常是完整的照片行/照片组6b. 用 PyMuPDF 渲染页面(用于图表和架构图):
import fitz
doc = fitz.open(pdf_path)
for page_num in range(len(doc)):
page = doc[page_num]
mat = fitz.Matrix(2, 2) # 2x 分辨率,页面变为约 1224x1584 像素
pix = page.get_pixmap(matrix=mat)
pix.save(f"figures/page_{page_num+1}.png")
6b. 用像素扫描法精确定位图表区域:
不要凭目测估坐标!必须用 numpy 扫描像素来精确确定图表边界:
import numpy as np
from PIL import Image
img = Image.open("figures/page_X.png")
arr = np.array(img)
# 逐行扫描,找内容区域的起止 y 坐标
for y in range(0, arr.shape[0], 5):
nw = np.sum(np.any(arr[y, x_start:x_end, :3] < 240, axis=1))
if nw > 10:
print(f"y={y}: non_white={nw}")
标准两栏论文在 2x 渲染下的参考坐标:
关键经验:
<240 比较安全6c. 裁剪后逐张保存小片段验证(不能只看缩略图!):
Read 工具显示的缩略图太小,可能看不清是否有残留文字。对关键图片(特别是图1这种紧邻作者名的),裁剪顶部/底部 50-80px 单独保存查看:
# 验证顶部是否有残留文字
top_strip = cropped_image.crop((0, 0, width, 80))
top_strip.save("fig1_top_check.png")
# 用 Read 工具单独查看这个小图
常见裁剪错误:
按论文结构分批写入,每批包含文字段落和对应位置的图片。每个 part 写完后 time.sleep(0.5) 防止频率限制。
写入文字块(可批量):
POST /open-apis/docx/v1/documents/<DOC_ID>/blocks/<DOC_ID>/children
Body: {"children": [<block1>, <block2>, ...]}
插入图片(三步流程,不可批量!):
Step 7a - 创建空 Image Block:
POST /open-apis/docx/v1/documents/<DOC_ID>/blocks/<DOC_ID>/children
Body: {"children": [{"block_type": 27, "image": {}}]}
返回 block_id(Image BlockID)。
Step 7b - 以 Image BlockID 为 parent_node 上传图片素材:
POST /open-apis/drive/v1/medias/upload_all
Form-data: [email protected], file_name=xx.png, parent_type=docx_image, parent_node=<IMAGE_BLOCK_ID>, size=<SIZE>
返回 file_token。
Step 7c - 更新 Image Block 设置图片:
PATCH /open-apis/docx/v1/documents/<DOC_ID>/blocks/<IMAGE_BLOCK_ID>
Body: {"replace_image": {"token": "<FILE_TOKEN>"}}
每张图片三步之间各 sleep 0.4 秒。
如果发现某张图裁剪有误需要替换:
无需删除重建 Block,直接对已有 Block 重新上传+patch 即可。
如果文档中有多余的测试块或错误内容,使用 batch_delete:
DELETE /open-apis/docx/v1/documents/<DOC_ID>/blocks/<PARENT_BLOCK_ID>/children/batch_delete
Body: {"start_index": 0, "end_index": 2}
start_index/end_index 是子块在父块 children 中的索引范围(左闭右开)。
| block_type | 字段名 | 说明 |
|---|---|---|
| 2 | text | 文本段落 |
| 3 | heading1 | 一级标题 |
| 4 | heading2 | 二级标题 |
| 5 | heading3 | 三级标题 |
| 22 | divider | 分割线(传空 {}) |
| 27 | image | 图片(创建时传空 {},不可直接传 token) |
文本/标题 Block 的 elements 数组可以混合 text_run 和 equation 两种元素:
{
"block_type": 2,
"text": {
"elements": [
{
"text_run": {
"content": "损失函数定义为:",
"text_element_style": {
"bold": false, "italic": false,
"strikethrough": false, "underline": false, "inline_code": false
}
}
},
{
"equation": {
"content": "L^{\\tau}(\\theta) = \\mathbb{E} \\| \\mathbf{v}_{\\theta} - \\mathbf{u} \\|^2"
}
},
{
"text_run": {
"content": ",其中...",
"text_element_style": {}
}
}
],
"style": {}
}
}
行内公式:在 elements 数组中混合 text_run 和 equation 元素,公式自然嵌入文字中。
独立居中公式:创建一个只包含 equation 元素的段落,设置 "style": {"align": 2} 居中:
{
"block_type": 2,
"text": {
"elements": [
{"equation": {"content": "\\mathbf{A}_t^{\\tau+\\delta} = \\mathbf{A}_t^{\\tau} + \\delta \\mathbf{v}_{\\theta}"}}
],
"style": {"align": 2}
}
}
公式中使用 LaTeX 语法,常用符号:
\mathbf{A}_t\theta, \tau, \epsilon, \pi\mathbb{E}\mathcal{N}(\mathbf{0}, \mathbf{I})\left\| \cdot \right\|^2A_t^{\tau}, a_{t+H-1}\mathbb{R}^{w \times d}格式原则:与原文对齐,不额外添加原文没有的子标题。独立公式单独成段居中,行内公式嵌入文字流中。
wiki:wiki、docx:document、drive:drive 权限"node_type": "origin"/home/ubuntu/.claude/skills/feishu_paper_tool.py 包含 FeishuDocWriter 类初稿写入飞书后,必须对每一章逐章进行 review 精翻。流程:
10a. 对照原文逐段检查:
10b. 常见问题清单:
10c. 修正方法:
resp = requests.get(f"{API}/docx/v1/documents/{DOC_ID}/blocks/{DOC_ID}", headers=H)
children = resp.json()["data"]["block"]["children"]
# 找到要修改的章节的 start_index 和 end_index
10d. Review 顺序: 按章节顺序逐章 review,每章完成后再进入下一章。不要批量处理多章——每章 review 需要重新读取 PDF 原文对照。
当用户说"我要名词解释"并给出术语列表时,执行以下流程:
在主翻译文档的 wiki 节点下创建子页面:
POST /wiki/v2/spaces/<SPACE_ID>/nodes
Body: {"obj_type":"docx","node_type":"origin","parent_node_token":"<主文档NODE_TOKEN>","title":"名词解释"}
记录子页面的 node_token(用于构造 URL)和 obj_token(用于写入内容)。
页面开头写入引导说明:
本页面收录论文中出现的关键术语和技术概念的解释。在主文档中,带下划线的术语可点击跳转到本页对应条目。
每个术语作为一个 heading2 条目,后跟解释段落。内容应包括:
找到主文档中该术语出现的位置,用 update_text_elements 把术语文字改为带链接的版本:
for b in all_blocks:
for el in b["text"]["elements"]:
if "text_run" in el and "术语名" in el["text_run"]["content"]:
# 找到了,记录 block_id 和文字内容
# 原文: "...我们使用 PaliGemma 作为基础模型..."
# 拆为三个 element:
elements = [
t("...我们使用 "),
t("PaliGemma", link="https://xxx.feishu.cn/wiki/<名词解释页NODE_TOKEN>"),
t(" 作为基础模型..."),
]
PATCH /docx/v1/documents/<DOC_ID>/blocks/<BLOCK_ID>
Body: {"update_text_elements": {"elements": [...]}}
链接格式:直接使用原始 URL,不需要 base64 编码:
{"text_run": {"content": "PaliGemma", "text_element_style": {
"link": {"url": "https://xxx.feishu.cn/wiki/<NODE_TOKEN>"}
}}}
注意:heading 块不支持 link 属性(会报 schema mismatch),只有 text 块中的 text_run 支持。
通常需要解释的术语类型: