<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Realtime on Armstrong Yan</title><link>https://yanqian.github.io/zh/tags/realtime/</link><description>Recent content in Realtime on Armstrong Yan</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Thu, 13 Aug 2026 13:59:31 +0800</lastBuildDate><atom:link href="https://yanqian.github.io/zh/tags/realtime/index.xml" rel="self" type="application/rss+xml"/><item><title>Hey Jarvis 的困难 I：让语音交互真正成立</title><link>https://yanqian.github.io/zh/posts/publish/building-hey-jarvis-voice-interaction/</link><pubDate>Thu, 13 Aug 2026 13:59:31 +0800</pubDate><guid>https://yanqian.github.io/zh/posts/publish/building-hey-jarvis-voice-interaction/</guid><description>&lt;p&gt;在 &lt;a href="https://yanqian.github.io/zh/posts/publish/building-hey-jarvis/" &gt;上一篇&lt;/a&gt; 里，我写到，Hey Jarvis 的第一个 Pipeline（串行语音处理流程）其实很快就实现了最初的需求。&lt;/p&gt;
&lt;p&gt;我说一句 “Hey Jarvis”，它录下问题，交给 AI 回答，再把答案读出来。&lt;/p&gt;
&lt;p&gt;如果只看流程图，这个语音助手已经完成了。&lt;/p&gt;
&lt;p&gt;但真正用起来以后，我发现语音交互里最困难的部分，往往不在“识别了什么”或者“回答了什么”，而在一些更基础的问题：它什么时候开始听？什么时候停止听？它能不能分清我的声音和自己的声音？一段对话结束以后，麦克风到底回到了谁手里？&lt;/p&gt;
&lt;p&gt;这些问题没有解决时，模型再聪明，使用者也会很快失去耐心。&lt;/p&gt;
&lt;h2 id="pipeline-的第一个问题它会听见自己"&gt;
 Pipeline 的第一个问题：它会听见自己
 &lt;a class="heading-link" href="#pipeline-%e7%9a%84%e7%ac%ac%e4%b8%80%e4%b8%aa%e9%97%ae%e9%a2%98%e5%ae%83%e4%bc%9a%e5%90%ac%e8%a7%81%e8%87%aa%e5%b7%b1"&gt;
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="链接到标题"&gt;&lt;/i&gt;
 &lt;span class="sr-only"&gt;链接到标题&lt;/span&gt;
 &lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;为了让我知道唤醒成功，Hey Jarvis 会先播放一句简短的确认音。&lt;/p&gt;
&lt;p&gt;最早是“在呢”，后来实时语音会话（Realtime）版本使用的是“嗯，我在，请说”。它听起来只是一个很小的体验细节，却引出了整个项目最早、也最反复的一类问题。&lt;/p&gt;
&lt;p&gt;Mac 的扬声器播放确认音时，旁边的麦克风也会收到这段声音。如果程序在确认音结束后才开始读取麦克风，那么音频缓冲区里可能还留着刚才的回声。对程序来说，这些数据和用户刚刚说出的话并没有天然区别。&lt;/p&gt;
&lt;p&gt;结果可能是这样的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;我说 “Hey Jarvis”
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Jarvis 回答“在呢”
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 麦克风重新收到“在呢”
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Jarvis 以为用户已经开口
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 录下一段错误的问题
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最直接的办法，是播放确认音时完全忽略麦克风。&lt;/p&gt;
&lt;p&gt;但这又制造了相反的问题。我听见“在呢”以后，可能会很自然地立刻开始说话，甚至在确认音的最后一个字还没有完全结束时就已经开口。如果这段时间的音频全部被丢掉，我的问题开头也会一起消失。&lt;/p&gt;
&lt;p&gt;比如我说“明天新加坡会下雨吗”，系统收到的可能只剩“新加坡会下雨吗”。这次看起来还能理解；换一个问题，少掉前几个字就可能完全改变意思。&lt;/p&gt;
&lt;p&gt;所以最终的处理不是简单地“听”或者“不听”，而是把确认音前后分成几个不同区域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确认音播放期间持续读取麦克风，避免旧声音堆积在缓冲区；&lt;/li&gt;
&lt;li&gt;明显属于扬声器回声、削波或溢出的片段不能成为“用户已经开口”的证据；&lt;/li&gt;
&lt;li&gt;靠近播放结束的一小段安全音频暂时保留，作为问题开头的候选；&lt;/li&gt;
&lt;li&gt;只有确认音结束后仍然观察到真实说话的迹象，候选音频才会进入正式录音；&lt;/li&gt;
&lt;li&gt;如果后面没有人继续说话，就安静地回到唤醒状态，不请求 AI。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套逻辑还需要估计环境底噪，并在最近的一组音频片段里确认持续的人声，而不是因为某一个突然变大的声音就开始录音。开始录音时，它还会把前面大约半秒的音频一起带上，尽量不丢掉第一个字。&lt;/p&gt;
&lt;p&gt;一个原本只有两个状态的问题——“在听”或“没在听”——最后变成了对回声、底噪、连续语音和录音前缓冲的共同判断。&lt;/p&gt;
&lt;h2 id="我在应该是一句承诺"&gt;
 “我在”应该是一句承诺
 &lt;a class="heading-link" href="#%e6%88%91%e5%9c%a8%e5%ba%94%e8%af%a5%e6%98%af%e4%b8%80%e5%8f%a5%e6%89%bf%e8%af%ba"&gt;
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="链接到标题"&gt;&lt;/i&gt;
 &lt;span class="sr-only"&gt;链接到标题&lt;/span&gt;
 &lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Pipeline 里的确认音主要表示唤醒词已经被识别。&lt;/p&gt;
&lt;p&gt;到了实时语音模式，它的含义必须更加严格。&lt;/p&gt;
&lt;p&gt;唤醒之后，Hey Jarvis 需要释放本地唤醒使用的麦克风，建立网络连接，完成实时对话配置，准备好播放远端声音，再开放麦克风输入。如果确认音放得太早，我听见“我在”就开始说话，但实时对话可能还没准备好，我说出的内容会直接消失。&lt;/p&gt;
&lt;p&gt;所以我后来给这句确认音定义了一个产品层面的含义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当“嗯，我在，请说”播放完，Hey Jarvis 才应该真正准备好听我说话。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实现这句话，需要等待两个条件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;实时对话已经配置完成
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ＋
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;确认音已经播放完成
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;开放麦克风输入
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这两个过程可以同时进行，但任何一个没有完成，用户输入都不能开放。&lt;/p&gt;</description></item></channel></rss>