作为一个喜欢听书的人,市面上虽然有不少阅读软件,但在桌面端往往难以找到一款轻量、免费且发音自然顺滑的本地听书工具。为此,我开发了 EdgeTTSPlayer —— 一款基于 edge-tts 和 pygame 构建的本地有声书播放器。
近期,为了对付体量庞大、排版混乱的网文 EPUB,以及彻底解放打包发布的双手,我对项目进行了一次大刀阔斧的重构。在此记录下这次迭代中踩过的坑与技术解决方案。
1. 驯服混乱的 EPUB:智能章节标题提取
痛点
在解析像《剑来》这样的网文 EPUB 时,我发现原有的章节解析算法完全失效,下拉列表里全变成了干瘪的“第 N 章”,甚至是一片空白。
深入扒开原生的 HTML 代码后,我发现了令人窒息的排版:
<div class="header1"><h2><b>第</b><b>一</b><b>章</b></h2></div>
<div class="part"></div>
<div class="header1"><h2><b>惊</b><b>蛰</b></h2></div>
<div class="part"><p>二月二,龙抬头...</p></div>
“第一章” 和 “惊蛰” 被硬生生拆分进了两个互相独立的 <h2> 标签,部分书源甚至连 <title> 和 <h> 标签都没有,全靠 <b> 加粗。
解决方案
我放弃了原本只匹配第一个 <h> 标签的简陋做法,重写了一套兼顾“规范排版”与“野生排版”的降维打击式提取算法:
- 多头合并:通过
BeautifulSoup抓取前 3 个h1-h3标签,清洗后将它们用空格强行拼接,完美复原第一章 惊蛰。 - 正文嗅探(针对极简标题):利用正则
^第[零一二...]+[章回]$进行探测。如果只提取到了“第一章”,则继续切分正文的段落(Paragraph)。只要“第一章”下一段的字数少于 20 个字,就判定其为副标题并强行抓取过来。 - 终极兜底:如果真的是没有任何标题的“三无”文件,程序会直接抽取前两个段落的内容,过滤掉所有换行与大段空白后,截取前 40 个字作为该章概要(例如
陈平安看着远处的山峰...)。
经过这套组合拳,即使是排版再糟糕的书源,也能在软件的侧边栏呈现出美观连贯的目录树。
2. 状态持久化与“无缝”交互体验
随播随记与全局记忆
听书最怕的不是报错,而是突然断电后“找不到上次听到哪儿了”。
为此,我设计了一套基于 JSON 的细粒度持久化方案:
- 全局偏好:你的专属发音人、语速、音量会被独立记录。下次打开任何新书,都会优先加载这些偏好设置。这里曾踩过一个小坑:UI 下拉框里显示的是带详细介绍的字符串(如
zh-CN-XiaoxiaoNeural (女)),保存时一定要剥离出纯净的ShortName,否则下次启动底层引擎会识别失败。 - 随播随记:后台每播放完一个 Chunk(碎片文本),就会把
chunk_index和联动的chapter_index写进缓存。我也在 UI 上新增了[💾 存进度]按钮以备不时之需。配合cache_version强刷新机制,完美解决了电子书解析缓存导致的脏数据问题。
实时介入:动态音量与语速感知
由于我的播放机制是“双缓冲架构”(一边播放当前句子,一边后台异步请求 Edge-TTS 预生成下一句),调整语速和发音人曾经需要“停止-重新播放”才能生效。
- 音量直连:我将界面的音量滑块直接绑定了
pygame.mixer.music.set_volume(),实现了完全实时的音量升降。 - 动态窥探:在后台线程每次准备生成下一个 Chunk 之前,我会通过
getattr动态抓取当前主线程中选择的发音人和语速。这样,当你嫌主角语速太慢而拉动滑块时,当前这半句话读完,下一句话会直接无缝切换成新语速,完全不需要暂停或打断体验!
3. 解放双手:GitHub Actions 跨平台全自动发版
随着功能完善,每次更新都要自己跑一次 PyInstaller 实在太折磨了。更何况身为 Windows 用户,想给 Mac 和 Linux 朋友提供可执行文件几乎不可能。
因此,我搭建了基于 GitHub Actions 的 CI/CD 自动化流水线。
核心实现:
on:
release:
types: [published]
permissions:
contents: write
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: windows-latest
- os: macos-latest
- os: ubuntu-latest
踩坑经验:
- 触发机制的羁绊:最开始想用打 Tag 的方式触发,但后来发现将触发条件改为
release: types: [published]最符合直觉。我们在 GitHub 网页端点击Draft a new release并发布的一瞬间,动作即被拉起,最后利用softprops/action-gh-release@v2就可以将编译产物自动挂载到刚创建的那个 Release 下,不需要自己去写复杂的 Release 创建脚本。 - 环境权限陷阱:GitHub 近期收紧了 Actions 默认权限。务必记得在 Workflow 顶部加上
permissions: contents: write,否则编译完的.exe根本没有权限上传到 Releases 页面里(会报HttpError: Resource not accessible by integration的错)。 - 跨平台依赖:在 Linux (Ubuntu) 虚拟服务器上跑 Tkinter 是缺少图形环境的,必须在脚本里手动执行
sudo apt-get install -y python3-tk补充依赖。此外,MacOS 编译出的并非单文件,而是xxx.app目录,需要单独针对 Mac 编写zip -r压缩指令。
现在,我只需要在 GitHub 随便点两下发布一个版本,系统就会拉起三台机器,几分钟后自动把 Windows版、macOS版、Linux版 打包压缩好呈现在眼前。这就是自动化的魅力!
