Readest TTS 日语 ruby 注音朗读改造:基于节点过滤器实现「读假名、不读汉字」
桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载导读本文剖析 Readest 中一个真实且典型的 TTS文本朗读工程问题朗读日文 EPUB 时引擎会照着ruby的基底汉字逐个发音导致「数多」被读成「すうた」而不是「あまた」——而rt注音振り仮名才是作者真正想让你听到的读音。解决方案的核心不是改写文本而是把「朗读哪一侧」的决策下沉到 DOM 节点过滤器NodeFilter中同时为 foliate-js 打了两个关键补丁。读完本文你将掌握为什么必须在过滤器层做替换而不能动文本、如何用「假名判定」做自门控、以及如何让朗读高亮与朗读内容精确对齐。一、问题背景引擎猜读音而rt才是唯一真相日本小说的汉字读音几乎无法从字形推导大部分汉字存在多个读音需要靠上下文选择专有名词完全不遵循规则轻小说作者还会故意使用「義訓」给汉字标注非常规读音例如把「神聖文字」标注为「ヒエログリフ」老式 EPUB 甚至用img渲染生僻汉字基底根本没有任何文本。因此在 apps/readest-app/src/utils/ruby.ts 的头部注释中明确写下了这条设计准则Thertis the only ground truth, so TTS speaks it and mutes the base run it annotates (#5539).即rt注音是唯一可信的发音来源TTS 朗读它并静音mute它所标注的基底。这就是 #5539 的全部语义让朗读引擎不再去猜汉字读音而是直接读出作者标注的假名。二、核心设计替换发生在过滤器而不是改写文本一个容易踩的坑是「直接把rt文本替换进基底文本」。文档明确警告替换必须发生在节点过滤器node filter层面绝不能通过改写文本实现。原因有两点都源于 Readest 的文档一致性约束#ttsDoc通常是「正在渲染的实时文档」在 apps/readest-app/src/services/tts/TTSController.ts 中#getLiveSectionDoc()会从view.renderer.getContents()中取回所有存活视图multiview paginator 会保留预加载的相邻章节里对应 section 的真实渲染文档而不只是主视图。即使是后台章节文档也必须与渲染内容逐字一致这是 #5406 的规则。Readest 的显示变换管线proofread 校对规则、simplecc 简繁转换、标点规范化等会在书籍加载器的data事件中重写文本如果 TTS 用未经变换的原始文档朗读语音会与屏幕显示脱节且按原始文档计算的 CFI 高亮会在变换后的实时文档上锚定到错误位置。为此 apps/readest-app/src/services/tts/transformDoc.ts 中的transformTTSSectionDocument()会把原始 section 文档重新序列化、重新派发同一条data事件、再重新解析从而保证 TTS 文档与展示文档内容完全一致#createSectionDoc中还会做 TTS 语音所需的 lang 修复见 TTSController.ts。如果在这个前提下直接改写文本高亮 CFI 就会漂移。所以正确做法是过滤器决定 walker 在每组 ruby 对中丢弃哪一侧实时范围的遍历和克隆片段cloned-fragment的遍历共用同一个过滤器marks 才能始终对齐。这一职责落在 apps/readest-app/src/utils/ruby.ts 的isMutedRubyNode()上它对一个节点给出「这是不是该静音的那一侧」的判定rpruby fallback 括号和rtc双层注音的第二层永远静音位于rt内的节点仅当该rt是「会被朗读的假名注音」时才不静音否则静音例如旁点注音、拼音注音、被注入的 WordLens 词条ruby的直接子节点基底当其后继rt是假名注音时静音因为注音将代替它朗读否则保持朗读与 ruby 无关的节点永不静音。rtForBaseRun()ruby.ts实现「基底对应哪个注音」的配对ruby 内容按「基底/注音」交替排列所以直接取下一个rt兄弟节点即可若遇到rtc说明这是双层注音结构注音不与基底交错此时放行基底。这个过滤器通过 apps/readest-app/src/services/tts/nodeFilter.ts 的createTTSNodeFilter()组装成完整的 TTS 节点过滤器——它先通过createRejectFilter排除画布、换行、脚注等常规内容再把 ruby 决策委托给isMutedRubyNode。live TTS 实例与时间线timeline枚举必须共用同一个过滤器否则时间线句子会与 marks 漂移——这是文档强调的「MUST segment identically」不变量。三、门控条件以rt是否假名为准而非书籍语言替换的开关不是书籍语言而是rt内容是否像假名。isKanaReading()ruby.ts的判定是至少含一个真正的假名字母且除此之外只允许假名、长音符「ー」、分隔符「・」、浊音/半浊音符号和空白。该判定是逐「基底/注音」对进行的这带来两个关键行为空rt不满足「至少一个假名」因此不触发替换其对应的送假名okurigana保持朗读。测试用例 ruby.test.ts 验证了ruby振rtふ/rtりrt/rt仮名rtがな/rt/ruby应朗读为ふりがな而不是错误的ふがな。判定自动把替换范围自门控在日语上旁点傍点・﹅●、、拼音、注音符号、拉丁文/汉字注疏以及 Readest 自己注入的带[cfi-inert]属性的 WordLens 词条全部保持原有行为继续读基底、静音注音。实现上有两个容易被忽略的细节「・」U30FB在 Unicode Script Extensions 中属于 Katakana但它既是片假名外来语内的分隔符又是书籍标记旁点的字符。因此「至少一个假名字母」不能写成\p{scxKatakana}必须使用显式字母区间KANA_LETTER /[ぁ-ゖゝ-ゟァ-ヺヽ-ヿ]/见 ruby.ts。判定用的两套正则KANA_LETTER至少一个假名与KANA_TEXT整体只允许假名/浊音点/长音符/分隔符/空白。测试用例 ruby.test.ts 对二者分别做了完整覆盖接受あまた、ヒエログリフ、コーヒー、ヴィクトリア・アルベルト拒绝・、・・・、﹅、●、、、空串、纯空白、hieroglyph、shēng、ㄕㄥ、神聖、かん字。四、两个 foliate-js 补丁SSML 生成与高亮绘制的配套修复仅靠节点过滤器并不够实际验证暴露出 foliate-js 的两处遗漏每一处都由一个失败测试证明补丁 1fragmentToSSML只对元素应用过滤器文本节点漏掉了在 packages/foliate-js/tts.js 的fragmentToSSML()中原本nodeFilter只对元素节点生效。而群组 rubygroup ruby的基底是ruby下的裸文本子节点没有rb包裹于是基底文本「泄漏」进了语音读出来变成「数多あまたの星」。修复后的代码tts.js在注释中说明了理由生成 marks 的文本 walker 已经遵守过滤器如果 SSML 转换对文本节点不遵守两者对「正在读什么」的认知就会不一致。现在文本节点nodeType 3和 CDATAnodeType 4都会先经过nodeFilter被FILTER_REJECT的文本直接跳过。补丁 2Overlayer.#splitRange收集了rt/rp/rtc/[cfi-inert]的矩形在 packages/foliate-js/overlayer.js 的#splitRange()中TreeWalker 的acceptNode原本会把注音节点也当作可绘制的文本节点导致在假名上方画出一个脱离基底的独立高亮框furigana 悬浮在基底上方高亮框也跟着悬浮。修复后overlayer.js// Ruby annotations sit on their own line above (or beside) // the base, so their rects would draw a second detached box // over the furigana. Never paint them — not the books own // ruby, not injected glosses. if (el?.closest?.(rt, rp, rtc, [cfi-inert])) return NodeFilter.FILTER_REJECTrt注音、rpfallback 括号、rtc第二层注音、[cfi-inert]注入内容如 WordLens 词条的矩形一律不收集。这个补丁同时修复了另一个长期问题标注annotation高亮时rt注音不应被高亮。五、expandRangeOverRuby朗读高亮的收尾拼图两个补丁与 TTSController 的expandRangeOverRuby是互补关系。在 TTSController.ts 的#getHighlighter()中高亮回调拿到的是朗读文本对应的 Range——对日语书而言这个 Range 锚定在rt假名内部。但 overlayer 现在拒绝绘制rt的矩形如果不做扩展就什么都画不出来。expandRangeOverRuby()ruby.ts的职责是当 Range 的起点或终点落在「会被朗读的假名注音」内部时把它向外扩展到整个ruby元素让高亮框画在读者目光所及的基底汉字上当起点终点都不在注音内时原样返回。它有一个刻意的对称行为对注疏型 rubyWordLens、拼音、旁点绝不扩展因为此时朗读的是基底、Range 已经覆盖基底扩展反而是破坏性的——而且对注入的 WordLens 词条尤其危险它带cfi-skip属性不贡献 CFI 步进setStartBefore/setEndAfter会解析到同一位置把 Range 塌缩为空例如/4/2/4,/1:13,/1:20塌缩为/4/2,/4,/4overlayer 会画出一个空高亮。这正是「词粒度 TTS 跳过所有被注释的词、而句子粒度却正常」的根因句子 Range 很少起止于 ruby 内部永远碰不到扩展逻辑。六、词级高亮的一致性isInertText委托给isMutedRubyNodeEdge TTS 会返回词边界word-boundary时间戳Readest 据此实现逐词高亮。在 apps/readest-app/src/services/tts/wordHighlight.ts 中isInertText()现在把「是否静音」委托给isMutedRubyNode()ruby 两侧中在 TTS 过滤器里被静音的一侧在这里同样视为 inert对假名注音即基底任何带cfi-inert属性的祖先子树注入的 WordLens 词条同样视为 inert。这样做的原因是词偏移匹配的基准串必须与朗读内容一致rangeTextExcludingInert()在生成匹配基线时会剔除这些 inert 文本wordHighlight.ts否则 Edge 返回的无注音边界词与 walker 走出的文本对不上偏移会错位。getTextSubRange()wordHighlight.ts在把词偏移映射回子 Range 时同样跳过 inert 文本节点。七、测试与验证8 变体合成 EPUB 逐项核对单测覆盖apps/readest-app/src/tests/utils/ruby.test.ts对isKanaReading/isMutedRubyNode做单元级验证覆盖群组 ruby裸文本基底、显式rb基底、交错单注音对漢rtかん/rt字rtじ/rt→かんじ、空rt送假名、gaiji 图片基底img alt喰rtく/rt→く、rp括号剔除、旁点保持基底、拼音/拉丁注疏保持基底、WordLens 注入词条保持基底、rtc静音、游离rt静音、无 ruby 文本不动、ruby元素自身不静音保证子节点可被遍历、rb元素节点也要静音。apps/readest-app/src/tests/document/tts.test.ts在真实TTS实例上验证speak(pruby数多rtあまた/rt/rubyの星/p) あまたの星以及高亮 Range 锚定在朗读的注音上range.toString()不含被静音的汉字、startContainer的父元素是rt。apps/readest-app/src/tests/document/overlayer-highlight-blocks.test.ts验证经expandRangeOverRuby扩展后overlayer 把高亮画在基底上。Chrome 实机验证文档记录了用专门构造的 8 变体日文 EPUB 在 Chrome 中验证的结果fv.tts.start()/next()依次吐出あまた / ヒエログリフ / かんじ / ふりがな / あまた / 本当(dots kept) / すみだ / ひとけ与预期完全一致fv.tts.setMark(0)走控制器真实高亮器高亮框贴合基底行、假名悬浮在框外。两个实战注意事项来自文档的验证经验本地开发时 Edge TTS 的 WebSocket 会失败所以验证要直接驱动view.tts而不是等待音频该分支的验证对象是合成的 8 变体 EPUB尚未在真实商业轻小说上复验——报告者 He1lscythe 手头有含 1695 处 ruby 注音的单行本需在下一个发布版本上回归测试。八、同一分支的附带修复播放器封面占位同一次改造还顺带修了一个不相关的小问题TTSMiniPlayer / TTSPlayerSheet 中一个加载失败的coverImageUrl会在面板里预留一条 128px 的空白带。该修复随本分支一起合并与 ruby 朗读无直接关系但说明这次改动还清理了播放器界面的一个视觉瑕疵。九、小结一次完整的「过滤层替换」实践回顾整个 #5539 的改造可以提炼出几条可复用的工程原则能用过滤器表达的替换就不要改写文本——尤其在文档与渲染必须逐字一致#5406的约束下改写文本会破坏 CFI 对齐门控条件要贴近语义而非语言标签——用「是否假名」而非「是否日文书」判定天然兼容旁点、拼音、注音符号和注入词条替换与高亮必须配套修改——SSML 文本泄漏、高亮矩形收集、Range 扩展三处缺一不可任何一处遗漏都会表现为语音或高亮异常让测试直接驱动真实过滤器——createTTSNodeFilter从 TTSController 中抽离到 apps/readest-app/src/services/tts/nodeFilter.ts就是为了让单测直接验证真实配置而非重复一份配置「MUST segment identically」不变量避免时间线句子与 marks 漂移。关键代码路径汇总供继续深入阅读判定与替换核心apps/readest-app/src/utils/ruby.tsTTS 节点过滤器apps/readest-app/src/services/tts/nodeFilter.ts词级高亮一致性apps/readest-app/src/services/tts/wordHighlight.ts控制器中的实时文档与高亮扩展apps/readest-app/src/services/tts/TTSController.ts文档变换管线#5406 规则apps/readest-app/src/services/tts/transformDoc.tsfoliate-js 补丁一SSML 文本节点过滤packages/foliate-js/tts.jsfoliate-js 补丁二overlayer 拒绝注音矩形packages/foliate-js/overlayer.js单元测试apps/readest-app/src/tests/utils/ruby.test.ts、apps/readest-app/src/tests/document/tts.test.ts、apps/readest-app/src/tests/document/overlayer-highlight-blocks.test.ts赞分享桌面应用跨平台前端【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址https://gitcode.com/gh_mirrors/re/readest点击查看免费下载相关推荐Readest TTS引擎解析多语言文本朗读技术实现Readest TTS引擎解析多语言文本朗读技术实现 Readest作为一款现代化电子书阅读器其文本朗读TTS功能是提升阅读体验的核心模块之一。本文将深桌面应用跨平台前端Readest 词典弹窗单词发音功能解析基于 Edge TTS 的独立单词朗读器实现Readest 词典弹窗单词发音功能解析基于 Edge TTS 的独立单词朗读器实现 词典查词后如何让生词开口说话Readest 通过 Issue 48桌面应用跨平台前端突破阅读边界newsnow全平台TTS语音朗读方案实现终极指南突破阅读边界newsnow全平台TTS语音朗读方案实现终极指南 newsnow是一款专注于实时热点新闻优雅阅读的开源项目通过聚合多平台资讯为用户提供一站式阅前端后端网页爬虫创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Altium Designer交互式BOM插件安装与版本兼容指南

Altium Designer交互式BOM插件安装与版本兼容指南

做PCB这行久了,你会越来越觉得,BOM这东西做得好不好,直接决定贴片厂对你的态度。以前我导出的BOM要么是Excel、要么是PDF,一大串位号、封装、料号堆在一起,工人得对着板子上的丝印一个个找,找错了就焊错&am…

2026/9/21 14:43:02 阅读更多 →
OrCAD Capture报错警告本质解析与工程化应对策略

OrCAD Capture报错警告本质解析与工程化应对策略

1. 为什么“ERROR”和“Warning”不是报错,而是设计意图的翻译错误OrCAD Capture里弹出的红色ERROR和黄色Warning,绝大多数人第一反应是“软件出问题了”,立刻去搜“OrCAD报错怎么解决”,结果翻遍论坛、看十篇教程,发现…

2026/9/21 14:43:02 阅读更多 →
Feathers 接入 Discord OAuth 登录:从应用配置到自定义策略的完整实战

Feathers 接入 Discord OAuth 登录:从应用配置到自定义策略的完整实战

Feathers 接入 Discord OAuth 登录:从应用配置到自定义策略的完整实战 【免费下载链接】feathers The API and real-time application framework 项目地址: https://gitcode.com/gh_mirrors/fe/feathers 本指南基于 Feathers 官方 Cookbook 中的 Discord 认证…

2026/9/21 14:43:02 阅读更多 →

最新新闻

释魂源码解析:3招搞定版本升级API全变痛点

释魂源码解析:3招搞定版本升级API全变痛点

释魂源码解析:3招搞定版本升级API全变痛点 版本升级后 API 全变了,你的代码直接跑不通?别慌,这就是很多开发者升级框架时的噩梦。光看报错日志是修不好的,必须下沉到源码解析层面,看清接口契约到底改了什么。…

2026/9/21 18:16:18 阅读更多 →
拒绝卡顿:Windows日志性能优化从入门到精通实战

拒绝卡顿:Windows日志性能优化从入门到精通实战

拒绝卡顿:Windows日志性能优化从入门到精通实战 微软官方文档关于 Event Log 的篇幅长达数百页,读完只想睡觉,抓不住核心性能瓶颈。 想要从 入门到精通 地掌控 Windows 日志系统,必须看透底层 I/O…

2026/9/21 18:16:18 阅读更多 →
FEDORALINUX转岗避坑指南:3个源码解析陷阱让你不再卡半天

FEDORALINUX转岗避坑指南:3个源码解析陷阱让你不再卡半天

FEDORALINUX转岗避坑指南:3个源码解析陷阱让你不再卡半天 刚接触FEDORALINUX的转岗朋友,是不是经常遇到这种场景:照着网上教程敲完命令,系统直接崩了?或者配置好开发环境,编译代码时卡半天没反应?别急着骂娘,这真不是你的问题…

2026/9/21 18:16:18 阅读更多 →
3个坑点搞定卡西欧黑金怎么调时间源码解析

3个坑点搞定卡西欧黑金怎么调时间源码解析

3个坑点搞定卡西欧黑金怎么调时间源码解析 版本升级后 API 全变了,手里那台卡西欧黑金手表的时间设置逻辑突然对不上号。别急着骂娘,这是很多硬件逆向工程新手的通病。想彻底搞懂卡西欧黑金怎么调时间,光看说明书没用,得直接上源码解析。 01…

2026/9/21 18:16:18 阅读更多 →
九局下半搞懂并发模型 新手避坑实战指南

九局下半搞懂并发模型 新手避坑实战指南

九局下半搞懂并发模型 新手避坑实战指南 看了一堆教程还是不会写项目?别怪自己笨,是没人告诉你“九局下半”在工程落地里到底卡在哪。很多新手避坑指南只讲理论,不讲实战中那些让你头秃的边界情况。今天咱们不整虚的,直接拆解这个核心概念在不同技术栈里…

2026/9/21 18:16:18 阅读更多 →
2026年9月前端开发AI编程工具对比测评:Copilot、Cursor、通义灵码等六款实测

2026年9月前端开发AI编程工具对比测评:Copilot、Cursor、通义灵码等六款实测

1. 前端开发选AI编程工具,先搞清楚你到底在选什么前端开发这个行当,这两年最大的变量不是框架更新,也不是构建工具换代,而是AI编程工具直接杀进了日常写代码的流程里。2026年9月这个时间节点往回看,市面上能叫得出名字…

2026/9/21 18:15:17 阅读更多 →

日新闻

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程

agents-generator 决策矩阵全解析:从项目检测到 AGENTS.md 规则生成的 16 步判定流程 【免费下载链接】agentic-awesome-skills AAS Core is the local, agent-first control plane for complete catalog discovery, agent-owned selection, stack validation, and …

2026/9/21 0:00:01 阅读更多 →
gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析

gin-vue-admin 前端工具函数全景指南:src/utils 复用规范与源码级解析 【免费下载链接】gin-vue-admin 🚀ViteVue3Gin拥有AI辅助的基础开发平台,企业级业务AI开发解决方案,内置mcp辅助服务,内置skills管理,…

2026/9/21 0:00:01 阅读更多 →
Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

Wox 全功能插件开发实战指南:基于 Python / Node.js 宿主与 WebSocket 的持久化插件体系

桌面应用AI 应用插件系统 【免费下载链接】Wox A cross-platform launcher that simply works 项目地址: https://gitcode.com/gh_mirrors/wo/Wox 点击查看 免费下载 全功能插件(Full-featured Plugin)是 Wox 三类插件实现方式中能力最完整的…

2026/9/21 0:00:01 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/21 3:13:20 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/21 2:19:36 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/21 4:51:05 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →