雙AI上臉實戰:雙音訊線、語音預生成與情緒通道
這篇解決的問題:兩個AI角色同時上臉直播,共用一條虛擬音訊線會讓兩張臉一起對嘴,回合之間的接話等待也會拖到數秒。適合誰讀:已經有單一AI角色能開口說話,正要擴充成雙角色上臉直播的人,需要Windows、一張入門顯示卡,並且會裝虛擬音訊驅動與VTube Studio。讀完能做到什麼:兩張臉分別對嘴、回合間的接話等待壓到0秒、語調隨情緒起伏但不會爆音。
接續 雙AI雜談台 架構與執行,記錄2026-06-12到06-13打通的部分。延遲的理論背景見 直播延遲優化方案。
雙臉對嘴與接話歸零已驗收,動作與推流長測隨後補齊
實跑 duo_talk.py,雙AI話題討論成立,選題與人設方向留待再調。雙VTS加雙音訊線讓兩角色各自動嘴,實測通過。預生成升級之後接話等待壓到0.0秒。情緒通道第一版做出來,LLM在句首標情緒標籤,餵給edge-tts調語調。雙VTS同時跑的負載測試也做了,數字在下文的硬體段落。待辦的是VTS內建動作(眨眼、呼吸、物理、idle)與OBS同框推流長測,這兩項在隨後完成,見文末。
一條虛擬音訊線只能同步一張嘴,雙角色要開兩條線
VTS的唇形同步原理是「聽麥克風音量開合嘴」。兩個角色若共用同一條虛擬線,兩張嘴會一起動,雙人感直接毀掉。解法是一角色一條線、一線驅動一張嘴:
| 角色 | Python送音到(播放端) | VTS實例的麥克風(錄音端) |
|---|---|---|
| 星野 | CABLE Input(VB-CABLE) | CABLE Output |
| 楓 | Hi-Fi Cable Input(新裝) | Hi-Fi Cable Output |
graph LR A[星野語音合成] --> B[CABLE Input] B --> C[CABLE Output] C --> D[VTS實例一:星野模型] E[楓語音合成] --> F[Hi-Fi Cable Input] F --> G[Hi-Fi Cable Output] G --> H[VTS實例二:楓模型]
官方推薦的VB-CABLE A+B是捐贈制,最低要付15美元才拿得到安裝包。同公司的Hi-Fi Cable免費直載,差別是它不做自動重採樣,兩端取樣率要手動對齊。整條管線統一在48kHz,剛好相容。若長時間直播發現Hi-Fi Cable不穩,備案是花錢買A+B。
角色設定檔加一個 audio_device 欄位,main.py 的 play_routed() 接受每次呼叫指定裝置,缺省走全域 VTS_AUDIO_DEVICE。多角色路由變成一角色一行設定。
技術細節:Hi-Fi Cable安裝與多開VTS的實際步驟
- 下載檔名是
HiFiCableAsioBridgeSetup_v1007.zip,附帶的ASIO Bridge應用程式用不到,虛擬線是驅動層裝置,裝完重開機就常駐,不用啟動任何程式。- 在Windows傳統音效面板把Hi-Fi Cable Input(播放)與Output(錄音)都設成48000 Hz。
- Hi-Fi Cable是2014年的舊驅動,長時間跑有機率不穩。
- Steam版VTS的第二個實例不要從Steam啟動,直接跑安裝資料夾的
VTube Studio.exe,一實例一模型,各選各的麥克風。
接話等待歸零,關鍵是把語音合成也放進預生成
只把LLM文字預生成藏進播音時間,理論上接話該無縫,實聽卻還有明顯的空隙。真兇是TTS:原本的流程是播完音才開始合成下一句,edge-tts的網路請求加上ffmpeg轉檔全部卡在這個空檔裡。文字被藏掉了,語音沒被藏掉。
解法是把 speak() 拆成兩段:synth_to_wav48() 只負責合成,play_routed() 只負責播放。duo_talk.py 的背景執行緒趁對方講話的時候,把LLM和TTS一次做完,輪到自己講話時直接播現成的wav。升級後接話等待壓到0.0秒。
藏延遲要對整條鏈算帳,LLM、TTS、轉檔、播放啟動,漏掉任何一段,省下來的時間就被那一段吃回去。每一段都要單獨量測才抓得到真兇。每題的第一輪沒有預生成可用,仍有完整延遲,但換題本來就有緩衝,這段可以接受。
技術細節:預生成wav的落地與清理
預生成的wav用
tempfile.mkstemp落地,播完即刪。若這句話被使用者的留言插話作廢,背景執行緒改成「join完再刪檔」,不阻塞主迴圈也不會漏檔。
情緒先接語調,同一個標籤之後也能接表情動作
edge-tts不支援情緒風格,SSML的express-as會被忽略,只吃rate、pitch、volume這三個參數。設計重點是先在文字產生的階段掛一個情緒標籤,讓這個標籤同時餵給語調調整,之後也能餵給表情動作。
system prompt要求每句句首輸出一個情緒標籤,從平靜、認真、開心、興奮、吐槽、驚訝、無奈裡選一個。程式拆下標籤後對映到rate/pitch的微調,疊加在人設本身的基準音高上。標籤本身不會被唸出來,句子中間如果又冒出一次標籤(模型偶爾會在句中換情緒重標一次),也要清乾淨。Gemini標得相當準,開場自動標興奮、吐槽的回嘴標對地方、收尾標平靜。
這個標籤是一個可以重複利用的訊號源。第二版規劃是用pyvts接VTS的WebSocket API,雙實例各開一個埠,把同一個標籤對映到表情或動作的hotkey,講到興奮就睜眼揮手,聲音和臉自動同步,不用另外寫判斷邏輯。語調本身的天花板尚未驗證,備案是Azure Speech官方API的同款聲線支援真正的express-as風格,或雲端GPU跑 GPT-SoVITS。
疊加的情緒參數太猛,會把聲音直接弄爆
情緒通道上線後,長句開始變成兩倍速的尖銳聲,男聲則直接電子破音。根因是上一節那組增量:星野的基準音高再疊上驚訝的加成,一路加到正40Hz,超出這條聲線本身撐得住的範圍。修法是把情緒增量調降,並加上疊加夾限正負15%、正負20Hz。男聲破音還多一個原因是播放緩衝來不及,改用 sd.OutputStream(latency="high") 補掉。
情緒標籤是疊加在人設基準上的。設計增量時要問的是疊在最極端的那個基準上會不會超出去。
技術細節:四個TTS穩定性坑的根因與修法
症狀 根因 修法 長句變兩倍速尖銳聲 情緒疊加過頭(星野基準加驚訝疊到正40Hz) 情緒增量調降,加疊加夾限正負15%/正負20Hz 男聲電子破音 pitch硬拉失真加播放緩衝underrun 同上夾限,加 sd.OutputStream(latency="high")edge-tts間歇NoAudioReceived連發 微軟服務端限流(官方issue #443,非版本問題) synth_edge最多四次指數退避重試,0.8到3.2秒中國腔 聲線是zh-CN 星野改zh-TW-HsiaoChen,楓維持zh-TW-YunJhe 驗收工具是
prototype/tools/test_tts_fix.py,兩角色乘四種情緒的長句合成全數過關。edge的台灣聲線目前只剩HsiaoChen(女)、HsiaoYu(女)、YunJhe(男)三個,要更像台灣腔就得上雲端聲線克隆。
VTS內建設定就能讓角色除了嘴巴都會動
上臉的動作分兩層。第一層不用寫程式:VTS內建的自動眨眼、自動呼吸、物理效果(頭髮衣服搖晃)、模型idle動作循環,每個實例各自設定一次,效果立竿見影。第二層是程式驅動,用pyvts觸發表情或動作的hotkey,訊號源就是前面那個情緒標籤。
技術細節:VTube Studio參數級設定備忘(兩個實例各做一次)
- 自動眨眼:模型設定→參數設定清單,展開
ParamEyeLOpen、ParamEyeROpen兩槽各勾Auto-Blink,不需要攝影機輸入。- 自動呼吸:找輸出是
ParamBreath的槽勾Auto-Breath,沒有就新增空槽(INPUT空、OUT選ParamBreath)。- 物理(頭髮、衣服):官方範例模型自帶
physics3.json,Physics Settings確認啟用,強度可調到110到120%。- idle動作:Basic Setup用檔案選擇器挑
.motion3.json,VTS會自動循環。- 不要動嘴:
ParamMouthOpenY要維持INPUT是VoiceVolume,別加眨眼或呼吸的勾。- 懶人法Auto-Setup一鍵配好,跑完要回頭檢查嘴的INPUT沒被改掉。
兩個VTS實例同時跑,顯卡負載不到一半
雙VTS實跑,硬體是GTX 1660 6GB:
| 指標 | 平均 | 峰值 | 上限 |
|---|---|---|---|
| GPU使用率 | 29% | 41% | — |
| VRAM | 2632 MiB | 2799 MiB | 6144 MiB |
硬體徹底不是瓶頸。OBS推流疊上去之後要再測一次,這時候該看的風險指標是OBS統計裡的渲染延遲錯過影格。編碼不必看,NVENC是獨立硬體,不佔用這裡量到的GPU使用率。
雙臉雙線打通了,中央音訊調度器還沒開始做
VTS的內建動作設定完成,OBS雙視窗同框加推流的長測也通過,雙臉雙線這條路整段打通。硬體從頭到尾都不是瓶頸。
還沒做的是中央音訊調度器,讓語音逐句串流、播放可以隨時取消,這會讓插話從「回合之間」細到「句子之間」。這件事尚未動手。