你對語音助理說:「幫我訂明天……下午三點的會議室。」你只是換一口氣,它卻已經插話。這篇 RealtimeVAD 教學,要處理的就是那個省略號:能否先偷偷準備答案,等你真的交棒才開口;你續講時,又能把舊草稿撤掉?
前半專為第一次接觸語音 Agent 的讀者寫;後半需要已有的 Python 3.9 以上環境,複製一段程式便能看懂事件順序。你會跑一個不連麥克風、不呼叫付費模型的 CPU Mock,再學會把真實 VAD 事件接進來。Mock 是本文自製並已執行的邏輯練習;音訊偵測、繁中效果與真實通話延遲則留給同條件錄音驗收,不預填成績。
本文依 2026 年 11 月 14 日查閱的官方規格、原始碼、套件資訊與 LiveKit 文件整理,固定以 RealtimeVAD 0.0.5 與提交 584d439 為閱讀版本;專案將此版標示為 Alpha。重點是播放前的預先回應;播放中的插話可接著讀 Gander 語音 Agent Mock。
先說結論:可用預先回應=先準備+可撤銷+交棒才播放
把語音 Agent 想成餐廳服務員:客人停頓時,可以先寫草單;客人補一句,就劃掉舊草單;確認點完,才把正確那份送出。pause 是「可以準備」,end 是「這段音訊結束」,輪到 Agent 發言則由更上一層決定。三道門分開,才有空間提早工作。
- 先讓草稿留在記憶體,暫時不播放、不送出訂單。
- 使用者續講就撤銷舊草稿的交付權,晚到的結果也要丟掉。
- 最終逐字稿、當前工作編號與交棒判斷一致,才開放輸出。
一、RealtimeVAD 教學先認五個零件
① VAD:「耳朵旁的聲音開關」。VAD 是 Voice Activity Detection(語音活動偵測),主要判斷音訊中是否有語音活動。ASR/STT 才是把聲音寫成文字;LLM 組織回答;TTS 把文字變成聲音。你可以先讀 Agent Harness 白話解釋,把這些元件看成同一張工作台。
② 安靜時鐘:「很快看見音量降下來」。RealtimeVAD 保留 Silero 的語音判斷,另用較短的音量檢查觀察安靜。官方規格將原生輸入限定為 16 kHz 單聲道 PCM;每秒 16,000 個取樣點,單聲道只有一條聲音軌。取樣率不同,先正確重取樣,不能只把檔頭數字改成 16 kHz。
③ 三種事件:「開口、可能停了、這段結束」。start 確認一段語音開始;pause 提醒你可能停下來;end 交出完成的 segment(音訊片段)。規格還有選用的 onset,屬於更早、尚待確認的開口線索。片段邊界是聲學判斷,句意完整與否是另一個問題。
④ 預先生成:「先寫草稿」。拿目前的轉錄試著生成回答,先留在緩衝區。LiveKit 的預先生成文件也把這種提前工作與最終發聲分開。代價是草稿可能報廢;已算過的工作量,不能因為結果丟掉就當作沒花過。
⑤ 交付閘門:「最後一次核對取餐號」。每份草稿帶工作編號與逐字稿快照。取消是請工人停手,撤權是告訴出餐檯「這個號碼不可交付」。兩者一起做:即使工人停得慢,出餐檯仍能拒收舊菜。
二、走一條完整時間線:換氣後,草單怎麼撤?
使用者先說「訂明天」,產生第一份草稿 A。接著停頓;你的系統可以讓 A 在背景準備,但保持播放器關閉。使用者續說「下午三點」,立即撤掉 A 的編號,再於下一個停頓建立包含完整新文字的草稿 B。就算 A 比 B 晚完成,A 也進不了播放器。
音訊片段結束後,再取得最終逐字稿。假設它仍是「訂明天下午三點」,而交棒控制器確認可以回覆,就採用 B;若最終文字改成「不要訂明天」,B 也要報廢,依新文字重新準備。輸出檢查要看完整輸入版本;只有相同文字,也要連同對話脈絡與工具設定一起核對。下面 Mock 先用字串代表這份輸入。
這裡有個容易接錯的地方。本版原始碼在同一 segment 內恢復語音時,會更新逐幀狀態,而非重新建立一個 start。因此本文接線使用 VAD(frame_events=True),在 event.kind == "frame" 且 event.frame.voiced 時,撤掉尚未交付的草稿。這是依當前偵測訊號處理續講;能否及時抓到輕聲,仍要用你的錄音測。

三、RealtimeVAD 教學實作:先跑可撤銷的 CPU Mock
先解決「我不知道取消到底有沒有生效」的痛點。建立空資料夾,將以下完整程式存為 pause_gate.py,執行 python3 pause_gate.py。它只用 Python 標準庫;complete() 人工指定背景工作何時完成,讓舊工作故意晚到,結果可重複檢查。
from dataclasses import dataclass
@dataclass
class Job:
epoch: int
text: str
result: str = ""
class Gate:
def __init__(self):
self.epoch = 0
self.job = None
self.phase = "listening"
self.final = ""
self.attempts = 0
self.delivered = []
def revoke(self):
self.epoch += 1
self.job = None
def start(self):
self.revoke()
self.phase, self.attempts = "listening", 0
def resume(self):
self.revoke()
self.phase = "listening"
print("REVOKE")
def new_job(self, text):
self.revoke()
self.job = Job(self.epoch, text)
return self.job
def pause(self, text):
if self.phase != "listening" or self.job or self.attempts >= 2:
return None
self.attempts += 1
print("DRAFT", text)
return self.new_job(text)
def end(self, final_text):
self.phase, self.final = "ended", final_text
def confirm(self):
assert self.phase == "ended"
if self.job is None or self.job.text != self.final:
self.new_job(self.final)
self.phase = "confirmed"
self.emit()
return self.job
def complete(self, job):
if job is None or job is not self.job or job.epoch != self.epoch:
print("DROP_STALE")
return
job.result = "回應:" + job.text
self.emit()
def emit(self):
if self.phase == "confirmed" and self.job and self.job.result:
print("PLAY", self.job.result)
self.delivered.append(self.job.text)
self.phase = "delivered"
# 人為事件;字串是假 ASR,complete() 是假 worker 完成。
g = Gate()
g.start()
old = g.pause("訂明天")
g.resume()
new = g.pause("訂明天下午三點")
g.complete(new) # 先完成,也只能留在草稿
g.complete(old) # 舊工作故意晚到
g.end("訂明天下午三點")
g.confirm() # 測試者明確批准交棒
assert g.delivered == ["訂明天下午三點"]
# 最終逐字稿修正:即使沒有觀察到 resume,也丟掉舊草稿。
g.start()
old = g.pause("取消訂單")
g.complete(old)
g.end("不要取消訂單")
fresh = g.confirm()
g.complete(old)
g.complete(fresh)
assert g.delivered[-1] == "不要取消訂單"
print("PASS")
看到 DRAFT 是準備;REVOKE 是撤權;DROP_STALE 是攔下舊結果;只有 PLAY 代表通過交付檢查。最後應印出 PASS。第一個情境即使 B 先完成,仍等到 confirm() 才輸出;第二個情境把「取消訂單」改成「不要取消訂單」,驗證最終文字校對會換掉草稿。
程式中的 PLAY 只是印字,並未產生或播放音訊;confirm() 由測試者明確呼叫,也不是一個已完成的語意模型。它刻意省略網路 worker、逾時重試、音訊播放器與外部工具;接入這些元件時,要把每次回呼都綁定同一份輸入版本,並在新的使用者輸入到來時停止或清空舊播放佇列。
若改成非同步工作,另向供應端傳送取消,並清理連線。Python 的 Task cancellation 文件說明取消會在可處理的機會拋出例外;cancel() 的呼叫時刻不等於遠端已停止計算。保留這裡的 epoch(世代編號)檢查,可以處理取消與完成恰好同時發生的競爭。
四、把真實音訊接上:三個邊界逐一替換
第 1 步:「先確定餵的是什麼聲音」
在獨立虛擬環境安裝核對版本:python3 -m venv .venv,啟用後執行 python -m pip install realtimevad==0.0.5。套件發布頁與專案設定要求 Python 3.9 以上。先準備你有權處理、無私人內容的 16 kHz 單聲道 PCM WAV;本文沒有執行套件模型推論,這一步是依發布設定提供的安裝路徑。
使用 from RealtimeVAD import VAD 與 vad = VAD(frame_events=True)。輸入轉換程式接受 float32、int16 或 int16 bytes;若用 float 音訊,先確認數值尺度與正負峰值。WAV 的整份檔案包含檔頭,要讀出 PCM 資料再餵入;32 位元浮點 bytes 也別冒充 16 位元 PCM bytes。
第 2 步:「偵測照跑,生成放到背景」
以下是接線偽碼,展示欄位與責任;asr_snapshot、turn_controller 與 worker 都需要你提供實作,並非套件內建函式。實際音訊回呼只送事件與複製必要資料,不在回呼內等待 ASR、LLM 或 TTS 完成。
vad = VAD(frame_events=True)
for chunk in pcm_chunks:
for event in vad.feed(chunk):
if event.kind == "start":
gate.start()
elif event.kind == "frame" and event.frame.voiced:
if gate.job is not None or gate.phase == "ended":
gate.resume() # 同段續講,先撤舊權限
elif event.kind == "pause":
audio_snapshot = copy_current_segment_audio()
enqueue_asr_snapshot(audio_snapshot)
elif event.kind == "end":
enqueue_final_asr(event.segment.audio)
# ASR 回呼先核對 segment / revision,才可 gate.pause(text) 或 gate.end(text)
# turn_controller 確認交棒後才 gate.confirm()
# worker 完成後仍經 gate.complete(job),不要直接呼叫播放器
這段偽碼刻意將 ASR 回呼的版本檢查寫在旁邊:從音訊送出到文字回來之間,使用者可能又說了話。為每個 segment 與輸入修訂編號,晚到的轉錄同樣丟掉。需要讀目前音訊時,官方提供 vad.audio();若一口氣餵入很大一塊音訊,回傳事件後讀到的是該次餵入後的目前緩衝,不保證剛好切在某個早期 pause。要留事件時點的快照,採小塊輸入並依樣本位置切齊。
第 3 步:「片段結束後,再決定誰說話」
最簡單的練習用按鍵確認交棒;之後才換成語意回合控制。LiveKit 的 turn detection 文件將 VAD、語意判斷與人工控制列為不同策略。若使用社群 RealtimeVAD LiveKit plugin,還要讀事件映射:其 pause 回呼與 LiveKit 由逐字稿觸發的預先生成是兩條接線,換掉 VAD 並不等於接好了草稿流程。
五、驗收速度前,先把誤切與浪費一起記帳
最常見的比較失真,是替 A 等比較久的靜音,替 B 等比較短,再把所有差距歸功於演算法。先保留完全相同音檔、分塊大小、門檻、靜音等待與前處理;原生流程與 LiveKit plugin 分開記。若要隔離安靜時鐘的影響,可依官方設計比較同版 VAD 的正常設定與 quiet_rms=0 對照,並完整保存其他參數;這回答的是該機制的增量,不是所有 Silero 產品的排名。
官方 README 的「提早約 100 ms」是作者單一說話者與擷取鏈路的 speech-off 觀察,不是整通對話提早同樣時間。舊版與不同基線的 110 ms 也不宜混在一起。本文不轉載成自己的測試結果;先到 原始量測腳本核對事件定義,再用自己的資料填表。
- 繁中停頓:自己錄「幫我訂明天……下午三點」「這筆不用……改成下週處理」。人工標出續講與句尾,檢查 pause 後有沒有播放舊內容。
- 輕聲與尾音:同一句正常講、放低聲音講;檢查開頭是否進入片段、尾音是否被截掉。別把錄音音量衰減當成真的換了一位說話者。
- 背景噪音:同一錄音加上有權使用的固定噪音,再另外錄真實噪音環境;兩組分開報,降噪與回音消除設定也要保存。
- 每個停頓一筆帳:記 pause 時點、是否續講、草稿開始與撤銷時間、完成後是否被丟棄、最終交棒與裝置首聲時間。
- 成績分開看:端點延遲、人工標註的誤切率、草稿取消比例、浪費 Token/生成音訊秒數、舊內容誤播次數。至少保留分母、逐筆紀錄與較慢回合,避免只報平均。
把「仍在講話卻切段」與「在合理停頓處切成兩段」分開標註;兩者對讀者體驗的代價不同。完整的時間戳可沿用 語音 Agent 延遲 Trace;任務與否定句的正確性則沿用 Voice Agent Eval。今天新增的那張帳,是「提早做了多少最後必須丟掉的工作」。
常見問題:八個先講答案的判斷
1. pause 一出現就該讓助理說話嗎?
不該。先讓可撤銷草稿開始,播放權保留給交棒控制器;否則換氣就會變成插嘴。
2. end 等於整個使用者回合結束嗎?
不等於。它關閉的是音訊片段;使用者是否已表達完,要由你的回合策略判斷。
3. 續講時等下一個 start 撤銷就好嗎?
不夠。同一 segment 的恢復語音要看逐幀訊號,另用最終轉錄校對補住漏掉的修訂。
4. 呼叫取消後,費用會歸零嗎?
不要這樣估。已經完成的計算仍屬於成本;記錄供應端用量與取消回應,並為每回合設預算。Mock 的兩次預先嘗試只是練習上限。
5. 一定要在 pause 時做 TTS 嗎?
不必。先只準備文字,較容易隔離錯誤;接上 TTS 後,把音訊緩衝也綁定工作編號,通過閘門才排入播放。
6. 可以把訂單工具一起預先執行嗎?
本練習不這樣做。它只建立記憶體草稿;付款、訂位與刪除等外部副作用,要在最終意圖與必要確認完成後另行授權執行。
7. 這裡已證明繁中比較快嗎?
還未建立這項證據。本文跑的是人工事件 Mock;繁中停頓、輕聲與噪音段落提供的是待執行驗收方法。
8. 哪些情況應先關掉預先生成?
若取消慢、舊結果仍會誤播,或草稿報廢超過你願意支付的預算,先等正式交棒再生成。等撤權測試穩定通過,再逐項開回。
給新手的三個重點
- 先分三道門:停頓提示、片段結束、正式交棒,各自有不同責任。
- 先撤權,再談快:每份轉錄、文字與音訊都攜帶輸入版本;丟掉晚到舊結果。
- 先記錯誤成本:同條件比較,讓誤切、漏偵測與浪費工作量一起進帳。
接著閱讀
左右滑動查看更多推薦
下一步:故意讓舊答案晚到一次
先跑 Mock,找到被攔下的 DROP_STALE,再把第二段假逐字稿換成自己的修正句。做出一份「草稿完成卻沒有提前播放」的紀錄後,才接真實音訊。可用預先回應=先準備+可撤銷+交棒才播放。這個小閘門站穩,省下來的等待才有意義;想循序補足系統觀念,可回到 AI 教學總覽,或用 AlphaLab 課程安排下一個完整實作。






