discover · compare · collections · requirements · contribute · feed · watching · stats · help with your tokens
zanbinxu/shuting · source
Snapshot 1 · 2026-11-05T08:35:22Z · TypeScript · MIT
Content fingerprint
a5ec81048dc165f18bc75068c153386c0ed89a43d18d548dd4772b8851237539GitHub description at capture
本地电子书阅读器,以「听读」为核心。EPUB / PDF / TXT / MOBI / AZW3,任意倍速不变调,可导出 MP3 离线听。Local e-book reader with listen-and-read: EPUB, PDF, TXT, MOBI, AZW3.
README at capture
<div align="center"> <img src="docs/logo.svg" width="112" height="112" alt="听书 ShuTing" /> # 听书 ShuTing **本地电子书阅读器 · 以「听读」为核心** 把微信读书那种「听书 + 重排」的体验搬到本地 —— 书在你自己硬盘上, 阅读位置、收听位置、标注、缓存、导出的 MP3 也都在本地。 一本书,可以读,也可以听。 [![Tests][badge-tests]][ci] [![Stars][badge-stars]][stargazers] [![Forks][badge-forks]][forks] [![Watchers][badge-watchers]][watchers] [![Issues][badge-issues]][issues] [![Release][badge-release]][releases] [![Downloads][badge-downloads]][releases] [![Last commit][badge-commit]][commits] [![License][badge-license]](#许可证) [![Platform][badge-platform]](#快速开始) [![Node][badge-node]](#从源码运行) [![PRs Welcome][badge-prs]](CONTRIBUTING.md) [简体中文](README.md) · [English](README.en.md) </div> --- ## 目录 - [这是什么](#这是什么) - [界面](#界面) - [功能](#功能) - [支持的格式](#支持的格式) - [快速开始](#快速开始) - [技术亮点](#技术亮点) - [实测数据](#实测数据) - [架构](#架构) - [开发](#开发) - [常见问题](#常见问题) - [路线图](#路线图) - [贡献](#贡献) - [许可证](#许可证) - [Star History](#star-history) --- ## 这是什么 一个跑在**你自己电脑上**的电子书阅读器,核心是「听读」: - **导入即读** —— 扫描你现有的电子书目录,不搬动文件,只记位置 - **重排正文** —— 把 EPUB / PDF / MOBI 都还原成可调字号、可换主题的连续文本 - **逐句朗读** —— 微软神经网络语音合成,跟读高亮 + 自动滚动 - **任意倍速** —— 1.1x / 2.6x / 3.0x… **不设上限**,且全程**不变调** - **位置分开记** —— 读到哪、听到哪各记一份,互不干扰 - **离线可听** —— 导出整本 MP3,地铁上没网也能听 和「网页版听书」的区别:**书不上传,位置不上云,缓存不依赖服务商**。 整个程序可以拷进 U 盘,换台电脑双击就能用,不装 Node、不装运行时。 ## 界面 <div align="center"> <img src="docs/screenshots/01-library.jpg" width="880" alt="书库界面" /> <sub><b>书库</b>:封面墙、格式筛选、进度与字数、最近阅读排序。左侧目录显示为 <code>~/…</code></sub> <img src="docs/screenshots/02-reader.jpg" width="880" alt="阅读器与听读界面" /> <sub><b>阅读器与听读</b>:目录带每章字数与听读时长、跟读高亮、划线与想法、底部播放条(播放 / 发音 / 导出 / 定时 / 倍速)</sub> <img src="docs/screenshots/03-formats.jpg" width="430" alt="格式顺序与默认格式" /> <img src="docs/screenshots/04-sort.jpg" width="430" alt="排序方式" /> <sub><b>左</b>:格式顺序与默认格式 —— 每种格式的数量、自定义顺序、默认打开格式(图中还展开了书库目录编辑) <b>右</b>:排序 —— 最近阅读 / 最近加入 / 按书名 / 按篇幅 / 按进度</sub> <img src="docs/screenshots/05-mini-player.jpg" width="880" alt="边逛边听" /> <sub><b>边逛边听</b>:返回书库后底部出现迷你播放条 —— 当前句、进度、暂停与停止都在,点标题回到那本书</sub> </div> > 截图取自真实的 4717 本书库(7.4 亿字)的浏览器模式。 > 注意左侧目录显示的是 `~/Downloads`、`~/Desktop`:**界面本身就把家目录折叠成 `~`**, > 所以截图、录屏、演示都不会露出用户名。 ## 功能 | 能力 | 状态 | 实测 | |---|---|---| | 扫描本地书库(多根目录、增量更新) | ✅ | 1134 个文件 / 首次 30 秒 / 无变化时 1.2 秒 / 零失败 | | **EPUB** 解析(元数据 / 目录 / 封面 / 正文 / 插图) | ✅ | 46 本全通过,含 2518 章的巨型合集 | | **TXT** 解析(GB18030 自动识别 / 章节切分 / 非书籍剔除) | ✅ | 506 本;自动剔掉依赖清单、设备配置这类假书 | | **PDF** 解析(书签目录 / 坐标重排 / 页眉页脚剔除) | ✅ | 398 本中 **312 本可读**,合 3776 万字 | | **MOBI / AZW3** 解析(PalmDOC 解压 / EXTH / KF8 双格式) | ✅ | 8 本中 **7 本可读**,合 164 万字 | | **听读**(逐句合成、跟读高亮、自动滚动) | ✅ | 真实 MP3 产出 | | **任意倍速**(1.1 / 2.6 / 3.0 … 无上限) | ✅ | 服务端 2.0x 上限 + 本地补偿,音高比 1.000x | | **保音高变速**(WSOLA 相位声码器) | ✅ | 1.5–4.0x 全范围音高不变(朴素重采样为 2.000x) | | **导出 MP3**(离线收听) | ✅ | 220 字/秒;19 万字的书约 15 分钟,纯 Node 无损拼接 | | **章节统计**(每章字数 / 读时 / 听时 + 全书进度) | ✅ | 「各章之和 == 整本」有断言守住 | | **跨书全文搜索** | ✅ | 倒排索引,中文按字 + 二字词,1721 段索引耗时 0.2 秒 | | **书签与划线标注**(精确到字) | ✅ | 段内偏移,28 项断言守住「不丢字」 | | **发音修正**(多音字词典,可自定义) | ✅ | 内置 42 条,只改朗读不改正文 | | **位置记忆**(读 / 听分开存) | ✅ | 关闭重开回原位 | | **定时关闭** | ✅ | 5–60 分钟,到时停播但保留进度 | | **格式栏自定义**(拖拽排序 / 设默认格式) | ✅ | 25 项断言覆盖边界情况 | | **音频缓存**(内容哈希 + LRU) | ✅ | 命中后快 334 倍 | | **便携部署**(拷 U 盘换电脑) | ✅ | 105.9 MB zip,解压双击即用,无需 Node | | DOCX / DOC | ⏳ | 已入库 133 本,正文解析待接入 | | 扫描版 PDF(无文字层) | ⏳ | 需要 OCR,是另一条技术路线 | | HUFF/CDIC 压缩的 MOBI | ❌ | 明确不支持(原因见常见问题) | ## 支持的格式 | 格式 | 正文 | 目录 | 图片 | 元数据 | 说明 | |---|:--:|:--:|:--:|:--:|---| | EPUB | ✅ | ✅ | ✅ | ✅ | 完整支持,含 OPF / NCX / 封面 | | TXT | ✅ | ✅ | — | 文件名 | GB18030 自动识别;行首标记切章 | | PDF | ✅ | ✅ | ⏳ | ✅ | 只抽文字;扫描版无文字层则明确报错 | | MOBI | ✅ | ✅ | ⏳ | ✅ | PalmDOC LZ77 / 不压缩 | | AZW3 | ✅ | ✅ | ⏳ | ✅ | KF8;双格式文件自动取新版正文 | | AZW | ✅ | ✅ | ⏳ | ✅ | 同 MOBI | | DOCX / DOC | ⏳ | — | — | — | 已入库,待接入 | **「图片」列**:EPUB 已支持(按需从 ZIP 提取 + 缓存 + 点击放大); PDF 与 MOBI 的插图待做(PDF 需要浏览器端渲染或解析图片对象, MOBI 需要按 `recindex` 取记录)。 ## 快速开始 ### 方式一:便携版(推荐给最终用户) 到 [Releases][releases] 下载 `ShuTing-<版本>-portable.zip`(约 106 MB), 解压到**任意目录**(含 U 盘),双击 `启动听书.cmd`。 - **不需要安装 Node.js**,不需要联网下载运行时 - 书库、缓存、导出的 MP3 都在程序目录的 `data\` 下 —— 整个文件夹拷走,数据跟着走 - 另有 `start-browser.cmd`:只启动服务、用浏览器访问(同一份包支持两种用法) > Windows SmartScreen 可能提示「未知发布者」—— 这是没有代码签名证书的正常现象, > 点「更多信息 → 仍要运行」即可。介意的话可以走方式三自己从源码跑。 ### 方式二:从源码运行 要求 **Node.js ≥ 22.6**(源码直接用 `.ts` 运行,依赖 Node 的原生类型擦除)。 ```bash git clone https://github.com/ZanbinXu/shuting.git cd shuting npm install npm run dev # 浏览器打开 http://127.0.0.1:5273 ``` 想要桌面窗口(而不是浏览器标签页): ```bash npm run install:electron # 下载 Electron 运行时 npm run build npm start ``` ### 方式三:自己打包便携版 ```bash npm run package # 产物在 dist/ npm run verify:package # 校验包完整(漏文件、体积、依赖) ``` ### 首次使用 启动后到侧栏「书库根目录」填上你的电子书文件夹,点「重新扫描」。 默认已经猜了 `Downloads` / `Desktop` / `Documents` 三个位置。 ## 技术亮点 几个「做之前以为简单、做起来发现全是坑」的地方,也是这个项目最值得看的部分。 ### PDF:从坐标里把版面还原出来 PDF 里没有「段落」这个概念,只有一堆带坐标的文字片段。而且 pdfjs 提供的 `hasEOL` **不可靠** —— 实测某页 29 个片段里只有 8 个带它,带它的还常常是 空字符串。所以只能靠 `transform` 里的坐标自己聚行、断段: - **聚行**:按 Y 坐标聚类(同一视觉行的 Y 有抖动) - **断段**:基准行距取「正间距里的**最小值**」而不是中位数 —— 段间空档会把中位数抬高,反而断不开段 - **首行缩进**:以**版心**为基准,不是拿上一行比(否则缩进段的第二行会被误判成新段) - **剔页眉页脚**:两条判据取并集。只靠「页边重复」会漏 —— 实测某技术书的页脚是作者名+页码(抽出来是 `Dzinosky39`), 它在版面里并不在末行;加上「同一段短文本出现在半数以上页面」才抓到, 一本 869 页的书因此少掉 **3147 段噪音** `src/core/pdf-reflow.ts` 是**纯函数**,不依赖 pdfjs,因此能用合成坐标做单测。 ### MOBI:拿格式自带的校验和验自己 写 MOBI 解压最危险的地方是「看起来是对的」。PalmDOC 头里的 `textLength` 就是解压后的**精确字节数**,等于格式白送了一个校验和: ``` 第一次跑:解压 817495 字节 / 声明 1000555 字节 ← 少了 18% 但章节目录、段落、中文全看起来正常,只有 194 个替换字符 ``` 追下去是尾部「额外数据」剥离算错,每条记录少几个字节,还每隔 4096 字节 把汉字从中间切断。修法不是继续猜那个长度编码,而是换思路:**格式保证 「除最后一条外每条记录解压后恰好 recordSize 字节」**,直接按这个不变量裁。 修完 7 本全部**逐字节吻合、零替换字符**。 另一个坑是标题识别:KF8(AZW3)用规范的 `<h1>/<h2>`,而老的 MOBI7 全书 0 个 h1/h2,只用 `<font size="6">`。但同一个标签也用于**首字下沉** (`<font size="6">我</font> 出生于1949年…`),一见大字就标标题会让整段正文 变成章标题 —— 实测《原则》因此被切成 143 章、标题是一整段正文。 判据改成「大字号 **且** 整段够短,且不是单个汉字」后是 63 章, 标题变成「1 我的探险召唤 (1949—1967年)」这样的真目录。 ### 任意倍速且不变调 微软服务端 `rate` 参数在 **+100%(2.0x)处饱和**,再高也不变快。 所以超过 2.0x 的部分交给本地**相位声码器(WSOLA)**: | 倍速 | 音高比(WSOLA) | 对照:朴素重采样 | |---|---|---| | 1.5x | 1.000x | 1.500x | | 2.6x | 1.000x | 2.000x | | 4.0x | 1.000x | 2.000x | 同时用 `AudioBufferSourceNode` 而不是 `<audio>` 标签播放 —— 后者的 `playbackRate` 会连带改变音高。 ### 朗读为什么不卡 逐句合成每次有 1.2–1.4 秒的固定开销,按句请求只有 6 字/秒, 一本 19 万字的书要合成 8.7 小时。改成**按 500 字批**合成后是 **220 字/秒**, 同一本书 15 分钟合成完。再叠一层内容哈希命名 + LRU 的本地缓存, 命中的句子从 1673 ms 降到 **5 ms**。 ### 双轨架构:桌面版与网页版是同一套代码 `src/renderer/src/backend.ts` 在运行时探测:有 Electron 桥就走 IPC, 否则走 HTTP。界面代码不感知自己跑在哪。因此开发时可以用浏览器调试 (`npm run dev`),发布时用 Electron 装进窗口(`npm start`), 两条路不会走偏。 ## 实测数据 | 项目 | 结果 | |---|---| | 测试套件 | **22 个套件 / 2843 项断言 / 全部通过**(集成测试随本机书库规模伸缩)| | 本机书库 | **4717 本**;3365 本可读,合 **7.4 亿字** | | PDF 解析覆盖 | **1771 本中 1069 本可读(60%)**;702 本无文字层(扫描版)| | MOBI/AZW3 覆盖 | **525 本中 450 本解压长度逐字节吻合(86%)**;73 本 HUFF/CDIC、1 本 DRM、1 本非文本 | | PDF 打开速度 | 461 页 2.0 秒;869 页 6.0 秒;**二次打开 5 毫秒**(磁盘缓存) | | 扫描速度 | **4870 个文件首次 210 秒**;内容未变化时只比对、不重新解析 | | 朗读合成 | 220 字/秒(批量);缓存命中 5 ms | | 音频缓存 | 命中后快 334 倍 | | 变速保音高 | 1.5–4.0x 音高比 1.000x | | 便携包 | 105.9 MB zip;解压 247 MB;生产依赖 20.4 MB | ## 架构 ``` src/shared/ 纯类型 + 同构逻辑(零 Node 依赖,两侧共用) types.ts 全部数据结构 text.ts 分句 / 倍速换算 / 时间格式化 wsola.ts 保音高变速(纯函数) stats.ts 时长与进度估算 format-order.ts 格式栏排序 src/core/ Node 侧核心逻辑(零 Electron 依赖) epub.ts EPUB:OPF / 目录 / 封面 / 正文 / 插图 txt.ts TXT:编码识别 / 章节切分 / 非书籍剔除 pdf.ts PDF:文字层判定 / 书签目录 / 抽样估算 / 磁盘缓存 pdf-reflow.ts PDF 版面重排(纯函数:片段 → 行 → 段落) mobi.ts MOBI/AZW3:PDB 容器 / MOBI 头 / EXTH / 标题识别 palmdoc.ts PalmDOC LZ77 解压(纯函数) bytes.ts 大端整数读取(纯函数) esm.ts CommonJS 产物里加载 ESM-only 依赖 scan.ts 目录扫描与增量同步 store.ts 书库持久化(JSON + 原子写盘 + 合并延迟写) tts.ts 语音合成 + 音频缓存(内容哈希 + LRU) search.ts 全文搜索倒排索引 pronunciation.ts 发音修正词典 export.ts 导出 MP3 server.ts 本地服务:静态文件 + JSON 接口 + 音频流 src/main/ Electron 主进程(内嵌同一个 server) src/preload/ contextBridge src/renderer/ React 界面 backend.ts 运行时在 IPC 与 HTTP 之间切换 scripts/ 构建、测试、基准、探针(40+ 个) ``` **格式归一**:所有格式最终都转成「章节 → 段落数组」这一种中间形态, 重排引擎、朗读引擎、搜索、导出、进度都只对接它。接入新格式时 唯一可能要动的是共享类型里的可选字段(例如 PDF 的页码标注)。 **产物按运行时隔离**(这条踩过坑):`out/` 是 CommonJS(主进程 / core), `out/renderer/` 是 ESM(浏览器)。两边 `outDir` 必须分开 —— 曾因同名文件互相覆盖,导致 `out/shared/text.js` 被编成 ESM、主进程 `require` 直接崩溃。 ## 开发 ```bash npm install --ignore-scripts # 见下方「环境约束」 npm run build # 清理 + 进程内编译 + rollup 打包 npm run dev # 服务模式(浏览器调试) npm start # 桌面窗口 npm test # 22 个套件 / 2843 项断言 npm run check # 前端加载链自检(白屏排查) npm run check:toc # 目录一致性校验(与原 EPUB 逐项比对) npm run verify:package # 便携包完整性 npm run verify:bundle # 打包产物功能自检(需先 npm run package) ``` 单个套件与探针: ```bash node scripts/test-pdf.ts # PDF 重排单测(42 项,合成数据) node scripts/test-pdf-integration.ts # PDF 集成(22 项,真实书 + 磁盘缓存) node scripts/test-mobi.ts # MOBI 单测(72 项,合成字节) node scripts/test-mobi-integration.ts # MOBI 集成(63 项,textLength 逐字节校验) node scripts/bench-pdf.ts # PDF 抽取性能基准 node scripts/probe-mobi.ts # 打印 MOBI 容器结构 node scripts/probe-mobi-reading.ts # 真实 MOBI 解压验证 ``` > `npm test` 跑的是**源码 `.ts`**;`out/` 里是编译产物,服务端跑的是后者。 > 改了源码要先 `npm run build` 再重启服务 —— 否则验的是旧代码。 > 这个坑真实踩过:`PDF_RULE_VERSION` 跳到 3 就是为了清理它留下的脏数据。 更细的工程记录(每条判定规则的依据、踩过的坑、当时的实测数据)见 [`docs/DESIGN.md`](docs/DESIGN.md)。 ### 环境约束(重要) 这个项目最初是在一个**受限沙箱**里开发的,几个约束直接决定了技术选型: | 约束 | 后果 | |---|---| | 禁止派生子进程并使用管道通信(`spawn EPERM`) | **不能用 Vite / esbuild** —— 它们的架构正是派生 + 管道。改用 `tsc`(进程内调用编译器 API)+ `rollup` | | 无法启动 GUI(Chromium 的 mojo 用命名管道) | Electron 窗口与 headless 截图都跑不起来,界面只能靠浏览器模式自验 | | 访问不到 github.com(TLS 被干扰) | Electron 二进制改从 `registry.npmmirror.com` 镜像下载 | | npm postinstall 会派生子进程 | 一律 `npm install --ignore-scripts`,Electron 单独装 | 如果你在不受限的环境里开发,这些约束都不存在 —— 上面这些绕行措施 (`scripts/tsc-run.ts`、`rollup.config.mjs`、`scripts/install-electron.ts`) 可以原样保留(它们能跑),也可以按自己喜好换回标准工具链。 ## 常见问题 <details> <summary><b>端口被占用了会怎样?会把我的用户名写进配置吗?</b></summary> **端口**:不会失败。默认用 5274(浏览器预览 5273),如果被占用会**自动顺延** 到下一个可用端口,全部占满时交给系统分配。所以不需要手动改端口。 如果端口上**已经有一个听书实例在跑**,程序会直接**复用它**而不是再起一个 —— 这一条比端口本身更重要:两个实例各持一份内存书库,后写盘的会覆盖先写的, 表现为「扫描结果过一会儿自己变回去了」。判断依据是 `/api/info` 里的应用标识, 不是「端口通不通」(别的程序占着也通)。 浏览器模式由服务端在启动后用**实际**端口打开浏览器,所以启动脚本里没有写死 URL。 **用户名**:设置里不会。凡是**程序自己写进去**的路径,落盘时一律把家目录折叠成 `~` (库根目录与缓存目录): ```jsonc // data/library.json —— 设置里不含任何用户名 "libraryRoots": ["~/Downloads", "~/Desktop", "~/Documents"] ``` 程序内部与界面显示时再展开成绝对路径(界面里也显示为 `~/…`,悬停可看全路径, 这样截图/录屏也不会露出用户名);换台电脑打开也能解析到**那台电脑**自己的家目录。 每本书的 `path` 则是这台机器上的绝对路径,**有意不折叠**:它是「这本书在这个位置」, 折叠了并不能让书在别人机器上打开(别人的文件不在这儿),反而让 `data/library.json` 不再是一张直接可读的表。`data/` 本身也不进版本库。 </details> <details> <summary><b>我删掉了一些电子书、又新增了一些,会同步吗?阅读进度会不会丢?</b></summary> 点「同步书库」即可。它**不是重建**,而是与磁盘对账: | 情况 | 处理 | |---|---| | 目录里多出来的书 | 新增入库 | | 文件内容变了(重新下载、覆盖更新) | 更新,并把阅读进度与标注**迁到新记录上** | | 文件已经不在了 | 从列表移除,但进度与标注**留下来** | | 什么都没变 | 跳过解析(所以第二次同步很快) | 「进度不会丢」包括三种最容易吃亏的情况: - **文件被删掉**:记录移出列表,阅读位置留成一条「无主记录」。文件以同样的 路径与大小回来时(重新下载、把盘插回来)自动接上。 - **同一本书换了文件**:书的 ID 含文件大小与修改时间,换了文件就会换 ID。 同步时会把进度与标注迁到新 ID 上,你感知不到这次换壳。 - **整个目录打不开**(移动硬盘没插、网络盘断开、分区没挂载):那个目录下的记录 **一条都不动**,只在界面上提示。否则拔一次盘就会丢掉整盘书的阅读位置 —— 而那是不可逆的。 留着但你确实不想要的无主记录,点书库右上角的「无主记录 N」清掉即可 (会二次确认)。这些记录平时只占几十 KB,所以不做自动过期。 </details> <details> <summary><b>界面上的新功能全都失灵 / 提示「服务端是旧版本」?</b></summary> 你正在用**新界面配旧服务进程**。 页面上的脚本与样式是每次请求都从磁盘读的,所以**刷新页面就会拿到新界面**; 而服务端代码在进程内存里,不重启就还是旧的。这个组合很难看出来, 表现却是「新功能全部失灵,界面上毫无异常」。 程序会主动检测并提示「服务端是旧版本」,按提示重启即可: - 浏览器预览:在运行服务的窗口按 `Ctrl+C`,再重新 `npm run dev` - 桌面版 / 便携版:关闭程序窗口,再重新双击启动 界面顶部出现这条提示时不要忽略 —— 在它消失之前,你看到的报错 (例如「未知接口」)都只是版本不匹配的副作用,不是你操作错了。 </details> <details> <summary><b>加了一个大目录,为什么书只进来一部分?</b></summary> 先看两点: 1. **扫描可能还在跑。** 一本几千个文件的库要十几分钟(每本 EPUB 完整解析、 每本 PDF 抽样、每本 MOBI 解压)。界面有进度条与当前文件名, 结束后给出「新增 / 更新 / 未变 / 拒绝 / 失败」的汇总。 2. **有没有失败的文件。** 扫描结束后若出现「N 个文件未能入库」, 点开就能看到是哪些、为什么(DRM、文件损坏、格式不支持等)。 单个坏文件**不会**影响其他文件。 > 早期版本这里有个严重 bug:遇到第一个解析失败的文件, > **整次扫描会直接中止**,且结果不落盘。表现是「加了个大目录, > 书库只进来一小部分,界面只弹一行莫名其妙的报错」, > 很容易被误认为「不支持中文路径」或「书太多装不下」。 > 已修复并有回归测试(`scripts/test-scan.ts`)。 </details> <details> <summary><b>改了代码,但界面行为没变?</b></summary> 十有八九是**服务进程还是旧的**。源码由 Node 直接当 `.ts` 加载, 改了文件不重启服务,跑的还是旧逻辑 —— 这时现象与代码完全对不上, 会往错误的方向排查很久。 开发入口现在会在启动时检查:若 `src/` 里有比本进程更新的改动, 会明确打印「你跑的可能是旧代码,请重启」。 另外注意:浏览器预览(`npm run dev`,端口 5273)与桌面版(`npm start`,端口 5274) 各有**独立的数据目录**(浏览器模式用项目下的 `data/`,桌面版用 `%APPDATA%/shuting/data/`,便携版用程序旁的 `data/`)。 它们各自在内存里持有一份书库,**同时开着并各自扫描时,后写盘的会覆盖先写的**。 建议只开一个。 </details> <details> <summary><b>为什么不用 Calibre 转换格式?</b></summary> 初版设计确实是「所有格式交给 Calibre 转 EPUB」,实测后发现不实用: ``` 输入:乡土中国(修订本).mobi 5.4 MB 运行:ebook-convert 转为 EPUB 观察:CPU 累计从 9.3s 涨到 122s,14 分钟仍未产出文件 ``` 单本十几分钟,几百本要按天算。**更关键的是可移植性**:便携版主打 「拷到别的电脑就能用、不装任何东西」,一旦依赖外部程序这条卖点就没了。 所以改成自己写解析器。实测反而更快:MOBI 解压一本 2.9 MB 的书不到 1 秒。 </details> <details> <summary><b>扫描版 PDF 为什么读不了?</b></summary> 扫描版是把每页**拍照**贴进 PDF,整份文件里没有字体对象、没有文字绘制指令, 自然抽不出文字。这需要 OCR,是另一条独立的技术路线(还得再带一个模型)。 程序会明确告诉你「几乎没有文字层(每页约 N 字)」,而不是让你点进去看到空白。 </details> <details> <summary><b>HUFF/CDIC 压缩的 MOBI 为什么明确不支持?</b></summary> 它需要两套表(霍夫曼码表 + CDIC 字典),公开资料里几处细节说法不一。 照猜写出来的解码器会「跑得通但输出乱码」,**那比直接报不支持更糟** —— 你会以为能读,实际读到的是垃圾。已确认的格式事实(记录位置、四个表偏移、 条目总数)都记在 `src/core/palmdoc.ts` 的注释里,留给下次接手。 校验手段还是 `textLength` 那个逐字节比对。 </details> <details> <summary><b>朗读需要联网吗?能离线听吗?</b></summary> 朗读调用微软云端语音合成,**听新内容需要联网**。但: - 听过的句子有本地缓存,断网可重播 - 要离线听整本,用阅读器里的「导出」生成 MP3(19 万字的书约 15 分钟) </details> <details> <summary><b>我的书会被上传吗?</b></summary> 不会。书本身**不搬动、不上传**,程序只记路径。书库、进度、标注、缓存 全部存在本地 `data/` 目录。唯一的网络请求是调用微软的语音合成接口 (只发送当前要朗读的那一句话)。 </details> ## 路线图 - [x] **M0** 工程骨架 + 纯逻辑层 + 听读可行性验证 - [x] **M1** 书库:多根目录扫描 → 持久化 → 卡片列表 - [x] **M2** EPUB 引擎:解析 + 重排 + 目录 + 阅读 - [x] **M3** 听读引擎:合成 + 跟读高亮 + 任意倍速 - [x] **M4** 记忆与统计:阅读 / 收听位置、百分比、时间预估 - [x] **M5a** TXT 解析:编码识别 + 章节切分 + 非书籍剔除 - [x] **M5b** 正文插图:懒加载提取 + 缓存 + 点击放大 - [x] **M5c** 发音修正:分句不断词 + 多音字词典 + 界面自定义 - [x] **M5d** 导出 MP3:批量合成 + 纯 Node 无损拼接 - [x] **M5e** PDF 解析:坐标重排 + 书签目录 + 磁盘缓存 - [x] **M5f** MOBI / AZW3 解析:PalmDOC 解压 + EXTH + 标题识别 - [x] **M6** 保音高变速 / 定时关闭 / 全文搜索 / 书签划线 - [x] **M7** 便携版打包(解压即用,无需 Node) - [ ] **M8** DOCX / DOC 解析 - [ ] **M9** PDF 与 MOBI 的正文插图 - [ ] **M10** HUFF/CDIC 压缩的 MOBI - [ ] **M11** 扫描版 PDF 的 OCR(可选,体积代价大) - [ ] **M12** 移动端适配与 PWA ## 贡献 欢迎提 Issue 和 PR,具体流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。 特别需要帮助的地方: - **更多格式** —— DOCX 是最容易接的(就是 ZIP + XML) - **HUFF/CDIC 压缩的 MOBI** —— 实测占书库 MOBI/AZW3 的 13%(525 本中 73 本) - **扫描版 PDF 的 OCR** —— 702 本没有文字层,目前只能明确报「扫描版,需要 OCR」 - **平台适配** —— 目前只在 Windows 上实测过;代码里没有平台特定逻辑,macOS / Linux 应该能跑 - **实测反馈** —— 尤其是听读手感、倍速听感、大书库下的性能 如果这个项目对你有用,**点个 Star** 是最实在的支持 ⭐ ## 许可证 [MIT](LICENSE) © 2026 Zanbin Xu 使用的第三方服务与数据: - 语音合成:[edge-tts](https://github.com/rany2/edge-tts)(微软 Edge 朗读接口,非官方) - PDF 解析:[pdfjs-dist](https://github.com/mozilla/pdf.js)(Apache-2.0) - 界面:[React](https://react.dev)(MIT) - 运行时:[Electron](https://www.electronjs.org)(MIT) ## Star History <a href="https://star-history.com/#ZanbinXu/shuting&Date"> <img src="https://api.star-history.com/svg?repos=ZanbinXu/shuting&type=Date" width="640" alt="Star History" /> </a> <!-- 链接引用:仓库改名后只需改这一段 --> [ci]: https://github.com/ZanbinXu/shuting/actions/workflows/ci.yml [stargazers]: https://github.com/ZanbinXu/shuting/stargazers [forks]: https://github.com/ZanbinXu/shuting/network/members [watchers]: https://github.com/ZanbinXu/shuting/watchers [issues]: https://github.com/ZanbinXu/shuting/issues [releases]: https://github.com/ZanbinXu/shuting/releases [commits]: https://github.com/ZanbinXu/shuting/commits/main [badge-tests]: https://img.shields.io/github/actions/workflow/status/ZanbinXu/shuting/ci.yml?style=flat-square&label=tests&logo=githubactions&logoColor=white [badge-stars]: https://img.shields.io/github/stars/ZanbinXu/shuting?style=flat-square&label=Stars&logo=github&color=ffcb47 [badge-forks]: https://img.shields.io/github/forks/ZanbinXu/shuting?style=flat-square&label=Forks&logo=github [badge-watchers]: https://img.shields.io/github/watchers/ZanbinXu/shuting?style=flat-square&label=Watchers&logo=github [badge-issues]: https://img.shields.io/github/issues/ZanbinXu/shuting?style=flat-square&label=Issues&logo=github [badge-release]: https://img.shields.io/github/v/release/ZanbinXu/shuting?style=flat-square&label=Release&logo=github [badge-downloads]: https://img.shields.io/github/downloads/ZanbinXu/shuting/total?style=flat-square&label=Downloads&logo=github [badge-commit]: https://img.shields.io/github/last-commit/ZanbinXu/shuting?style=flat-square&label=Last%20commit [badge-license]: https://img.shields.io/github/license/ZanbinXu/shuting?style=flat-square&label=License [badge-platform]: https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-5B7CFA?style=flat-square&logo=windows&logoColor=white [badge-node]: https://img.shields.io/badge/Node.js-%E2%89%A5%2022.6-339933?style=flat-square&logo=nodedotjs&logoColor=white [badge-prs]: https://img.shields.io/badge/PRs-welcome-8E4DFF?style=flat-square&logo=github&logoColor=white