把 PDF 讲义转成 Word,文字、公式、表格、图片都是原生对象,不是截图。 公式转出来是 Word 原生公式,可以直接编辑、可以被搜索。
跟 pdf_to_word(本地版)是同一套出 Word 的管线,区别只在识别那一步
放到了 MinerU 的云端:
本地版 pdf_to_word |
云端版(这个) | |
|---|---|---|
| 显卡 | 必须有 NVIDIA 独显 | 不用 |
| 首次下载 | 7.4 GB(运行库 2.8 + 模型 4.6) | 不用 |
| 依赖体积 | 5.1 GB | 95 MB |
| Office | 不用装(2026-09-09 起 XSL 随包) | 不用装(转公式那个文件已内置) |
| 额度 | 无限 | 每个 mineru 账号每天 1000 页优先解析 + 5000 个文件,可攒多个号叠加 |
| 文件 | 全程不出本机 | 要上传到 MinerU 的服务器 |
| 进度 | 逐页百分比 + 倒计时 | 只有「第几份 / 共几份」 |
| 并排的配图 | 原样保留 | 拆成一张一行 |
门槛从「N 卡 + 7.4 GB」降到「一个免费 token」。
本地版用的是 MinerU 的 hybrid-engine 后端,解析强度开到 effort=high。
云端 API 没有这两个开关 —— 它只认五个参数:model_version(只能填 vlm
或 pipeline)、language、is_ocr、enable_formula、enable_table。
往请求里塞 backend=hybrid-engine 或 effort=high 不报错,但服务端直接忽略
(2026-09-09 实测:带不带这两个参数,产出的 word/document.xml 哈希一模一样)。
差别落在并排的配图上。讲义里「甲、乙、丙」三张图排一行、选择题的 A/B/C/D 四张图排一行,这是常态。本地版能把这一行当成一个完整图形区域整体切下来,进了 Word 还是并排的;云端只能一张图算一块,四个选项变成四行,一道题占掉大半页。
同一份 21 页物理讲义,两边实测:
本地版 pdf_to_word |
云端版(这个) | |
|---|---|---|
| 图片块 | 29 | 40(并排图被拆散) |
| 原生公式 | 147 | 192 |
| 填空线 | 40 | 42 |
公式和填空线这边更多,图片块那栏是输的 —— 40 比 29 多出来的 11 块不是多认了 11 张图,是原来并排的一张被切成了好几张。
怎么选:讲义、试卷这类版式要紧、又有 N 卡的,用 本地版;没显卡,或者只要把内容 捞成可编辑的文字和公式、不在乎图排成几行,云端版够用而且省事。
- 双击
PDF转Word.exe - 第一次打开会让你填 MinerU 的 API token —— 去 mineru.net 注册,免费。 每个账号每天 1000 页优先解析、5000 个文件
- 把 PDF 拖进窗口,点「开始转换」
不用装 Office。 公式要用微软的 MML2OMML.XSL 转换,这个文件已经跟软件
一起打包好了(runtime/xsl/),装了就能用。WPS 也不用装 —— 转出来的是标准
.docx,用什么打开是你的事。
文件会上传。 解析在 MinerU 的服务器上跑,不能上传的材料请用本地版。
有两个包,按你的机器选:
| 你的 Mac | 下载 |
|---|---|
| M1/M2/M3/M4 等 Apple 芯片 | teach-studio-<版本>-mac-arm64.dmg |
| Intel 芯片(2020 年前的老机器居多) | teach-studio-<版本>-mac-x64.dmg |
不确定的话点左上角苹果图标 →「关于本机」,看「芯片」那一行。
🔴 第一次打开会被系统拦住,提示"无法打开,因为无法验证开发者"。这不是软件坏了,是因为这个包没有花钱买苹果的开发者签名。放行一次就好,以后正常:
打开「系统设置 → 隐私与安全性」,往下拉,会看到一行"已阻止使用 PDF转Word",点旁边的「仍要打开」。
其余用法跟 Windows 完全一样:填 token、拖文件、点开始。token 和转换历史存在 ~/Library/Application Support/teach-studio/。
mineru 一个手机号能注册一个账号、微信还能再注册一个,每个号的每日额度 分开算。软件里可以填 1~10 个 token(左下角「设置」里改):
- 每转一份之前,挑「今天用得最少」的那个号
- 某个号额度到顶了(服务端返回
-60018),自动换下一个接着转,不用管 - 用量显示在设置页里
额度实际是两条,分开算:
| 限制 | 性质 | |
|---|---|---|
| 文件解析个数 | 5000 / 天 | 硬上限,撞了直接拒绝(-60018) |
| 优先解析页数 | 1000 / 天 | 软上限,超了不是不给用,是排队降优先级 |
软件记的是页数——它才是实际会碰到的那条(一份讲义二三十页,五十份 就到 1000 了;5000 个文件正常传不完)。而且因为它是软的,账只用来 「先试哪个号」,从不拿它拦人。
🔴 那个用量数字只算得到通过本软件转掉的。 你自己在 mineru 网页上传的 文件,软件无从知道,所以这个数只会偏小。它只用来决定「先用哪个号」, 真到顶了以服务端的答复为准。
┌ 添加文件 添加文件夹 移除全部 输出:跟原 PDF 放一起 更改 ┐
├──────────────────────────────────────────────────────────────┤
│ 文件(共 3 份) 全选 全不选 页数 │
│ ☑ 解不等式.pdf 10 页 移除 │
│ ☑ 电场中力的性质.pdf 第 7、12 页没有文字层 23 页 移除 │
│ ● 太大的.pdf 这份 812 页,超过云端单次上限 移除 │
├──────────────────────────────────────────────────────────────┤
│ ● 2 个 token · 今天已用 340 页 设置 历史 关于 选中 2 份 · 33 页 [开始] │
└──────────────────────────────────────────────────────────────┘
拖进来默认全勾,直接点「开始转换」就行 —— picked 是三态:true /
false / 键不存在 = 选中,新加进来的不用挨个勾。点整行任意位置都能
勾(checkbox 设了 pointer-events:none 让点击穿透到行上),一批几十份时
这个差别很大。没勾的那行文件名会调淡,页数固定 52px 右对齐成一列。
没有进度条,也没有倒计时。 云端只告诉你 pending / running / done,
给不出百分比 —— 与其编一个数,不如老实显示「正在转第 2 份,共 5 份」和
云端当前那行状态。
转着的时候还能继续加文件。 新拖进来的进「待办」,摆在主列表下面 单独一块,写着「待办 N 份,这批转完自动接上」。这批转完自动晋升成新一批, 不用你点。中途按了停止的话不会自动开新的 —— 待办并回待转清单,由你决定。
待办不混进主列表是有意的:混了就得在一张表里区分「这批的」和「下批的」, 而那正是「4 个文件下面又冒出一模一样 4 个」那个 bug 的土壤。分开摆, 一眼看清哪些在转、哪些排着。
「停止」只是不再等它。 任务提交出去就在 MinerU 那边跑着,额度照扣。 这跟本地版当场杀进程有本质区别,界面上没有含糊其辞。
报告:全都干净时不出现。有转失败的、有扫描页、有公式没转成、或者 删掉过图内文字,才在底下给一个「看报告」。开头写着「没列出来的不代表一定 对」。
出现的时候每一份都列,成功的也列 —— 给「N 页 · 公式 8/8 · 表 2 · 图 3」 和落盘路径。只列有问题的那几份看着清爽,但「没列出来」既可能是干净、也可能 是被漏了,两种情况长得一模一样,用户没法拿它一份份核对 Word。报告不落盘, 所以底栏给了「复制」。
日志:转换中和转完都能点,摊开云端每轮返回的那几行状态。数据一直都在
(后端每轮都返回 lines),原先只在当前那行右边显示最后一句。
历史:任何时候都能点,最多留 200 条,关掉软件也还在。
转过的东西按内容指纹存着(_tmp/cache/<指纹>/),指纹是
sha256(PDF 内容) + 提交参数。再转同一份文件时直接用上次的识别结果,
不上传、不扣额度,日志里会说一句「这份转过,不占额度」。
所以:
- 同一份 PDF 放在两个目录、改了名字 —— 照样各出一份 Word,但只扣一次额度
- 改了提交参数(比如
is_ocr)或者换了模型 —— 指纹跟着变,自动重新解析, 不需要另写一套「缓存失效」的逻辑 - 十天没碰过的桶自动清掉
🔴 不能按文件名存。两份不同内容的「讲义.pdf」会落在同一个位置互相 覆盖;你把 PDF 改了重新导出、名字没变,也会拿到旧产物——那种错最难查, 因为界面上一切正常。(这条是本地版的教训,原样搬过来。)
设置页最下面。点一下会去 GitHub 查有没有新版本,有的话可以直接更新, 不用自己去下载解压。
这套是从本地版搬来的,它攒了三轮实测的教训:
- 六条线路并发跑,谁先成功用谁。不是依次试 —— 名单第一条是直连, 网络封了 GitHub 的话要先干等 6 秒超时才轮到第二条,全试一遍最坏 30 秒。 并发之后典型 1.2 秒。
- 失败了也给线路明细。连不上时用户最想知道的就是「到底哪条不通」, 而那份明细是查更新时顺手就有的,不给等于白扔。
- 拿不到校验值时报警,但不阻拦。硬拒绝会让更新按钮直接作废。
- 只推业务代码(0.4 MB),Electron 和 pandoc 那些运行时一个字节不动。
- 依赖对不上就不让装。判据是 Release 里那份
requires.json跟本机实际 装的包比对,不是「次版本号变了没有」—— 后者建立在「依赖变了必须进次版本」 这个约定上,约定靠人不出错。
镜像名单的行为会变,而且是双向地变(同一个镜像 09-01 能代理 API、 09-02 变 403、09-03 又反过来),所以候选写一串、并发实测、谁快用谁, 不去维护什么"最优镜像"的静态列表。
从 pdf_to_word 移植。出 Word 那一整套一行没改——
pipeline/todocx.py 824 行 公式占位符定位、Office XSL、表格边框、
图片按英寸还原、主题字体、图内文字过滤
pipeline/tomath.py 259 行 LaTeX → MathML → OMML
pipeline/probe.py 117 行 体检
pipeline/vendor/katex KaTeX,🔴 忘搬就是「一个公式都没转成」
runtime/xsl/MML2OMML.XSL 微软的 XSL,🔴 忘搬同上
新写的:
pipeline/mineru_api.py 替掉本地版的 extract.py(496 行起子进程)
pipeline/store.py token(多个)、每天的页数记账、转换历史
接缝只有两个字段:extract 那一步交出 md(markdown 路径)和
auto_dir(含 images/ 的目录),下游完全不知道前面是子进程还是 HTTP。
云端跑的就是同一个 MinerU 引擎,产物格式一模一样(2026-09-08 实测):
| 本地 | 云端 | |
|---|---|---|
| markdown | <名字>.md |
full.md |
| 图片 | images/ |
images/ |
| 引用 |  |
 |
| 行内公式 | $...$ |
$...$ |
| 块级公式 | $$...$$ |
$$...$$ |
| 表格 | HTML | HTML |
文件名从 <名字>.md 变成 full.md 不影响 —— 找产物是按后缀扫的。
POST /api/v4/file-urls/batch 申请 OSS 上传位 → batch_id
PUT <签名位> 🔴 不能带 Content-Type,带了 403
GET /api/v4/extract-results/batch/… 轮询到 state=done
GET full_zip_url → 解压 full.md + images/
🔴 HTTP 200 不等于成功。 实测:拿不存在的 task_id 去查,返回的是
HTTP 200 {"code":-60012}。所以每个返回都要看信封里的 code。
没有查额度的接口。 翻遍官方文档也没有 —— 所以「今天还剩多少页」只能
本地自己记(logs/usage.json),而且天生偏小。真正准的信号是错误码
-60018(每日任务数量已达上限),撞上了就换号。
MML2OMML.XSL 是微软随 Office 分发的文件,本地版的做法是去用户机器上
找一份现成的(注册表 + 常见目录)。云端版把它打包进来了
(runtime/xsl/),tomath.find_xsl() 优先读自带那份。
🔴 这是版权文件,内置是作者 2026-09-08 拍板的,前提是这软件只发给
几个熟人。哪天要给外人用或者正经发布,必须换成能自由分发的实现
(texmath / fiduswriter-mathml2omml 那几个开源的)—— 产物放同一个位置,
bundled_xsl() 一行都不用改。
开发机上验不了「没装 Office 也能用」 —— 开发机装着 Office,代码随时
可能走到注册表那条路上去,测出来的「能用」是假的。所以测试里特意把
registry_candidates 和 XSL_CANDIDATES 全堵死,只留自带那份,跑通
端到端才算数(Test自带的XSL)。
那边 2026-09-08 三个 agent 查出三十余条,这份在同样的位置做了不同的选择:
| 本地版栽的 | 这里怎么做的 |
|---|---|
HTTP.get 不检查 r.ok,404 当正常数据,为它写的 catch 是死代码 |
httpGet 第一版就检查状态码,且有测试钉着 |
| 任务表淘汰会把 running 的挤掉,互斥失效 | 只淘汰终态,有测试 |
| 轮询返回浅拷贝,序列化在锁外,日志被工作线程截断 | 在锁内就拷好,有测试 |
| 同一条忙碌判据抄四处,一处漏了新增项 | 只有 _busy() 一个函数 |
| 五处「功能写好了没人调」 | 契约测试扫后端每条路由有没有前端调用点 |
| 报告页返回按钮在内容后面,被挤出屏幕 | 按钮在前,测试断言 按钮位置 < 内容位置 |
.venv\Scripts\python.exe -m unittest discover -s tests -q # 后端 115 条
node tests\front_check.js # 前端 85 条
前端那些是真渲染:在 Node 里用假 DOM 把 pages.js 跑起来,断言它吐出来
的 HTML。不是「函数存在就算过」。
pipeline/ 业务:体检、云端解析、出 Word、落盘
server/ 本地 HTTP(绑 127.0.0.1,端口随机)
app/ Electron 外壳 + 前端(无框架,全局 state + 整页重绘)
runtime/ 打包进来的 pandoc 和 MML2OMML.XSL
tests/ 后端 115 条 + 前端 85 条
都在 logs/ 下,而 logs/ 在 .gitignore 里:
logs/token.json {"slots": 2, "tokens": ["sk-...", "sk-..."]}
logs/usage.json {"2026-09-08": {"<token 的 sha256 前 12 位>": 340}}
token 不加密 —— 加密要有密钥,密钥又得存在同一台机器上。真正的边界是
文件系统权限。但显示一律打码(store.masked),日志、历史、报错里一个
字符都不许出现,有测试盯着。
账本存指纹不存原文 —— 它跟 token.json 躺在同一个目录,凭据只该有一个 出处。指纹不可逆,换了 token 指纹跟着变,旧账自然作废。