kitt-stt · dev-report · 2026-09-15-eot-backends

可切換 EOT backend

turn_model.backend 出廠為 vad;所有 turn idle 時,狀態頁可 process-wide 切換 turnsensevadsmart-turn,重啟回 YAML。三種 backend 的 wait_secs 均為 0.1;兩個 scoring backend 的 threshold 均為 0.6。VAD 不跑模型,出廠路徑為 web-ui stop debounce 0.4 秒 + STT wait 0.1 秒;COMPLETE 立即 commit。不支援 per-car 或 mid-turn 切換。
目前:offline 1661 passed / 33 skipped;current-default CUDA live 12 passed / 3 failed 2 組量測 artifact 未驗 出廠 vad · wait 0.1s 分支 research/vad-turnsense-eot baseline 67160fa

🎛️§0 · 結論與範圍

ID 對照:本輪驗收與同號歷史子項

ID 對照

本輪驗收條件使用 A1–A10。附錄 B 的反應式排程表沿用 2026-07-29 的 A1–A6;同號並存時以下表兩欄為準,不是同一條需求。

ID本輪(2026-09-15-eot-backends)同號歷史(若有)
A1出廠 nested:backend vad;三種 backend wait 均為 0.1;兩個 scoring backend threshold 均為 0.6。資產在 turnsense:2026-07-29 排程:沒有前次延遲時使用 initial_interval
A2頂層純量或 sibling map、殘留 complete_wait_secs、舊三檔、enabled、頂層 turn:、頂層資產鍵拒絕啟動2026-07-29 排程:間隔為延遲 × margin,設地板但不設上限
A3vad.wait_secs 接受 0.1–3.0。backend=vad 不建構 TurnSense,等待後 commit reason=vad2026-07-29 排程:margin 必須大於 1
A4overlay 只改 backend: smart-turn 繼承三個區塊2026-07-29 排程:最小間隔必須大於 0
A5nested wait 越出 0.1–3.0、threshold 越出 0–1 拒絕啟動2026-07-29 排程:初始間隔必須大於 0
A6deploy.sh 從 nested turnsense:keyonnx_filecmvn_file2026-07-29 排程:環境變數可覆寫 YAML margin
A7COMPLETE 嚴格大於門檻並立即 commit;incomplete/invalid 等 wait;滑桿只改 incomplete;JSON 仍是 per-backend map;全行程共用、不依車
A8smart-turn 呼叫 _predict_endpoint;過門檻 reason=model;不呼叫 append_audio。兩個 scoring backend 共用 model reason;VAD 使用 vad reason;三種 backend 下 KWS/force 仍優先
A9SW-OPS-04SW-EOT-01SW-EOT-08 路徑為 nested;出廠 VAD 路徑為 400+100=500。不宣稱修 490/510
A10狀態頁可在所有 turn idle 時切換 backend;busy 回 409;目標 detector 準備失敗保留原 backend;既有與新 connection 共用新 detector;重啟回 YAML
名詞定義與背景術語

名詞定義

名詞/ID意義與本報告用途
EOT backend / turn_model.backendYAML 值是行程啟動預設,出廠為 vad;狀態頁選擇後由單一「儲存設定」在所有 turn idle 時 process-wide 切換 turnsensevadsmart-turn,重啟回 YAML。它選的是 verdict 來源,不是關掉 VAD,也不是 per-user 開關。Turn 等待時間(wait_secs)旋鈕只改目前 backend 的等待秒數,不是 backend,也不改 complete threshold。
wait_secs / Turn 等待時間 / SW-OPS-04YAML、JSON 與 API 欄位維持 wait_secs;狀態頁在 Turn settings 顯示 Turn 等待時間(wait_secs)。TurnSense/Smart Turn 用於 incomplete/invalid/runtime error;VAD 用於收到 stop 後的 continuation window。三種 backend 各自持有 wait_secs,出廠皆為 0.1,範圍 0.1–3.0;滑桿覆寫目前 running backend 到 turn_settings.json(per-backend map,全行程共用、不依車)。兩個 scoring backend 的 complete_threshold 出廠皆為 0.6,YAML-only,嚴格大於才 COMPLETE。預設 VAD backend 的靜音到 commit 為 web-ui stop debounce 0.4 秒 + kitt-stt VAD wait 0.1 秒,不扣 ASR。
turnsense / vad / smart-turnturnsense 沿用 CUDA 語尾三分類;vad skip 模型並在合格 VAD stop 後等待 turn_model.vad.wait_secs。VAD stop 由 kitt-web-ui LoggingSileroVADAnalyzer 產生,預設 VADParams(stop_secs=0.4);兩段時間前後相加。smart-turn 是 kitt-stt 第三個 detector:對 VAD-stop 快照呼叫 Pipecat LocalSmartTurnAnalyzerV3._predict_endpoint,門檻 probability > turn_model.smart-turn.complete_threshold(出廠 0.6),commit reason=model。既有測試檔名中的 smart-turn 場景不一定代表使用此 backend。
skipped / reason=model / reason=vadeot.model.state=skipped 表示沒有模型機率。TurnSense/Smart Turn 的正常 commit 用 reason=model;VAD backend 用 reason=vad。Wire reason 只區分 model-backed 與 VAD,不再區分兩種模型 backend。
KWS / STT / ASRKWS 是喚醒詞偵測;本版用 STT(語音轉文字,也稱 ASR)的 final transcript 做文字比對,尚無聲學 KWS。三種 EOT backend 都仍跑 ASR 與 KWS。
Active / Standby / manualActive/Standby 是共用喚醒狀態;manual 是略過 match_wake 的 KWS method,不等於 Standby,也不是 EOT backend。
VAD / segment / turnVAD 偵測語音起訖;一段 VAD 音訊是 segment,一次送交下游的對話輸入是 turn,可包含多段 segment。VAD stop 不一定立即結束 turn;backend=vad 時,在 KWS 與強制停止之後,正常 stop 會等待 vad.wait_secs,期間的新語音延續同一 turn。
EOT / TurnSense / EotController / SmartTurnDetectorEOT 是 turn 結束判定。TurnSense 提供語尾三分類;SmartTurnDetector 包裝 Pipecat CPU ONNX。detector 只回 TurnSenseResult。KittSttService 選出 TurnOutcome,由 EotController 執行 WAIT/COMMIT/CANCEL。Detector 不送 frame、不跑 KWS、不擁有 deadline。
HR / ITN / s2tHR 是同音替換,配置於 recognizer;ITN 將口語數字正規化,包含 OpenCC s2t(簡轉繁)。interim 只做 s2t,final 做完整 ITN。本輪不改這條路徑。
SW_id / PRD F_id / UXSW_id 是 SW 規格的穩定需求編號;PRD F_id 是產品功能分組;UX 條款補充互動要求。附錄 B 行為欄描述要求,結果欄描述驗證,兩者不等同。本輪新增可切換 EOT backend,並把 SW-OPS-04SW-EOT-01SW-EOT-08 的路徑改成 nested YAML。
fbank / LFR / CMVN聲學前處理的濾波器組特徵/低幀率堆疊/均值變異數正規化。TurnSense 路徑仍依此前處理;VAD backend 不跑這段。
RTF / CER / WER處理時間與音訊時長之比/字元錯誤率/詞錯誤率。本輪未量測,不作效能比較。
掉句 / 餘裕掉句是評測分帶時漏掉輸入句;餘裕是實測值距判定門檻尚有多少空間。兩者出現在 rebase 後繼承的語言政策需求:前者守住評測樣本完整,後者說明英文閂鎖門檻對偶發誤判的容忍空間;本次只補入現行契約,未重跑其登記測試。
SNR / FP32 / bf16訊噪比與浮點精度格式。出現在累積訓練/模型測試;本輪未做 live 精度驗證。
TensorRT / execution provider / sm89模型執行後端與 GPU 架構;RTX 4090 為 sm89(compute capability),TensorRT engine 綁定該架構、不可跨機搬。model.provider 與 turn_model.backend 是不同設定。
MatMulInteger / MatMulNBits / QDQ量化矩陣乘法算子與量化/反量化節點;SW mapping 保留部署相容性檢查,不代表本輪執行量化模型。
歷史驗收 ID附錄 B 的 A1–A6 是 2026-07-29 排程驗收;B1/B2 是 interim/final ITN 拆分;B14-R1、B14-R2、B19-R1、B19-R2、B19-R3 是既有併發測試子項,不是本輪 EOT backend 的新需求。

本輪設定

設計與施工依據: 2026-09-15-eot-backends-design.md2026-09-15-eot-backends.md。 Kickoff baseline 67160famain)。出廠 YAML 依 backend 分組,啟動預設為 vad;selector 可選 turnsensevadsmart-turn。overlay 只改 backend: smart-turn 時 deep-merge 繼承三個區塊。頂層 sibling map 與頂層資產鍵在啟動時失敗。

路徑出廠用途
turn_model.turnsense.wait_secs0.1TurnSense incomplete wait;滑桿覆寫目前 running backend
turn_model.smart-turn.wait_secs0.1Smart Turn incomplete wait
turn_model.turnsense.complete_threshold0.6TurnSense COMPLETE 機率門檻;YAML-only;嚴格大於
turn_model.smart-turn.complete_threshold0.6Smart Turn COMPLETE 機率門檻;YAML-only
turn_model.vad.wait_secs0.1VAD stop 後的 continuation window;與 kitt-web-ui stop debounce 分開
turn_model.turnsense.{key,onnx_file,cmvn_file}turnsense-1.1/model_fp32.onnx/am.mvn換 TurnSense 版本改這裡;deploy.sh 從 nested 區塊讀
本輪不改變的責任

狀態頁的 backend selector、wait slider 與 Debug tools 屬於 runtime control;操作與觀測行為見§2

🔀§1 · EOT 判定路徑

VAD stop 到 commit

EOT backend Speech path VAD stop 之後,turn_model.backend 選擇 TurnSense、Smart Turn 或 VAD 判定來源,三條路徑最後都交給同一個 EotController。 Speech path KittSttService · turn_model.backend 決定 VAD stop 後的判定來源 VAD stop + ASR final 建立 segment / turn-tail snapshot turn_model.backend? turnsense smart-turn vad TurnSenseDetector analyze() reason=model SmartTurnDetector analyze() reason=model VadDetector WAIT wait_secs reason=vad EotController.apply 共用 WAIT / COMMIT 與 eot metadata contract
圖 C1 · Speech path:turn_model.backend 只替換 VAD stop 後的判定來源;三種 backend 最後都交給同一個 EotController。
架構圖解

三種 backend 的 EOT 輸出契約

Backend 只改判定來源,沒有改下游 Frame 或 metadata 外形。每個 segment 都使用同一組 eot.model keys;尚未 commit 時 eot.decision=null,commit carrier 才填入同一組 decision keys。

Shared EOT wire contract TurnSense reason=model Smart Turn reason=model VAD reason=vad TranscriptionFrame / ZonalUserStoppedSpeakingFrame 相同 carrier 類型與 metadata.eot namespace eot model {state, p_complete, p_incomplete, p_invalid, wait_ms} 觀測用 decision null | {action: commit, reason, forced, wait_ms} action == commit 且 reason 是非空字串? no CONTINUE 保持 turn open yes END TURN trigger_user_turn_stopped() model / vad 都通過同一個 consumer gate
圖 C2 · 三種 backend 共用同一個 eot envelope;web-ui 依 action 與非空 reason 結束 turn。
turn_model.backendeot.model 差異正常 commit 的 eot.decision.reasonFrame contract
turnsensestate=complete/incomplete/invalid/error;模型成功時三個 probability 有值modelTranscriptionFrame 或無 final 時的 ZonalUserStoppedSpeakingFrame;WAIT/COMMIT 與 metadata keys 相同
smart-turnstate=complete/incomplete/error;只有 p_complete 有值model
vadstate=skipped;probability 全為 nullwait_ms=vad.wait_secsvad
kitt-web-ui 相容性:smart-turnvad 不會讓 stop strategy 失敗

SttEotUserTurnStopStrategy 接受 TranscriptionFrameUserStoppedSpeakingFrame,並要求 decision.action == "commit"reason 是非空字串。reason=modelreason=vad 都會正常呼叫 trigger_user_turn_stopped()。來源:kitt-web-ui backend/src/kitt/agent/turn_strategy.py:83-103

UI observer 只讀 eot.model.p_completestatewait_ms,不讀 decision.reason。VAD 的 p_complete=null 只代表不送 turn_prediction 顯示,不影響 stop strategy 關閉 turn。來源:kitt-web-ui backend/src/kitt/agent/observers.py:149-174

reason 收斂為判定類型:TurnSense 與 Smart Turn 都是 model-backed,使用 model;VAD 沒有呼叫模型,使用 vad。若要分辨兩種模型 backend,讀取行程設定與 server log,不再從 wire reason 判斷。

⚙️§2 · Runtime controls

狀態頁以單一「儲存設定」套用 backend 與目前 backend 的 wait_secs。backend 只在所有 turn idle 時切換,忙碌時回 409;成功後立即影響既有與新 connection,重啟回 YAML。設定是 process-wide,不依車;complete_threshold 維持 YAML-only。

Status page debug tools

操作實作行為邊界
Debug audio: on/off動態套用到現存 connection;新 connection 讀同一個 process runtime 值。開啟才累積後續 PCM,關閉只停止新增資料;切換當下不寫檔。各 connection 仍只在 disconnect / cleanup() 輸出自己的 WAV;先開後關的既有 buffer 仍會寫出。上限沿用 audio.dump_max_duration_secs=180。來源:src/server.py:63-68,108-119,253-266src/kitt_stt_service.py:319-325,384-454,566-567
下載最新 traceGET <base>/debug/trace/latest 回傳 TraceManager 最近一次週期 flush 已完成原子寫入的 trace_*.json.gz;沒有已 flush 檔案時回 404。按鈕不主動 flush,不回傳仍在記憶體中的 event。來源:src/server.py:269-282utils/perfetto_trace.py:320-370
開啟 Perfetto以新分頁開啟 https://ui.perfetto.dev/瀏覽器不自動上傳 trace;操作者自行把剛下載的 gzip 拖入 Perfetto UI。

🧪§3 · 驗證與已知缺口

2026-09-15 · kickoff baseline 67160fa,最終 HEAD 基於 origin/main 3c0282d7。本次出廠設定調校以 tests/test_turn_backend_config.pytests/test_status_page_settings.py 驗證(60 passed);kitt-web-ui 的 VAD runtime settings/rejection selected tests 為 14 passed。主機 GPU 以獨立 port 啟動目前設定:log 顯示 [EOT] backend=vad、TurnSense preload skipped、SenseVoice ready,HTTP status page 回傳 data-current="vad"。以目前 v3b 模型的隔離 CUDA WebSocket 重播另有 41 passed。TurnSense runtime 為 15 passed,完整 offline 為 1661 passed、33 skipped;current-default v3b CUDA live 為 12 passed、3 failed。整合結果完整命令如下。

本輪核心契約

PRD FeatureSW_idBehaviourExecutable evidenceResult
STT_EXTRASW-OPS-04YAML 依 backend 分組;滑桿可改目前 running backend 的 wait,reset 只清除該 backend override;全行程共用、不依車test_turn_backend_config.py · test_policy.py · test_smart_turn_detector.py · test_status_page_settings.py✓ PASS
F_3.2 / UX§3.1SW-EOT-01路徑改為 turn_model.<backend>.wait_secscomplete_threshold;COMPLETE 立即 commit;出廠 VAD 路徑 400+100=500test_turn_backend_config.py · test_eot_kws_precedence.py · test_policy.py✓ PASS
STT_EXTRA / F_3.2SW-EOT-08smart-turn 讀 turn_model.smart-turn;overlay 只改 backend 即繼承三個區塊;狀態頁只在所有 turn idle 時切換,失敗保留原 backendtest_turn_backend_config.py · test_smart_turn_detector.py · test_eot_kws_precedence.py · test_status_page_settings.py · test_runtime.py✓ PASS
驗證範圍結果
設定/status/核心 EOT contractPASS · current-default config/status 60 passed、TurnSense runtime 15 passed、startup contract 2 passed
完整 offline regression1661 passed, 33 skipped · tests(排除 live/sherpa-onnx)於 host test environment 完整結束
v3b CUDA live 已通過範圍41 passed · 拒識、退出詞、wake/rate、主動問候/latency 與車載 hotword;主機 GPU startup/status 亦通過
current-default v3b targeted live12 passed, 3 failed · SW-W-10SW-B-03 與未註冊的 SW-TURN-04;詳見測試結果
量測 artifactNOT-VERIFIED · SW-LANG-15SW-LANG-17 缺 GPU+TTS 量測產物
未驗證平台NOT-VERIFIED · k8s/Orin/Thor live

已知缺口

§4 · 判定與後續

目前結論

nested 設定、三種 backend、idle-only process-wide 切換與共用 Frame contract 已由 focused/offline/startup 驗證。current-default live 仍有三個明確失敗與兩組未備量測 artifact,因此不將整體 live 宣稱為通過。Artifact 已 PUBLISHEDkitt-stt-eot-backend-switch.pages.dev

🏗️附錄 A · STT SW Stack 架構與資料流

正文 Speech path 說明 turn_model.backend=turnsense / vad / smart-turn 的判定路徑;本節呈現共用系統架構與 Frame flow。圖面來源:2026-09-04-itn-latency-optimization.html 的既有架構圖;標籤與程式引用以目前 branch 為準。相對 main,本輪新增三種 EOT backend 與 nested YAML;ASR/KWS/EotController 仍共用。

STT SW Stack

STT SW stack · 單一 offline SenseVoice;interim 與 final 共用 stream kitt-core FrameProcessorServer msgpack / WebSocket KittSttService(src/kitt_stt_service.py) per-connection processor · per-user UserSession(src/session/) interim 路徑(偽串流) 反應式排程決定「何時重解」 持續 stream 只餵新增音訊 後處理只做 s2t final 路徑(權威) 整段重新解碼 沿用 interim 的 stream,只補尾巴 HR(recognizer)→ ITN(詞庫) 已抽好的 fbank BatchASRManager(src/asr/batch_asr_manager.py) 一個 recognizer + 一把鎖 · SherpaOnnxModelCache 單例(preload) 喚醒與 turn 控制 KWS · EotController · turn_model.backend offline SenseVoice(sherpa-onnx,CUDA) sherpa-onnx-sense-voice-kitt-wake-exit-vocab-lora-v3b · FP32
Figure 1 · STT SW stack — 本輪新增 turn_model.backend 選 EOT 判定來源;interim/final 仍共用單一 SenseVoice,EotController 仍是唯一 commit 執行者。

create_stt_processor 每連線建立 KittSttService;UserSessionManager 以 user_id 分開音訊/turn,同一連線共用 KWSStreamManager。preload 時 SherpaOnnxModelCache 跨連線共用 recognizer 與 lock;模型由 DVC 取得,部署沿用 deploy.sh → Skaffold → K8s/Orin/Thor。來源:src/server.py:67src/session/user_session_manager.py:10src/model_cache.py:9utils/model_manager.py:154

Frame 處理邏輯

主流程(實線) AudioRawFrame / VAD start streaming_interim 且可輸出?排程與 Active / bypass gate yes InterimTranscriptionFrame no VAD stop · segment / turn-tail snapshot final ASR ∥ selected turn detectorturnsense / smart-turn / vad Active 或 wake / bypass 命中?輸出權限與 KWS arbitration noCANCEL yes exit word 命中? yesagent_standby+ CANCEL no 非空 final 且可輸出?_should_emit_final yesTranscriptionFrame no wake-only? yesTTSSpeakFrame no EotController outcome?唯一 commit owner CANCELdrop pending turn WAIT保持 turn + EOT deadlinenext VAD start 可取消 COMMIT commit type?reason metadata model / vadfinal 或 Zonal stop另含 timeout / forced reason 非同步/控制事件(虛線)BotStarted / BotStoppedInterruption(agent_active / standby)STTUpdateSettingsFrameEndFrameKWS idle / watchdog timeoutper-user EOT deadline改變 state、設定或後續 commit ◇ 決策 ▭ 處理/Frame 實線=segment 主流程 虛線=可非同步改變狀態或觸發 deadline commit
Figure 2 · Frame 處理流程 — 圖中明確分開輸出權限、exit、wake-only 與 EOT outcome;WAIT、CANCEL、COMMIT 及 model/timeout/forced 類型皆由 EotController 收斂,旁支事件以虛線標示影響點。

EOT 與獨立事件

EotController 結果輸出與狀態後續事件位置
WAIT有 final_frame 就先送出,不附 commit 的 eot.decision;turn 保持開啟。排程 per-user EOT deadline;新 VAD start 取消 deadline 並延續 turn。deadline 到期且仍有效,改以 explicit stop commit。src/eot.py:186
src/eot.py:258
COMMIT只選一個載體:有 final 用 TranscriptionFrame;無 final 用 ZonalUserStoppedSpeakingFrame。載體附 eot.decision關閉該 user 的 turn;若 room Active,重設共用 wake idle timer。src/eot.py:221
CANCEL丟棄連線內所有 pending turn;EotController 本身不發 commit frame。呼叫端已送出或收到 agent_standby,供下游丟棄累積內容。src/eot.py:210

Frame I/O 契約 · 以 KittSttService 為邊界

IN/OUT 表示收/送,不等同於 FrameDirection。Web UI 控制通常從 DOWNSTREAM 收入,LM standby 從 UPSTREAM 收入;服務產生的文字與狀態通常送往 DOWNSTREAM。下表列本服務特別處理或產生的 Frame,未列出的基底 passthrough 不新增本服務的行為契約。_emit_seat_load 只寫 trace,不送 ASRMetadataFrame(src/kitt_stt_service.py:1228)。

Table 1 · Frame I/O contract — KittSttService 收送的 Frame;EOT commit 仍由 EotController 送出,backend 只改判定來源與 reason
Frame方向觸發 case動作/條件Metadata/payload位置(file:line)
StartFrameINpipeline 啟動呼叫基底 start,送模型/主機 metadata;模型與 session manager 已在 service 建構時準備。model_name、system infosrc/kitt_stt_service.py:325
AudioRawFrameINPCM chunk 到達依 user_id 選 session;正常 chunk 更新 preroll/turn tail,utterance_active 時累積 segment 並評估 interim 排程。帶 metadata["max_utterance_duration"] 的 chunk 不進 segment buffer、不解碼,仍交由基底往下游傳給 VAD。audiosample_ratenum_channelsuser_id、可選 max_utterance_durationsrc/kitt_stt_service.py:616
src/kitt_stt_service.py:644
src/kitt_stt_service.py:1173
src/kitt_stt_service.py:1200
VADUserStartedSpeakingFrameINVAD 開始建立或延續 per-user turn,取消舊 EOT deadline;以 preroll 起始 segment。user_id、start timingsrc/kitt_stt_service.py:683
VADUserStoppedSpeakingFrameINVAD 停止快照 segment/turn tail,交棒 stream,並行 final ASR 與選定 EOT detector。已知 eot_force_reason 可要求 force-complete;不直接代表 EOT commit。backend=vad 略過 TurnSense 推論。user_ideot_force_reasonsrc/kitt_stt_service.py:785
src/kitt_stt_service.py:1513
UserStartedSpeakingFrameIN上游 legacy semantic start本服務不以此建立或重開 turn;原 frame 交由基底路徑傳遞。原 frame payload(passthrough)src/kitt_stt_service.py:998
UserStoppedSpeakingFrameIN上游 legacy semantic stop本服務不以此結束 turn;原 frame 交由基底路徑傳遞,STT 自行決定 EOT。原 frame payload(passthrough)src/kitt_stt_service.py:998
BotStartedSpeakingFrameINTTS 開始播放Active 改掛 bot_speaking_watchdog_secs;Standby 取消 shared timer。無新增 payload;更新 shared timersrc/kitt_stt_service.py:918
BotStoppedSpeakingFrameINTTS 播放結束Active 且無 user 正在 utterance 中時,重設 shared idle timeout。無新增 payload;更新 shared timersrc/kitt_stt_service.py:950
InterruptionFrameINreason=agent_active;Web UI 手動 Active啟用共用 wake state、重設 idle timer;各 session 從當下重啟辨識,丟棄切換前 segment。stt/manual 都接受此控制。reason=agent_activeuser_idsrc/kitt_stt_service.py:1035
src/kitt_stt_service.py:662
InterruptionFrameINreason=agent_standby;Web UI(DOWNSTREAM)清除共用 wake state,結束所有 pending turn;交由基底傳遞。reason=agent_standbyuser_idsrc/kitt_stt_service.py:1009
InterruptionFrameINreason=agent_standby;LM(UPSTREAM)同樣取消 room-wide pending turn,另向 DOWNSTREAM relay 讓 Web UI 同步。reason=agent_standbyuser_idsrc/kitt_stt_service.py:1024
STTUpdateSettingsFrameIN設定 delta由基底設定路徑呼叫 _update_settings;依 delta 更新支援的 wake/bypass 設定,不代表所有 YAML 欄位都可熱切換。支援欄位的 settings deltasrc/kitt_stt_service.py:1188
EndFrameINpipeline 收尾取消 idle timer;仍在 utterance 的 session 可跑 final,使用 apply_eot=false,不發 EOT commit。之後丟棄 pending turn,交由基底收尾。無新增 payloadsrc/kitt_stt_service.py:1044
ASRMetadataFrameOUTstart 完成送 model_name 與 CPU/GPU/RAM 資訊;不是文字或 EOT 載體。model_name、CPU/GPU/RAMsrc/kitt_stt_service.py:325
InterimTranscriptionFrameOUTinterim decode 有更新文字features.streaming_interim=true 且 _should_emit_interim 通過;須同時符合排程條件。後處理只做 s2t,不作 final 的權威輸出。textuser_id、timestamp、languagesrc/kitt_stt_service.py:1253
src/kitt_stt_service.py:1289
InterimTranscriptionFrameOUT無可送 final,但 room 仍 Active送空文字清除 interim;Standby 清除顯示改由 agent_standby 通知處理。text=""user_id、timestampsrc/kitt_stt_service.py:1776
TranscriptionFrameOUT非空 final,_should_emit_final 通過,且非 exit帶 eot.model;WAIT 先送 segment final 並等待,COMMIT 在同一 frame 附 eot.decision。EndFrame 收尾例外不套用 EotController。textuser_id、timestamp、language、metrics、eot.modeleot.decisionsrc/kitt_stt_service.py:1692
src/eot.py:186
ZonalUserStoppedSpeakingFrameOUTCOMMIT 且沒有 final_frame如有效 EOT deadline 到期、wake-only、ASR error;以 explicit stop 承載 eot.decision,不是每個 final 都加送 stop。user_ideot.modeleot.decision、reasonsrc/eot.py:221
InterruptionFrameOUTSTT wake 命中/Active re-wakereason=agent_active;喚醒或重申共用 Active 狀態。manual 不以 transcript 觸發這條路徑。reason=agent_activeuser_idsrc/kitt_stt_service.py:594
src/kitt_stt_service.py:594
InterruptionFrameOUTexit word 命中reason=agent_standby;清除 wake state 並取消 room-wide pending turn,不送 exit transcript。reason=agent_standbyuser_idsrc/kitt_stt_service.py:1408
src/kitt_stt_service.py:1426
InterruptionFrameOUTStandby 且無可送 finalreason=agent_standby;通常是 wake/bypass 無匹配,CANCEL / no_wake 丟棄 pending turn。reason=agent_standbyuser_idsrc/kitt_stt_service.py:1785
InterruptionFrameOUT共用 wake idle/watchdog timeout 到期reason=agent_standby;callback 先確認無 user 正在 speaking/finalizing。這個 timer 與 per-user EOT deadline 不同。reason=agent_standbysrc/kitt_stt_service.py:482
InterruptionFrameOUT轉送 LM 的 UPSTREAM agent_standby向 DOWNSTREAM relay 同一 frame,讓 Web UI 回 Standby。reason=agent_standbyuser_idsrc/kitt_stt_service.py:1024
TTSSpeakFrameOUT符合 Standby wake-only 全部條件送 wake.response_text,append_to_context=false;COMMIT / wake_only 另以 explicit stop 結束控制 turn。textappend_to_context=falsesrc/kitt_stt_service.py:1800
src/kitt_stt_service.py:1811

附錄 B · 完整 SW ↔ test mapping

本附錄保留目前 SW 規格的完整 traceability,供審閱與報告契約使用;不是首頁的結果摘要。標示「2026-09-15 v3b retest」者以本輪結果為準,其餘是現行契約的既有驗證資訊;目前結論見§3,命令見重現方式

Feature 總覽

下表計算需求列,不是 pytest case 數;同一需求可能跨 Feature,同一測試也可涵蓋多個需求,因此各組不能相加當成測試總數。執行命令見重現方式

PRD F_idFeature有 STT SW_id?需求列數2026-09-14 驗證狀態
F_2One-shot(喚醒詞 + 指令連說)有;見分組細表12 個需求列PASS 10 / FAIL 2
F_3聆聽等待 / 智慧斷句 / 解除喚醒時機有;見分組細表26 個需求列PASS 18 / FAIL 6 / PARTIAL 2
F_4.3語音打斷 Barge-in / 解除喚醒詞有;見分組細表3 個需求列FAIL 1 / PASS 2
UX 專有PRD 無對應 F_id有;見分組細表3 個需求列FAIL 2 / PASS 1
STT_EXTRAPRD 未描述、STT 已具備有;見分組細表76 個需求列PASS 62 / PARTIAL 1 / — 10 / FAIL 2 / SKIP 1
F_1多音區識別/控制下游/OUT_OF_SCOPE未執行下游驗收
F_4.1背景併行下游/OUT_OF_SCOPE未執行下游驗收
F_4.2後令壓前令下游/OUT_OF_SCOPE未執行下游驗收
F_5連續指令下游/OUT_OF_SCOPE未執行下游驗收
F_6上下文理解下游/OUT_OF_SCOPE未執行下游驗收
F_2 · One-shot(喚醒詞 + 指令連說)
SW_id行為(規格)測試結果
SW-W-03Standby→Active(喚醒詞在句首):喚醒詞+指令連說,辨識其後之指令;喚醒詞不得出現在 final,且不得因前期聽歪而誤顯示喚醒詞test_wake.py✓ PASS
SW-W-04喚醒詞 <停頓> 指令 亦能喚醒,final 無喚醒詞test_wake.py✓ PASS
SW-W-05喚醒詞不在句首不觸發test_standby.py✓ PASS
SW-W-08Active 期間:喚醒後任何話都辨識並往後送;句首喚醒詞於 final 過濾、非句首喚醒詞保留;interim 出現喚醒詞可接受test_active.py✓ PASS
SW-W-09Active 句首喚醒詞 + smart-turn,仍不顯示喚醒詞test_active.py✓ PASS
SW-W-13Standby 第一句未命中 → turn 關閉,故第二句句首的喚醒詞是新 turn 的句首正常觸發喚醒test_smart_turn_wake.py✓ PASS
SW-W-14英文出廠喚醒詞 Hi Foxtron 須觸發(大小寫無關),且喚醒詞不得留在 finaltest_wake_factory_words.py✓ PASS
SW-W-15第一次主動問候的時機UX§1.3.2):喚醒成立後須送出問候的 TTSSpeakFrame,STT 側上界 500ms。狀態頁可 process-wide 儲存 ui_handler_p99_secstts_handler_p99_secs(秒、三位小數、最小 0、無上限),下一次喚醒套用;兩個 handler legs 也用於第二次問候,非 per-car 設定。test_active_greeting.py · test_wake_latency.py · tests/test_greeting_settings.py✓ PASS
2026-09-15 v3b retest;handler settings 12 passed
SW-W-16喚醒成立後 <550ms 內有人說話 → 不觸發第一次主動問候。窗的起點是 InterruptionFrame(agent_active) 送出的那一刻——下游能觀測到的「喚醒成功」就是這個 frametest_active_greeting.py✓ PASS
2026-09-15 v3b retest
SW-W-17喚醒詞+指令連說須觸發喚醒:連讀成一句時同樣要送出 InterruptionFrame(agent_active),不因喚醒詞未單獨成句而不通知下游test_active_greeting.py✓ PASS
SW-W-18喚醒詞+指令連說不得出現問候語:使用者在喚醒當下就把指令說完了,等同已在抑制窗內說過話test_active_greeting.py✓ PASS
SW-WM-01喚醒詞比對與剝除:latin 大小寫無關、分隔字元先剝除、喚醒詞後可直接接指令剝除喚醒詞後指令完整保留tests/test_wake_matcher.py✓ PASS
F_3 · 聆聽等待 / 智慧斷句 / 解除喚醒時機
SW_id行為(規格)測試結果
SW-W-01非喚醒(Standby):說喚醒詞以外任何句子,STT 不出任何文字、不往後送;相似音喚醒詞不得觸發test_standby.py✓ PASS
SW-W-02非喚醒下說無關長語音,需立即 smart-turn stoptest_standby.py✓ PASS
SW-W-07smart-turn 斷句不因 VAD 誤判過度斷句test_smart_turn_wake.py✓ PASS
SW-W-09Active 句首喚醒詞 + smart-turn,仍不顯示喚醒詞test_active.py✓ PASS
SW-W-10Active 期間 smart-turn 功能正常(跨段黏合)test_active.py✗ FAIL
SW-W-11Active 第二句開頭喚醒詞不過濾(正常顯示)test_smart_turn_wake.py✓ PASS
SW-W-12斷句前後不得因雜音產生語助詞(嗯/啊/喔)test_active.py✓ PASS
SW-W-13Standby 第一句未命中 → turn 關閉,故第二句句首的喚醒詞是新 turn 的句首正常觸發喚醒test_smart_turn_wake.py✓ PASS
SW-WM-02alias 只能買回實測誤聽,不得擴大誤觸發:全中文 alias 與 canonical 距離 ≤1 字;普通語句不得誤觸發tests/test_wake_matcher.py✓ PASS
SW-B-03Prefix + smart-turn 正常合併test_bypass_prefix.py✗ FAIL
SW-B-08特定指令於 smart-turn 第一句句首觸發,只傳「關閉空調」test_bypass_cmd.py✓ PASS
SW-B-10Standby 第一句未命中 → turn 關閉,故第二句句首的 Prefix 是新 turn 的句首正常觸發test_smart_turn_bypass.py✓ PASS
SW-B-11同上,第二句句首的特定指令正常觸發test_smart_turn_bypass.py✓ PASS
SW-X-01Timeout 15s:TTS 說完起算,15s 無人說話 → 回 Standbytest_timeout.py✓ PASS
SW-X-02重新喚醒後 timeout 重新計時test_timeout.py✓ PASS
SW-X-05第二次主動問候的時機UX§1.3.3):喚醒起算 =10秒 任何座位都沒說話 → 播「你好,需要我為你做什麼呢?」test_active_greeting.py · test_remaining_ux_gaps.py✓ PASS
2026-09-15 v3b retest
SW-X-0610 秒內有人說話 → 不觸發第二次主動問候test_active_greeting.py✓ PASS
2026-09-15 v3b retest
SW-X-07退出聆聽路徑一UX§1.3.4):第二次問候播完後=5秒 無人說話 → 回 Standbytest_active_greeting.py✓ PASS
2026-09-15 v3b retest
SW-TURN-01一個 turn 內只有第一個 VAD segment 是 turn head;其餘為續段,不跑喚醒/免喚醒偵測tests/test_turn_arbitration.py✓ PASS
SW-TURN-02續段的送出權依 bypass 種類授權:prefix 授予(SW-B-03 需要),exact 不授予SW-B-07 的語意)tests/test_turn_arbitration.py✓ PASS
SW-TURN-03新 turn head 清除上一個 turn 的指令送出權,續段則保留tests/test_turn_arbitration.py✓ PASS
SW-EOT-01STT 是唯一 EOT authority:每個 VAD stop 由目前 backend 判定。TurnSense/Smart Turn 評分 turn tail;COMPLETEp_complete > turn_model.<backend>.complete_threshold(兩個 scoring backend 出廠皆為 0.6),然後立即 commit。模型的 INCOMPLETEINVALID 與 VAD backend 的正常 stop 都等該 backend 的 wait_secs,期間有新語音則延續同一 turn。立即完成的非空 final 與 eot.decision 在同一個 TranscriptionFrametests/test_eot_state.pytests/turnsense/test_policy.pytests/test_eot_kws_precedence.pytests/test_turn_backend_config.py · livetest_remaining_ux_gaps.py部分覆蓋
SW-EOT-02KWS 事件(standby 無匹配/離開詞/agent_standby/連線結束)一律丟棄 turn,優先於強制停止與模型判定;被丟棄的 turn 永不進 LLM。ASR final 失敗不屬 KWS cancel:該段不送 transcription,改以 asr_error 結束 turn離線:tests/test_eot_kws_precedence.py · live(wire contract)test_forced_stop.py✓ PASS
SW-EOT-05kitt-web-ui 在 UX§8.2 超限後標記 metadata["max_utterance_duration"];STT 對後續音訊不進 buffer、不做 interim 解碼,但仍往下游傳給 VAD。VAD STOP 時以標記前的音訊出 final,是否拒識由 kitt-web-ui 決定。tests/test_max_utterance_audio.py✓ PASS
SW-EOT-06已廢止:長文字拒識改由 kitt-web-ui RejectionGate 負責N/A
SW-EOT-07已廢止:隨拒識門檻移出 STTN/A
F_4.3 · 語音打斷 Barge-in / 解除喚醒詞
SW_id行為(規格)測試結果
SW-X-04說解除喚醒詞 → 切 StandbyUX§2.1 指定 13 個:謝謝/再見/退出/退下/滾蛋/滾/Thank you/Thanks/Goodbye/ByeBye/Dismiss/Quiet/Shut up)livetest_exit_words.py · test_btn_control.py · 離線比對層:tests/test_exit_word.py✓ PASS
2026-09-15 v3b retest;13 詞各 10 次
SW-WM-03退出詞 alias 必須錨定到已設定的退出詞:alias 群組的 canonical 不在 exit_words 內則整組忽略tests/test_wake_matcher.py✓ PASS
SW-EOT-02KWS 事件(standby 無匹配/離開詞/agent_standby/連線結束)一律丟棄 turn,優先於強制停止與模型判定;被丟棄的 turn 永不進 LLM。ASR final 失敗不屬 KWS cancel:該段不送 transcription,改以 asr_error 結束 turn離線:tests/test_eot_kws_precedence.py · live(wire contract)test_forced_stop.py✓ PASS
UX 專有 · PRD 無對應 F_id
SW_id行為(規格)測試結果
SW-X-08拒識之後必須維持聆聽UX§10.2):越過 UX§8.2 門檻的語句丟棄不往下送,但不得因此離開 Active——拒識是「裝沒聽到」,不是「結束這一輪」test_rejection_thresholds.py✓ PASS
2026-09-15 v3b retest;>15 秒/>50 字
SW-LANG-01整句英文指令須被辨識,且不需切換任何語言設定test_bilingual.py✓ PASS
SW-LANG-02同一句內中英夾雜時兩種文字都要保留:辨識器須在句中換文字系統,不是為整句選一種語言test_bilingual.py✓ PASS
2026-09-15 v3b live 重跑
STT_EXTRA · PRD 未描述、STT 已具備
SW_id行為(規格)測試結果
SW-W-19kws.method.name=stt 執行 transcript 喚醒;manual 跳過 match_wakestrip_wake_prefixtest_kws_method_policy.py · test_kws_method_manual.py✓ PASS
SW-B-12manual 保留 match_bypass;已驗證 current-segment exact/prefix、非句首不觸發與 prefix 跨段 continuationtest_kws_method_policy.py · test_kws_method_manual.py✓ PASS
SW-UI-07stt / manual 共用 InterruptionFrame(agent_active/agent_standby) 控制test_kws_method_policy.py · test_kws_method_manual.py✓ PASS
SW-CFG-03kws.method.name 預設 stt、只接受 stt / manual;舊設定鍵拒絕啟動test_kws_method_config.py · test_status_page_settings.py✓ PASS
SW-B-01Prefix(default「冷氣溫度*」「導航到*」):非喚醒下前綴符合即整句往後送test_bypass_prefix.py✓ PASS
SW-B-02Prefix 不在句首不觸發test_bypass_prefix.py✓ PASS
SW-B-05特定指令(exact,default「關閉空調」):非喚醒下整句相符即往後送test_bypass_cmd.py✓ PASS
SW-B-06特定指令 不在句首不觸發test_bypass_cmd.py✓ PASS
SW-B-07特定指令句中夾雜其他話不觸發test_bypass_cmd.py✓ PASS
SW-X-03按 WebUI 綠色 active 按鈕 → 回 Standbytest_btn_control.py✓ PASS
SW-UI-01WebUI Standby 按鈕可啟動喚醒(灰→綠),之後任何話都辨識往後送test_btn_control.py✓ PASS
SW-UI-02每次 connect 後狀態不壞:Connect→喚醒→disconnect ×5 都正常test_btn_control.py✓ PASS
SW-UI-03頻繁切換穩定:連點喚醒按鈕 ≥10 次後,切換仍正常test_btn_control.py✓ PASS
SW-UI-04連點後於 Standby 說「嗨鴻華」仍能觸發test_btn_control.py✓ PASS
SW-UI-05講話中瞬間切 Standby,WebUI 不得有灰字卡住test_btn_control.py✓ PASS
SW-UI-06講話中瞬間切靜音(mic),WebUI 不得卡住STT_EXTRA
SW-MU-01同車多 user 喚醒狀態同步(按鈕同開同關)—(歸屬:web-ui)
SW-MU-02多 user 按鈕/語音喚醒穩定度(交互點擊 ≥5 次仍正常)—(歸屬:web-ui)
SW-MU-03UserA 講話中(final 未送),UserB 切 standby → STT 立即停聽、WebUI 無卡字—(歸屬:web-ui + STT)
SW-MU-04新 connect 的 user 同步當前喚醒狀態—(歸屬:web-ui)
SW-MU-05開關 mic 不影響喚醒狀態同步—(歸屬:web-ui)
SW-MU-06多 user timeout 穩定:長句期間不得誤切 standby—(歸屬:web-ui + STT)
SW-MU-07自定義喚醒詞在多 user 間同步(含刪除同步)—(歸屬:web-ui)
SW-MU-08多車隔離:Car1 喚醒/自定義詞不影響 Car2—(歸屬:web-ui(STT 以 ?car= 分段))
SW-CFG-01可經 WebUI 客製化喚醒詞/免喚醒詞,行為與原廠一致test_stt_config.py✓ PASS
SW-CFG-02刪除客製化詞後不再觸發test_stt_config.py✓ PASS
SW-PERF-01連續長句(≥90 秒)講話期間,interim 重新解碼排程不得因緩衝區持續變長而失控卡住test_audio_stress.py✓ PASS
SW-PERF-02反應式 interim 重解碼排程的設定解析:間隔由上一次解碼延遲 × 安全係數決定,並受下限約束tests/test_audio_cfg.py✓ PASS
SW-PERF-03整車多人同時講話(1 條連線、4 個座位帶不同 user_id):interim 重新解碼排程不得因共用辨識器的鎖爭用而卡住每個座位都必須拿到自己的 final,且不得有非本車座位的 user_id(不串話)tests/functional/live/test_multiuser_concurrency.py(5 條)|離線機制測試 tests/test_interim_concurrency.py(13 條)、tests/test_itn_off_frame_path.py(3 條)✓ PASS
SW-PERF-04喚醒通知須在 200ms 內送出UX§1.3.1):UI 動效由此 frame 觸發test_wake_latency.py✓ PASS
2026-09-15 v3b retest
SW-PERF-05喚醒率回歸下限 ≥90%UX§1.1test_wake_rate.py✓ PASS
SW-EOT-03語尾模型的音訊前處理必須與訓練時逐位元相同(fbank 80 → LFR 7/6 → CMVN,裁到最後 8 秒),否則三分類機率不成立tests/turnsense/test_frontend.py✓ PASS
SW-EOT-04語尾模型只在 CUDA 上執行(與 SW-OPS-06 同一原則,見 kitt-stt-sw-spec.md §10):provider 未生效時伺服器拒絕啟動而非降級到 CPU;行程內單例、推論序列化且不佔用 event looptests/turnsense/test_runtime.py✓ PASS
SW-EOT-08可切換 EOT backend(2026-09-14):turn_model.backend 是啟動預設;狀態頁可在所有 connection 的 turn idle 時 process-wide 切換,busy 回 409,目標 detector 準備失敗保留原 backend,既有與新 connection 同步套用,重啟回 YAML。vad 不 preload/不推論 TurnSense,正常 VAD stop 等 turn_model.vad.wait_secs 後 commit reason=vadsmart-turn 不 preload TurnSense,對 VAD-stop 快照呼叫 _predict_endpointp > turn_model.smart-turn.complete_threshold(出廠 0.6)則 COMPLETE 並立即 commit reason=model。TurnSense/Smart Turn 都是 model-backed,wire reason 共用 model。非法值、enabled、頂層 turn: 與頂層 sibling map 拒絕啟動。缺 runtime 且 active backend 仍為 turnsense 時不得偽裝成 reason=vadtests/test_turn_backend_config.pytests/test_smart_turn_detector.pytests/test_eot_kws_precedence.pytests/turnsense/test_runtime.pytests/test_status_page_settings.py✓ PASS
SW-ITN-01final 的 ITN:中文數字正規化 + 繁簡轉換tests/test_text_converter.py✓ PASS
SW-ITN-02interim 與 final 的 ITN 刻意不同:interim 只做 s2t(),final 做完整 itn();故兩者在數字上可以不同,這是設計不是 bugtests/test_batch_asr_manager_itn_split.py✓ PASS
SW-ITN-03「這個數字字元是不是數字」由詞庫決定而非執行期斷詞器;緊鄰受保護詞的真數字仍必須轉換;執行期不得持有斷詞器(jieba 不在 runtime 依賴內)tests/test_itn_lexicon.py(43,offline)✓ PASS
SW-ITN-04整數與小數(含正負號)轉阿拉伯數字:1負十-10負三點一四一六九九九九九-3.141699999test_text_converter.py::test_allpytest -k "G1- or G2- or G12-"✓ PASS
SW-ITN-05百分比(含正負號)轉阿拉伯數字:百分之一1%百分之負零點五-0.5%test_text_converter.py::test_allpytest -k G3-✓ PASS
SW-ITN-06分數(含正負號)轉阿拉伯數字:二分之一1/2負三分之一-1/3test_text_converter.py::test_allpytest -k G4-✓ PASS
SW-ITN-07日期/時間:數字轉阿拉伯數字,單位詞維持中文一月五號1月5號十一點五十九分五十九秒11點59分59秒test_text_converter.py::test_allpytest -k "G5- or G8-"✓ PASS
SW-ITN-08程度副詞維持全中文、不得數字化:一點點一些十分滿意 等 40+ 慣用語test_text_converter.py::test_allpytest -k G7-✓ PASS
SW-ITN-09完整車內出貨指令語料 495 列、29 個產品功能分類,逐句 100% 正確;分類本身不得靜默缺漏test_itn_lexicon.py::test_every_incar_command_normalises_as_adjudicatedtest_every_product_category_is_represented✓ PASS
SW-OPS-01模型解析:get_local_model_path 維持純函式(不觸發下載);ensure_local_model_path 只在 bundle 真的缺失時 pull、且只 pull 自己那個 .dvc;失敗訊息必須指名該跑的指令tests/test_model_manager.py✓ PASS
SW-OPS-02出貨模型的五處必須一致config-basic.yamlmodel.keyutils/model_manager.py 註冊表、三個 Dockerfile*COPYdeploy.shMODEL_DVC_FILES.dockerignore 的 build context 白名單tests/test_augment.py✓ PASS
SW-OPS-03Per-turn debug log 檢索端點GET /debug/logs,2026-08-20):features.enable_debug_log_endpoint 關閉時回 404;開啟時可依 trace_id(來自 WS 握手 ?trace_id=,經 logger.contextualize 標記,比照既有 ?car= 機制)與時間窗(since_ms/until_ms/minutes)篩選 in-memory ring buffer(debug.debug_log_buffer_lines 筆數上限);零筆符合回 200 而非錯誤tests/test_debug_log_buffer.pytests/test_debug_log_config.pytests/test_debug_log_endpoint.pytests/functional/live/test_debug_logs.py✓ PASS
SW-OPS-04Per-backend EOT 等待與 complete 門檻:YAML 依 backend 分組。三種 backend 各自持有 wait;兩個 scoring backend 另持有 threshold;TurnSense 資產在 turnsense: 下。wait 出廠皆為 0.1;狀態頁顯示為 Turn 等待時間(wait_secs),範圍 0.1–3.0 秒,滑桿覆寫目前 running backend,全行程共用、不依車。complete threshold 出廠皆為 0.6(0–1,YAML-only,嚴格大於)。COMPLETE 立即 commit;scoring backend incomplete 到期用 timeout,VAD 到期用 vad。頂層 sibling map、頂層資產鍵、純量 wait與殘留 complete_wait_secs 拒絕啟動。JSON overlay 仍是 per-backend map;舊純量 JSON 只套用 turnsensetests/test_turn_backend_config.pytests/turnsense/test_policy.pytests/test_smart_turn_detector.pytests/test_status_page_settings.pytests/test_eot_kws_precedence.py✓ PASS
SW-OPS-05live 測試 harness 不得產生 false passtests/docker-test.sh 重用映像的新鮮度檢查,必須涵蓋 Dockerfile 所有 COPY 進映像的路徑。清單以 Dockerfile 為來源推導驗證,不得手工維護——漏一個路徑就會讓套件對著不是受測版本的程式碼跑出綠燈tests/test_augment.py::test_docker_test_rebuilds_when_any_baked_in_path_changes✓ PASS
SW-OPS-06provider=trt 不得靜默降級成 CUDA(與 SW-EOT-04 是同一原則的兩個實例:指定的 provider 沒生效就拒絕啟動),兩層:① 註冊層——TensorRT EP 註冊失敗時(onnxruntime build 沒有 TRT、libnvinfer 不在載入路徑、ORT 退回選項),vendored sherpa-onnx 中止行程而非 fallback;② 執行層——recognizer 建好之後,若 model.provider 要的是 trt 而 libnvinfer 未常駐於本行程,服務啟動必須失敗。①擋不到②:engine 因 sm 或 TRT 版本不符而無法反序列化時 EP 仍註冊成功,ORT 可以把節點丟回 CUDA。兩者的共同理由是該降級在執行期不可見——服務照常啟動、答案正確,只是慢數倍,該輪就因此量到一組標著 TensorRT 卻跑在 CUDA 的數字tests/test_trt_provider_guard.py✓ PASS
SW-OPS-07C-X1 部署契約:host network、ClusterFirstWithHostNet、NVIDIA RuntimeClass、既有 Gateway 與 localhost 服務互連;預設 CLI 在操作端 build/push Thor image 後部署,--deploy-only 只部署既有 imagetests/test_cx1_deployment.py(6 項)✓ PASS
2026-09-15 v3b retest;C-X1 route 已對齊 v6.0.0
SW-FT-01訓練語料建構:目標詞以 config-basic.yaml 為單一事實來源;句型展開不污染標籤;音色池與配額可重現;AISHELL-3 全程只當評估、永不進訓練tests/test_finetune_corpus.py✓ PASS
SW-FT-02量測與判定:MODEL/PRODUCT 雙軸(別名不計入模型能力)、三軸判定須同時成立、checkpoint 選擇規則(一般 ASR 品質門檻未通過者不得被選中、全 NO-GO 時不得回傳贏家)tests/test_finetune_eval.py✓ PASS
SW-FT-03LoRA 接線:不可達目標必須拋錯而非靜默不啟用;可訓練集合非空且只含 lora;merge 後鍵集合=base 且權重確實改變tests/test_finetune_lora.pySKIP
SW-FT-04波形增強:SNR 精度、訓練/評估池不相交、確定性、不削波;預設 AUG_RATIO=0.25(波形增強,與出貨模型一致)tests/test_augment.py✓ PASS
SW-FT-05論文基準計分不得與一般 ASR 品質驗收計分共用程式碼;中英文正規化分流、論文目標值釘住;精度以 --model-file 選擇(預設 model.onnx,歷史呼叫方式不變),且寫進 report JSON 讓每個數字帶著它的精度來源tests/test_paper_bench.py✓ PASS
SW-FT-06模型量化匯出:sherpa-onnx 的 metadata_propsmodel_type / lfr_window_size / neg_mean / inv_stddev / lang_* / with_itn …)在 INT8 與 FP16 轉換後必須逐鍵存活——少一個,sherpa-onnx 就載不起來;FP16 圖必須保持 fp32 的 graph I/Okeep_io_types),否則餵 fp32 waveform 會型別不符;INT8 的量化參數(op_types_to_quantize / weight_type只允許存在一份,exporter 與獨立量化工具共用同一個函式,不得各寫一份而漂移tests/test_quantize_bundle.py✓ PASS
SW-FT-07量測後端可換為 ONNX--onnx-dir(sherpa-onnx)與 --init-param(PyTorch)互斥,避免報告出現一個不知道用哪個後端量的數字;--require-cache 時,付費 TTS 語料只要有一個 clip 不在快取裡就在發出任何 TTS 請求之前失敗(Google TTS 按字計費,一次誤觸就是真實支出)tests/test_eval_onnx_backend.py✓ PASS
SW-FT-08運算子的裝置可攜性:量化會把 MatMul 換成 MatMulIntegerMatMulNBits 等運算子,而目標裝置的 execution provider 未必實作它們——未實作時 ORT 會靜默指派回 CPU,模型照跑、文字照樣正確,只是速度不是部署時假設的那個。工具須:①列出圖中所有運算子;②列出某個量化方案新增/移除了哪些運算子;③以 ORT profiler 的實際節點指派回答「哪些落在目標 provider、哪些退回 CPU」;④把「provider 根本沒載入」與「部分運算子退回」分開報告(前者是環境問題,後者才是運算子支援問題)。輸入由圖的簽章合成,所以在沒有語料的邊緣裝置上也能重跑tests/test_op_support.py✓ PASS
SW-DOC-01新增測試檔必須在本規格留下紀錄:掛在某個 SW_id 之下,或明示「刻意不給 ID」與理由。反向亦然——本規格不得引用已不存在的測試檔(重新命名或刪除後留下的空指向,比未覆蓋更危險,因為它看起來有覆蓋)。§8/§9 兩區與其 ID 前綴不得被靜默移除tests/test_spec_traceability.py✓ PASS
SW-DOC-02cycle 報告的累積 ledger 只增不減:最新一份報告必須涵蓋歷輪聯集的所有 SW_id(不只是對前一輪比對——那會讓某個 ID 消失一輪後就永遠消失,因為下一次比的是兩份都已缺它的報告)。報告檔名須與 docs/dev-specs/ 的 cycle stem 對應tests/test_report_integrity.py✓ PASS
SW-DOC-03報告要定義自己用的技術名詞CLAUDE.md Rule 5):凡讀者需要查才看得懂、且結論依賴其意義的名詞(sm89RTFMatMulIntegerCMVN…),必須在報告的名詞定義節裡說明「是什麼」與「該輪為何重要」。⚠️ 機械檢查的範圍是「有名詞表的報告,其涵蓋必須完整」;「報告有沒有名詞表」判不了誰是該輪的 cycle,故列入 close-out checklisttests/test_report_glossary.py✓ PASS
SW-DOC-04報告的圖表要能被讀懂CLAUDE.md §6 item 4b):每張圖需有 aria-label;每張數據圖需有 caption,且 caption 要寫出結論而非重複標題(架構/流程圖屬 item 4,豁免)。⚠️ 規則不追溯——報告以「至少有一張帶 caption 的圖」表示採用此慣例,之後其圖才受檢;該輪之前無任何報告為圖加 captiontests/test_report_charts.py✓ PASS
SW-DOC-05報告的章節交叉引用必須指得到CLAUDE.md §6 item 7e):報告是獨立閱讀的文件,§N 是讀者從結論走到證據的唯一途徑;重新編號章節時,引用它的文字必須同步改。⚠️ 檢查只驗指得到,無法判斷「§9 其實該寫 §10」。既有 4 份報告的 12 個懸空引用列成 KNOWN_DANGLING 明帳,只能減不能加tests/test_report_crossrefs.py✓ PASS
SW-DOC-06報告的 ID 對照表要逐項定義CLAUDE.md Rule 3):報告引用的每一個 ADEFR 都要在表裡有自己一列——只定義「家族」與範圍不算數,那回答的是「A 是什麼」不是「其中某一個 id 是什麼」。⚠️ 規則不追溯:以報告是否含「ID 對照」章節判定是否受檢;既有 2 份報告的 18 個未定義 ID 列成 KNOWN_UNDEFINED 明帳,只能減不能加tests/test_report_ids.py✓ PASS
SW-DOC-07報告的小節要掛在自己的章節底下CLAUDE.md §6 item 7f):不得有 <h3> 排在本節 <h2> 之前;編號 N.M 的小節必須位於編號 N<section> 內;站內連結必須指得到錨點。該輪發生兩次——插入時以「下一個 h2」定位,內容落進下一節,而 HTML 解析/標籤平衡/錨點檢查全部看不出來tests/test_report_structure.py✓ PASS
SW-DOC-08報告引用的測試檔必須存在CLAUDE.md §3 懸空引用原則):ledger 是審閱者判斷「這個需求有沒有人守」的依據,指向不存在的檔案讀起來和有覆蓋一模一樣,比空白更糟——空白至少會促使人去查。SW-DOC-01 守的是規格↔測試,這條守的是報告↔測試,而後者才是審閱者實際會讀的。⚠️ 只驗檔名;函式是否存在、結果是否與實跑相符,仍是收尾檢查表的工作tests/test_report_test_refs.py✓ PASS
SW-DOC-09累積 ledger 裡「該需求所屬 cycle新增」的高亮必須跟著換手:沒被該需求所屬 cycle新增的列要清掉 class="hl",只有該需求所屬 cycle自己新增的列能保留,且要帶明確日期(CLAUDE.md §6 rule 7g)。原編號 SW-DOC-08,rebase 到 2026-09-04-ai-agent-ux-alignment 時撞號而改號test_report_highlighting.py(1)✓ PASS
已廢止 / 已移除(保留以滿足累積 ledger 只增不減,SW-DOC-02;不代表仍有覆蓋)
SW_id行為(規格)測試結果
SW-W-06smart-turn 第二句開頭說喚醒詞不觸發喚醒 · 2026-08-17 廢止,與 SW-W-02 互斥,見 SW 規格的廢止紀錄。由 SW-W-13 取代
SW-B-04Prefix 出現在 smart-turn 第二句句首不觸發 · 2026-08-17 廢止,與 SW-W-02 互斥,見 SW 規格的廢止紀錄。由 SW-B-10 取代
SW-B-09特定指令於 smart-turn 第二句句首不觸發 · 2026-08-17 廢止,與 SW-W-02 互斥,見 SW 規格的廢止紀錄。由 SW-B-11 取代
SW-TURN-04幽靈 ID(未註冊)——2026-09-05 循環的後續驗證方向指出 tests/functional/live/test_remaining_ux_gaps.py 的 docstring 引用了 SW-TURN-04UX§3.1 490/510 ms 斷句邊界),但 kitt-stt-sw-spec.md 從未登記此 ID;保留以滿足 SW-DOC-02 的累積 ledger。test_remaining_ux_gaps.py(live,docstring 引用)—(未註冊)
SW-PERF-06跨座位批次解碼——2026-08-31 一輪判定不採用(機制正確但只在鎖爭用時才形成,對 10 秒以內的音訊是負收益),程式碼與其 5 條測試已隨定案從樹上移除。列在這裡是因為那一輪的報告引用了它( Rule 4)—(已移除)
STT_EXTRA · SenseVoice CTC 熱詞 / Context-biasing(沿用)
SW_id行為(規格)測試結果
SW-HW-01from_sense_voice(decoding_method="modified_beam_search")未提供 hotwords_file 時必須正常建構並解碼,不得因無條件呼叫熱詞載入而 hard-exittests/test_sensevoice_hotword_python_api.py✓ PASS
SW-HW-02載入 hotwords_file 後,對已知因該詞彙被誤聽的音檔,解碼輸出必須改變並更接近正確答案tests/test_sensevoice_hotword_python_api.py✓ PASS
SW-HW-03hotword_debug 預設 False,此時 hotword_debug_json 維持空字串tests/test_sensevoice_hotword_python_api.py✓ PASS
SW-HW-04hotword_debug=True 時,解碼結果必須帶有逐 frame 的真實除錯軌跡tests/test_sensevoice_hotword_python_api.py✓ PASS
SW-HW-05除錯軌跡的每個 frame 必須額外帶 beams:依 total_score 排序的候選路徑分數拆解tests/test_sensevoice_hotword_python_api.py;C++ 側 offline-ctc-prefix-beam-search-decoder-test.cc::DebugRankedBeamsExposeWhyTheFlipHappened✓ PASS
SW-HW-06add_hotwords_dict({phrase: weight_or_None}):執行期純新增熱詞,不重建 recognizer、不讀檔案,且不覆寫既有詞tests/test_sensevoice_hotword_python_api.py;C++ 側 offline-ctc-prefix-beam-search-decoder-test.cc::SetContextGraphAffectsOnlySubsequentDecodes✓ PASS
SW-HW-07建構時的 hotwords_file 參數維持既有行為:讀取純文字檔轉成 C++ 端可用的熱詞資料結構tests/test_sensevoice_hotword_python_api.py✓ PASS
SW-HW-08features.enable_hotwordshotwords.{file,score,max_active_paths} 正式接進 BatchASRManager._build()KittSttService:只有熱詞檔含真正內容時才切到 modified_beam_search,否則(含空檔)維持 greedy_search;空檔或純註解檔維持 no-op。tests/test_batch_asr_manager_hotwords.py(6)、tests/test_config_hotwords.py(2)、tests/test_hotwords_default_file.py(3)✓ PASS
STT_EXTRA · 反應式 Interim 排程(沿用,離線)
驗收 ID行為測試(file::test)結果
A1第一次 interim 無前次延遲 → 用 initial_intervaltest_audio_cfg::test_next_interval_uses_initial_when_no_prior_latency✓ PASS
A2間隔 = max(延遲×margin, 地板),無上限test_audio_cfg::test_next_interval_is_latency_times_margin_above_the_floor / test_next_interval_has_no_upper_ceiling✓ PASS
延遲很小時被地板夾住test_audio_cfg::test_next_interval_clamped_to_floor_for_small_latency✓ PASS
A3–A5margin≤1 / 地板≤0 / 初始值≤0 → ValidationErrortest_audio_cfg::test_safety_margin_must_be_greater_than_one / test_min_interval_must_be_positive / test_initial_interval_must_be_positive✓ PASS
A6ASR_INTERIM_SAFETY_MARGIN 覆寫 / 未設時吃 YAMLtest_audio_cfg::test_env_var_overrides_margin / test_no_env_var_keeps_yaml_default✓ PASS
STT_EXTRA · SW-PERF-01:永久 live 回歸測試(沿用,live)
驗收 ID行為測試(file::test)結果
SW-PERF-01連線無錯誤test_audio_stress::test_audio_stress_no_errors✓ PASS
SW-PERF-0190 秒連續長句仍收到 finaltest_audio_stress::test_audio_stress_final_received✓ PASS
SW-PERF-01time to final 有界test_audio_stress::test_audio_stress_time_to_final_bounded✓ PASS
SW-PERF-01相鄰 interim 最大間隔有界test_audio_stress::test_audio_stress_interim_gap_bounded✓ PASS
STT_EXTRA · interim/final ITN 拆分(沿用,離線)
驗收 ID行為測試(file::test)結果
B1interim 只套用 s2t(),不做數字正規化test_batch_asr_manager_itn_split::test_interim_inference_only_applies_s2t_not_numeral_normalization✓ PASS
B2final 仍套用完整 itn()(s2t + 數字),不受影響test_batch_asr_manager_itn_split::test_offline_inference_still_applies_full_itn✓ PASS
STT_EXTRA · 併發排程(沿用,離線 13 條,單一檔 tests/test_interim_concurrency.py
驗收 ID行為測試(file::test)結果
B14-R1RTF 只計 decode_stream() 的時間,不含鎖等待test_interim_concurrency::test_decode_ms_and_latency_ms_agree_with_no_contention / test_decode_ms_excludes_lock_queueing_but_latency_ms_includes_it✓ PASS
B14-R2每個 session 有獨立buffer_lock,與 inference_lock 分開,且真的序列化併發存取test_interim_concurrency::test_buffer_lock_is_per_session_and_separate_from_inference_lock / test_buffer_lock_serializes_append_against_read✓ PASS
B19-R1_interim_redecode_body 不得被 inline await;恰有一次背景派發,且必須是 self.create_tasktest_interim_concurrency::test_interim_redecode_is_dispatched_never_awaited✓ PASS
B19-R2×人數 不得偷渡回來:呼叫端只能帶 1 個位置引數,函式簽名不得有人數參數test_interim_concurrency::test_the_schedule_is_never_given_a_speaker_count✓ PASS
B19-R3三個已移除的旗標保持移除FeaturesCfg 無欄位、config-basic.yaml 無鍵,舊 override 檔仍可載入且為惰性test_interim_concurrency::test_the_removed_knobs_stay_removed✓ PASS
下游 / OOS(無 STT SW_id / 測試 — 歸屬 NLU · DM · TTS · web-ui,沿用)
F_id / SW_idFeature / 行為歸屬
F_1多音區識別/控制(權限·AreaID·多區並行)下游 + STT per-user_id hook
F_4.1語音打斷_背景併行處理下游 DM/TTS
F_4.2語音打斷_後令壓前令下游 DM 仲裁
F_5連續指令下游 NLU 拆解(UX§4.2.1 拆解成功率同歸此列)
F_6上下文理解下游 DM 快取(UX§5 保留輪數同歸此列)
F_3.4c最大錄音時限自動停止kitt-web-ui——✓ 已實作,設定 20s(2026-09-04 UX cycle 記錄的使用者確認;2026-09-05 未驗證 Web UI)
SW-UI-06講話中切 mic 靜音、WebUI 不卡字web-ui(無 STT 測試)
SW-MU-01..08多使用者/多車喚醒同步、隔離web-ui(STT 提供 per-user_id / ?car= hook)
UX§1.3.1UX§7UX§9UX§10(除 11.3)問候語 UI/Guardrail/回復策略/顯示策略下游 UI/LLM/TTS
STT_EXTRA · 2026-08-31 特徵增量化測試(無 SW_id,保留原列)
SW_id行為測試結果

刻意不掛 SW_id
interim 重解碼的特徵抽取增量化:增量餵入與一次性餵入產生的特徵必須逐 bit 相同; 持續 stream 不得跨 utterance 復用;該句的 final 沿用同一個 stream 只補未餵的尾巴; 餵入失敗必須可見且可復原(拋 IncrementalFeedError、游標只在成功後推進、 半餵的 stream 丟棄重建);交棒不得在 interim 餵入中途發生(否則 final 重複餵同一段音訊) tests/test_offline_stream_incremental.py(8)
tests/test_batch_asr_manager_incremental_prep.py(8)
tests/test_interim_incremental_wiring.py(6)
tests/test_final_stream_reuse.py(9)
31/31 PASS · 2026-09-05 offline

同一 MR 的整合驗證

最終 HEAD 基於 origin/main 3c0282d7,現行 SW 規格共有 159SW_id。此表只記錄同一 MR 併入的非 EOT 路徑之最終結果;EOT 結論仍見上方 §3。

最終整合結果

需求/範圍本輪結果證據或未驗原因
SW-DOC-10SW-FT-09..13SW-HW-10SW-LANG-03..14SW-LANG-16SW-LANG-18..30SW-OPS-08SW-PERF-07✓ PASS登記的 unit/scenario tests 443 passed;其餘輔助測項 114 passed;完整 offline 1661 passed、33 skipped。
SW-HR-01SW-HW-09✓ PASS公司名行為與熱詞測項通過;安裝 hr-build group 的 pynini 後,FST 與詞表重建相等性 test_committed_fst_is_built_from_the_word_maps 1/1 通過。
SW-LANG-15SW-LANG-17NOT-VERIFIED測試正確 skip:缺 lid_risk.jsonprobe.json GPU+TTS 量測 artifact;不能把沒有量測資料當 PASS。
SW-LANG-02、non-wake/10-second prompt/vehicle hotwords✓ PASScurrent-default v3b CUDA live:中英夾雜兩案、non-wake、10-second prompt、除霧與小憩模式均通過。
SW-W-10SW-B-03、未註冊 SW-TURN-04✗ FAILcurrent-default vadwait_secs=0.1 下重現 3 例。前兩例的 fixture 沒有選 smart-turn 卻期待跨段黏合;490 ms 例缺 UX 要求的 500 ms command wait。
最終 HEAD 的詳細測試證據
範圍結果說明
Status/config focused60 passed設定與狀態頁讓三種 backend 接受 0.1–3.0 秒,並涵蓋空 VAD summary、VAD 模式 wake/wait 儲存與 reset、WAV 180 秒 hard cap/逐筆與全部 ZIP 下載,以及 runtime backend controls。
Startup backend contract2 passedVAD 不建構 TurnSense;明確選擇 TurnSense 時 preload failure 中止啟動。
Report contract140 passed, 1 skipped最終 HTML 的結構、連結、SW_id、圖表與測試引用檢查。
Offline full1661 passed, 33 skipped以目前 v3b workspace 完整重跑;先前 sandbox 卡住與缺 WAV 的結果均未在 host test environment 重現。
current-default v3b CUDA live12 passed, 3 failed失敗為 test_active_smart_turntest_prefix_smart_turn、490 ms command-wait boundary;10-second prompt 已通過。
2026-09-15 v3b Live WebSocket/CUDA41 passed隔離 container:拒識 2、退出詞 13×10、wake/false wake/rate、主動問候/latency、車載 hotword;JUnit 0 errors/0 failures,43m33s。
重現方式 · 最終 HEAD 驗證命令
驗證範圍命令
Current default config + status(60 passed)PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -q tests/test_turn_backend_config.py tests/test_status_page_settings.py
Current startup backend contract(2 passed)PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -q tests/turnsense/test_runtime.py -k 'vad_backend_skips_runtime_construction or turnsense_backend_preload_failure_still_aborts_startup'
Current-default host GPU startup/status(PASS)以臨時 overlay 將 server.port 設為未使用的 <port>PYTHONPATH=./sherpa-onnx/build ASR_CONFIG_FILE=<overlay.yaml> .venv/bin/python -m src.server;再以 curl --fail http://127.0.0.1:<port>/stt/sensevoice 確認 data-current="vad"
TurnSense runtime(15 passed)PYTHONDONTWRITEBYTECODE=1 uv run --frozen --group test pytest -q --tb=short tests/turnsense/test_runtime.py
Offline full(1661 passed, 33 skipped)PYTHONDONTWRITEBYTECODE=1 uv run --frozen --group test pytest -q --tb=short tests --ignore=tests/functional/live --ignore=tests/sherpa-onnx
v3b selected offline(220 passed)uv run --frozen --group test pytest -q tests/test_max_utterance_audio.py tests/test_eot_state.py tests/test_eot_kws_precedence.py tests/test_turn_backend_config.py tests/test_status_page_settings.py tests/test_cx1_deployment.py tests/test_exit_word.py tests/test_wake_matcher.py tests/test_hotwords_default_file.py;另跑 tests/test_wake_false_accept.py(2)與 tests/test_greeting_settings.py(12)
v3b Live WebSocket/CUDA(41 passed)以同步模型快照的 isolated Docker image:HOST_PORT=<free-port> LIVE_TEST_TARGET=tests/functional/live/test_rejection_thresholds.py ./tests/docker-test.sh --build;再跑 test_exit_words.pytest_wake.pytest_wake_factory_words.pytest_wake_rate.pytest_active_greeting.pytest_wake_latency.pytest_hotwords_vehicle_terms.py
2026-09-15 report contract(140 passed, 1 skipped)uv run --frozen --group test pytest -q tests/test_report_*.py
current-default v3b CUDA live(12 passed, 3 failed)以同步模型快照的 isolated Docker image:IMAGE=kitt-stt:fail-notverified-20260915 HOST_PORT=<free-port> LIVE_TEST_TARGET=tests/functional/live/test_active.py ./tests/docker-test.sh;再跑 test_bypass_prefix.pytest_bilingual.pytest_remaining_ux_gaps.pytest_hotwords_vehicle_terms.py
Staticgit diff --check · bash -n deploy.sh

附錄 C · 未覆蓋範圍與後續工作

Cycle:2026-09-15-eot-backends · baseline 67160fa · 驗證結果 · 判定 · 未覆蓋範圍 · Artifact PUBLISHED