Ankita 如何在每次回答後保持語音監聽 25 秒
How I built a voice loop that keeps listening for 25 seconds after every answer
Ankita 在 v2.7.5 加入追問時段,每次回答後持續完整監聽 25 秒,讓用戶毋須再次説出喚醒詞即可追問;時段會在偵測到聲音時重新計時,結束後則返回待命。用戶可在設定中把時段調整為 0 至 300 秒,並以 5 秒為步進。這項功能沒有説話者識別,背景聲音也可能延長監聽;Ankita 以 MIT 授權開源。
How I built a voice loop that keeps listening for 25 seconds after every answer
Talking to a voice assistant with a wake word gets old fast. You say the name, ask the thing, get the answer — and then, to ask the obvious follow-up, you have to say the name again. Every turn starts with a ritual word, and the conversation never feels like a conversation. It feels like a series of commands.
Ankita is a personal AI companion for the desktop — a little companion, a lot more possible — and its voice loop had exactly this problem. The fix I shipped in v2.7.5 is a follow-up window: after Ankita finishes an answer, the microphone stays in full listening for 25 seconds, so a follow-up needs no wake word at all. If the window closes, the session drops back to standby, where only the name wakes it. It sounds trivial. The interesting part is everything the window has to get right to not become a liability.
The ladder, not a switch
Voice in Ankita is not "on" or "off". It is a ladder with three rungs, and the code in desktop/electron/voice.mjs walks it explicitly:
- Full listening. Speech is captured and acted on.
- Standby. Only the wake word is acted on. Speech that is not the wake word is dropped, and the panel shows a hint — "Say Ankita first." — throttled to once per 4 seconds so a long aside does not flicker the hint.
- Paused. The microphone closes until the user resumes voice.
After a finished request, settle() decides where the session lands. With the wake word on and the follow-up window enabled, it calls openFollowUp(): the session stays in full listening, a timer is set for the window length, and the panel shows "Go ahead — I am still listening." When the timer fires, the session falls to standby. With the window set to 0, settle() skips straight to standby — the strict mode where the name is required every time.
The defaults are in src/core/config.mjs: voiceFollowUpMs: 25000, voiceIdleMs: 15000 (how long a bare "Ankita" keeps full listening open), voiceStandbyMs: 600000 (how long standby keeps the mic warm before closing it). The Settings panel exposes the window as a slider from 0 to 300 seconds in 5-second steps, and the behaviour toggles are read live from the config on every decision — saving in Settings takes effect without ending voice.
The detail that makes it work: the window restarts on sound
A fixed 25-second countdown would be a bad design. Say "and also…" at second 24 and the window would close mid-sentence. So touchIdle() has a special case: inside the follow-up window, detected sound restarts the window itself, not the idle timer that would push an awake session back to standby while the user is still talking. The window is really "25 seconds of silence after the last sound", not "25 seconds after the answer".
Two more details keep the loop honest. First, the session never sleeps during speech or active work — sleep() defers if a job is still running, so a long task cannot silence its own completion report. Second, the stop word still works inside the window: saying stop interrupts speech and cancels in-flight work, confirmed out loud with a spoken "Stopped." The window is for follow-ups, not a trap.
The honest limits
This is a 25-second open microphone with no speaker identification. Any voice in the room during the window counts as a turn — a housemate, a video playing, a notification read aloud. The window-restarts-on-sound rule makes this worse, not better: a noisy room can hold the window open indefinitely, because every sound resets the countdown. The 25-second default is a guess, not a measured optimum — I picked it because it covers a natural pause between turns without leaving the mic hot for minutes.
There is also a subtler failure. The panel hint says "Go ahead — I am still listening", which invites speech, but the session cannot tell speech addressed to Ankita from speech that merely happens nearby. The wake word solved that problem by being explicit. The window trades it away for fluency.
Try breaking it
The code is in desktop/electron/voice.mjs (settle(), openFollowUp(), touchIdle()), and the defaults in src/core/config.mjs. Tell me I'm wrong: is a 25-second open window the right default, or should the window only extend on speech that is actually addressed to the companion — and how would you tell addressed speech from background noise without a wake word to lean on?
Ankita is open source (MIT) at https://github.com/akyourowngames/A.N.K.I.T.A — if you have a better rule for when a voice loop should stop listening, I would genuinely like to hear it.
來源:DEV Community · dev.to