跳到主要內容

【2026 最新】RealtimeVAD 教學:停頓就能先回答嗎?從 CPU Mock 學會撤銷與交棒

最後更新: ·
RealtimeVAD 教學:停頓先別播,續講撤銷與確認交棒示意

你對語音助理說:「幫我訂明天……下午三點的會議室。」你只是換一口氣,它卻已經插話。這篇 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 教學:pause 先準備,續講撤權,交棒確認後才播放
草稿可以提前完成;播放權要留到交棒確認。圖為狀態接線示意,不是實際音訊延遲測量。

三、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 課程安排下一個完整實作。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

週日 Weekly 週報+週二、週四 Alpha Signal,每週最多三封。

我們不會 spam,隨時可退訂。已訂閱?管理訂閱偏好(會寄登入連結到你的信箱)

主題篩選與退訂說明

寄送日期以台灣時間為準。主題偏好會篩選每期精選,並非每篇新文章的即時通知;當期符合主題的文章不足時會略過寄送。

未勾選任何主題會收到所有主題的精選,並不會停止寄信。

取消勾選 Alpha Signal 可停止週二、週四短報,仍保留週日週報。若要停止所有電子報,請使用信件底部的「取消訂閱」。