Day 06 我把目前的語音對話流程拆開來看,發現最大的問題不是某個模型特別慢,而是整條 Pipeline 採用 Sequential 的方式執行。
目前流程大致是:
Speech End
↓
Final STT
↓
LLM 完整生成
↓
TTS 完整合成
↓
Playback
每一個階段都必須等待上一個階段全部完成,使用者才會聽到 AI 的第一句話。
所以 Day 07 的目標,就是把這條流程改成 Streaming Voice Pipeline。
Streaming 不一定代表模型本身計算得更快,而是讓不同階段不需要彼此等待完整結果,而可以更早開始工作。
Day 02 做 STT 時,我使用的是 Rolling Transcription。
概念是每隔固定時間,重新辨識目前累積的音訊:
0~2 秒 → STT
0~4 秒 → STT
0~6 秒 → STT
所以畫面上雖然可以持續看到辨識結果更新,看起來很像 Streaming,但實際上每次還是重新處理前面的音訊。
因此它比較接近:
Rolling / Pseudo-Streaming ASR
而真正的 Streaming ASR,則是持續接收新的 Audio Chunk,保留前面的處理狀態,再增量產生新的 Partial Result。
可以簡單比較:
Day 02:Rolling Transcription
→ 每隔 2 秒重新辨識累積音訊
Day 07:Streaming ASR
→ 持續接收 Audio Chunk
→ 保留前面的狀態
→ 增量輸出 Partial
這也是從 Day 02 到 Day 07 很明顯的一個技術演進。
完成 STT 之後,下一個要改的就是 LLM。
原本流程是:
Final STT
↓
LLM
↓
完整 Response
↓
TTS
也就是一定要等整段回答全部生成完成,才能交給 TTS。
今天則改成 LLM Token Streaming。
LLM 在生成內容時,會持續輸出新的 Token,而不是等全部完成才一次回傳。
例如:
今天
今天如果
今天如果覺得
今天如果覺得有點累
...
這樣就代表,後面的 TTS 其實可以提前開始工作。
但這裡也不能每一個 Token 都直接丟給 TTS。
如果變成:
今 → TTS
天 → TTS
記 → TTS
得 → TTS
不但會產生大量 TTS 呼叫,播放出來也會非常破碎。
所以中間需要加入一層 Buffer。
我在 LLM 和 TTS 中間加入了 Sentence / Chunk Buffer。
流程變成:
LLM Token Streaming
↓
Text Buffer
↓
累積到適合長度
↓
切成 Chunk
↓
TTS
例如 LLM 已經產生:
今天如果覺得有點累,
遇到逗號或累積到一定長度後,就可以先把這一段送進 TTS。
同時間,LLM 繼續生成:
可以先休息一下,不要太勉強自己。
這樣就不用等完整答案生成完畢。
不過 Chunk 的大小也需要調整。
如果切太短:
今天 /
記得 /
多喝水 /
語音容易變得破碎、不自然。
但如果切太長,又會失去 Streaming 提早播放的優勢。
因此 Chunk 的切分必須在自然度與延遲之間取得平衡。
目前 TTS 我先使用 Kokoro 做 Chunked TTS。
也就是:
Text Chunk 1 → Audio 1
Text Chunk 2 → Audio 2
Text Chunk 3 → Audio 3
它還不是真正逐 Audio Frame 輸出的 Streaming TTS,但已經可以讓 LLM 與 TTS 部分重疊執行。
接著,我再加入 Playback Queue。
當 TTS 產生 Audio Chunk 後,就依序放進播放佇列:
Audio 1
↓
Audio 2
↓
Audio 3
播放器只需要持續從 Queue 裡拿下一段音訊播放。
這時就會出現真正的 Pipeline 重疊:
Playback:播放 Audio 1
TTS:產生 Audio 2
LLM:生成 Chunk 3
也就是三個階段可以同時進行。
原本是:
STT
↓
LLM 完整生成
↓
TTS 完整生成
↓
Playback
今天則變成:
Mic
↓
Rolling / Streaming ASR
↓
LLM Token Streaming
↓
Sentence / Chunk Buffer
↓
Chunked TTS
↓
Playback Queue
↓
AI 邊生成邊說
Streaming 最大的改變不是讓整段回答更快完成,而是讓第一段語音可以更早播放。
因此今天我再次觀察:
Speech End → First Audio
這個指標。
因為對使用者來說,真正影響體感的不是 AI 什麼時候「全部回答完」,而是使用者停止說話後,要等多久才能聽到 AI 開始回應。
今天最大的問題變成:
Chunk 到底要怎麼切?
目前需要考慮:
遇到逗號要不要切?
句號一定要切嗎?
最少需要幾個字?
最多要等待多久?
Audio Queue 要預先累積多少?
Chunk 太小會讓語音變得零碎,Chunk 太大又會增加等待時間。
所以 Streaming 並不是單純把 stream=True 打開就完成,真正需要處理的是 LLM、TTS 與 Playback 之間的協調。
Day 06 我找出了 Sequential Pipeline 中大量等待的問題。
Day 07 則開始把 LLM、TTS 與 Playback 改成可以重疊執行。
今天最重要的一個觀念是:
Streaming 不一定讓模型本身算得更快,而是讓不同階段不用彼此等待完整結果,可以更早開始工作。
現在 AI Companion 已經從:
使用者說完
↓
AI 想完
↓
AI 合成完
↓
AI 才開始說
慢慢變成:
使用者說完
↓
AI 開始生成
↓
前面的文字先做 TTS
↓
第一段語音先播放
↓
後面的內容繼續生成
也就是開始做到真正的「邊生成、邊說」。
但現在還有下一個問題:
如果 AI 正在講話,而使用者突然說:
「等等,我不是這個意思。」
系統目前還是會繼續把原本的回答講完。
所以 Day 08 要做的,就是 Barge-in 即時打斷,讓 AI 在說話時也能偵測使用者新的語音,立即停止 Playback、清空 Audio Queue,並取消目前的 TTS 與 LLM,重新回到 Listening 狀態。