用 NeuDecide 打造的语音赛车游戏:喊一声就能开车
Voice racer game built with OSS model NeuDecide
Neuphonic 的 43 MB 语音转工具调用模型 NeuDecide 被用来驱动一款像素风赛车游戏,玩家直接说 "go left"、"speed up"、"jump" 即可操控,无需语音转文字或聊天机器人。模型可在本机原生运行,也可通过 WebAssembly 完全跑在浏览器内,支持 6 条语音指令,原生引擎从说完到游戏响应约 200 ms,浏览器引擎约 0.3-0.6 s。
一款用你的声音驾驶的像素艺术赛车游戏。说 "go left"、"speed up" 或 "jump",赛车就会照做。
整个流程中没有语音转文本,也没有聊天机器人。NeuDecide 是 Neuphonic 推出的一个小巧(43 MB)的语音到工具调用模型,它能听到你的声音并直接选出游戏指令。它运行在你自己的机器上:可以原生地运行在页面旁边,也可以完全通过 WebAssembly 在浏览器内运行。
六个语音指令,由原生引擎判定。语音是 macOS 文本转语音的声音,作为麦克风输入播放到浏览器中。GIF 没有声音,所以字幕显示了所说的话。
工作原理
flowchart LR
mic["Microphone"] --> vad["Voice detector<br/>(energy VAD)"]
subgraph nd["NeuDecide, run by our JavaScript engine"]
ae["Audio encoder"] --> te["Tool encoder"]
menu["Tool menu<br/>9 game commands + noAction, as JSON"] --> te
te --> dec["Constrained decoder<br/>(only valid tool-call JSON)"]
end
vad -- "16 kHz utterance" --> ae
dec -- "moveLeft()" --> gate["Confidence gate"]
gate --> game["Game"]
加载中
- 语音检测器监听麦克风,一旦你沉默 120 毫秒,就会切分出每一句话。
- NeuDecide 会结合一份工具菜单来读取音频(
moveLeft、moveRight、moveToLane、speedUp、slowDown、jump、dodge、start、pause、noAction,见src/voice/tools.js)。 受语法约束的解码器只能写出有效的工具调用,而游戏会在调用确定的那一刻就执行动作,甚至在解码完成之前。 - 置信度门控会丢弃不确定的调用和“无动作”的回答,所以大多数闲聊不会触发任何操作。
- 游戏会把调用转化为动作:变道、加速、刹车、跳跃或闪避(游戏会选择最安全的车道)。
运行该模型的引擎是 neudecide Python 包(分词器、约束解码、
推理循环)在 ONNX Runtime 上的 JavaScript 移植版:原生引擎用 onnxruntime-node,浏览器中用 onnxruntime-web(WASM,
运行在 Web Worker 中)。更多细节见 docs/ARCHITECTURE.md。
快速开始
你需要 Node 22 或更高版本、一个麦克风,以及桌面版 Chrome(或 Edge)。
git clone https://github.com/vimalananddev/voice-racer.git
cd voice-racer
npm install下载模型(一次即可,约 43 MB)。它是免费的,但在 Hugging Face 上需要授权:登录,在
https://huggingface.co/neuphonic/neudecide 接受条款,然后使用 Hugging Face CLI,hf:
brew install hf # or: pipx install huggingface_hub or: uv tool install huggingface_hub hf auth login # paste a read token from https://huggingface.co/settings/tokens hf download neuphonic/neudecide --local-dir models/neudecide
没有 CLI?从模型页面的 Files and versions 标签页下载 config.json、tokenizer.json、audio_encoder_q4.onnx、tool_encoder_q4.onnx 和
decoder_step_q4.onnx 到 models/neudecide/。
(models/README.md 有详细说明。)然后:
npm start
- 打开 http://localhost:5180。
- 等待出现 Ready!,点击 Turn on the mic 并允许使用麦克风。
- 说 "start the game",然后开始驾驶:"go left"、"go right"、"speed up"、"jump"……
按 Enter 而不是点击,可以一次性打开麦克风并开始比赛(然后跳过第 3 步)。
npm start 在端口 5180 上运行游戏页面,并在 ws://127.0.0.1:5190 上运行原生语音引擎,两者都只在本机运行。Ctrl-C 可同时停止两者。端口被占用?PORT=5280 VOICE_PORT=5290 npm start(此时打印的地址会带有 ?localport=5290)。npm run check 会运行一个不需要麦克风的快速冒烟测试。
npm run dev 只运行游戏页面,因此会使用浏览器内引擎。没有原生引擎时,浏览器控制台会在每次页面加载时显示一个连接到 ws://127.0.0.1:5190 的 WebSocket 失败:这是页面在寻找原生引擎,属于预期现象。
语音指令
| 说 | 作用 | 命中率* |
|---|---|---|
| "start the game" | 开始、崩溃后重新开始,或继续(按 Enter 也可以) | 19/24 |
| "go left" · "go right" | 向左 / 向右变一条车道 | 30/32 · 31/32 |
| "lane number three" | 直接前往 1-4 号车道 | 53/56 |
| "speed up" | 加速约 2.5 秒 | 24/24 |
| "slow down" | 刹车约 2 秒 | 24/24 |
| "jump" | 跳过锥桶、油污和坑洞(不能跳过汽车) | 23/24 |
| "dodge it" | 游戏会把你移到最安全的车道 | 23/24 |
| "pause the game" · "resume" | 暂停 / 继续 | 21/24 · 24/24 |
* 合成语音(macOS 文本转语音)、安静房间、麦克风中无游戏声音、原生引擎。真实语音和房间会有所不同。
提示:
- 戴上耳机(或按 M 键将游戏静音)。麦克风中的游戏声音是漏掉“左”和“右”的主要原因:有它时,单车道命令的识别率从大约十分之九降到大约一半。
- 说 “go left”/“go right”,而不是只说那个词。每次左/右移动一条车道;说两次可到达边缘车道,或使用车道编号(“lane number four”)。“Move left”、“shift left”和“switch right”也可以。
- 驾驶时不要说“alright”:模型会听到“right”并变换车道。“All right”、“that's right”和“yeah right”也会这样。聊天前按 V 键将麦克风静音。
- 当命令被漏掉时,通常什么也不会发生。如果游戏意外暂停,说“resume”。
- 序数车道(“second lane”、“last lane”)和俚语(“floor it”)几乎从不生效。
引擎与延迟
本 README 中的所有延迟和命中率数据均在 M1 Pro MacBook Pro(桌面 Chrome 中的浏览器引擎)上使用合成语音测得。
| 引擎 | 如何选择 | 运行位置 | 从你说话结束到游戏做出反应 |
|---|---|---|---|
| 原生(默认) | npm start,或 ?engine=local |
页面旁边的 onnxruntime-node(工具编码器在 M1 Pro 上使用 5 个线程;其他机器上为 min(6, cores - 2)) |
~200 ms(模型时间 ~80-100 ms) |
| 浏览器 | ?engine=browser,或当没有原生引擎响应时自动使用 |
Web Worker 中的 onnxruntime-web(WASM) | ~0.3-0.6 s(4 个线程:~0.35 s;只有 1 个线程工作时 ~0.7 s) |
| 远程 | ?engine=ws://host:5190/?token=… |
另一台机器上的原生引擎 | 那台机器的模型时间 + 网络 |
约 200 ms 包括语音检测器等待 120 ms 以确认你已停止说话(使用 ?ptt=1 时,它会在你松开通话键时开始)。几乎所有的模型时间都花在工具编码器上:它从第一层就将工具菜单与你的音频混合,因此无法缓存,并且每条命令都会运行。未使用 WebGPU:它在 Apple GPU 上失败(模型需要 11 个存储缓冲区,而限制是 10)。
如果引擎在游戏中途停止,页面会显示“Voice is reconnecting…”并自行重新连接。在默认模式下,如果原生引擎在 4 秒内没有恢复,就会被浏览器引擎替换(重新加载可恢复)。键盘始终可用。
在另一台机器上运行引擎
该引擎是 scripts/remote-voice-server.mjs。它已在 macOS 和 Linux(arm64)上测试过,应该能在任何 onnxruntime-node 可运行的地方运行。CPU 核心更多的机器每条命令更快。在那台机器上,使用该仓库,npm install 并放置好模型:
TOKEN=pick-a-secret HOST=0.0.0.0 PORT=5190 npm run engine
然后在装有游戏的机器上打开 http://localhost:5180/?engine=ws://<that-machine>:5190/?token=pick-a-secret。?dev=1 面板会以引擎报告的主机名来命名它;用 ENGINE_NAME=desktop(任意名称)启动它以显示其他内容,例如在你录制面板之前(?record=1 只显示“Remote”)。当 HOST 不是 127.0.0.1 时,始终设置一个 TOKEN,并在你信任的网络上使用它:该链接是明文 ws://。更简单且私密的方式:将引擎保留在 127.0.0.1 上并使用 SSH 隧道,ssh -N -L 5195:127.0.0.1:5190 you@that-machine,然后打开 ?engine=ws://127.0.0.1:5195。
引擎在启动时会打印其线程设置;TOOL_THREADS=n 会尝试其他设置。有关所有选项和协议,请参阅 scripts/remote-voice-server.mjs 的开头部分。
URL 选项
| 选项 | 作用 |
|---|---|
?dev=1 |
开发者面板:显示工具调用、延迟、引擎、置信度和决策日志,而不是简洁面板 |
?record=1 |
适合录屏的清爽布局(9:16 窗口:单列;4:5、1:1 或 16:9:双列),更安静的游戏音效,更快的昼夜循环和更繁忙的交通 |
?ptt=1 |
按住说话:说话时按住 T(或按住麦克风按钮) |
?engine=local · ?engine=browser · ?engine=ws://host:port/?token=… |
选择引擎(默认:如果原生引擎能响应就用原生,否则用浏览器;native 和 wasm 也可以) |
?voice=mock |
完全不用模型:键盘伪造语音事件(用于调试 UI) |
?minconf=0.6 |
更严格的置信度门槛(默认 0.45;调低会让噪声通过) |
?tools=fast |
更短的工具菜单:更快,但准确度更低 |
?tod=fast · ?traffic=busy |
?record=1 之外的录制节奏(=normal 在内部将其关闭) |
?tod=night |
从那个时间点开始(day、sunset、dusk、night 或 dawn) |
?sfx=0 |
完全关闭游戏音效 |
?ec=0 · ?ns=0 · ?agc=0 |
关闭浏览器对麦克风的回声消除 / 噪声抑制 / 自动增益(默认开启:它们有助于防止游戏声音进入麦克风) |
?resampler=own · ?resampler=browser |
强制麦克风重采样到 16 kHz 的方式(默认:Chrome / Edge 用浏览器自带的,其他环境用我们的) |
?brand=@yourhandle |
角落里的一个小水印 |
开发者选项:?god=1(车辆不会撞毁)和 ?keepalive=1(在后台标签页中继续运行)。浏览器
控制台还有更多:见 docs/ARCHITECTURE.md。
左:?dev=1(来自一次测试运行的真实决策和延迟)。右:?record=1 在 9:16 窗口中。
键盘
| 按键 | 功能 |
|---|---|
| V | 麦克风开 / 关 |
T(配合 ?ptt=1) |
按住说话 |
| ← → 或 A D | 向左 / 向右一条车道 |
| Shift + ← → | 最左 / 最右(仅键盘和触摸按钮) |
| 1 2 3 4 | 切换到该车道 |
| ↑ W / ↓ S | 加速 / 刹车 |
| Space / X | 跳跃 / 闪避 |
| Enter / P | 开始或继续 / 暂停 |
| M | 游戏音效开 / 关 |
在触摸屏上,游戏下方会出现触摸按钮。用手机游玩需要自己在游戏服务器前搭建 HTTPS:server.mjs 只监听 127.0.0.1,而浏览器只允许在 https:// 或
localhost 上使用麦克风。
限制
- 仅支持英语。
- 使用合成语音测试。上述数字来自 macOS 文本转语音语音,而非真人。 在依赖它之前,请用自己的声音和麦克风试一试。
- 麦克风中的游戏声音影响很大,尤其是光秃秃的“左”/“右”。请使用耳机。
- 闲聊可能触发指令。即使有置信度门槛,测试中约 1/8 的闲聊或噪声片段触发了某些操作 (通常是暂停、闪避或变道),而且有些短语,比如“thanks for watching”,经常会触发闪避。 与人交谈时请静音麦克风(V)或使用按住说话。
- 在桌面版 Chrome 中,浏览器引擎比原生引擎慢约 2 倍,在无法多线程运行的浏览器中则慢得多。主要在 macOS 的桌面版 Chrome 中开发和测试。
项目结构
index.html, styles.css the page
server.mjs static server (127.0.0.1 only; COOP/COEP headers for multi-threaded WASM)
scripts/
start.mjs npm start: the game server + the native engine
remote-voice-server.mjs the native voice engine (WebSocket), also for another machine
vendor-ort.mjs copies onnxruntime-web into vendor/ort/ (runs after npm install)
check.mjs npm run check: smoke test, no microphone or audio files
src/
main.js wiring: engine choice, microphone, voice events -> game actions
game/ the racer: world, traffic, sprites and font (all drawn in code), lighting, HUD,
synthesised sound effects
ui/ the voice panel (show-overlay.js; overlay.js is the ?dev=1 panel), layout,
title card, touch buttons, mock voice
voice/ NeuDecide in JavaScript: tokenizer, constrained decoding, inference core, Web Worker,
microphone capture, VAD, confidence gate, tool menu
models/README.md how to get the model (the weights go in models/neudecide/, never in git)
docs/ ARCHITECTURE.md, screenshots
没有构建步骤,也没有打包器:纯 ES 模块作为静态文件提供。
致谢
- NeuDecide 由 Neuphonic 开发(Apache-2.0):
模型,以及这个 JavaScript 引擎移植自的
neudecidePython 包。 - ONNX Runtime 由 Microsoft 开发(MIT):
onnxruntime-web和onnxruntime-node。 - SentencePiece 由 Google 开发(Apache-2.0):JavaScript 分词器移植自它。
- llguidance 由 Microsoft 开发(MIT):约束解码器的强制 token 逻辑改编自其
toktrie。 - ws(MIT)。
- 所有游戏美术都是用代码绘制的原创像素画;所有音效都用 Web Audio 合成。
详情见 NOTICE。
Voice Racer 是一个独立项目,与 Neuphonic 无关联,也未获得其认可。NeuDecide 是 Neuphonic 的模型;此处使用该名称仅用于指代该模型。
许可证
Apache License 2.0。版权所有 2026 Vimal Anand。
NeuDecide 模型权重不属于本仓库,其使用受 Hugging Face 上各自条款的约束。
来源:Hacker News · github.com



