开源项目

moonshine

moonshine

低延迟语音 AI 工具包,集成 STT、意图识别、TTS 及对话流框架,专为构建实时语音代理和界面设计。亮点在于全本地运行、模型自研且优于 Whisper 的流式精度与响应速度,支持跨平台部署(iOS/Android/Raspberry Pi 等),并内置语义匹配的对话管理。注意:英语模型 MIT 许可,其他语言模型采用非商业的 Moonshine Community License。

README

Moonshine Voice Logo

Moonshine Voice

面向所有人的语音接口

Moonshine Voice 是一个面向开发者的开源 AI 工具包,用于构建实时语音智能体和应用。

  • 一切在设备端运行,因此快速、私密,且无需账户、信用卡或 API 密钥。
  • 该框架和模型针对实时流式应用进行了优化,通过在用户说话时完成大量工作,提供低延迟响应。
  • 所有语音转文本模型均基于我们的前沿研究从零训练而成,因此我们能够在顶尖水平上提供比 Whisper Large V3 更高的准确率,并可提供仅 1MB 的微型模型用于受限部署
  • 跨平台集成简单,同一库可在 PythoniOSAndroidmacOSLinuxWindows树莓派物联网设备微控制器DSP 和可穿戴设备上运行。
  • 开箱即用。其高层 API 提供了常见任务的完整解决方案,如转写、文本转语音、语音克隆、说话人识别(diarization)、命令识别和对话式智能体,因此你仅需一个库即可构建语音应用。
  • 支持多种语言,包括英语、西班牙语、普通话、日语、韩语、越南语、乌克兰语和阿拉伯语(STT),以及英语、西班牙语、阿拉伯语、德语、法语、印地语、意大利语、日语、韩语、荷兰语、葡萄牙语、俄语、土耳其语、乌克兰语、越南语和普通话(TTS)。

快速开始

加入我们的 Discord 社区获取实时支持

iOS、Android、macOS、Windows 和树莓派的示例应用已作为独立压缩包发布在 GitHub Releases 上(主要是 {platform}-{Project}.tar.gz,与 examples/ 下的文件夹名称匹配;Windows 也提供了 moonshine-voice-windows-x86_64.tar.gz 用于 C++ 示例)。完整发布下载列表见示例部分。

Python

pip install moonshine-voice
moonshine-voice mic --language en

监听麦克风并实时打印更新后的转写字幕。

moonshine-voice intent

监听用户定义的动作短语(如“打开灯”),使用语义匹配识别自然语言变体。更多信息请查看我们的“入门”Colab 笔记本视频

moonshine-voice tts --language en_us --text "Hello world"

合成并朗读文本。

iOS

下载 github.com/moonshine-ai/moonshine/releases/latest/download/ios-Transcriber.tar.gz,解压后在 Xcode 中打开 Transcriber/Transcriber.xcodeproj 项目。

Android

下载 github.com/moonshine-ai/moonshine/releases/latest/download/android-Transcriber.tar.gz,解压后在 Android Studio 中打开 Transcriber 文件夹。

Linux

下载git clone 此仓库,然后运行:

cd core
mkdir -p build
cd build
cmake ..
cmake --build .
./moonshine-cpp-test

macOS

Moonshine Voice 支持 Apple Silicon (arm64) 和 Intel (x86_64) 的 Mac。

下载 github.com/moonshine-ai/moonshine/releases/latest/download/macos-MicTranscription.tar.gz,解压后在 Xcode 中打开 MicTranscription/MicTranscription.xcodeproj 项目。

Windows

下载 github.com/moonshine-ai/moonshine/releases/latest/download/windows-cli-transcriber.tar.gz,解压后在 Visual Studio 中打开 cli-transcriber\cli-transcriber.vcxproj 项目。

压缩包内包含库和模型文件,按 Ctrl+Shift+B 或 F7 即可生成可执行文件。

树莓派

你需要插入 USB 麦克风以获取音频输入,但 Python pip 包已针对树莓派优化,可直接运行:

 sudo pip install --break-system-packages moonshine-voice
 moonshine-voice mic --language en

我在 YouTube 上录制了一个屏幕录像帮助您入门;您也可以下载 github.com/moonshine-ai/moonshine/releases/latest/download/raspberry-pi-my-dalek.tar.gz 获得一些有趣的树莓派特定示例。README 包含了使用虚拟环境进行 Python 安装的信息(如果您不想使用 --break-system-packages)。

查看 github.com/moonshine-ai/pi-help-bot 获取更高级的示例。

何时选择 Moonshine 而非 Whisper?

总之:当你处理实时语音时。

模型 WER # 参数量 MacBook Pro Linux x86 R. Pi 5
Moonshine Medium Streaming 6.65% 245 百万 107ms 269ms 802ms
Whisper Large v3 7.44% 15 亿 11,286ms 16,919ms N/A
Moonshine Small Streaming 7.84% 123 百万 73ms 165ms 527ms
Whisper Small 8.59% 244 百万 1940ms 3,425ms 10,397ms
Moonshine Tiny Streaming 12.00% 34 百万 34ms 69ms 237ms
Whisper Tiny 12.81% 39 百万 277ms 1,141ms 5,863ms

参见基准测试了解这些数字的测量方式。

OpenAI 发布的 Whisper 模型系列 是开源语音转文本领域的一大进步。它们提供了多种尺寸,让开发者可以在计算量和存储空间与准确率之间权衡,以适应其应用。其最大的模型(如 Large v3)的准确率更是超越了谷歌、苹果等大型科技公司之外的任何现有技术。在 Moonshine,我们是 Whisper 的早期热情采用者,并且至今仍是这些模型及其周边优秀框架(如 FasterWhisper 等)的忠实粉丝。

然而,在构建需要实时语音接口的应用时,我们发现 Whisper 无法提供我们所需的一些特性:

  • Whisper 始终以 30 秒输入窗口运行。批量处理音频时这不是问题——你通常可以在文件中向前查找,找到约 30 秒的语音块进行处理。但语音接口无法向前查找以从其输入流中创建更大的块,而短语很少超过 5 到 10 秒。这意味着编码器和解码器要浪费大量计算资源处理零填充,导致返回结果的延迟更长。由于用户界面的首要要求之一是响应性(通常定义为延迟低于 200ms),即使是在算力充裕的平台,这也会损害用户体验,而在资源受限的设备上则几乎无法使用。
  • Whisper 不缓存任何内容。语音接口的另一个常见需求是在用户说话时显示反馈,让用户知道应用正在监听并理解他们。这意味着在用户说一句话的过程中,要反复调用语音转文本模型。大多数音频输入是相同的,只是末尾有一小段新增内容。尽管大部分输入不变,Whisper 每次都从头开始,对已经处理过的音频执行大量冗余计算,与固定输入窗口一样,造成不必要的延迟,损害用户体验。
  • Whisper 对许多语言支持不佳。Whisper 的多语言支持是一项了不起的工程壮举,展示了单一模型能处理多种语言甚至提供翻译的能力。OpenAI 的这张图表(原始数据见附录 D-2.4)显示了在 15 亿参数的巨大模型下,词错误率(WER)的下降情况。

语言图表

列出了 82 种语言,但只有 33 种的 WER 低于 20%(我们认为可用的标准)。在边缘设备常用的 Base 模型尺寸下,只有 5 种语言达到低于 20% 的 WER。韩语、日语等亚洲语言是拥有大量技术创新的大市场的母语,但 Whisper 在这些语言上的准确率不足以用于大多数应用。OpenAI 通过云端 API 提供的专有内部版本 Whisper 似乎准确率更高,但并未以开放模型形式提供。

  • 支离破碎的边缘支持。Whisper 周围已经形成了一个出色的生态系统,有许多成熟的框架可用于部署模型。然而,这些框架往往专注于桌面级机器和操作系统。也有一些项目可用于 iOS、Android 或 Raspberry Pi OS 等边缘平台,但它们往往具有不同的接口、能力和优化水平。这使得构建需要运行在多种设备上的应用变得异常困难。

所有这些限制驱使我们创建自己的模型系列,以更好地满足实时语音接口的需求。这花费了一些时间,因为与来自网络的文本数据量相比,可用开放语音数据集的规模很小。经过大量的数据收集工作,我们得以发布第一代 Moonshine 模型。这些模型消除了固定输入窗口的限制,并进行了其他架构改进,在实时语音应用中延迟显著低于 Whisper,通常快 5 倍或更多。

然而,我们不断遇到需要在更受限平台上实现更低延迟的应用。我们还希望提供比初始模型最高端(相当于 Base)更高的准确率。这促使我们推出了第二代 Moonshine 模型,具备以下特性:

  • 灵活的输入窗口。你可以提供任意长度的音频(但我们建议保持在 30 秒以下),模型仅针对该输入进行计算,无需零填充。这为我们带来了显著的延迟提升。
  • 流式缓存。我们的模型现在支持随时间逐步添加音频,并缓存输入编码和部分解码器状态,从而跳过更多计算,显著降低延迟。
  • 语言专用模型。我们为多种语言收集了数据并训练了模型,包括阿拉伯语、日语、韩语、西班牙语、乌克兰语、越南语和中文。正如我们在《Moonshine 风味》论文中讨论的那样,我们发现,如果限制模型只专注于一种语言,相比训练一个多语言模型,在相同的尺寸和计算量下可以获得更高的准确率。
  • 跨平台库支持。我们自己构建应用,需要能够将这些模型部署到 Linux、macOS、Windows、iOS 和 Android 上,并能从 Python、Swift、Java 和 C++ 等语言中使用。为此,我们设计了一个可移植的 C++ 核心库,处理所有处理任务,使用 OnnxRuntime 实现跨平台的良好性能,然后为所有必要的高级语言构建了原生接口。这使得开发者只需学习一个 API,就能在几乎任何他们想要运行的地方进行部署。
  • 比 Whisper V3 Large 更好的准确率。在 HuggingFace 的 OpenASR 排行榜上,我们最新的英文流式模型“Medium Streaming”实现了比 OpenAI 最准确的 Whisper 模型更低的词错误率。尽管 Moonshine 版本仅使用 2.5 亿参数,而 Large v3 使用了 15 亿参数,这使得它更容易部署在边缘设备上。

希望这能让你清楚地了解 Moonshine 与 Whisper 的对比。如果你在云端使用 GPU 处理批量数据,吞吐量最重要,那么 Whisper(或 Nvidia 的 Parakeet 等替代方案)在批处理方面具有优势,但我们相信在实时语音领域,我们无法被击败。我们构建的框架和模型正是我们最初开始构建具有语音接口的应用时所希望拥有的。因此,如果你正在处理实时语音输入,请试试 Moonshine

使用库

Moonshine API 旨在处理捕获和转写实时语音的细节,为应用开发者提供专注于可操作事件的高层 API。我将使用 Python 来说明其工作原理,但该 API 在所有支持的语言中是一致的。

架构

我们的目标是构建一个任何开发者都能快速上手使用的框架,即使没有任何语音技术经验。我们抽象掉了许多不必要的细节,提供了简单的接口,让你专注于构建应用,这也体现在我们的系统架构中。

基本流程如下:

  • 创建一个 TranscriberIntentRecognizer 对象,具体取决于你是要获取说的文本,还是仅需要知道用户请求了一个动作。
  • 附加一个 EventListener,当重要事件发生时(如短语结束或动作被触发)会被调用,以便你的应用做出响应。
  • 使用 TextToSpeech 对象实现双向对话。

传统上,为应用或产品添加语音接口需要集成大量不同的库,以处理捕获音频并将其转化为可执行动作所需的所有处理。主要步骤包括麦克风捕获、语音活动检测(将连续音频流分割成语音片段)、语音转文本、说话人识别和意图识别。每个步骤通常涉及不同的框架,大大增加了集成、优化和维护这些依赖关系的复杂性。

Moonshine Voice 在一个库中包含了所有这些阶段,并抽象掉了应用响应语音所需的关键信息之外的一切——无论你是想要转写文本还是触发动作。

Moonshine Voice 架构

大多数开发者应当能够将库视为一个黑盒,当有趣的事情发生时它会通知你,然后使用我们基于事件的类来实现应用逻辑。当然,该框架完全开源,语音专家可以深入挖掘底层,但并非必须如此才能使用它。

概念

Transcriber 接收音频输入并将任何语音转换为文本。这是你使用 Moonshine 需要创建的第一个对象,你需要为其提供已下载模型的路径。

MicTranscriber 是一个基于通用转写器的辅助类,负责使用平台内置支持(例如 Python 中的 sounddevice)连接到麦克风,并在捕获时输入音频。

Stream 是音频输入的处理程序。流的存在是因为你可能需要同时处理多个音频输入,而一个转写器可以通过多个流支持这些输入,而无需重复模型资源。如果你只有一个输入,转写器类本身包含了与流相同的方法(start/stop/add_audio),你可以直接使用该接口而无需关心流。

TranscriptLine 是一个数据结构,保存转写中一行文本的信息。当有人说话时,库会等待短暂的停顿(在书面语言中可能是标点位置),然后开始新的一行。这些不完全是句子,因为语音停顿并不一定是句子结束的标志,但它确实将语音音频分成了可视为短语的片段。一行包含状态信息,如该行是否刚启动、仍在说话中或已完成,以及它的开始时间和持续时间。

Transcript 是按时间顺序排列的行列表,保存已识别文本的信息,以及捕获时间等其他状态。

TranscriptEvent 包含关于转写变更的信息。事件包括新行开始、行内文本更新、行完成。事件对象包含它所指的转写行作为成员,持有该行的最新状态。

TranscriptEventListener 是一个协议,允许应用定义的函数在转写事件发生时被调用。这是大多数应用与转写结果交互的主要方式。当实时语音发生时,应用通常需要在识别到新语音时进行响应或显示结果,这种方法允许你以与传统用户界面事件(如触摸屏手势或按钮鼠标点击)类似的方式处理这些变化。

IntentRecognizer 是一种 TranscriptEventListener,当检测到预编程的意图时,允许你调用不同的回调函数。这对于构建语音命令识别功能非常有用。

TextToSpeech 对象合成用于播放给用户的音频。

DialogFlow 对象管理用户与智能体之间的对话。

Dialog 对象为每次对话交互创建,允许智能体与用户进行多步骤讨论。

语音转文本快速入门

我们为大多数平台提供了示例,作为第一步,我建议查看我们为你所针对的系统提供的示例。

接下来,你需要将库添加到你的项目。我们的目标是为所有主流平台提供使用其原生包管理器的预构建二进制文件。在 Python 上,这意味着使用 pip 安装;在 Android 上,是 Maven 包;在 macOS 和 iOS 上,我们通过 SPM 提供 Swift 包。

转写器需要访问你使用的模型文件,因此在下载它们后,你需要将它们放在应用可以找到的位置,并记录下路径。如果你计划分发应用,通常需要将它们作为资源添加到 IDE 中;如果只是实验,你可以使用硬编码路径。下载脚本完成后会告诉你模型在驱动器上的位置及其架构类型。

现在你可以尝试创建一个转写器。Python 中如下所示:

transcriber = Transcriber(model_path=model_path, model_arch=model_arch)

如果找不到模型或出现其他错误,将抛出异常并包含问题信息。你也可以检查控制台中来自核心库的日志,这些日志会打印到 stderr 或系统的等效位置。

现在,我们将创建一个包含应用逻辑的监听器,当转写更新时触发,并将其附加到你的转写器:

class TestListener(TranscriptEventListener):
    def on_line_started(self, event):
        print(f"行开始:{event.line.text}")

    def on_line_text_changed(self, event):
        print(f"行文本更改:{event.line.text}")

    def on_line_completed(self, event):
        print(f"行完成:{event.line.text}")

transcriber.add_listener(listener)

转写器需要一些音频数据来工作。如果你想用麦克风尝试,可以将创建转写器的行改为使用 MicTranscriber,但如果你想先试用 .wav 文件进行测试,以下是如何输入:

    audio_data, sample_rate = load_wav_file(wav_path)

    transcriber.start()

    # 以块的形式循环音频数据,模拟来自麦克风或其他源的实时流式传输。
    chunk_duration = 0.1
    chunk_size = int(chunk_duration * sample_rate)
    for i in range(0, len(audio_data), chunk_size):
        chunk = audio_data[i: i + chunk_size]
        transcriber.add_audio(chunk, sample_rate)

    transcriber.stop()

这里需要注意的重要事项:

  • 我们使用 Moonshine 库中的便捷函数 load_wav_file() 从 wav 文件创建单声道音频数据数组。
  • 我们通过 start() 激活转写器的处理代码。
  • 循环以块的形式添加音频。这些块可以是任意长度和任意采样率,库负责所有内务处理。
  • 添加音频时,你添加的事件监听器将被调用,并提供关于最新语音的信息。

在真实应用中,你将从一个音频处理程序调用 add_audio(),该程序接收来自源的音频。由于库可以处理任意持续时间和采样率,只需确保音频是单声道,然后按原样输入。

转写器默认每 500ms 分析一次输入语音。你可以通过转写器构造函数的 update_interval 参数更改此设置。对于流式模型,大部分工作是在添加音频时完成的,并在短语结束时自动完成,因此更改此设置通常不会对工作负载或延迟产生巨大影响。

关键要点是,你通常不需要担心转写数据结构本身,事件系统会在重要事情发生时通知你。如果你确实需要检查状态,可以通过调用 update_transcription() 手动触发转写更新,该函数返回一个包含当前会话所有信息的转写对象。

通过在转写器(或流)上调用 start()stop(),我们开始和结束一个会话。每个会话关联一个转写文档,并且在每次 start() 调用时都会全新开始,因此你应该在之前复制转写对象中所需的任何数据。

转写器类还提供了一个更简单的方法 transcribe_without_streaming(),用于当你有一段来自过去的音频数据(如文件或录音)想要分析时。

我们还提供了基类 Transcriber 的一个特化版本 MicTranscriber。其实现取决于语言和平台,但它应该提供一个自动连接到系统主麦克风的转写器。这样可以轻松地从该常见源开始转写语音,因为它支持与基类相同的所有监听器回调。

转写事件流

库与应用之间的主要通信渠道是通过传递给已注册监听器函数的事件。共有五种主要事件类型:

  • LineStarted。当检测到新语音段的开始时,发送给监听器。它可能包含也可能不包含任何文本,但由于它在语音开始附近分发,该文本可能会随时间变化。
  • LineUpdated。每当一行的任何信息(包括持续时间、音频数据和文本)发生变化时调用。
  • LineTextChanged。仅在行的关联文本更新时调用。这是 LineUpdated 的子集,专注于常见需求——尽可能频繁刷新显示给用户的文本,以保持交互体验。
  • LineSpeakersChanged。仅在启用可选的 identify_speakers 选项时触发。当附加到一行的说话人跨度发生变化时调用。与其他行事件不同,对于已经完成的行也可能触发,因为 diarization(说话人分离)算法会在收到更多音频时不断优化说话人分配。
  • LineCompleted。当我们检测到有人暂停说话并结束了当前段时发送。行数据结构包含文本和持续时间的最终值。

我们提供关于这些事件的保证:

  • 对于任何段,LineStarted 始终且仅被调用一次。
  • 对于任何段,LineCompletedLineStarted 之后始终且仅被调用一次。
  • LineUpdatedLineTextChanged 仅在该段的 LineStarted 之后、LineCompleted 事件之前被调用。
  • 不保证会调用这些更新事件(实际上可以通过将 update_interval 设置为非常大的值来禁用)。
  • 对于任何给定的流,在任何时候最多只有一行处于活动状态。
  • 一旦调用了 LineCompleted,库将永远不会再更改该行的文本、时间或音频数据。唯一的例外是行的说话人跨度:当启用 identify_speakers 时,这些跨度可能会被修订(由 LineSpeakersChanged 信号通知),因为 diarization 会重新聚类最近语音的滑动窗口。早于 diarization_cluster_window_sec 的音频赋值会被冻结。
  • 如果在转写器或流上调用 stop(),任何活动行都将触发 LineCompleted
  • 每一行都有一个 64 位的 lineId,设计为足够唯一以避免冲突。
  • 从第一个 LineStarted 事件开始,该 lineId 在行的生命周期内保持不变。

对话式智能体快速入门

许多应用需要一个能够理解用户所说内容并适当响应的语音智能体。为了尽可能简化,我们让您可以定义不同的对话流。一个流可以简单如响应一个查询,也可以是具有多个步骤、带有分支的对话,并执行操作。

要定义这些流,你需要使用一个 DialogFlow 对象,回调函数接收 Dialog 参数。以下是一个简单流的示例,取自 github.com/moonshine-ai/pi-help-bot 示例代码:

    def report_ip_address(d: Dialog):
        ip = _find_local_ip()
        if ip is None:
            yield d.say("抱歉,找不到本地 IP 地址。")
            return
        speech_ip = re.sub(r"(\d)", r"\1 ", ip.replace(".", " dot "))
        yield d.say([
            f"好的。你的本地 IP 地址是 {speech_ip}。",
            f"重复一遍,是 {speech_ip}。"
        ])

    dialog_flow.register_flow("我的 IP 地址是多少?", report_ip_address)

这将注册 report_ip_address() 函数,当用户说出类似“我的 IP 地址是多少”的内容时被调用。匹配基于语义进行,因此像“告诉我你的 IP 地址”或“你能告诉我本地 IP 地址吗?”这样的其他措辞也应该能触发它。你可以注册任意数量的顶级对话启动器,系统会监听并将结果路由到语义上最接近的那个。

函数本身接收一个 Dialog 参数,代表当前的对话交互。在这个简单案例中,我们不需要用户的任何额外输入,因此我们直接使用 say() 来传达所请求的信息。为了清晰,我们将 IP 地址的每个数字拆分成独立的单词,并将连接句点替换为明确的“dot”,这样 192.178.4.72 就变成了“1 9 2 dot 1 7 8 dot 4 dot 72”,因为这是口头表述它们的惯例方式。

对于更复杂的对话,比如设置一个新的 Wi-Fi 网络,你可以直接在 Python 中定义多个步骤和分支点:

    def connect_to_wifi(d: Dialog):
        input_ssid = yield d.ask("你的 Wi-Fi 网络叫什么名字?说“列表”可以从列表中选择,或者说“拼写”来拼写名称的开头部分")
        input_ssid = input_ssid.strip()

        networks = _scan_wifi_networks()

        if input_ssid.lower().strip(string.punctuation) == "list":
            yield d.say("对想要连接的网络说“是”。")
            for network in networks:
                if (yield d.confirm(f"{network}?")):
                    input_ssid = network
                    break
        elif input_ssid.lower().strip(string.punctuation) == "spell":
            input_ssid = yield d.ask("拼写网络名称的开头部分。", mode=SPELLED)

        found_ssid = fuzzy_match_network(input_ssid, networks)
        if found_ssid is None:
            yield d.say(f"抱歉,找不到与 {input_ssid} 匹配的网络。")
            return

        password = yield d.ask(
            f"请逐个字符拼写 {found_ssid} 的 Wi-Fi 密码,完成后说“完成”。",
            mode=SPELLED,
        )

        yield d.say(f"正在连接到 {found_ssid}。")

        try:
            result = subprocess.run(
                ["sudo", "nmcli", "device", "wifi",
                    "connect", found_ssid, "password", password],
                capture_output=True, text=True, timeout=30,
            )
        except FileNotFoundError:
            yield d.say("抱歉,此系统上未找到网络管理器。")
            return
        except subprocess.TimeoutExpired:
            yield d.say("抱歉,连接尝试超时。")
            return

        if result.returncode == 0:
            yield d.say(f"已连接到 {found_ssid}。")
        else:
            print(f"[ERROR] nmcli stderr: {result.stderr}", file=sys.stderr)
            yield d.say(
                f"抱歉,无法连接到 {found_ssid}。"
                "请检查网络名称和密码后重试。"
            )

    dialog_flow.register_flow("连接 Wi-Fi", connect_to_wifi)

该函数首先询问用户要加入的网络名称:

input_ssid = yield d.ask("你的 Wi-Fi 网络叫什么名字?...")

Dialog 类允许你向用户提问,并返回包含用户回答的字符串。与常规 Python 代码相比,唯一不寻常的特性是 yield 关键字。因为用户可能需要一些时间才能回答,我们使用 yield 将控制权交还给主脚本,直到收到用户的响应。这是 DialogFlow 的通用模式,你会在等待用户说话的任何地方看到它,以避免阻塞。

        if input_ssid.lower().strip(string.punctuation) == "list":
            yield d.say("对想要连接的网络说“是”。")
            for network in networks:
                if (yield d.confirm(f"{network}?")):
                    input_ssid = network
                    break

我们的示例应用支持几种不同的输入方法——遍历网络列表、拼写前几个字母或说出名称。这里我们通过循环遍历所有可用网络并询问用户是否是他们想要的那个来实现列表方法。你可以看到,常规的循环和条件语句在 Python 中按预期工作。

对于每个网络,我们调用 confirm(),提出一个问题并等待肯定或否定的结果。与系统中的所有匹配一样,这是基于语义进行的,因此“好的”、“确定”和“继续”等也可以正常工作,就像直截了当的“是”一样。

        password = yield d.ask(
            f"请逐个字符拼写 {found_ssid} 的 Wi-Fi 密码,完成后说“完成”。",
            mode=SPELLED,
        )

密码输入很棘手,因为它们由任意字母、数字和符号组成,因此用户必须逐个拼出。Moonshine 通过 mode=SPELLED 参数支持这一点。它要求用户逐个字符拼出,并使用微调后的模型识别用户说出的每个字符。除了支持常规发音如“aitch”或“capital zee”外,它还支持北约字母表(“alpha”、“bravo”等),甚至简短描述性短语如“E as in elephant”。它会复述听到的内容,并允许你删除错误。

        try:
            result = subprocess.run(
                ["sudo", "nmcli", "device", "wifi",
                    "connect", found_ssid, "password", password],
                capture_output=True, text=True, timeout=30,
            )
        except FileNotFoundError

_[原 README 过长已截断]_
开源项目moonshine-ai2026-07-20原文

相关内容