)
做過桌面機(jī)器人的人大概都經(jīng)歷過同一個臨界點(diǎn)演示前網(wǎng)絡(luò)一切正常機(jī)器人像個話癆一旦路由器抽風(fēng)它立刻退化成一個會眨眼的塑料擺件。我上一個項(xiàng)目就栽在這兒——客戶在展廳里說把音量調(diào)小一點(diǎn)云端鏈路正好卡住機(jī)器人沉默了六秒場面非常安靜。事后復(fù)盤問題不在云端而在于我把所有語音交互都壓在了同一條管路上。后來我改成雙管線板子上掛一顆WT2606A負(fù)責(zé)全部離線命令詞——開關(guān)燈、調(diào)音量、前進(jìn)后退、表情切換這類高頻、要求一喊就動的指令全部在本地閉環(huán)完成一旦識別到的是開放式提問你覺得今天天氣怎么樣給我講個笑話主控就把音頻或轉(zhuǎn)寫文本交給在線多輪對話鏈路由云端的語音識別、大模型和語音合成接力。兩條管線各自獨(dú)立網(wǎng)斷了設(shè)備至少還能用語音延遲也從一兩秒壓到幾百毫秒。這套結(jié)構(gòu)聽起來不復(fù)雜真做起來坑非常密集200條命令詞怎么規(guī)劃才不打架、UART 一問一答的協(xié)議為什么不夠用、機(jī)器人自己說話時麥克風(fēng)聽到自己怎么辦、離線切在線的一瞬間為什么老是吞字。下面就把選型思路、硬件通路、協(xié)議幀設(shè)計(jì)、狀態(tài)機(jī)和聯(lián)調(diào)排查順序逐個拆開講適合正在做語音交互硬件的人也適合想給自研小機(jī)器人加一層離線兜底的玩家。1. 為什么語音鏈路必須拆成本地和云端兩根管子1.1 WT2606A 在整機(jī)里到底承擔(dān)什么角色先把定位說清楚很多人在這一步就走偏了。WT2606A 這類離線語音模組的本質(zhì)是一顆帶語音識別的播放器它自己能干三件事聽喚醒詞、匹配內(nèi)置命令詞表、播放預(yù)置音頻。它不是一個通用計(jì)算平臺你沒法在它上面跑對話邏輯也不要指望它理解上下文。它的價值在于確定性——同一個詞第1次和第1000次識別的結(jié)果一致響應(yīng)時間穩(wěn)定在幾百毫秒而且完全不需要網(wǎng)絡(luò)。我習(xí)慣把它當(dāng)成一個按鍵擴(kuò)展器來理解你原本要用20個物理按鍵才能表達(dá)的指令現(xiàn)在用20個詞就搞定了而且用戶不用找按鍵在哪。這個心智模型一旦建立后面所有設(shè)計(jì)決策都順了——凡是能映射成按鍵的指令都該扔給它凡是需要理解、推理、記憶的都別為難它。需要注意的是不同批次的模組和固件版本在詞條容量、喚醒靈敏度、串口指令集上會有出入。我拿到模組的第一個動作永遠(yuǎn)是連上廠家的配置工具把默認(rèn)詞表全部讀出來看一遍確認(rèn)這版固件到底支持多少條詞、有沒有二次喚醒、串口主動上報是哪幾條指令。別照著網(wǎng)上某篇兩年前的帖子就開干版本差異足夠讓你調(diào)一整天。1.2 職責(zé)邊界怎么劃哪些話走本地哪些話才值得上云邊界劃分的原則只有一條看響應(yīng)的時效要求而不是看句子的復(fù)雜程度。打開臺燈和你覺得人類會移民火星嗎從語法上都是句子但前者必須在300毫秒內(nèi)給出動作反饋后者慢兩秒用戶完全能接受。按這個標(biāo)準(zhǔn)我把指令分成三類指令類型典型例子響應(yīng)要求走哪條鏈路判斷理由即時控制類前進(jìn)、停止、調(diào)速、開關(guān)燈越快越好最好小于500ms本地離線用戶有物理預(yù)期慢一點(diǎn)就感覺機(jī)器壞了狀態(tài)查詢類電量多少、現(xiàn)在幾點(diǎn)、音量幾檔1秒內(nèi)可接受本地優(yōu)先答不上再上云答案短且固定沒必要繞一圈云端開放式對話類講個笑話、聊聊天、問知識3秒內(nèi)可接受在線多輪本地詞表根本覆蓋不了必須上模型這張表是我反復(fù)調(diào)整過三版的成果。最初的版本我把現(xiàn)在幾點(diǎn)這類也放到了云端理由是反正要聯(lián)網(wǎng)查時間結(jié)果發(fā)現(xiàn)用戶一天要問十幾次每次都要等一秒多體驗(yàn)非常割裂。后來改成主控本地維護(hù)時鐘直接由板子上的語音文件或 TTS 播報延遲瞬間掉到200毫秒以內(nèi)。反過來也有踩過的坑我一度想把音量調(diào)到百分之三十這種帶參數(shù)的指令放進(jìn)離線詞表。做法是枚舉百分之十到百分之一百共十條詞占掉5%的容量識別率還不錯。但用戶會說小聲一點(diǎn)太吵了這些模糊表達(dá)離線詞表接不住。最后我的做法是精確數(shù)值走離線模糊表達(dá)走在線兩條路都留著用戶用哪種說法都能得到響應(yīng)。1.3 喚醒之后的時序賬先算清楚再動手設(shè)計(jì)任何語音交互系統(tǒng)之前我建議先把延遲預(yù)算寫在紙上。這個習(xí)慣救過我很多次。以用戶說一句話機(jī)器人回應(yīng)為一個完整周期我的預(yù)算表大致長這樣喚醒詞檢測到串口上報200400ms取決于喚醒詞長度和靈敏度配置本地命令詞識別并回傳結(jié)果300600ms若轉(zhuǎn)云端音頻上傳首包200500ms家庭 Wi-Fi 環(huán)境下云端語音識別返回文本300800ms大模型首 token 返回5001500ms取決于模型規(guī)模和提示詞長度語音合成首包返回并開始播放200500ms把這些數(shù)字加起來你會發(fā)現(xiàn)一件事純云端鏈路的最壞情況接近4秒用戶會覺得機(jī)器人在發(fā)呆。而混合鏈路的平均體驗(yàn)是常用指令300500ms開放對話1.52.5秒。用戶對2秒的容忍度來自他知道這次問的是復(fù)雜問題而對開燈等2秒就完全不能忍。這就是拆管線的意義——不是技術(shù)炫技是把延遲花在用戶能接受的地方。2. 200條命令詞不是填滿就行詞表規(guī)劃才是真功夫2.1 先分組再選詞別一上來就寫詞拿到200條容量的人第一反應(yīng)通常是這么多隨便寫。我第一版就是這么干的按功能順序往下排寫到第80條的時候發(fā)現(xiàn)前面已經(jīng)有三個打開開頭的詞了識別時互相搶。后來我改成先分組再填詞結(jié)構(gòu)一下子就清楚了動作組約50條前進(jìn)、后退、左轉(zhuǎn)、右轉(zhuǎn)、加速、減速、急停、原地轉(zhuǎn)圈狀態(tài)組約30條電量、溫度、速度檔位、當(dāng)前模式、剩余時間交互組約40條你好、安靜一點(diǎn)、聲音大一點(diǎn)、別動、跟我走、跳個舞查詢組約30條現(xiàn)在幾點(diǎn)、今天幾號、你是誰、你會什么場景組約30條進(jìn)入待機(jī)、開始巡檢、返回充電、演示模式預(yù)留組約20條給后續(xù)迭代留的坑位預(yù)留組這個習(xí)慣非常值錢。產(chǎn)品上線后一定會加功能如果一開始把200條塞滿加一個詞就得刪一個詞還得重新做全量測試。留20條緩沖后面加功能時直接從預(yù)留組拿成本幾乎為零。2.2 音近詞沖突識別率翻車最常見的原因離線語音識別最容易出的問題不是聽不懂而是聽錯而且錯得非常有規(guī)律——它會混淆音近詞。我在測試臺上統(tǒng)計(jì)過誤識別樣本排在前面的幾類非常典型沖突類型具體例子表現(xiàn)處理方式聲母相近前進(jìn) vs 錢進(jìn)偶發(fā)誤觸發(fā)改詞換成向前走韻母相近停止 vs 挺直高頻誤識別換詞用停下替代長度相近且節(jié)奏相同左轉(zhuǎn) vs 左轉(zhuǎn)圈短句被長句搶長句加前綴改成來個左轉(zhuǎn)圈單字指令開誤觸發(fā)率極高嚴(yán)禁使用單字或雙字極短詞語氣詞開頭嗯打開用戶習(xí)慣拖音詞表里不要包含語氣詞我的經(jīng)驗(yàn)規(guī)則是命令詞至少三個字最好四到五個字且首尾音節(jié)要有明顯差異。向前走比前進(jìn)穩(wěn)得多停下動作比停止錯得少。另外同一組內(nèi)的詞盡量不要共享前兩個字否則模組在匹配時會反復(fù)在幾個候選之間搖擺。還有一個反直覺的結(jié)論詞表越長單條識別率不一定越低但誤觸發(fā)率會明顯上升。因?yàn)楹蜻x變多模組的置信度判定更容易出錯。所以能精簡就精簡200條是上限不是目標(biāo)我實(shí)際項(xiàng)目里通常只用120150條剩下的留白。2.3 別名與模糊匹配讓200條詞表用出400條的效果用戶不會按你寫的詞說話。你寫向前走他說往前走前進(jìn)過來。解決辦法有兩條路我通常一起用。第一條路是別名。很多配置工具支持一條命令詞掛多個別名別名不占獨(dú)立詞條或只占很小的配額。我會給每個高頻指令配23個常見說法比如向前走掛上往前走向前過來一點(diǎn)。這一步能把用戶第一次嘗試就成功的概率顯著拉高。第二條路是主控兜底。模組識別不出來的時候一定會給出一個未識別的上報。這時主控可以做一個判斷如果距離喚醒不超過5秒就把這段音頻轉(zhuǎn)給在線識別鏈路讓云端判斷用戶想干什么。這就形成了本地優(yōu)先、云端兜底的分層結(jié)構(gòu)用戶幾乎不會遇到說了沒反應(yīng)的情況。要注意的一點(diǎn)是兜底的成本控制。如果每一次未識別都往云端發(fā)音頻流量和調(diào)用費(fèi)用會很難看。我的做法是設(shè)置一個短窗口比如喚醒后8秒和次數(shù)上限比如最多兜底兩次超出就播報一句我沒聽清你可以說……并給出幾個提示詞引導(dǎo)用戶往詞表里靠。3. 硬件通路上的坑比軟件多得多3.1 麥克風(fēng)、功放、喇叭的物理布局決定上限很多人把語音識別的效果問題歸到算法上實(shí)際上硬件布局的影響往往更大。我這幾年最深的體會是麥克風(fēng)的位置比麥克風(fēng)的型號重要。有幾個原則我是踩坑之后才真正記住的麥克風(fēng)要盡量遠(yuǎn)離喇叭最好放在機(jī)身的另一側(cè)或頂部并且麥克風(fēng)的進(jìn)音孔不要和喇叭口朝向同一方向。我做過一個圓柱形機(jī)器人麥克風(fēng)被放在底部一圈裝飾燈旁邊喇叭在正面結(jié)果只要音量超過60%識別率就斷崖式下跌——喇叭的聲音通過桌面反射進(jìn)了麥克風(fēng)。麥克風(fēng)進(jìn)音孔要有獨(dú)立的腔體前面加防塵網(wǎng)和一點(diǎn)點(diǎn)聲學(xué)海綿。聽起來很講究其實(shí)就是別讓孔直接對著外殼的空腔否則會產(chǎn)生駐波某些頻段被抵消人的聲音進(jìn)去就變悶了。走線也要注意。麥克風(fēng)的模擬走線如果和功放的電源線、電機(jī)的驅(qū)動線捆在一起底噪會明顯上升。我現(xiàn)在的做法是麥克風(fēng)信號線單獨(dú)走并且盡量短必要時用屏蔽線屏蔽層單點(diǎn)接地。3.2 回聲與自激機(jī)器人一邊說話一邊聽必然出問題這是雙管線架構(gòu)里最容易被忽略的一環(huán)。機(jī)器人正在播放好的正在為你播放音樂用戶接著說停如果麥克風(fēng)一直開著它會聽到自己的喇叭聲輕則誤識別重則觸發(fā)自己的命令形成死循環(huán)——我遇到過最離譜的一次機(jī)器人反復(fù)觸發(fā)自己的音量加大指令一路加到最大聲整個實(shí)驗(yàn)室都在看它。解決思路有三層從簡單到復(fù)雜第一層是半雙工策略也就是說話時閉麥。播放開始就把識別關(guān)掉播放結(jié)束再打開。實(shí)現(xiàn)最簡單但會打斷用戶用戶說停的時候機(jī)器人還在播它聽不見。所以我通常只在對延遲不敏感的播報場景用這招。第二層是播放時降低麥克風(fēng)增益并開啟關(guān)鍵詞白名單。播放期間只保留停下安靜別說了這類打斷詞其他一律忽略。這樣既能接住用戶的打斷意圖又大幅降低誤觸發(fā)。這是我最常用的方案性價比最高。第三層是真正做回聲消除AEC讓設(shè)備全雙工。這需要參考信號也就是正在播放的音頻和麥克風(fēng)信號嚴(yán)格同步對采樣時鐘和延遲對齊要求很高。如果主控算力夠可以用現(xiàn)成的語音前端庫如果算力緊張建議老老實(shí)實(shí)用第二層。3.3 供電和串口電平那些偶發(fā)死機(jī)的真兇有一類問題特別折磨人設(shè)備運(yùn)行十幾分鐘隨機(jī)重啟或者語音播報時偶發(fā)卡死。查了半個月代碼沒結(jié)果最后發(fā)現(xiàn)是供電。功放播放瞬間的電流沖擊非常大尤其是播報低頻音效的時候。如果電源設(shè)計(jì)余量不足電壓會被瞬間拉低主控復(fù)位。我的標(biāo)準(zhǔn)做法是在功放電源腳就近并一顆大容量電解電容我一般用470μF到1000μF加一顆0.1μF的小電容大電容應(yīng)付瞬態(tài)小電容濾高頻。就這么一個小改動能解決一大半隨機(jī)重啟。串口電平也必須確認(rèn)清楚。模組的 UART 是3.3V還是5V主控那邊是否兼容如果電平不匹配短時間可能看不出問題長時間會有通信錯誤率上升甚至燒口。另外串口線不要和電機(jī)驅(qū)動線并行我習(xí)慣在串口線上串一顆小電阻做限流成本幾分錢能換一份安心。4. UART 協(xié)議與主控狀態(tài)機(jī)整個系統(tǒng)的骨架4.1 為什么簡單的一問一答協(xié)議不夠用剛開始我用的協(xié)議非常樸素主控發(fā)一條查詢模組回一條結(jié)果。跑起來才發(fā)現(xiàn)問題一大堆。首先是異步事件。用戶隨口說了一句喚醒詞模組需要主動上報我被喚醒了這不是主控問出來的而是模組自己冒出來的。如果協(xié)議里只有應(yīng)答幀沒有主動上報幀主控就不知道怎么處理。其次是幀邊界。語音識別結(jié)果是變長的播放狀態(tài)也是變長的如果不做幀頭幀尾和長度字段很容易把兩幀粘在一起解析出錯。我現(xiàn)在的幀結(jié)構(gòu)是這樣的幀頭(2B) 長度(1B) 類型(1B) 數(shù)據(jù)(nB) 校驗(yàn)(1B) 幀尾(2B) 0xAA55 n2 0x01~0x0F ... 累加和 0x5A5A類型字段用來區(qū)分用途0x01 是命令詞識別結(jié)果0x02 是喚醒事件0x03 是播放狀態(tài)變化0x04 是音量上報0x05 是主控下發(fā)的播報請求。校驗(yàn)我用最簡單的累加和夠用而且計(jì)算量小不需要上 CRC。第三是時序耦合。一問一答模型下主控發(fā)出去就得等期間沒法處理別的事情。真實(shí)場景里主控要同時管電機(jī)、傳感器、屏幕所以必須改成事件驅(qū)動。串口我用中斷接收加環(huán)形緩沖區(qū)解析出完整幀后丟進(jìn)一個隊(duì)列主循環(huán)里從隊(duì)列取事件分發(fā)處理。一段解析代碼大概是這個形狀// 環(huán)形緩沖區(qū)里找?guī)^湊齊一幀后校驗(yàn) while (rb_available(rx_rb) 8) { if (rb_peek_u16(rx_rb) ! 0x55AA) { rb_drop_one(rx_rb); continue; } uint8_t len rb_peek_at(rx_rb, 2); if (rb_available(rx_rb) len 6) break; // 幀還沒收全等 uint8_t buf[64]; rb_copy_out(rx_rb, buf, len 6); if (!checksum_ok(buf, len 6)) continue; // 校驗(yàn)失敗丟棄 event_push(parse_type(buf), payload_of(buf)); // 入隊(duì)交給主循環(huán) }提示解析循環(huán)里千萬不要做耗時操作尤其是不要在里面直接調(diào)用播放或網(wǎng)絡(luò)請求。中斷上下文里只做搬運(yùn)和校驗(yàn)業(yè)務(wù)邏輯一律交給主循環(huán)否則后面加功能會非常痛苦。4.2 狀態(tài)機(jī)的六個狀態(tài)和遷移條件協(xié)議定完接下來是把行為組織成狀態(tài)機(jī)。我用了六個狀態(tài)跑了大半年沒出過狀態(tài)錯亂狀態(tài)含義進(jìn)入條件退出條件IDLE待機(jī)只等喚醒詞上電或會話超時收到喚醒事件LOCAL_WAIT等待本地命令詞喚醒成功識別成功 / 超時2秒 / 兜底觸發(fā)EXECUTING本地指令執(zhí)行中本地識別成功動作完成CLOUD_WAIT等待云端返回兜底觸發(fā)或明確走在線收到文本或音頻 / 超時5秒SPEAKING播報中有待播內(nèi)容播放結(jié)束事件INTERRUPT打斷處理播放中收到打斷詞停止播放后回到 IDLE 或 LOCAL_WAIT有兩點(diǎn)經(jīng)驗(yàn)特別想分享。第一超時時間一定要給足并且可配。我最初把 LOCAL_WAIT 的超時設(shè)成1秒結(jié)果用戶說完喚醒詞停頓一下再說話就進(jìn)不了識別了。后來改成2秒并且做成配置項(xiàng)不同場景可以調(diào)。第二SPEAKING 狀態(tài)必須能被 INTERRUPT 打斷而且打斷后要決定回到哪個狀態(tài)。如果用戶打斷是為了提新需求應(yīng)該回到 LOCAL_WAIT 繼續(xù)聽如果只是讓它閉嘴就回 IDLE。我的做法是根據(jù)打斷詞的語義來區(qū)分停下回 IDLE等一下保持 LOCAL_WAIT。4.3 打斷barge-in在資源受限設(shè)備上怎么實(shí)現(xiàn)打斷是語音交互里體驗(yàn)提升最明顯、也最難做好的功能。理想情況是機(jī)器人正在說一大段話用戶中途說停聲音立刻斷掉然后聽用戶接著說。實(shí)現(xiàn)上有兩個關(guān)鍵點(diǎn)。第一是播放必須可中斷如果音頻是一次性塞進(jìn)播放緩沖區(qū)不可打斷的那你只能等它播完。我的做法是把長播報切成小片段比如每200毫秒一段在片段之間檢查是否有打斷標(biāo)志這樣停止延遲可以控制在200毫秒以內(nèi)用戶感覺是立刻停。第二是打斷詞表要極簡。我通常只放三到五個詞停下、閉嘴、別說了、等一下。詞越少誤觸發(fā)概率越低識別也越穩(wěn)。有段時間我為了顯得智能把打斷詞擴(kuò)到十幾個結(jié)果機(jī)器人經(jīng)常在自己播報的間隙誤觸發(fā)反而變得更遲鈍。5. 在線多輪對話這一半鏈路拆解與延遲優(yōu)化5.1 云端鏈路的四段拆分每一段都能優(yōu)化在線部分看起來是把音頻發(fā)上去把回答拿回來但拆開看至少有四段語音識別音頻轉(zhuǎn)文本、對話生成文本進(jìn)文本出、語音合成文本轉(zhuǎn)音頻、以及網(wǎng)絡(luò)傳輸。每一段的優(yōu)化手段都不一樣。語音識別這一段流式識別比整段上傳體驗(yàn)好很多。用戶說完最后一個字識別結(jié)果基本同時出來省掉了上傳整段音頻的時間。代價是實(shí)現(xiàn)復(fù)雜度上升如果不想搞流式至少要保證音頻格式正確——我見過不少問題是因?yàn)椴蓸勇侍铄e寫16k實(shí)際是8k導(dǎo)致識別結(jié)果全是亂碼。對話生成這一段首 token 時間比總時間重要。用戶在意的是機(jī)器人多久開始回應(yīng)而不是整段生成完要多久。所以要用支持流式輸出的接口拿到第一個片段就送去合成邊生成邊合成邊播放。語音合成這一段預(yù)合成高頻回復(fù)收益巨大。像好的我在沒聽清你再說一遍這種固定話術(shù)完全可以離線預(yù)置根本不用調(diào)云端合成。我在項(xiàng)目里預(yù)置了大約30句高頻回復(fù)直接砍掉了一部分最常用的合成延遲。網(wǎng)絡(luò)傳輸這一段保持長連接是關(guān)鍵。每次請求都重新握手會白白加上幾百毫秒用長連接或連接池把這部分省掉。5.2 多輪對話的記憶到底怎么管多輪這兩個字聽起來高級本質(zhì)上就是每次請求把前面的對話一起帶上。但帶多少、怎么帶有講究。我的一般策略是滑動窗口加重摘要保留最近6到8輪完整對話更早的內(nèi)容壓縮成一段簡短摘要附在系統(tǒng)提示里。這樣既保證了上下文連貫又不會讓請求體無限膨脹。實(shí)測對延遲影響很明顯——上下文從3000字降到1000字首 token 時間能少兩三百毫秒。另外兩個細(xì)節(jié)。第一會話要有明確的開始和結(jié)束。我的狀態(tài)機(jī)在 IDLE 狀態(tài)停留超過30秒就清空上下文下一次喚醒是新會話。否則用戶早上聊的內(nèi)容會一直影響到晚上模型回答會變得莫名其妙。第二角色設(shè)定要寫成可維護(hù)的文件而不是散在代碼里。我見過把提示詞硬編碼在三個不同文件里的項(xiàng)目改一句話要全局搜索。后來我統(tǒng)一抽成一個配置配合版本號管理調(diào)優(yōu)時對比效果方便很多。# 每次請求組裝的最小上下文結(jié)構(gòu)示意 payload { session_id: session_id, # IDLE 超時后重新生成等于手動開新會話 system: character_prompt, # 角色設(shè)定單獨(dú)配置、單獨(dú)版本管理 summary: rolling_summary, # 早期對話的壓縮摘要 messages: recent_turns[-8:], # 最近 8 輪完整對話 stream: True # 必須開流式否則首字延遲不可控 }5.3 從云端音頻拿回到設(shè)備播放中間還有一段路模型返回音頻片段之后不要直接一股腦塞給播放器。我的做法是先做一層小緩沖攢夠大約300500毫秒的音頻再開始播然后邊播邊往后補(bǔ)。這個緩沖有兩個作用一是抹平網(wǎng)絡(luò)抖動避免播放斷斷續(xù)續(xù)二是給打斷邏輯留出檢查窗口。如果主控和語音模組是分開的模組負(fù)責(zé)本地詞表和播放云端返回的音頻需要通過串口或存儲介質(zhì)交給模組播放。走串口時要注意帶寬16k 單聲道 16bit 的音頻每秒是32KB115200 波特率理論上一秒也就11KB出頭根本傳不動。所以要么用更低的采樣率要么壓縮要么讓主控自己帶一個獨(dú)立音頻通道。這是選型階段就要想清楚的事等到聯(lián)調(diào)再發(fā)現(xiàn)就來不及了。6. 聯(lián)調(diào)階段的問題排查順序照著走能省很多時間6.1 喚醒不靈敏與頻繁誤喚醒先別改代碼喚醒問題幾乎全部可以歸結(jié)為兩類該響應(yīng)的時候不響應(yīng)不該響應(yīng)的時候亂響應(yīng)。這兩個現(xiàn)象看起來相反但排查順序是一樣的我習(xí)慣按下面這個表往前走現(xiàn)象先查什么常見原因處理手段喊三遍才醒麥克風(fēng)拾音是否夠進(jìn)音孔堵塞、增益太低調(diào)增益檢查腔體站著不動也會醒環(huán)境噪聲與閾值靈敏度設(shè)太高降一檔靈敏度加二次確認(rèn)只在某個方向能醒麥克風(fēng)指向性單麥盲區(qū)調(diào)整安裝角度或加第二顆麥播放時亂醒自播放串?dāng)_麥克風(fēng)聽到了自己的聲音播放時閉麥或加打斷白名單有一個經(jīng)驗(yàn)值得單獨(dú)說不要把靈敏度調(diào)到最高再回頭解決誤喚醒。很多人的思路是先保證能喚醒結(jié)果誤喚醒多到?jīng)]法用再來降參數(shù)等于白折騰。正確順序是先設(shè)定一個合理的中間值用真實(shí)場景錄音跑測試集看識別和誤觸發(fā)的比例然后再微調(diào)。我一般會把測試集分成安靜室內(nèi)、電視背景音、有人說話三種分別測一遍。6.2 串口丟幀與亂碼九成是時序和電平問題串口問題排查我有固定套路按順序來基本十分鐘能定位第一步降速驗(yàn)證。把波特率降到9600再測一遍如果問題消失說明是時序余量不足或線太長不是協(xié)議問題。第二步看波形。有條件的話用邏輯分析儀抓幾幀重點(diǎn)看起始位是否干凈、有沒有毛刺、空閑電平是否正確。電平反了的話波形會完全不對一眼就能看出來。第三步查緩沖區(qū)溢出。如果主控在解析時阻塞太久接收中斷來不及處理硬件緩沖區(qū)就溢出了表現(xiàn)為丟幀。解決辦法是把環(huán)形緩沖區(qū)開大一點(diǎn)并且在中斷里只做搬運(yùn)不做解析和業(yè)務(wù)。第四步加超時重傳。對于主控下發(fā)給模組的關(guān)鍵指令比如播報請求我會加一層確認(rèn)和重傳重傳上限三次。這一層看起來啰嗦但能把偶發(fā)丟幀對用戶體驗(yàn)的影響降到幾乎為零。6.3 離線轉(zhuǎn)在線時的吞字本質(zhì)是狀態(tài)遷移設(shè)計(jì)問題這是我最想重點(diǎn)講的一個坑因?yàn)樗羌軜?gòu)層面的問題不是調(diào)參能解決的?,F(xiàn)象是這樣的用戶說前進(jìn)……呃你覺得今天天氣怎么樣前半句是本地指令后半句是開放問題。如果狀態(tài)機(jī)在本地識別成功后立刻播放應(yīng)答音效并進(jìn)入 EXECUTING用戶在呃之后的語音就被靜音掉了云端根本收不到完整問題。用戶的感覺是我說了兩件事它只做了一件。我的解決方案是在本地識別成功后不立刻閉麥而是保留一個短暫的續(xù)聽窗口。具體做法是本地指令立刻執(zhí)行保證響應(yīng)速度同時麥克風(fēng)繼續(xù)拾音大約1.5秒如果這段時間內(nèi)檢測到用戶還在說話就啟動云端鏈路把這段音頻送上去如果沒有就正常結(jié)束。這里有個細(xì)節(jié)要注意本地執(zhí)行的動作可能會產(chǎn)生聲音比如電機(jī)啟動而麥克風(fēng)在續(xù)聽容易串?dāng)_。所以執(zhí)行前我會先判斷動作的噪聲等級噪聲大的動作比如啟動電機(jī)就關(guān)閉續(xù)聽窗口避免自激噪聲小的動作比如切換表情就保留。另外云端返回結(jié)果之后要不要放棄本地的執(zhí)行結(jié)果我的選擇是不放棄兩者疊加。用戶既想前進(jìn)又想知道天氣兩件事都應(yīng)該被滿足。這個策略在實(shí)測中的滿意度明顯高于后到的覆蓋先到的。7. 上線前我必做的幾項(xiàng)驗(yàn)證項(xiàng)目做到能跑通演示離能交付還有一段距離。下面這幾項(xiàng)驗(yàn)證是我每一版都會做的每次都能揪出點(diǎn)東西。第一項(xiàng)是斷電斷網(wǎng)測試。把網(wǎng)斷掉把所有本地指令過一遍確認(rèn)每一條都能正常響應(yīng)沒有任何一條依賴云端。然后再把網(wǎng)接上確認(rèn)云端鏈路能自動恢復(fù)不需要重啟。這個測試聽起來基礎(chǔ)但我見過不止一個項(xiàng)目在斷網(wǎng)后需要重啟才能恢復(fù)。第二項(xiàng)是長時間連續(xù)運(yùn)行測試。讓設(shè)備連續(xù)跑12小時中間不斷有語音輸入觀察內(nèi)存占用和響應(yīng)時間是否劣化。常見的劣化來源是會話上下文沒釋放、音頻緩沖區(qū)沒回收。我一般會在代碼里加一個輕量的內(nèi)存水位日志每半小時打一條跑完看曲線。第三項(xiàng)是多說話人測試。找至少三個人男女各一用不同的語速和音量各說一遍全部高頻指令。離線識別對音色是有偏好的同一個人調(diào)出來的參數(shù)換個人可能就差很多。如果差異特別大就得回頭去看詞表選詞是不是太依賴某個音色了。第四項(xiàng)是極限場景測試。具體包括在最大音量播報時下指令、在電機(jī)全速運(yùn)行時下指令、在距離設(shè)備三米和三十厘米兩個位置分別下指令。這幾項(xiàng)測完心里的底就差不多了。第五項(xiàng)是誤觸發(fā)統(tǒng)計(jì)。讓設(shè)備在開著電視的房間里待兩小時統(tǒng)計(jì)所有非預(yù)期的喚醒和識別次數(shù)。這個數(shù)字如果在兩小時內(nèi)超過5次說明靈敏度或詞表還需要收緊。我現(xiàn)在的目標(biāo)是兩小時不超過3次達(dá)到這個水平用戶基本不會有抱怨。注意這幾項(xiàng)驗(yàn)證最好在真實(shí)場景完成不要在實(shí)驗(yàn)室安靜環(huán)境下過一遍就算完。噪聲環(huán)境下的表現(xiàn)才是用戶實(shí)際體驗(yàn)的起點(diǎn)安靜環(huán)境下的數(shù)據(jù)只能作為上限參考。最后分享一個我在實(shí)際項(xiàng)目里逐漸養(yǎng)成的小習(xí)慣每次調(diào)整詞表或者提示詞之后都在配置里記一行變更說明和時間。語音交互的調(diào)優(yōu)是長期工作半年后回頭看你會發(fā)現(xiàn)為什么當(dāng)初把這條詞刪掉了這種問題非常常見而一行變更記錄能省掉一整天的回溯。