基于 n8n 的 PDF 翻译器
安装 n8n
安装 n8n 很简单,使用以下命令:
npm install n8n -g
配置环境变量
因为我们在本地环境启动 n8n,所以需要配置一些环境变量。你可以在终端中使用以下命令来设置环境变量:
export R2_BUCKET=your_bucket_name
export R2_ACCESS_KEY_ID=your_r2_access_key_id
export R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
export R2_ENDPOINT=your_r2_endpoint
export GEMINI_API_KEY=your_gemini_api_key
以上环境变量是为了配置 n8n 与 Cloudflare R2 和 Gemini API 的连接信息。
启动 n8n
启动 n8n 的命令是:
n8n
这将会在本地的 http://localhost:5678
启动 n8n 的 Web 界面。
注意不要不安装 n8n 就直接按照官方文档使用 npx n8n
,因为这样每次启动的时候都要重新下载依赖,速度会特别慢。
n8n 工作流架构图
flowchart TD
A["Webhook (upload PDF) <br> POST /marker-to-zh <br> binary: pdf"] --> B["Save PDF <br> /writeBinaryFile → /data/in.pdf"]
B --> C["ExecuteCommand <br> Marker → Markdown <br> marker_single → stdout(md)"]
C --> D["Function <br> Chunk Markdown<br>按代码围栏安全分段"]
D --> E["Split In Batches <br> batch=1"]
E --> F["HTTP Request <br> Gemini 翻译"]
F --> G["Function <br> Parse 翻译结果"]
G --> E
E --> H["Merge (collect) <br> 收集所有段落译文"]
H --> I["Function <br> Assemble 中文 Markdown <br> 拼接为完整 md"]
I --> J["Function <br> 转 Binary(UTF-8) <br> index.md"]
J --> K["WriteBinaryFile <br> 保存中文 MD → /data/book/index.md"]
K --> L["ExecuteCommand <br> pdf-book-exporter → /data/book-cn.pdf"]
L --> M["ExecuteCommand <br> 封面/封底合成 <br> (gs→A4, qpdf 合并) → /data/final-cn.pdf"]
%% 并行:基于原始文件名生成 Key
A --> N["Function <br> 生成 Key <br> books/<原名>-zh.pdf"]
%% 上传分支
M --> O["ReadBinaryFile <br> 读取 /data/final-cn.pdf → binary.data"]
O --> P["S3 Upload → Cloudflare R2 <br> fileName = {{$json.key}} <br> Bucket = {{$env.R2_BUCKET}}"]
N --> P
P --> Q["Function <br> 生成下载链接 <br> https://assets.jimmysong.io/${key}"]
Q --> R["Respond to Webhook <br> 返回 {url}"]
没问题,我给你一个“能跑起来”的最小指引,包含两种触发方式:
- A)本地上传 PDF(
multipart/form-data
) - B)只给一个 PDF 的下载链接(n8n 自动去拉)
并把节点如何连线、各节点关键配置、curl 测试命令都写清楚。
一、把节点连起来(按下图/说明连线)
连线方法:在上游节点右侧的小圆点按住拖到下游节点左侧小圆点即可;有些节点(如 Split in Batches)有两条输出:
- 第一条(上边)→ 正常流转
- 第二条(下边)→ 用来“收集合并”
主链路(上传文件方式 A)
Webhook (upload PDF)
→ Save PDF (/data/in.pdf)
→ Marker → Markdown
→ Chunk Markdown
→ Split In Batches
├─(1 号输出)→ Gemini 翻译 → Parse 翻译结果 → (回到) Split In Batches(2 号输入口)
└─(2 号输出)→ Merge (collect)
→ Assemble 中文 Markdown
→ 转 Binary(UTF-8)
→ 保存中文 MD (/data/book/index.md)
→ 导出 PDF(pdf-book-exporter) (/data/book-cn.pdf)
→ 封面/封底合成 (/data/final-cn.pdf)
→ 读取合成 PDF (供上传)
→ 上传到 Cloudflare R2 (S3)
→ 生成下载链接
→ HTTP 响应
旁路(生成 Key)
Webhook (upload PDF) → 生成 Key
生成 Key → 上传到 Cloudflare R2 (S3)
注意:
- Split In Batches:把“Parse 翻译结果”的输出接回它的第二个输入口(界面中在节点下方)。
- 封面/封底合成 → 需要连出两条:一条到“读取合成 PDF (供上传)”,另一条可直接连到“S3 上传”(有的版本可以让两个上游都指向 S3)。
二、关键节点配置(逐个核对)
1)Webhook(上传 PDF)
- Path:
marker-to-zh
- Options → Binary data:✅ 勾选
- 你会得到两个 URL:Test URL 和 Production URL
2) Save PDF
- Operation:
binaryToFile
- File Name:
/data/in.pdf
- Data Property Name:
pdf
(这就是 Webhook 表单字段名,等会 curl 里也用pdf
)
3) Marker → Markdown (Execute Command)
- Command:
bash
- Arguments:
-lc
- Input(脚本内容):就是我给你的
marker_single ...
那段 - 要求你的主机能找到:
marker_single
、gs
、qpdf
、pdf-book-exporter
若报“找不到命令”,把它们加到 PATH 或用绝对路径。
4) Chunk Markdown (Function)
- 用我给你的分段代码(已贴好)
5) Split In Batches
- Batch Size:
1
- 1 号输出 →
Gemini 翻译
- 2 号输出 →
Merge (collect)
Parse 翻译结果
的输出接回 Split in Batches 的第二个输入口(表示“这段已处理好,可以继续下一段”)
6)Gemini 翻译(HTTP Request)
- URL:
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-pro:generateContent
- Send body:✅
- JSON / Body:用我给你的模板(含提示语)
- Authentication:
None
- Query params:
key = {{$env.GEMINI_API_KEY}}
7)Parse 翻译结果(Function)
- 用我贴的解析代码(把返回的
parts[].text
拼起来)
8)Merge (collect)
- Mode:
Merge by Field
或Multiplex
都行,这里用默认即可(我们只是把所有分段收集再传给下游)
9)Assemble 中文 Markdown(Function)
- 把所有段按
idx
排序后join('\n')
10)转 Binary(UTF-8)(Function)
- 输出
binary.data
,文件名设成index.md
11)保存中文 MD(Write Binary File)
- File Name:
/data/book/index.md
- Data Property Name:
data
12)导出 PDF(Execute Command)
- 执行:
pdf-book-exporter --input /data/book --output /data/book-cn.pdf
13)封面/封底合成(Execute Command)
- 可选:若
/data/cover.pdf
、/data/back-cover.pdf
不存在也不影响 - 产物:
/data/final-cn.pdf
14)读取合成 PDF(Read Binary File)
- File Path:
/data/final-cn.pdf
- Binary Property Name:
data
15)生成 Key(Function)
- 代码(从 Webhook 的原始文件名生成 key):
const fileName = $input.first().binary.pdf.fileName || 'document.pdf';
const base = fileName.replace(/\.[^.]+$/, '');
const key = `books/${base}-zh.pdf`;
return [{ json: { key } }];
16)上传到 Cloudflare R2(S3)
-
Credentials:用你在 Credentials → AWS 里建的
R2-credentials
-
Endpoint:
https://<accountid>.r2.cloudflarestorage.com
- 勾选 Force path style(兼容性更好)
- Operation:
Upload
- Bucket Name:
={{$env.R2_BUCKET}}
- Binary Property Name:
data
(就是“读取合成 PDF”的 binary 名) - File name:
={{$json.key}}
17)生成下载链接(Function)
- 代码:
const key = $json.key || 'books/final-cn.pdf';
return [{ json: { url: `https://assets.jimmysong.io/${key}` } }];
18)HTTP 响应(Respond to Webhook)
- Response Mode:
Last node
三、如何运行(本地部署)
1)准备环境变量(Node 安装 / .env)
export GEMINI_API_KEY="你的_Gemini_API_Key"
export R2_BUCKET="你的_R2_桶名"
export R2_ACCESS_KEY_ID="你的_R2_AccessKey"
export R2_SECRET_ACCESS_KEY="你的_R2_SecretKey"
export R2_ENDPOINT="https://<accountid>.r2.cloudflarestorage.com"
n8n start
或者写入
.env
后n8n start
。
2)在编辑器点开 Webhook 节点,点 Execute Workflow(测试模式)
- 复制 Test URL(形如
http://localhost:5678/webhook-test/marker-to-zh
)
3A)方式 A:本地上传 PDF(multipart)
curl -X POST \
-F "pdf=@/绝对路径/what-is-ebpf.pdf" \
"http://localhost:5678/webhook-test/marker-to-zh"
成功后会返回:
{"url":"https://assets.jimmysong.io/books/what-is-ebpf-zh.pdf"}
3B)方式 B:只给一个下载链接
想支持“给 URL 就行”,加两个小改动:
- Webhook 改成同时接受 JSON(不用勾额外选项,默认可收 JSON)。
-
在
Save PDF
前插一个 HTTP Request(Download) 节点(或替换 Save 逻辑): -
Method:
GET
- URL:
={{$json.url}}
- Response Format:
File
- Binary Property:
pdf
(让它与 Save PDF 的dataPropertyName=pdf
对齐) - 然后把 Webhook → HTTP Request(Download) → Save PDF 连起来。
这样你就可以:
curl -X POST "http://localhost:5678/webhook-test/marker-to-zh" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/what-is-ebpf.pdf"}'
小提示:再加一个 Function 读取 URL 文件名并写入
binary.pdf.fileName
,以便“生成 Key”节点能用 URL 的 base 名来命名。
四、常见坑位排查
- 找不到命令:
marker_single
/pdf-book-exporter
/gs
/qpdf
没在 PATH。 → 用绝对路径或在 n8n 启动脚本里export PATH=...
- S3 上传报权限/Endpoint:检查
R2-credentials
中的 Endpoint、Key/Secret;勾选 Force path style。 - Webhook 无响应:测试时要用 Test URL;激活(Active)后用 Production URL。
- 文件夹权限:确保 n8n 进程对
/data
有读写权限;不行就换你的用户家目录下的路径。
上传文件方式
我看你现在的 Webhook 节点是新版 UI,它已经没有单独的 Binary data 开关了,但功能还在——关键是在 Save PDF 节点(Write Binary File)里,把 dataPropertyName
配成 pdf
,就能接收上传的文件二进制内容。
你本地部署的情况下,要跑这个 workflow 有两种方式:
方式 A:本地直接上传 PDF
- 在 n8n 编辑器里点击 Webhook 节点,然后点击 Listen for test event(你现在截图里的按钮)。
- 复制它显示的 Test URL(比如
http://localhost:5678/webhook-test/marker-to-zh
)。 - 用 curl 在终端发一个 multipart 请求上传 PDF(字段名必须和 Save PDF 节点
dataPropertyName
一致,比如pdf
):
curl -X POST \
-F "pdf=@/绝对路径/what-is-ebpf.pdf" \
"http://localhost:5678/webhook-test/marker-to-zh"
- 上传成功后,Webhook 节点会触发后续流程,最后 HTTP Response 节点会返回生成的下载链接。
方式 B:给一个 PDF 下载链接(不直接上传文件)
如果你希望只传一个 URL,让 n8n 自己去下载:
-
在 Webhook 节点和 Save PDF 节点之间,加一个 HTTP Request 节点:
-
Method:
GET
- URL:
={{$json.url}}
- Response Format:
File
- Binary Property:
pdf
(和 Save PDF 节点的dataPropertyName
保持一致) - 然后用 curl 或 Postman 发 JSON 请求:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"url":"https://assets.jimmysong.io/books/what-is-ebpf.pdf"}' \
"http://localhost:5678/webhook-test/marker-to-zh"
这样 HTTP Request 节点会下载 PDF 并交给 Save PDF 节点处理。
我建议你现在先用 方式 A 跑通一次(直接上传 PDF),因为这是最简单确认整个链路能不能跑的办法。 等确认没问题,再加一个 HTTP Request 节点实现 方式 B(从链接下载 PDF)。
要我帮你直接画一张 方式 B 的节点连接图,你照着在 n8n 里插节点就能实现“传 URL 自动下载”的功能吗?这样你一次就能配好两种入口。