# DSH KITT 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 网页界面打造的语音插件——以西班牙语为首选语言,并且在无需注视浏览器的情况下也能使用。 按下一个按键,说出你的需求,智能体会用语音回答。一个小巧的悬浮窗会停留在你正在使用的任何应用之上,告诉你当前的状态。 > **状态:1.0 版,每天在用。** 语音对话、悬浮窗和全局快捷键已完成并每天在使用。界面支持中文、英文和西班牙语。本地 Whisper 尚未实现;见 [缺失的功能](#缺失的功能)。 ## 为什么还需要一个语音插件 Harness 已有不错的语音插件,但没有一个能做到下面两件事,而其中一件不是靠努力就能做到的: - **它们不懂西班牙语。** 它们的语音识别是为中文和英语设计的。`dsh-kitt-voice` 使用 Whisper——真正多语言的识别模型——并用 Piper 朗读,后者拥有优秀的西班牙语语音且完全在本地运行。 - **浏览器一失焦它们就失效**——因为插件活在网页里,而网页听不到别人没有给它的按键,也没法显示在全屏游戏之上。这正是悬浮窗存在的原因。 ## 它能做什么 - **真正的语音对话,而不是听写。** 按一次就开始说。它听到你说完,转写、发送、等待回复并朗读出来,然后继续听。回合之间不需要按任何按钮。 - **能分辨语音和噪音。** 在嘈杂的房间里——电视、音乐、从扬声器传来的引擎声——用音量来判断你说完没有会失败:任何噪音都会被当成说话,而在免提模式下就意味着替你把噪音发给智能体。这里由真正的检测器——Silero——来决定。在本项目中的实测阈值是 0.30:静音 0.04,引擎噪音 0.13,低沉的轰鸣 0.10,哨声 0.16。后三者调大音量后,任何音量计都会被骗过。 - **按键说话,如果你更喜欢这种方式。** 按下麦克风或你的按键,说话,再按一次。文字会进入消息框;由你决定何时发送。 - **边写边读。** 在对话中,回复会一句一句地朗读,而智能体还在继续撰写,这样长回答不会先沉默十五秒。只有句子结束之后才会被朗读出来:半句话加一个停顿听起来像故障。代码块只会被说出名称,不会被逐字朗读。 - **你可以打断它。** 在它朗读时开口,它就会停下。这个门槛不是事先挑的数字:每次回复 开始的前半秒,麦克风会先听,而它听到的**就是**回声,因为那时还没有人说话。要算作 人声,必须超过这个底线三倍并持续三分之一秒。它在每次回复时重新测量,所以中途戴上 耳机会自动适应,而关门声太短,不足以触发它。 - **每次回复只用一种声音。** 引擎、语音和语速在回复开始时决定,并保持到回复结束。如果所选引擎失败,该回复的其余部分改用系统语音朗读,状态栏会说明;下一次回复会再次尝试该引擎。一次回复绝不会中途换声音。 - **两个页面,一套按键。** 当 Harness 在多个地方打开时,按键只会送到正在使用语音的页面——如果都没有在用,就送到你面前的那个。其他页面保持安静。 - **值得一听的声音。** 104 个神经语音,按语言和国家分组:45 个西班牙语——西班牙和美洲所有西语国家——47 个英语和 12 个中文。它们由微软的朗读服务提供,无需密钥、无需账户,**代价说得很清楚:回复的文本会离开你的电脑。** 除此之外什么都不会离开。 - **按你自己的节奏。** 朗读速度可以在半速到双倍之间调节,并适用于全部三种引擎:系统语音、Piper 和神经语音。听和读不一样,用别人的节奏听长回复很难跟上。在悬浮窗的菜单里设置即可,无需改任何文件。 - **对话有铃声。** 打开对话时响起升调,挂断时响起降调,让你仅凭声音就知道它正在聆听——而这正是你不看屏幕的时候。 - **界面支持三种语言。** 中文、英文和西班牙语,网页和悬浮窗都是如此。悬浮窗的语言在它自己的菜单里选择,与你说活时所用的语言相互独立。 - **或者什么都不离开。** 指向一个 Piper 语音文件夹,合成就在本机离线完成。就算两者都没有,它也能用系统自带的语音说话。从第一分钟起就能用;更好的语音是改进,不是前提。 - **在哪里都能用的快捷键。** 分配一个全局快捷键,就可以从任何应用——游戏、编辑器、任何东西——里和智能体说话。方向盘按钮映射到该按键同样有效。 - **自己选择设备。** 麦克风和声音输出分开选择,因为好的麦克风和好的扬声器很少是同一个设备。 - **它总是告诉你正在发生什么**——聆听中、转写中、朗读中——而一旦出错,它会说出是哪一部分、为什么。 ## 安装 ``` dsh plugin --profile web add dsh-kitt-voice ``` 该命令会把包安装到 Harness 配置文件中,并自动把插件追加到配置文件的 bundle 列表(声明了 `dsh.bundle` 的依赖会自动加入层栈)。重启 Harness——彻底停止,而不是简单地重新启动,否则你仍在和旧进程对话。在输入框工具栏会出现麦克风和扬声器按钮。 卸载: ``` dsh plugin --profile web remove dsh-kitt-voice ``` 同一个命令会同步 bundle 列表,只移除这个插件。旧版建议手动编辑 `package.json`,那属于旧版 CLI(会重写整个列表)的做法;当前版本按已安装状态同步。 **不使用 npm,而是使用本地克隆:** 让配置文件指向克隆目录。 ``` dsh plugin --profile web add link:/绝对路径/to/dsh-kitt-voice ``` 如果你的 Harness 使用其他配置文件名,把 `web` 换成你的名字。 ### 关于包管理器打印的警告 安装时会提到某个依赖的安装脚本未被执行:`msedge-tts`,它的 `preinstall` 是 `npx only-allow pnpm`。 **没有缺失任何东西,而且那个脚本不应该被执行。** 它不构建任何内容 —— 它是该库用来 强制**其自身**贡献者使用 pnpm 的一道闩,在任何其他包管理器下都会故意失败。该库发布 的是已编译的 JavaScript,不含原生代码。 **在 npm 下这只是一条警告**,安装会正常完成。在干净的机器上实测:`npm install` 以 0 退出,该库列出 322 个语音并返回真实音频,什么都没有构建,也没有安装任何本地语音。 **在 pnpm 下 —— 也就是框架配置文件所使用的 —— 它是一个错误**,会让整条命令失败: ``` [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: msedge-tts@2.0.7 ``` pnpm 会在你配置文件的 `pnpm-workspace.yaml` 里写下一行请你决定,并且让它保持未决: ```yaml allowBuilds: msedge-tts: set this to true or false ``` 把它设为 `false`,然后重新运行命令: ```yaml allowBuilds: msedge-tts: false ``` 看到就马上处理,因为只要那一行还没有决定,该配置文件里的**每一次**安装都会失败 —— 包括别人的插件,而那是一种相当令人困惑的发现方式。 ## 配置 除转写密钥外,一切都在 **设置 → 插件 → dsh-kitt-voice**:识别器、语言、辅助词汇、语音文件夹、语音、麦克风和声音输出。 **转写密钥来自 Harness 自己的凭据库**——和智能体密钥所在的地方一样。无需创建任何东西,也无需重启:以 `GROQ_API_KEY` 保存即可,插件会找到它。如果你的密钥存在别的名字下,把 `apiKeyRef` 指向那个名字。 如果凭据库为空,则使用环境变量 `DSH_KITT_API_KEY` 或 `GROQ_API_KEY`。 密钥不会显示在设置里,也永远不会到达浏览器。页面只询问*是否*已配置,通过一个不可能返回值的调用。每次请求都会重新解析,所以更换密钥立即生效。 ### 所有可以修改的设置 | 设置 | 作用 | | --- | --- | | `speechRate` | 朗读回复的速度。1 是语音本身的语速;0.5 是一半,2 是两倍。三种引擎都适用,包括系统语音——它由页面朗读而不是服务器。 | | `uiLang` | 插件自身界面的语言:西班牙语、英语或简体中文。与转写语言相互独立——你可以用西班牙语口述而界面保持英文。 | | `buttonColours` | 控件各有自己的颜色,或者全部为白色。颜色让人一眼看出每个控件的作用;朴素模式是给觉得那样太吵的人准备的。 | | `overlayAuto` | 一使用语音就自动打开悬浮窗,并随框架一起关闭。默认关闭:一个自己冒出来的窗口,是没有人要求过的窗口。 | | `micLabel` | 使用哪个麦克风,**按名称**。留空表示系统默认。刻意按名称而不按 id:浏览器会给每个来源分配不同的 id 来指代同一个物理设备,所以在悬浮窗里选中的 id 在页面里毫无意义。 | | `outputLabel` | 回复从哪个扬声器或耳机播放,按名称。 | ### 识别器 | 选项 | 需要账户 | 在桌面应用中可用 | 说明 | | --- | --- | --- | --- | | 浏览器(默认) | 否 | **否** | 仅 Chrome 和 Edge;音频经过浏览器厂商 | | Groq Whisper | 是 | 是 | 精度和速度最佳;需要在凭据库中有密钥 | 浏览器识别器是默认项,让新用户几秒钟内就能说话。它在 Electron 中不可用——对象存在但识别每次都会失败——所以当 Harness 嵌入桌面应用时,插件会切换到 Groq 并明确告知。 ### 西班牙语中夹杂的英语词 说西班牙语的人会在西语句子中间说出 `setup`、`brake bias`、`understeer`。只告诉 Whisper「西班牙语」,它会把它们按读音写成西语(`cetap`、`breik baias`),智能体收到的就是乱码。设置中的**辅助词汇**会发送给 Whisper,让这些术语保持英语。请按你自己的领域编辑它。 ## 语音检测器 免提对话必须知道你何时说完一句话。那是一个模型——Silero v5——加上它的运行时,一共约十六兆字节。 **它们不随本包一起发布。** 大多数安装语音插件的人只想按一个按钮说话;让所有人都为可能永远不用的模式背负十六兆字节是不礼貌的。它们按以下两种方式之一到达,顺序如下: 1. **一个你已有的文件夹**,在设置中指定为 `vadDir`——什么都不下载; 2. **一次有引导的下载**,第一次开启对话时先告知大小。 无论哪种方式,文件都由 **Harness 自己**回传给页面,浏览器永远不会自行访问互联网,而且固定列表中的六个名字是唯一能成为路径的名字。 是六个文件,不是五个:检测器自己的包不携带推理运行时。它期望页面上已有一个,并且要先加载。 ## 悬浮窗 ``` cd overlay start.cmd Windows ./start.sh macOS 和 Linux ``` Electron 不随包发布:Harness 是网页应用,大多数人不想要桌面窗口。启动器会使用你已经安装的 Electron——把 `DSH_KITT_ELECTRON` 指向它——或在这里运行 `npm install` 获取一个。 一条悬浮在所有东西之上(包括全屏游戏)的横条,**横条就是控制区**——和 Harness 自带工具栏里的完全相同:同样的图形、同样的颜色、同样的尺寸,因为它们本就是同一组控制,只是出现在两个地方。 - 左侧的品牌字母 **K**——KITT 模式:免提连续对话,无需再按任何键。模式开启时它会显示当前状态的颜色——等待时蓝色,聆听时绿色,朗读时红色——思考或朗读时它的光晕会轻轻脉动。两边都可以关闭。旁边的「kittcat.com」文字会打开网站,而且只在空闲时; - **红色**麦克风——按一次开始,说完再按一次;文字会落在输入框里,**由你按 Enter 发送**; - **扬声器**——再听一遍最后的回复,或让它停下; - **琥珀色**带斜线的麦克风——**静音**。它是真的停止检测器,不是假装。这是为你没有看屏幕的时刻准备的:有人过来跟你说话,或者你要播放视频。静音只是让对话待命,不会挂断; - **齿轮**——其他一切:麦克风、扬声器、语音、语速、语言和按钮配色;快捷键;停止朗读和外形; - **×**——不打开菜单直接关闭窗口。关闭不是死路:下次启动语音时插件会重新打开它——但不会在你关掉它的那场对话中途重新打开——Harness 工具栏里的齿轮也随时能打开。 **边框**承载状态,可以用余光读取:**静止时无颜色,聆听时为绿色——并随测得的音量增长——思考时为蓝色并缓慢呼吸,朗读时为红色。** 如果出错,会显示闪烁的 **ERROR** 字样,无需解读任何颜色。 按住即可拖到任何位置,它会记住你留在哪里。 **快捷键**(在菜单中分配):`F8` 说话并发送,`F9` 开始或结束对话,`F7` 静音麦克风,`F10` 再次听回复,`F11` 停止朗读(在对话中同时停止等待回复),`F6` 打开菜单。想用方向盘按钮,就在方向盘自己的软件里把它映射到这些按键之一——无需任何手柄代码。属于整个系统的按键(Ctrl+C、Alt+F4 等)会被拒绝:全局快捷键会把按键从机器上的**所有**应用中夺走。 如果你的 Harness 不在 3081 端口,设置 `DSH_KITT_PORT`。它只接受**端口**,绝不接受 URL:窗口只能访问 loopback。 ## 结构 ``` lib/ 插件 index.js 服务器端:设置、HTTP 路由、捕获最后一条回复 client.js 浏览器端:控制、录音、设置卡片 guard.js 谁可以调用路由 transcribe.js 语音转文字 speak.js 使用本地 Piper 语音朗读 chunk.js 把回复切分成可朗读的片段 neural.js 神经语音,以及它们会让什么离开你的电脑 overlay.js 使用语音时打开悬浮窗 vad.js 语音检测器的文件,以及它们如何到达 lastfromlog.js 从会话日志恢复最后一条回复 apikey.js 每次调用解析密钥,从不缓存 log.js 一行启动信息,以及被拒绝的请求——绝不含密钥 freshness.js 检测服务器运行的是旧版插件 paginas.js 多个页面同时打开时由哪一个响应按键 overlay/ 悬浮窗(独立的 Electron 应用) main.js 窗口、外形和位置 shortcuts.js 系统级快捷键 requests.js 窗口可以向 Harness 请求的封闭列表 textos.js 窗口显示的所有文字,三种语言 index.html 它绘制的内容 test/ 值得保护的部分 ``` 两半从不共享内存。它们通过 `/dsh-kitt-voice` 下的十三条 loopback 路由通信:`config`、`settings`、`devices`、`voices`、`transcribe`、`speak`、`last`、`state`、`command`、`orders` 和 `vad/status`、`vad/download`、`vad/file`。每一条都会检查调用者。状态流向为页面 → 主机 → 悬浮窗;`command` 流向相反,是浏览器之外的按键到达页面的方式。 ## 安全 - **每条路由都检查调用者。** Loopback 不是隐私:你访问的任何页面都可以让你的浏览器向 `127.0.0.1` 发送请求。请求必须来自 loopback,携带 `Origin` 的请求必须指向这台服务器——相同的 loopback 写法、相同的端口(`Origin` 和 `Host` 都由调用者写入,所以永远不信任它们彼此一致)。拒绝不会透露关于这台机器的任何信息。 - **转写密钥永远不会到达浏览器**,也永远不会被记录。页面只知道是否已配置。 - **语音名不能变成路径。** 在拼接到文件夹之前会经过严格校验。 - **悬浮窗被牢牢锁住**:上下文隔离开启、页面中没有 Node、沙箱、无导航、无新窗口、无浏览器权限,并且只能访问 `127.0.0.1` 上的可配置端口——永远不会接受别人给它的 URL。它的页面完全不发起网络调用:所有请求都由主进程对照 `overlay/requests.js` 的封闭列表转发,所以即使它自己的代码也无法把它指向别的服务器。 - **文件名同样不能变成路径。** 检测器的文件按固定六项列表按名提供;其他一切在构造路径之前就被拒绝。 - **关闭窗口时归还全局快捷键。** ## 测试 ``` npm test ``` 88 项测试,使用 `node --test` 运行,无需构建步骤。它们覆盖出错代价最高的部分:谁可以调用路由、语音名能否逃出它的文件夹、回复切分器承诺了什么、日志回退永远不会在它所服务的路由内部抛错、窗口的请求白名单、朗读句子切分器、交给语音的文本片段、两个页面同时打开时由哪一个响应按键,以及速度档位。 ## 缺失的功能 - **本地 Whisper。** 会消除桌面应用中对密钥的需求。它需要模型管理和音频转换,尚未实现。 - **Windows 以外的一切。** 这里没有任何 Windows 专属的东西——语音、窗口和快捷键都有对应方案——但它只在 Windows 上运行过。欢迎报告。 ## 已经付过代价的陷阱 二十四个各花掉一整个下午的问题,连同它们的症状一并写下: [已经付过代价的陷阱](https://github.com/kittcat-lab/dsh-kitt-voice/blob/main/DOCUMENTACION/TRAPS.zh.md)。它们全都是在使用中暴露的, 而不是靠读代码发现的,而且没有一个报错。动到哪一部分之前,先读属于它的那一条。 ## 许可证 MIT——见 [LICENSE](LICENSE)。参考过的既有项目在 [NOTICE](NOTICE) 中致谢。如何参与:[CONTRIBUTING.md](CONTRIBUTING.md)。变更记录:[CHANGELOG.md](CHANGELOG.md)。 由 [Kitt Cat](https://kittcat.com) 开发 · kittcat.com English: [README.md](README.md) · Español: [README.es.md](README.es.md)