言之有物:做一个当场揪出口头禅的表达训练应用
灿若繁星先生

你有没有录过自己即兴发言?不录不知道,一回放,「嗯」「然后」「就是说」「可能差不多」密密麻麻。这些问题词说话时完全无感,旁人听得清清楚楚,偏偏自己最后一个知道。

我做了个桌面应用来治这个毛病,叫言之有物(Weighty Words)。对着麦克风说一段话,语音实时转成大字字幕,填充词、犹豫词、笼统词当场标红标黄,说完再生成一份结构化分析报告。整个链路是:实时语音识别 → 本地词库分析 → AI 反馈。

项目已开源:GitHub - canyuda/weighty-words,macOS(dmg)和 Windows(exe)安装包都在 Releases 里。

从哪来的

这个项目 fork 自 Sisi 的开源项目 expression-trainer,表达训练的思路来自她,在此致谢。原项目是 MIT 协议,我的修改同样以 MIT 开源,LICENSE 里保留了原作者的版权声明。

原版给了我表达的训练思路和最初的原型,我在它基础上做了大幅扩展:语音识别从单一引擎扩到四家云端加一个完全离线的本地引擎,AI 反馈从写死的几个端点扩成多提供商加三协议自定义接入,另外补上了词库编辑器、训练规则定制、媒体文件导入、统一日志、双平台打包和一套测试。

用起来是什么样的

按下空格开始说话,主界面是一张「纸面画布」,实时字幕大字铺在上面。问题词当场高亮:

标记 含义 例子
🔴 红色波浪下划线 填充词 嗯、啊、那个、然后
🟠 橙色波浪下划线 犹豫词 可能、我觉得、差不多
🟡 黄色底色 笼统词 很快、开心、很多

右侧洞察栏实时累计各类问题词的数量和表达密度。每说满 30 字(可调),「实时反馈」面板会弹出一条不超过 8 个字的提示,比如「又是那个」。说完点「结束」再点「生成报告」,报告随模型输出逐字上屏,内容包含:总评(0–100 分)与定位、亮点、逐句编辑(原文 → 建议 → 原因)、用词精准度替换表、行为模式分析、数据与下次练习重点。同一份文稿再点一次直接显示已有报告,不会重复调模型,想重来就在弹窗里点「重新生成」。

除了麦克风,还有两条进料路径:粘贴逐字稿,离线分析现成文稿;导入音频或 mp4 视频(取音轨),按 1 倍速真实节奏走完整训练链路——拿以前的录音练,或者做可重复的全链路测试都行。

词库本身完全开放:四类词条(填充词 / 犹豫词 / 笼统词映射 / 情绪词)在编辑器里可视化增删改,带撤销栈,支持导入导出和恢复出厂,保存即生效。训练规则页还能配四样东西——训练目标、自定义触发规则、表达风格、额外口癖词——实时反馈和最终报告都会按这些定制。

几个技术决策

项目是 Electron 桌面应用,纯 JavaScript(CommonJS),没有框架、没有打包器。渲染进程就是原生 HTML/JS,index.html 做单窗口路由外壳,工作台、设置、词库编辑器、训练规则四个页面都是页内 section。作为一个工具型桌面应用,这套「原始」技术栈换来的是零构建链、随起随改、任何人打开就能读懂的代码。

下面几个设计点值得展开说说。

音频流跨进程

Electron 的进程边界是这个项目最核心的架构约束。音频采集在渲染进程(getUserMedia + AudioContext,重采样到 16kHz,每块 4096 采样转 Int16Array),经 IPC feed-audio 单向发到主进程;识别结果由主进程经 asr-result 事件推回渲染进程,云端和本地引擎统一 {text, isFinal} 这个契约;渲染层拿到文本再经 analyze-text IPC 做词库分析、字幕上色和统计。

1
2
3
4
渲染进程                          主进程
麦克风 16kHz PCM ──feed-audio──▶ ASR 引擎(云端 WebSocket / 本地 sherpa-onnx)
字幕高亮·统计 ◀──asr-result── 识别结果 {text, isFinal}
──analyze-text─▶ 本地词库分析

安全上走的也是 Electron 当前推荐配置:contextIsolation: true、nodeIntegration: false,主进程能力全部经 contextBridge 暴露成 window.api。

识别引擎抽象:五选一,失败不切换

语音识别做了统一的引擎抽象,接口就五个动作:init / feed(Int16) / stop / onResult / onError。实现有五种:

  • 四家云端:阿里云百炼、腾讯云、火山引擎、讯飞
  • 一个本地引擎:sherpa-onnx 流式 paraformer,中英双语,int8 量化,约 237MB,从 Hugging Face 或国内镜像下载,带 SHA-256 校验和断点续传

引擎模式三档可选:「自动」在已配置的云端里按优先级择优(百炼 > 腾讯云 > 火山 > 讯飞),没有任何云端配置时兜底本地;也可以强制指定某一家,或完全离线只走本地。

有一个我坚持的细节:所有引擎失败都不自动切换。识别挂了就把错误归类(鉴权 / 额度 / 网络 / 服务端 / 音频)直接报给你,附上建议和「去设置」入口。自动降级到另一个引擎听起来更「智能」,但你拿到的那份分析报告是哪个引擎出的、质量打了几折,就成了黑箱。训练工具要的是可信,不是永不报错。

关掉云端的一键「优化」

各家云 ASR 基本都带「语气词过滤」开关——服务端把嗯、啊、那个这些不顺滑的地方洗掉,输出一段干净的转写文本。对会议纪要这是好事,对这个项目是灾难:语气词本身就是词库分析的核心数据源,洗掉了,「填充词出现 17 次」这个统计就不存在了。

所以调用百炼时固定 disfluency_removal_enabled: false,宁可转写结果难看,也要保住原始的口语面貌。这个参数是该项目的「禁改项」之一。

词库分析在本地,不上云

字幕高亮和统计不依赖任何云端服务:四类词表 + 自实现的最大正向匹配分词(最大词长 6),在渲染进程一句 analyze-text 就地完成。这意味着识别引擎哪怕走云端,「你说了哪些口水词」这件事也不出本机;粘贴逐字稿分析更是全程零网络。

AI 反馈的多提供商适配

最终报告由 LLM 生成,内置智谱 GLM、OpenAI、DeepSeek、Ollama 四个预设,自定义接入支持三种协议:OpenAI Chat Completion、OpenAI Responses、Anthropic Messages。每个条目可以配多份 Key、行内切换模型,逐模型做连通性测试。所有调用统一走超时保护和 SSE 流式,报告逐字上屏。

开发测试主要基于智谱 GLM,国内网络友好,报告质量出色;DeepSeek 成本极低,质量同样在线。想一分钱不花,Ollama 本地跑。

配置透明:明文 JSON + 掩码

全部用户配置存在 ~/.weighty-words/ 下的三个明文 JSON 里:settings.json、rules.json、words.json。可以直接看、直接备份、直接手改。API Key 的安全性靠文件权限兜底(POSIX 0600,仅当前用户可读),界面上只显示掩码(前 5 位 + 后 5 位),明文经白名单 IPC 按需换取。写入走串行队列加原子写,不会因为并发保存写坏配置。

「明文存储」听起来像偷懒,其实是选择:配置对用户完全透明,比塞进一个加密的二进制 blob 更好备份、更好迁移,也更容易排查问题。密钥安全这个责任,交给文件系统和操作系统。

测试

单元测试用 vitest,全部离线运行——fetch 打桩,不发真实请求。Electron 冒烟用 Playwright,启动时通过环境变量(EXPRESSION_TRAINER_AUTOMATION=1)加隔离的配置目录,测试不会污染真实配置。npm test 一条命令跑完全部。

怎么获取

到 Releases 下载对应平台安装包。

macOS 注意:应用没有做 Apple 公证,首次打开若提示「已损坏」,是 Gatekeeper 对未签名应用的拦截,执行一次下面的命令即可:

1
xattr -cr "/Applications/Weighty Words.app"

Windows 安装时若被 SmartScreen 拦,点「更多信息 → 仍要运行」。

想完全离线用,进设置页下载本地识别模型(237MB)后切到「仅本地」——识别和词库分析都不再需要网络,AI 反馈用 Ollama 也可以本地跑。

小结

这个项目解决的是一件小事:让「口癖」从无感变成有感。技术上没有炫技,五家识别引擎的统一抽象、关掉语气词过滤、本地分词、明文配置,每个决策都在往「可信、透明、可控」的方向使劲。

如果你也有表达训练的需求,或者想看看一个无框架 Electron 应用的完整长相,欢迎来 GitHub - canyuda/weighty-words 逛逛,提 issue、提 PR 都欢迎。