夜雨聆风学习资料网

ARTICLE · 1067479

Tauri 2 + Rust 打造的免费电脑版文字转语音软件源码拆解

Tauri 2 + Rust 打造的免费电脑版文字转语音软件源码拆解

01

PART

一、痛点:免费又好听的 TTS,为什么没有好用的桌面版?

INSIGHT

用过 Microsoft Edge 浏览器「大声朗读」的人,多半会对它的中文音色有点惊讶:语调自然、有停顿、有呼吸感,明显优于不少号称「AI 配音」的在线工具。这项能力背后的服务就是 Edge TTS——微软为 Edge 提供的在线语音合成接口,音色覆盖多种语言和口音,对个人使用免费。

尴尬的是,它没有一个像样的桌面入口。想把一批稿件、笔记、书稿变成音频,开发者通常只有三条路,每条都硌脚:

写 Python 脚本调 edge-tts 库。音色、语速、格式都可控,但没有界面、没有进度、没有失败重试,改一个参数就要改代码重跑;

用在线 TTS 网站。字数有上限、调用有频率限制,本地文档还得逐段复制粘贴,长稿基本不可行,隐私也存疑;

接商业 TTS API。音质和稳定性好,但要密钥、要计费、必须联网调用厂商服务,内容要往云端送。

这三条路之间,恰好缺一个东西:一个「本地桌面 + 免费音色 + 可批量 + 有 GUI」的工具。开源项目 tts-vue-next 就落在这个位置上。

02

PART

二、项目定位:把在线合成能力,封装成本地生产力工具

TOOLS

用一句话概括:tts-vue-next 是一款基于 Microsoft Edge TTS 免费在线语音服务的跨平台桌面语音合成工具,它以 Tauri 2 + Rust 深耦合后端,把原本需要联网命令行才能完成的 Edge TTS 合成与批量音频生产,封装成可离线管理、带 GUI 的本地生产力应用。

注意关键词是「本地管理」而不是「本地合成」——语音合成仍然发生在微软的在线服务上,但文本切分、并发调度、重试、格式转换、文件落盘、偏好持久化全部在本地完成。这种分层决定了它的能力边界,也决定了它的价值。

和常见方案相比,差异点大致是:

对比 Python edge-tts 脚本:多了图形界面、批量队列、实时进度与失败重试、多格式导出,以及开箱即用的 FFmpeg,不用自己写胶水代码;

对比在线 TTS 站点:没有粘贴字数与频率焦虑,支持拖拽导入 txt/md/docx 本地文档,输出直接落到指定目录;

对比商业 API:无需密钥、无需计费,音色由 Edge TTS 提供,适合个人与小团队的非商业生产;

对比系统自带 TTS(如 Windows SAPI):音色自然度高一个档次,且支持语速/音调/音量细调与四种导出格式。

它的技术组合也很清晰:Vue 3 + TypeScript + Vuetify 3 + Pinia + Vue Router + Vue I18n 负责界面与状态,Rust + Tauri 2 + Tokio + reqwest + FFmpeg 负责网络、音频与文件系统,再加 Vite、Vitest、Happy DOM 组成开发与测试链路。

03

PART

三、核心能力全景:从输入到最终产出

INSIGHT

把项目按数据流拆成四层,会清楚很多:

输入层:TTS 页的实时文本输入框,以及批量页的文件上传(支持 .txt、.md、.markdown、.docx,可拖拽);

处理层:Rust 后端的 WSS 流式合成、长文本分块、Markdown 清洗、并发调度与失败重试、FFmpeg 格式转换;

输出层:MP3 / WAV / OGG / FLAC 四种格式,保存路径可自定义;

结果层:内置播放器试听、批量任务进度面板、单文件重试、明暗主题与中英双语界面、启动时的版本检查。

基于对源码的实际阅读,它真正实现(而不是 README 口号)的核心能力有下面这些:

① Edge TTS 实时流式合成。后端 src-tauri/src/edge_tts/communicate.rs 用 tokio-tungstenite 接入 Edge TTS 的 WSS 服务,按字节长度切分长文本并逐分块流式合成,通过 Arc<Mutex<CommunicateState>> 累积各分块音频,同时用 watch 通道支持超时与取消。用户可以粘贴任意长度的文本,长文本被自动分段处理而不卡住界面,还能随时中止,不必等一整段合成完成才能操作。

② 多格式音频导出与本地保存。src-tauri/src/audio/ffmpeg.rs 中的 save_audio / convert_audio 对 mp3/wav/ogg/flac 做扩展名校验,mp3 直接写盘,其他格式先转成临时 mp3 再经 FFmpeg 转换;FFmpeg 程序按环境变量、应用捆绑二进制的优先级解析。用户无需自己安装配置 FFmpeg,就能产出工程常用的四种音频格式。

③ 批量文件转换与进度跟踪。src/stores/batch.ts 负责去重添加文件、按配置或源路径生成输出目录、用 crypto.randomUUID 生成任务 ID 并维护 pending/processing/done/error 状态;配套 Rust 命令读取 txt/md/docx 文本并去除 Markdown 标记(file.rs 中的 strip_markdown)。整批文档可一次转成语音,实时看到每个文件的处理状态,并支持单文件重试。

④ 语音列表抓取与按语言筛选。src/stores/voices 与 voices.rs 命令拉取可用语音,Voice 类型含 VoiceTag 元数据,store 支持按 locale 切换并联动选中语音,持久化选择缺失时回退到首个可用语音。下次打开即用,不用反复查找音色。

⑤ 丰富的合成参数可调。src/types/index.ts 定义 TtsParams(文本、语音、语速等)与 TtsSettings(格式、并发、重试、主题等),测试会验证 rate/pitch/volume 的字符串格式。用户可调节语速、音调、音量并为不同用途产出贴合需求的听感。

⑥ 并发与重试可配置的处理管线。Settings.vue 提供重试次数(1-10)与文件并发数(1-5)下拉配置并持久化到 settings store;batch/tts 命令按该配置驱动并发执行与失败重试。大批量转换时可按本机性能调节并发以提高吞吐,并通过重试降低网络抖动导致的单文件失败率。

⑦ 本地化与主题自适应。App.vue 用 matchMedia 监听 prefers-color-scheme 自动切换明暗主题,并提供独立手动开关;utils/appLocale.ts 统一归一化 zh-CN/en-US,按 localStorage → app_language → navigator.language 的优先级回退,默认 zh-CN。

⑧ 应用内版本检查与更新提示。AppLayout.vue 启动时 fetch GitHub 最新 Release 的 tag_name,用 isVersionNewer 逐段比较 semver,发现新版本可点击打开发布页(opener);fetch 失败则静默处理。用户无需手动去仓库查看。

把八条串起来看,它覆盖的其实是「单条文本试听」「成批文档产出」两条主线,再补上偏好、主题、语言、更新这些体验细节,形成一个完整的桌面工具闭环。

04

PART

四、技术架构与设计取舍

ARCHITECTURE

项目是典型的三层结构:前端(Vue 3)→ 桥接层(Tauri 2 command/event)→ 后端(Rust)。理解每一层放什么,比记住用了哪些库更重要。

为什么是 Tauri 而不是 Electron?

Electron 用 Node.js 做后端,打包体积大、常驻内存高;Tauri 用系统 WebView 渲染界面,后端是原生 Rust。对 TTS 这类需要处理长连接、二进制音频、文件系统写入的工具来说,Rust 后端几乎是天然优势:内存安全、异步生态成熟(Tokio),还能直接调用系统能力。

为什么把 TTS 逻辑放在 Rust,而不是前端 JS?

这是项目最关键的取舍。合成逻辑没有写在前端,而是沉到 Rust:

与 Edge TTS 的通信是 WSS 长连接,需要稳定的异步运行时,tokio-tungstenite 正合适;

长文本要按字节切分并逐块累积音频,涉及大量二进制缓冲,交给 Rust 更高效;

超时与取消需要精确的控制流,watch 通道比前端事件简单可靠;

文件写入与 FFmpeg 调用本身就是系统级操作,放在前端要绕一大圈。

前端只做两件事:收集用户意图(文本、参数、文件、路径),以及把后端返回的状态渲染出来。职责边界干净,出错也更容易定位。

数据流概览

...

用户在 TTS 页输入文本 + 选择语音/语速

  ↓

前端调用 Tauri command(携带 TtsParams)

  ↓

Rust: communicate.rs 切分文本 → WSS 流式合成 → 累积音频字节

  ↓

Rust: ffmpeg.rs save_audio 校验格式 → 直写或临时转换 → 落盘

  ↓

前端拿到事件/结果 → 播放器试听 或 批量进度面板更新

批量路径多了一层调度:batch.ts 把文件列表转成带状态的任务队列,按并发数分发,每个任务内部再复用上面的合成与保存链路,失败任务按重试次数回退,最终把 pending/processing/done/error 四个状态反馈给 UI。

05

PART

五、关键模块源码拆解

INSIGHT

5.1 流式合成核心:edge_tts/communicate.rs

这是整个项目最有技术含量的文件。它要做的事是:连上 Edge TTS 的 WSS 服务,把长文本切成若干块逐块发送,把返回的音频帧收集起来,同时支持超时和取消。

...

// 结构示意(非逐行源码)

let state = Arc::new(Mutex::new(CommunicateState::default()));

let (cancel_tx, mut cancel_rx) = watch::channel(false);

// 1. 按字节长度把长文本切分为多个 chunk

// 2. 每个 chunk 通过 tokio-tungstenite 建立的 WSS 连接发送

// 3. 接收二进制音频帧,累积进 CommunicateState

// 4. 用 select! 同时监听数据帧、取消信号与超时

select! {

 frame = ws.next() => { /* 累积音频 */ }

 _ = cancel_rx.changed() => { /* 主动中止 */ }

 _ = tokio::time::sleep(timeout) => { /* 超时退出 */ }

}

几个设计点值得注意:

按字节长度切分而不是按字符或句子。Edge TTS 对单次请求的文本长度有限,按字节切分能更准确地对齐服务端限制,同时避免把多字节字符切坏;

**状态用 Arc<Mutex<CommunicateState>> 共享**,意味着合成任务可以被多个异步分支同时持有引用,比如取消分支需要在收到信号时安全改写状态;

**watch 通道承载取消信号**,比一次性事件更适合「广播式」的中止通知;配合超时,用户既能在合成中途点击停止,也能在网络卡死时自动退出,而不是无限等待。

这种「流式 + 可中断」的设计,直接决定了体验上的一个关键差异:长文本不会让界面僵住,也不需要等全部合成完才能操作。

5.2 音频保存:audio/ffmpeg.rs 的两段式策略

合成得到的是一段音频数据,但要落成用户选择的格式,还需要一次转换。项目的做法很务实——mp3 直写,其他格式先转临时 mp3 再调 FFmpeg

...

pub async fn save_audio(...) -> Result<...> {

 // 1. 校验目标扩展名是否属于 mp3 / wav / ogg / flac

 // 2. 若为 mp3:直接把音频字节写入目标路径

 // 3. 其他格式:先写一个临时 mp3,再调 convert_audio 转换到目标格式

 // 4. 转换完成后清理临时文件

}

因为 Edge TTS 返回的音频本身就接近 mp3 编码,mp3 直写可以省掉一次无损转码;而 wav/ogg/flac 需要的编码能力,交给 FFmpeg 处理最稳妥。

FFmpeg 程序的解析优先级也很讲究:先看环境变量指定的路径,再看应用捆绑的二进制。这样一来,普通用户装机即可用(跟着应用走),进阶用户也能用环境变量指向自己的 FFmpeg 版本。这正好呼应了 README 里的 pnpm ffmpeg:sync——通过 postinstall 钩子把二进制同步进来,减少手工配置。

5.3 批量调度:stores/batch.ts

批量页的所有复杂度都压在这个 Pinia store 里。它至少解决四个问题:

去重添加:拖拽或选择文件时,相同路径的文件不会重复入队,避免重复劳动;

输出目录生成:根据用户配置的输出目录,或回退到源文件所在路径来生成目标目录,保证结果可预期;

任务 ID:用 crypto.randomUUID 为每个任务生成唯一标识,重试和状态更新都围绕这个 ID 进行;

状态机:每个文件维护 pending / processing / done / error 四种状态,UI 只需要按状态渲染,重试就是把 error 重新推回 pending。

一个容易被忽略的细节是路径分隔符判断:Windows 用反斜杠、macOS/Linux 用正斜杠,拼接输出路径时必须区分,否则跨平台会出错。这类细节处理得好不好,往往决定了工具能不能真的跨平台用。

5.4 文本读取与 Markdown 清洗:file.rs

批量导入支持 .txt、.md、.markdown、.docx。这里有两个必须处理的问题:

读取不同格式:纯文本直接读,docx 需要解包提取文本;

去掉 Markdown 标记:file.rs 中的 strip_markdown 会把 #*、链接语法等标记清理掉。

这一步很关键——如果把 ## 标题**加粗** 原样喂给 TTS,朗读出来就是一堆奇怪符号。清洗之后,文本才真正「适合被听见」

5.5 语音列表:stores/voices 与 voices.rs

语音选择依赖 Edge TTS 的语音清单。Rust 命令负责拉取,前端 store 负责组织:

Voice 类型带有 VoiceTag 元数据(性别、风格、支持角色等),用于展示与筛选;

store 支持按 locale 切换,选中语音与语言联动,方便对比不同口音;

持久化恢复时,如果上次选的语音已经不存在,会回退到首个可用语音,而不是留下空选择。

这个回退逻辑看着小,但它避免了一个常见尴尬:用户重装或服务端调整语音列表后打开应用,界面直接报错或空白。

5.6 类型契约:types/index.ts

TypeScript 在这里不是装饰。TtsParams 描述一次合成需要什么(文本、语音、语速等),TtsSettings 描述应用级偏好(输出格式、并发数、重试次数、主题等)。更细致的是联合类型的使用:BatchFile.status 用字面量联合限定状态集合,OutputFormat 用枚举限定四种格式。

好处是编译器会替你挡掉一批错误——比如把并发数写成字符串、把状态写成不存在的值,在开发阶段就会报出来。单元测试里还专门验证了 rate/pitch/volume 的字符串格式,因为这些参数最终要交给 Edge TTS 的 SSML 层解析,格式错了会静默失败。

5.7 偏好管理:主题、语言与持久化

偏好层由三块拼成:

主题:App.vue 用 matchMedia('(prefers-color-scheme: dark)') 监听系统设置,配合手动开关,实现自动 + 手动双模式;

语言:utils/appLocale.ts 把语言归一化成 zh-CN / en-US,按 localStorage → app_language → navigator.language 的优先级回退,默认 zh-CN;

状态持久化:借助 pinia-plugin-persistedstate,把设置和语音选择写进本地存储。

语言回退的优先级设计得挺克制:用户显式选择过,就用用户的;没选过,看应用级配置;都没有,再看系统语言;系统语言也不认识,兜底中文。这种多级回退在跨平台桌面应用里几乎是标配,因为用户的系统语言和偏好经常不一致。

5.8 版本检查:AppLayout.vue

启动时,AppLayout.vue 会 fetch GitHub 最新 Release,取出 tag_name,用 isVersionNewer 做逐段 semver 比较。发现新版本就展示入口,点击通过 opener 打开下载页;如果请求失败(比如没网、被限流),静默处理,不打扰用户。

「失败静默」是一个成熟的小细节:更新检查是锦上添花,让它阻塞启动或因网络异常弹错误,反而会伤害体验。

5.9 可测性与 Vite 配置

项目给关键逻辑配了 Vitest + happy-dom 的单元测试,覆盖点包括:AppLayout 的导航与版本比较、batch 的去重与命令参数、tts 的音频 URL 生命周期、voices 的持久化回退。为了可测,代码里用了 data-testid 标注元素,测试环境用内存 storage 替代真实存储。

Vite 配置也针对 Tauri 开发做了细致处理:固定 1420 端口且 strictPort(避免端口漂移导致 Tauri 连不上 dev server)、HMR 走 1421、忽略 src-tauri 目录监听(避免 Rust 编译产物触发前端热更新)、关闭 clearScreen 以保留 Rust 端的错误输出。这些小配置看似琐碎,却是「开发体验顺不顺」的分水岭。

06

PART

六、项目效果展示

INSIGHT

先看主界面。TTS 页把文本输入、语音选择、语速/音调/音量调节、播放器和保存按钮集中在一屏,采用的是带玻璃拟态效果的现代化布局。

[[IMAGE:1]]

批量转换页则是另一种信息密度:文件列表、每个文件的状态、整体进度、并发与重试配置都在这里。

[[IMAGE:2]]

设置页负责输出与处理两类偏好——默认保存路径、默认输出格式、显示语言、转换后自动播放,以及最大重试次数和文件/分段并发数。

[[IMAGE:3]]

07

PART

七、完整运行流程:一次合成到底发生了什么

INSIGHT

「用户点击合成」「音频落盘」串一遍,会更直观:

1

启动与初始化:应用启动,App.vue 判断系统主题,utils/appLocale.ts 确定界面语言,Pinia 从持久化存储恢复设置与上次选中的语音;

2

加载语音:前端调用语音列表命令,Rust 拉取可用语音,store 按当前 locale 组织并回退选中项;

3

用户输入:在 TTS 页粘贴文本,调节语速、音调、音量,选定语音与输出格式;

4

发起合成:前端构造 TtsParams,通过 Tauri command 交给 Rust;

5

文本切分:communicate.rs 按字节长度把长文本切成多个 chunk;

6

WSS 流式合成:通过 tokio-tungstenite 建立的连接逐块发送,接收音频帧并累积进 CommunicateState,期间可用 watch 通道取消或由超时兜底;

7

保存与转换:save_audio 校验扩展名——mp3 直写;wav/ogg/flac 先写临时 mp3 再经 FFmpeg 转换;

8

结果回传:前端拿到音频地址或文件路径,播放器可试听,或直接提示保存成功;

9

批量路径:若是批量模式,batch.ts 对每个文件重复第 4-8 步,按并发数并行,失败任务按配置重试,UI 实时刷新四个状态;

10

收尾:任务完成后按设置决定是否自动播放,所有偏好写回持久化存储。

08

PART

八、快速上手

INSIGHT

环境要求:Node.js ≥ 18pnpm ≥ 8Rust ≥ 1.70。系统依赖方面,Windows 无需额外配置;macOS 需要 Xcode Command Line Tools;Linux 需要 libwebkit2gtk-4.0-devbuild-essentialcurlfilex11-utilslibxdo-devlibssl-devlibayatana-appindicator3-devlibrsvg2-dev 等。

...

# 克隆项目

git clone <repository-url>

cd tts-vue-next

# 安装前端依赖

pnpm install

# 同步 FFmpeg 二进制(安装后自动执行)

pnpm ffmpeg:sync

# 启动开发模式

pnpm tauri dev

第一次 pnpm tauri dev 会编译 Rust 后端,耗时取决于机器,之后增量编译会快很多。Windows 上基本是「clone → install → dev」三步;Linux 用户则需要先补齐上面列出的系统库,否则构建会在 webkit2gtk 或 appindicator 环节报错。

09

PART

九、适用场景、限制与改进方向

INSIGHT

谁最适合用

自媒体创作者、播客与有声书生产者:需要为大量稿件或章节生成旁白,人工录音成本太高,而在线 TTS 站点又有字数与频率限制。这个项目可以拖拽批量导入 txt/md/docx,配置并发与重试后一键成批产出多格式音频,音色自然且免费。

无障碍与学习辅助用户:想把长文档、Markdown 笔记转成可听内容,又不想被网页版 TTS 的字数和粘贴限制困扰,还能顺手调语速、音调、音量,界面支持中英双语和深浅色自适应,门槛低。

偏自动化的开发者与小团队:希望把 TTS 能力本地化、可脚本化调用,需要控制输出格式、并发、重试与保存路径,而不是依赖不可控的第三方 SaaS。Rust 后端以 Tauri 命令暴露文件读写、音频转换、语音列表等能力,参数可配置,还带 Vitest 测试和类型检查,方便二次开发。

跨平台桌面用户(Windows / macOS / Linux):不同系统上装 TTS 工具往往依赖繁琐,尤其 Linux 需要额外系统库,还担心 FFmpeg 等二进制的配置问题。Tauri 2 跨平台打包、安装后自动同步 FFmpeg、分平台的文件定位实现,能明显减少手工配置。

典型使用场景

把一篇长文章或 Markdown 笔记粘进 TTS 页,调好语速音调实时试听,再保存为 MP3;

批量拖入整个文件夹的 txt/docx 书稿,设置并发数与输出目录,一次性生成全套有声读物音频;

为某个外语口音挑音色,切换语言对比试听后再导出;

弱网环境下转换出错时,依赖重试机制或对失败的单文件手动重试补齐产出;

夜间使用时让界面自动跟随系统切换深色主题,或手动切换中英文界面;

启动时通过侧栏版本检查发现新版本,一键跳到 GitHub Release 下载更新。

必须知道的限制

强依赖微软在线服务:断网或服务端策略变化时无法合成,也没有本地离线模型可选;

不适用于商业合规场景:免费在线服务的可用性与授权条款会随时间变化,正式商业项目需要评估替代方案;

FFmpeg 会带来体积增长:捆绑二进制的策略牺牲了安装包体积,换来的是开箱即用;

长文本仍受服务端限制:代码通过分块缓解,但极端长文本的稳定性取决于网络与服务端行为。

可能的改进方向

增加更细粒度的分段并发可视化,让用户知道是哪一段在重试;

把合成结果缓存起来,文本未变时跳过重复合成;

为批量任务增加暂停/恢复与失败导出清单,方便大规模返工;

补充更多导出参数(采样率、比特率)以适配播客等专业场景。

///

LAST

十、总结与行业启发

SUMMARY

tts-vue-next 的价值,不在于它发明了什么新算法,而在于它把一个「其实存在但很麻烦」的能力——Edge TTS——用工程化的方式交付了出来。它做了三件对的事:

把重逻辑放到正确的层:WSS 流式合成、分块累积、超时取消、音频转换、文件写入全部在 Rust,前端只负责意图与展示,边界干净;

把体验的坑一个个填平:FFmpeg 自动同步、语言多级回退、语音选择持久化回退、失败静默的版本检查、跨平台路径处理,这些不是亮点功能,却是「能不能日常用」的关键;

把可测性当回事:Vitest + happy-dom 覆盖版本比较、批量去重、音频 URL 生命周期等关键路径,配合 TypeScript 的联合类型与枚举,把一类低级错误挡在编译期。

放到更大的视角看,这类项目的启发是:当某个能力已经有免费、好用的云端接口,真正稀缺的往往不是接口本身,而是把它变成「顺手工具」的那层工程。谁能把这层做好——管好并发、重试、格式、偏好、跨平台——谁就能把一个命令行能力,变成用户每天愿意打开的桌面应用。

如果你正好有批量音频生产的需求,或者想找一个 Tauri 2 + Rust 的实际工程范本,tts-vue-next 都值得 clone 下来翻一翻源码。🚀

我是 满眼星光,一个热爱分享技术干货的人。

既然看到这里了,如果觉得有用,随手点个赞、在看、转发三连吧。

点赞
在看
转发

THANKS FOR READING

相关学习资料