Skip to content

基于 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 URLProduction 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_singlegsqpdfpdf-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 paramskey = {{$env.GEMINI_API_KEY}}

7)Parse 翻译结果(Function)

  • 用我贴的解析代码(把返回的 parts[].text 拼起来)

8)Merge (collect)

  • Mode:Merge by FieldMultiplex 都行,这里用默认即可(我们只是把所有分段收集再传给下游)

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

或者写入 .envn8n 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 就行”,加两个小改动:

  1. Webhook 改成同时接受 JSON(不用勾额外选项,默认可收 JSON)。
  2. Save PDF 前插一个 HTTP Request(Download) 节点(或替换 Save 逻辑):

  3. Method:GET

  4. URL:={{$json.url}}
  5. Response Format:File
  6. Binary Property:pdf(让它与 Save PDF 的 dataPropertyName=pdf 对齐)
  7. 然后把 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

  1. 在 n8n 编辑器里点击 Webhook 节点,然后点击 Listen for test event(你现在截图里的按钮)。
  2. 复制它显示的 Test URL(比如 http://localhost:5678/webhook-test/marker-to-zh)。
  3. 用 curl 在终端发一个 multipart 请求上传 PDF(字段名必须和 Save PDF 节点 dataPropertyName 一致,比如 pdf):
curl -X POST \
  -F "pdf=@/绝对路径/what-is-ebpf.pdf" \
  "http://localhost:5678/webhook-test/marker-to-zh"
  1. 上传成功后,Webhook 节点会触发后续流程,最后 HTTP Response 节点会返回生成的下载链接。

方式 B:给一个 PDF 下载链接(不直接上传文件)

如果你希望只传一个 URL,让 n8n 自己去下载:

  1. 在 Webhook 节点和 Save PDF 节点之间,加一个 HTTP Request 节点:

  2. Method: GET

  3. URL: ={{$json.url}}
  4. Response Format: File
  5. Binary Property: pdf(和 Save PDF 节点的 dataPropertyName 保持一致)
  6. 然后用 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 自动下载”的功能吗?这样你一次就能配好两种入口。