turn_model.backend 出廠預設為 vad;狀態頁選擇後由單一「儲存設定」在所有 turn idle 時 process-wide 切換 turnsense、vad 或 smart-turn,重啟回 YAML。三種 backend 的 wait_secs 出廠皆為 0.1;兩個 scoring backend 的 threshold 皆為 0.6。VAD 不跑模型,收到 VAD stop 後才開始等待;kitt-web-ui 的 VAD stop debounce 出廠為 0.4 秒,因此出廠 VAD 路徑為 500ms(不扣 ASR)。狀態頁尚未收到 VAD runtime 資料時不顯示空的 vad 區塊。COMPLETE 立即 commit。turn_settings.json 仍是 per-backend wait map。本輪驗收條件使用 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,設地板但不設上限 |
| A3 | vad.wait_secs 接受 0.1–3.0。backend=vad 不建構 TurnSense,等待後 commit reason=vad | 2026-07-29 排程:margin 必須大於 1 |
| A4 | overlay 只改 backend: smart-turn 繼承三個區塊 | 2026-07-29 排程:最小間隔必須大於 0 |
| A5 | nested wait 越出 0.1–3.0、threshold 越出 0–1 拒絕啟動 | 2026-07-29 排程:初始間隔必須大於 0 |
| A6 | deploy.sh 從 nested turnsense: 讀 key/onnx_file/cmvn_file | 2026-07-29 排程:環境變數可覆寫 YAML margin |
| A7 | COMPLETE 嚴格大於門檻並立即 commit;incomplete/invalid 等 wait;滑桿只改 incomplete;JSON 仍是 per-backend map;全行程共用、不依車 | — |
| A8 | smart-turn 呼叫 _predict_endpoint;過門檻 reason=model;不呼叫 append_audio。兩個 scoring backend 共用 model reason;VAD 使用 vad reason;三種 backend 下 KWS/force 仍優先 | — |
| A9 | SW-OPS-04/SW-EOT-01/SW-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.backend | YAML 值是行程啟動預設,出廠為 vad;狀態頁選擇後由單一「儲存設定」在所有 turn idle 時 process-wide 切換 turnsense、vad 或 smart-turn,重啟回 YAML。它選的是 verdict 來源,不是關掉 VAD,也不是 per-user 開關。Turn 等待時間(wait_secs)旋鈕只改目前 backend 的等待秒數,不是 backend,也不改 complete threshold。 |
wait_secs / Turn 等待時間 / SW-OPS-04 | YAML、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-turn | turnsense 沿用 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=vad | eot.model.state=skipped 表示沒有模型機率。TurnSense/Smart Turn 的正常 commit 用 reason=model;VAD backend 用 reason=vad。Wire reason 只區分 model-backed 與 VAD,不再區分兩種模型 backend。 |
| KWS / STT / ASR | KWS 是喚醒詞偵測;本版用 STT(語音轉文字,也稱 ASR)的 final transcript 做文字比對,尚無聲學 KWS。三種 EOT backend 都仍跑 ASR 與 KWS。 |
| Active / Standby / manual | Active/Standby 是共用喚醒狀態;manual 是略過 match_wake 的 KWS method,不等於 Standby,也不是 EOT backend。 |
| VAD / segment / turn | VAD 偵測語音起訖;一段 VAD 音訊是 segment,一次送交下游的對話輸入是 turn,可包含多段 segment。VAD stop 不一定立即結束 turn;backend=vad 時,在 KWS 與強制停止之後,正常 stop 會等待 vad.wait_secs,期間的新語音延續同一 turn。 |
| EOT / TurnSense / EotController / SmartTurnDetector | EOT 是 turn 結束判定。TurnSense 提供語尾三分類;SmartTurnDetector 包裝 Pipecat CPU ONNX。detector 只回 TurnSenseResult。KittSttService 選出 TurnOutcome,由 EotController 執行 WAIT/COMMIT/CANCEL。Detector 不送 frame、不跑 KWS、不擁有 deadline。 |
| HR / ITN / s2t | HR 是同音替換,配置於 recognizer;ITN 將口語數字正規化,包含 OpenCC s2t(簡轉繁)。interim 只做 s2t,final 做完整 ITN。本輪不改這條路徑。 |
| SW_id / PRD F_id / UX | SW_id 是 SW 規格的穩定需求編號;PRD F_id 是產品功能分組;UX 條款補充互動要求。附錄 B 行為欄描述要求,結果欄描述驗證,兩者不等同。本輪新增可切換 EOT backend,並把 SW-OPS-04、SW-EOT-01 與 SW-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.md、
2026-09-15-eot-backends.md。
Kickoff baseline 67160fa(main)。出廠 YAML 依 backend 分組,啟動預設為 vad;selector 可選 turnsense/vad/smart-turn。overlay 只改 backend: smart-turn 時 deep-merge 繼承三個區塊。頂層 sibling map 與頂層資產鍵在啟動時失敗。
| 路徑 | 出廠 | 用途 |
|---|---|---|
turn_model.turnsense.wait_secs | 0.1 | TurnSense incomplete wait;滑桿覆寫目前 running backend |
turn_model.smart-turn.wait_secs | 0.1 | Smart Turn incomplete wait |
turn_model.turnsense.complete_threshold | 0.6 | TurnSense COMPLETE 機率門檻;YAML-only;嚴格大於 |
turn_model.smart-turn.complete_threshold | 0.6 | Smart Turn COMPLETE 機率門檻;YAML-only |
turn_model.vad.wait_secs | 0.1 | VAD 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 區塊讀 |
VADParams.stop_secs 出廠值調為 0.4 秒,並維持 STT wait_ms 的 0.1 秒顯示精度。不接 Pipecat append_audio。不出廠改成 smart-turn。不做 mid-turn 或 per-user 切換。不從體感扣 ONNX 推論。不修 complete 路徑 490/510 切句。狀態頁將 EOT backend 與 Turn 等待時間(wait_secs)收在 Turn settings,兩者由頁面下方單一「儲存設定」套用;backend 被 active turn 拒絕時不會把 timeout 寫到錯的 backend。Debug tools 以分隔線獨立成區,提供 Debug audio: on/off 動態開關、「下載全部 WAV」、「下載最新 trace」與「開啟 Perfetto」;頁面載入時列出已完成 WAV 的數量與檔名,每個檔名仍可單獨下載。「下載全部 WAV」使用 ZIP 打包目前已完成檔案。只列 connection cleanup 已完成寫檔的 WAV,不會強制 flush 尚未斷線的 PCM。這些是觀測工具,不改 Frame contract 或 EOT 判定結果。
turn_model.backend 只替換 VAD stop 後的判定來源;三種 backend 最後都交給同一個 EotController。turn_model.backend 是啟動預設;狀態頁只在所有 turn idle 時替換 process-wide detector(src/turn_detection.py)。TurnBackend.TURNSENSE:呼叫 TurnSenseDetector.analyze;complete 則 reason=model。TurnBackend.SMART_TURN:呼叫 SmartTurnDetector.analyze → _predict_endpoint;complete 則 reason=model。不呼叫 append_audio。TurnBackend.VAD:不跑模型,回傳 state=skipped 的 WAIT;到期由 EotController 以 reason=vad commit。不得把 turnsense 缺 runtime 的 skipped 解讀成 VAD mode。EotController.apply。強制停止的 reason 保持 mic_off/max_duration/audio_stall。Backend 只改判定來源,沒有改下游 Frame 或 metadata 外形。每個 segment 都使用同一組 eot.model keys;尚未 commit 時 eot.decision=null,commit carrier 才填入同一組 decision keys。
eot envelope;web-ui 依 action 與非空 reason 結束 turn。turn_model.backend | eot.model 差異 | 正常 commit 的 eot.decision.reason | Frame contract |
|---|---|---|---|
turnsense | state=complete/incomplete/invalid/error;模型成功時三個 probability 有值 | model | TranscriptionFrame 或無 final 時的 ZonalUserStoppedSpeakingFrame;WAIT/COMMIT 與 metadata keys 相同 |
smart-turn | state=complete/incomplete/error;只有 p_complete 有值 | model | |
vad | state=skipped;probability 全為 null,wait_ms=vad.wait_secs | vad |
smart-turn 與 vad 不會讓 stop strategy 失敗
SttEotUserTurnStopStrategy 接受 TranscriptionFrame/UserStoppedSpeakingFrame,並要求 decision.action == "commit" 與 reason 是非空字串。reason=model 與 reason=vad 都會正常呼叫 trigger_user_turn_stopped()。來源:kitt-web-ui backend/src/kitt/agent/turn_strategy.py:83-103。
UI observer 只讀 eot.model.p_complete、state、wait_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 判斷。
| 操作 | 實作行為 | 邊界 |
|---|---|---|
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-266;src/kitt_stt_service.py:319-325,384-454,566-567。 |
| 下載最新 trace | GET <base>/debug/trace/latest 回傳 TraceManager 最近一次週期 flush 已完成原子寫入的 trace_*.json.gz;沒有已 flush 檔案時回 404。 | 按鈕不主動 flush,不回傳仍在記憶體中的 event。來源:src/server.py:269-282、utils/perfetto_trace.py:320-370。 |
| 開啟 Perfetto | 以新分頁開啟 https://ui.perfetto.dev/。 | 瀏覽器不自動上傳 trace;操作者自行把剛下載的 gzip 拖入 Perfetto UI。 |
2026-09-15 · kickoff baseline 67160fa,rebase 到 origin/main 3c0282d7。本次出廠設定調校以 tests/test_turn_backend_config.py 與 tests/test_status_page_settings.py 驗證(60 passed);kitt-web-ui 的 VAD runtime settings 測試為 3 passed。主機 GPU 以獨立 port 啟動目前設定:log 顯示 [EOT] backend=vad、TurnSense preload skipped、SenseVoice ready,HTTP status page 回傳 data-current="vad"。擴大 runtime suite 不計為通過:未改動的 test_inference_runs_off_the_event_loop_thread 在 11 個 runtime case 後無進展而中止。完整命令與紀錄
| PRD Feature | SW_id | Behaviour | Executable evidence | Result |
|---|---|---|---|---|
| STT_EXTRA | SW-OPS-04 | YAML 依 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.1 | SW-EOT-01 | 路徑改為 turn_model.<backend>.wait_secs/complete_threshold;COMPLETE 立即 commit;出廠 VAD 路徑 400+100=500 | test_turn_backend_config.py · test_eot_kws_precedence.py · test_policy.py | ✓ PASS |
| STT_EXTRA / F_3.2 | SW-EOT-08 | smart-turn 讀 turn_model.smart-turn;overlay 只改 backend 即繼承三個區塊;狀態頁只在所有 turn idle 時切換,失敗保留原 backend | test_turn_backend_config.py · test_smart_turn_detector.py · test_eot_kws_precedence.py · test_status_page_settings.py · test_runtime.py | ✓ PASS |
| 驗證範圍 | 結果 |
|---|---|
| 2026-09-15 pre-default-adjustment branch integration focused | 184 passed, 12 skipped · config/policy/三種 detector/precedence/runtime/status/wake scan/超長語句標記/greeting settings;skip 均因隔離 worktree 缺 models/turnsense-1.1/am.mvn |
| 2026-09-15 report contract | 142 passed, 2 skipped · HTML/cross-reference/SW_id mapping/圖表/測試引用 |
| 2026-09-15 current default config/status focused | 60 passed · 出廠 backend=vad、三種 wait=0.1、VAD wait reset、VAD 模式 status/idle backend 切換與 target policy |
| 2026-09-15 expanded runtime sweep | NOT-VERIFIED · test_inference_runs_off_the_event_loop_thread 在 11 個 runtime case 後無進展而中止;未將 partial run 視為通過 |
| 2026-09-14 Smart Turn/precedence focused | 44 passed · detector threshold/runtime error/reason metadata/KWS 與 force precedence;fake model calls 在測試 event loop 內執行,避免 Python 3.10 pytest executor teardown 卡住 |
| 2026-09-15 current startup backend contract | 2 passed · VAD 不建構 TurnSense;明確選擇 TurnSense 時 preload failure 中止啟動 |
| Debug tools 後完整 offline regression | NOT-VERIFIED · 收集 1105 項後卡在既有 test_batch_asr_manager_incremental_prep.py,數分鐘無進展後中止;未把 partial run 當成通過 |
| 同一次命令中的既有 UX 缺口 | 16 failed · 7 個既有 UX 缺口;9 個缺 gitignored WAV corpus |
| 2026-09-15 current-default host GPU startup/status | PASS · backend=vad、TurnSense preload skipped、SenseVoice ready;HTTP status page 回傳 data-current="vad" |
| pre-default-adjustment Live WebSocket/CUDA | 3 passed · test_forced_stop.py 的 max_duration/mic_off/audio_stall 都在隔離容器中立即 commit final;下方 88 筆仍是歷史 live ledger |
驗證界線:本次出廠設定 focused 與主機 GPU startup/status 均通過。擴大 runtime suite 的 test_inference_runs_off_the_event_loop_thread 在 11 個 runtime case 後無進展而中止,未列為通過;完整 offline 的既有 UX/缺 WAV 限制仍見上表。
| Gate | 狀態 | 說明 |
|---|---|---|
| 範圍符合使用者要求與 non-goals | PASS | 三種 EOT backend 可選;出廠 backend 為 VAD、三種 backend 各有 0.1 秒 wait;COMPLETE 立即 commit;JSON overlay 仍是 map;滑桿套用目前 backend;web-ui VAD stop 出廠為 0.4 秒 |
| 出廠設定離線驗收 | PASS | config/status focused 60 passed;TurnSense preload failure case 明確選擇 turnsense backend |
| Status page runtime controls | PASS | status/config focused 測試涵蓋 audio 開關、WAV 180 秒 hard cap、清單與逐筆下載、trace 下載、空 VAD summary、VAD 模式 wake/wait 儲存、wait override reset、backend idle gate 與 rollback |
| Live GPU/STT server | PASS | 主機 GPU 以目前 YAML 啟動;backend=vad、TurnSense preload skipped、SenseVoice ready,status page 回報 data-current=vad |
| Figure 1 / Figure 2 / Table 1 | PASS | 附錄 A 依序內嵌 stack、frame flow、I/O 契約 |
| Artifact 發布 | NOT-PUBLISHED | 本地報告已在公開版本之後更新;目前 URL 尚未包含本輪 runtime controls 內容 |
正文 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 仍共用。
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:67、src/session/user_session_manager.py:10、src/model_cache.py:9、utils/model_manager.py:154。
src/kitt_stt_service.py:599、src/kitt_stt_service.py:1253)。backend=vad 會略過模型但產生固定 WAIT;force stop、scoring backend 的空 turn tail或缺 detector 會回 skipped。VAD WAIT 經 outcome_from_verdict(..., backend=vad) 保留 reason=vad。src/kitt_stt_service.py:1408、src/kitt_stt_service.py:1800)。src/kitt_stt_service.py:1773、src/kitt_stt_service.py:1513、src/kitt_stt_service.py:1044)。| EotController 結果 | 輸出與狀態 | 後續事件 | 位置 |
|---|---|---|---|
WAIT | 有 final_frame 就先送出,不附 commit 的 eot.decision;turn 保持開啟。 | 排程 per-user EOT deadline;新 VAD start 取消 deadline 並延續 turn。deadline 到期且仍有效,改以 explicit stop commit。 | src/eot.py:186src/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 |
IN/OUT 表示收/送,不等同於 FrameDirection。Web UI 控制通常從 DOWNSTREAM 收入,LM standby 從 UPSTREAM 收入;服務產生的文字與狀態通常送往 DOWNSTREAM。下表列本服務特別處理或產生的 Frame,未列出的基底 passthrough 不新增本服務的行為契約。_emit_seat_load 只寫 trace,不送 ASRMetadataFrame(src/kitt_stt_service.py:1228)。
reason。| Frame | 方向 | 觸發 case | 動作/條件 | Metadata/payload | 位置(file:line) |
|---|---|---|---|---|---|
StartFrame | IN | pipeline 啟動 | 呼叫基底 start,送模型/主機 metadata;模型與 session manager 已在 service 建構時準備。 | model_name、system info | src/kitt_stt_service.py:325 |
AudioRawFrame | IN | PCM chunk 到達 | 依 user_id 選 session;正常 chunk 更新 preroll/turn tail,utterance_active 時累積 segment 並評估 interim 排程。帶 metadata["max_utterance_duration"] 的 chunk 不進 segment buffer、不解碼,仍交由基底往下游傳給 VAD。 | audio、sample_rate、num_channels、user_id、可選 max_utterance_duration | src/kitt_stt_service.py:616src/kitt_stt_service.py:644src/kitt_stt_service.py:1173src/kitt_stt_service.py:1200 |
VADUserStartedSpeakingFrame | IN | VAD 開始 | 建立或延續 per-user turn,取消舊 EOT deadline;以 preroll 起始 segment。 | user_id、start timing | src/kitt_stt_service.py:683 |
VADUserStoppedSpeakingFrame | IN | VAD 停止 | 快照 segment/turn tail,交棒 stream,並行 final ASR 與選定 EOT detector。已知 eot_force_reason 可要求 force-complete;不直接代表 EOT commit。backend=vad 略過 TurnSense 推論。 | user_id、eot_force_reason | src/kitt_stt_service.py:785src/kitt_stt_service.py:1513 |
UserStartedSpeakingFrame | IN | 上游 legacy semantic start | 本服務不以此建立或重開 turn;原 frame 交由基底路徑傳遞。 | 原 frame payload(passthrough) | src/kitt_stt_service.py:998 |
UserStoppedSpeakingFrame | IN | 上游 legacy semantic stop | 本服務不以此結束 turn;原 frame 交由基底路徑傳遞,STT 自行決定 EOT。 | 原 frame payload(passthrough) | src/kitt_stt_service.py:998 |
BotStartedSpeakingFrame | IN | TTS 開始播放 | Active 改掛 bot_speaking_watchdog_secs;Standby 取消 shared timer。 | 無新增 payload;更新 shared timer | src/kitt_stt_service.py:918 |
BotStoppedSpeakingFrame | IN | TTS 播放結束 | Active 且無 user 正在 utterance 中時,重設 shared idle timeout。 | 無新增 payload;更新 shared timer | src/kitt_stt_service.py:950 |
InterruptionFrame | IN | reason=agent_active;Web UI 手動 Active | 啟用共用 wake state、重設 idle timer;各 session 從當下重啟辨識,丟棄切換前 segment。stt/manual 都接受此控制。 | reason=agent_active、user_id | src/kitt_stt_service.py:1035src/kitt_stt_service.py:662 |
InterruptionFrame | IN | reason=agent_standby;Web UI(DOWNSTREAM) | 清除共用 wake state,結束所有 pending turn;交由基底傳遞。 | reason=agent_standby、user_id | src/kitt_stt_service.py:1009 |
InterruptionFrame | IN | reason=agent_standby;LM(UPSTREAM) | 同樣取消 room-wide pending turn,另向 DOWNSTREAM relay 讓 Web UI 同步。 | reason=agent_standby、user_id | src/kitt_stt_service.py:1024 |
STTUpdateSettingsFrame | IN | 設定 delta | 由基底設定路徑呼叫 _update_settings;依 delta 更新支援的 wake/bypass 設定,不代表所有 YAML 欄位都可熱切換。 | 支援欄位的 settings delta | src/kitt_stt_service.py:1188 |
EndFrame | IN | pipeline 收尾 | 取消 idle timer;仍在 utterance 的 session 可跑 final,使用 apply_eot=false,不發 EOT commit。之後丟棄 pending turn,交由基底收尾。 | 無新增 payload | src/kitt_stt_service.py:1044 |
ASRMetadataFrame | OUT | start 完成 | 送 model_name 與 CPU/GPU/RAM 資訊;不是文字或 EOT 載體。 | model_name、CPU/GPU/RAM | src/kitt_stt_service.py:325 |
InterimTranscriptionFrame | OUT | interim decode 有更新文字 | features.streaming_interim=true 且 _should_emit_interim 通過;須同時符合排程條件。後處理只做 s2t,不作 final 的權威輸出。 | text、user_id、timestamp、language | src/kitt_stt_service.py:1253src/kitt_stt_service.py:1289 |
InterimTranscriptionFrame | OUT | 無可送 final,但 room 仍 Active | 送空文字清除 interim;Standby 清除顯示改由 agent_standby 通知處理。 | text=""、user_id、timestamp | src/kitt_stt_service.py:1776 |
TranscriptionFrame | OUT | 非空 final,_should_emit_final 通過,且非 exit | 帶 eot.model;WAIT 先送 segment final 並等待,COMMIT 在同一 frame 附 eot.decision。EndFrame 收尾例外不套用 EotController。 | text、user_id、timestamp、language、metrics、eot.model/eot.decision | src/kitt_stt_service.py:1692src/eot.py:186 |
ZonalUserStoppedSpeakingFrame | OUT | COMMIT 且沒有 final_frame | 如有效 EOT deadline 到期、wake-only、ASR error;以 explicit stop 承載 eot.decision,不是每個 final 都加送 stop。 | user_id、eot.model、eot.decision、reason | src/eot.py:221 |
InterruptionFrame | OUT | STT wake 命中/Active re-wake | reason=agent_active;喚醒或重申共用 Active 狀態。manual 不以 transcript 觸發這條路徑。 | reason=agent_active、user_id | src/kitt_stt_service.py:594src/kitt_stt_service.py:594 |
InterruptionFrame | OUT | exit word 命中 | reason=agent_standby;清除 wake state 並取消 room-wide pending turn,不送 exit transcript。 | reason=agent_standby、user_id | src/kitt_stt_service.py:1408src/kitt_stt_service.py:1426 |
InterruptionFrame | OUT | Standby 且無可送 final | reason=agent_standby;通常是 wake/bypass 無匹配,CANCEL / no_wake 丟棄 pending turn。 | reason=agent_standby、user_id | src/kitt_stt_service.py:1785 |
InterruptionFrame | OUT | 共用 wake idle/watchdog timeout 到期 | reason=agent_standby;callback 先確認無 user 正在 speaking/finalizing。這個 timer 與 per-user EOT deadline 不同。 | reason=agent_standby | src/kitt_stt_service.py:482 |
InterruptionFrame | OUT | 轉送 LM 的 UPSTREAM agent_standby | 向 DOWNSTREAM relay 同一 frame,讓 Web UI 回 Standby。 | reason=agent_standby、user_id | src/kitt_stt_service.py:1024 |
TTSSpeakFrame | OUT | 符合 Standby wake-only 全部條件 | 送 wake.response_text,append_to_context=false;COMMIT / wake_only 另以 explicit stop 結束控制 turn。 | text、append_to_context=false | src/kitt_stt_service.py:1800src/kitt_stt_service.py:1811 |
需求列是 2026-09-14 SW 規格快照;結果以本輪重跑為準。完整 SW_id/test mapping 保留供審閱,未重跑的 live 一律標 NOT-VERIFIED;先前 cycle 的 88 筆案例只保留為歷史 ledger,不計入本輪結果。範圍與平台限制見附錄 C,本輪命令見測試證據。
下表計算需求列,不是 pytest case 數;同一需求可能跨 Feature,同一測試也可涵蓋多個需求,因此各組不能相加當成測試總數。執行命令見重現方式。
| PRD F_id | Feature | 有 STT SW_id? | 需求列數 | 2026-09-14 驗證狀態 |
|---|---|---|---|---|
| F_2 | One-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_EXTRA | PRD 未描述、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 | — | 未執行下游驗收 |
| SW_id | 行為(規格) | 測試 | 結果 |
|---|---|---|---|
SW-W-03 | Standby→Active(喚醒詞在句首):喚醒詞+指令連說,辨識其後之指令;喚醒詞不得出現在 final,且不得因前期聽歪而誤顯示喚醒詞 | test_wake.py | ✓ PASS |
SW-W-04 | 喚醒詞 <停頓> 指令 亦能喚醒,final 無喚醒詞 | test_wake.py | ✓ PASS |
SW-W-05 | 喚醒詞不在句首不觸發 | test_standby.py | ✓ PASS |
SW-W-08 | Active 期間:喚醒後任何話都辨識並往後送;句首喚醒詞於 final 過濾、非句首喚醒詞保留;interim 出現喚醒詞可接受 | test_active.py | ✓ PASS |
SW-W-09 | Active 句首喚醒詞 + smart-turn,仍不顯示喚醒詞 | test_active.py | ✓ PASS |
SW-W-13 | Standby 第一句未命中 → turn 關閉,故第二句句首的喚醒詞是新 turn 的句首,正常觸發喚醒 | test_smart_turn_wake.py | ✓ PASS |
SW-W-14 | 英文出廠喚醒詞 Hi Foxtron 須觸發(大小寫無關),且喚醒詞不得留在 final | test_wake_factory_words.py | ✓ PASS |
SW-W-15 | 第一次主動問候的時機(UX§1.3.2):喚醒成立後須送出問候的 TTSSpeakFrame,STT 側上界 500ms。狀態頁可 process-wide 儲存 ui_handler_p99_secs/tts_handler_p99_secs(秒、三位小數、最小 0、無上限),下一次喚醒套用;兩個 handler legs 也用於第二次問候,非 per-car 設定。 | test_active_greeting.py · test_wake_latency.py · tests/test_greeting_settings.py | ✗ FAIL handler delay runtime settings:✓ PASS |
SW-W-16 | 喚醒成立後 <550ms 內有人說話 → 不觸發第一次主動問候。窗的起點是 InterruptionFrame(agent_active) 送出的那一刻——下游能觀測到的「喚醒成功」就是這個 frame | test_active_greeting.py | ✗ FAIL |
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 |
| SW_id | 行為(規格) | 測試 | 結果 |
|---|---|---|---|
SW-W-01 | 非喚醒(Standby):說喚醒詞以外任何句子,STT 不出任何文字、不往後送;相似音喚醒詞不得觸發 | test_standby.py | ✓ PASS |
SW-W-02 | 非喚醒下說無關長語音,需立即 smart-turn stop | test_standby.py | ✓ PASS |
SW-W-07 | smart-turn 斷句不因 VAD 誤判過度斷句 | test_smart_turn_wake.py | ✓ PASS |
SW-W-09 | Active 句首喚醒詞 + smart-turn,仍不顯示喚醒詞 | test_active.py | ✓ PASS |
SW-W-10 | Active 期間 smart-turn 功能正常(跨段黏合) | test_active.py | ✗ FAIL |
SW-W-11 | Active 第二句開頭喚醒詞不過濾(正常顯示) | test_smart_turn_wake.py | ✓ PASS |
SW-W-12 | 斷句前後不得因雜音產生語助詞(嗯/啊/喔) | test_active.py | ✓ PASS |
SW-W-13 | Standby 第一句未命中 → turn 關閉,故第二句句首的喚醒詞是新 turn 的句首,正常觸發喚醒 | test_smart_turn_wake.py | ✓ PASS |
SW-WM-02 | alias 只能買回實測誤聽,不得擴大誤觸發:全中文 alias 與 canonical 距離 ≤1 字;普通語句不得誤觸發 | tests/test_wake_matcher.py | ✓ PASS |
SW-B-03 | Prefix + smart-turn 正常合併 | test_bypass_prefix.py | ✗ FAIL |
SW-B-08 | 特定指令於 smart-turn 第一句句首觸發,只傳「關閉空調」 | test_bypass_cmd.py | ✓ PASS |
SW-B-10 | Standby 第一句未命中 → turn 關閉,故第二句句首的 Prefix 是新 turn 的句首,正常觸發 | test_smart_turn_bypass.py | ✓ PASS |
SW-B-11 | 同上,第二句句首的特定指令正常觸發 | test_smart_turn_bypass.py | ✓ PASS |
SW-X-01 | Timeout 15s:TTS 說完起算,15s 無人說話 → 回 Standby | test_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 | ✗ FAIL |
SW-X-06 | 10 秒內有人說話 → 不觸發第二次主動問候 | test_active_greeting.py | 部分覆蓋 |
SW-X-07 | 退出聆聽路徑一(UX§1.3.4):第二次問候播完後再 =5秒 無人說話 → 回 Standby | test_active_greeting.py | 部分覆蓋 |
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-01 | STT 是唯一 EOT authority:每個 VAD stop 由目前 backend 判定。TurnSense/Smart Turn 評分 turn tail;COMPLETE 當 p_complete > turn_model.<backend>.complete_threshold(兩個 scoring backend 出廠皆為 0.6),然後立即 commit。模型的 INCOMPLETE/INVALID 與 VAD backend 的正常 stop 都等該 backend 的 wait_secs,期間有新語音則延續同一 turn。立即完成的非空 final 與 eot.decision 在同一個 TranscriptionFrame | tests/test_eot_state.py、tests/turnsense/test_policy.py、tests/test_eot_kws_precedence.py、tests/test_turn_backend_config.py · live:test_remaining_ux_gaps.py | 部分覆蓋 |
SW-EOT-02 | KWS 事件(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-05 | kitt-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 | 已廢止:隨拒識門檻移出 STT | — | N/A |
| SW_id | 行為(規格) | 測試 | 結果 |
|---|---|---|---|
SW-X-04 | 說解除喚醒詞 → 切 Standby(UX§2.1 指定 13 個:謝謝/再見/退出/退下/滾蛋/滾/Thank you/Thanks/Goodbye/ByeBye/Dismiss/Quiet/Shut up) | live:test_exit_words.py · test_btn_control.py · 離線比對層:tests/test_exit_word.py | ✗ FAIL |
SW-WM-03 | 退出詞 alias 必須錨定到已設定的退出詞:alias 群組的 canonical 不在 exit_words 內則整組忽略 | tests/test_wake_matcher.py | ✓ PASS |
SW-EOT-02 | KWS 事件(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_id | 行為(規格) | 測試 | 結果 |
|---|---|---|---|
SW-X-08 | 拒識之後必須維持聆聽(UX§10.2):越過 UX§8.2 門檻的語句丟棄不往下送,但不得因此離開 Active——拒識是「裝沒聽到」,不是「結束這一輪」 | test_rejection_thresholds.py | ✗ FAIL |
SW-LANG-01 | 整句英文指令須被辨識,且不需切換任何語言設定 | test_bilingual.py | ✓ PASS |
SW-LANG-02 | 同一句內中英夾雜時兩種文字都要保留:辨識器須在句中換文字系統,不是為整句選一種語言 | test_bilingual.py | ✗ FAIL |
| SW_id | 行為(規格) | 測試 | 結果 |
|---|---|---|---|
SW-W-19 | kws.method.name=stt 執行 transcript 喚醒;manual 跳過 match_wake 與 strip_wake_prefix | test_kws_method_policy.py · test_kws_method_manual.py | ✓ PASS |
SW-B-12 | manual 保留 match_bypass;已驗證 current-segment exact/prefix、非句首不觸發與 prefix 跨段 continuation | test_kws_method_policy.py · test_kws_method_manual.py | ✓ PASS |
SW-UI-07 | stt / manual 共用 InterruptionFrame(agent_active/agent_standby) 控制 | test_kws_method_policy.py · test_kws_method_manual.py | ✓ PASS |
SW-CFG-03 | kws.method.name 預設 stt、只接受 stt / manual;舊設定鍵拒絕啟動 | test_kws_method_config.py · test_status_page_settings.py | ✓ PASS |
SW-B-01 | Prefix(default「冷氣溫度*」「導航到*」):非喚醒下前綴符合即整句往後送 | test_bypass_prefix.py | ✓ PASS |
SW-B-02 | Prefix 不在句首不觸發 | 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 按鈕 → 回 Standby | test_btn_control.py | ✓ PASS |
SW-UI-01 | WebUI 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-03 | UserA 講話中(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 | ✗ FAIL |
SW-PERF-05 | 喚醒率回歸下限 ≥90%(UX§1.1) | test_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 loop | tests/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=vad。smart-turn 不 preload TurnSense,對 VAD-stop 快照呼叫 _predict_endpoint,p > 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=vad | tests/test_turn_backend_config.py、tests/test_smart_turn_detector.py、tests/test_eot_kws_precedence.py、tests/turnsense/test_runtime.py、tests/test_status_page_settings.py | ✓ PASS |
SW-ITN-01 | final 的 ITN:中文數字正規化 + 繁簡轉換 | tests/test_text_converter.py | ✓ PASS |
SW-ITN-02 | interim 與 final 的 ITN 刻意不同:interim 只做 s2t(),final 做完整 itn();故兩者在數字上可以不同,這是設計不是 bug | tests/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.141699999 | test_text_converter.py::test_all(pytest -k "G1- or G2- or G12-") | ✓ PASS |
| SW-ITN-05 | 百分比(含正負號)轉阿拉伯數字:百分之一→1%、百分之負零點五→-0.5% | test_text_converter.py::test_all(pytest -k G3-) | ✓ PASS |
| SW-ITN-06 | 分數(含正負號)轉阿拉伯數字:二分之一→1/2、負三分之一→-1/3 | test_text_converter.py::test_all(pytest -k G4-) | ✓ PASS |
| SW-ITN-07 | 日期/時間:數字轉阿拉伯數字,單位詞維持中文:一月五號→1月5號、十一點五十九分五十九秒→11點59分59秒 | test_text_converter.py::test_all(pytest -k "G5- or G8-") | ✓ PASS |
| SW-ITN-08 | 程度副詞維持全中文、不得數字化:一點點、一些、十分滿意 等 40+ 慣用語 | test_text_converter.py::test_all(pytest -k G7-) | ✓ PASS |
| SW-ITN-09 | 完整車內出貨指令語料 495 列、29 個產品功能分類,逐句 100% 正確;分類本身不得靜默缺漏 | test_itn_lexicon.py::test_every_incar_command_normalises_as_adjudicated/test_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.yaml 的 model.key、utils/model_manager.py 註冊表、三個 Dockerfile* 的 COPY、deploy.sh 的 MODEL_DVC_FILES、.dockerignore 的 build context 白名單 | tests/test_augment.py | ✓ PASS |
SW-OPS-03 | Per-turn debug log 檢索端點(GET ,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.py、tests/test_debug_log_config.py、tests/test_debug_log_endpoint.py、tests/functional/live/test_debug_logs.py | ✓ PASS |
SW-OPS-04 | Per-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 只套用 turnsense | tests/test_turn_backend_config.py、tests/turnsense/test_policy.py、tests/test_smart_turn_detector.py、tests/test_status_page_settings.py、tests/test_eot_kws_precedence.py | ✓ PASS |
SW-OPS-05 | live 測試 harness 不得產生 false pass:tests/docker-test.sh 重用映像的新鮮度檢查,必須涵蓋 Dockerfile 所有 COPY 進映像的路徑。清單以 Dockerfile 為來源推導驗證,不得手工維護——漏一個路徑就會讓套件對著不是受測版本的程式碼跑出綠燈 | tests/test_augment.py::test_docker_test_rebuilds_when_any_baked_in_path_changes | ✓ PASS |
SW-OPS-06 | provider=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-07 | C-X1 部署契約:host network、ClusterFirstWithHostNet、NVIDIA RuntimeClass、既有 Gateway 與 localhost 服務互連;預設 CLI 在操作端 build/push Thor image 後部署,--deploy-only 只部署既有 image | tests/test_cx1_deployment.py(6 項) | ✗ FAIL 5 passed/1 failed:test_cx1_default_builds_pushes_and_deploys 因本機缺 lora-v2 模型目錄 |
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-03 | LoRA 接線:不可達目標必須拋錯而非靜默不啟用;可訓練集合非空且只含 lora;merge 後鍵集合=base 且權重確實改變 | tests/test_finetune_lora.py | SKIP |
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_props(model_type / lfr_window_size / neg_mean / inv_stddev / lang_* / with_itn …)在 INT8 與 FP16 轉換後必須逐鍵存活——少一個,sherpa-onnx 就載不起來;FP16 圖必須保持 fp32 的 graph I/O(keep_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 換成 MatMulInteger/MatMulNBits 等運算子,而目標裝置的 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-02 | cycle 報告的累積 ledger 只增不減:最新一份報告必須涵蓋歷輪聯集的所有 SW_id(不只是對前一輪比對——那會讓某個 ID 消失一輪後就永遠消失,因為下一次比的是兩份都已缺它的報告)。報告檔名須與 docs/dev-specs/ 的 cycle stem 對應 | tests/test_report_integrity.py | ✓ PASS |
SW-DOC-03 | 報告要定義自己用的技術名詞(CLAUDE.md Rule 5):凡讀者需要查才看得懂、且結論依賴其意義的名詞(sm89/RTF/MatMulInteger/CMVN…),必須在報告的名詞定義節裡說明「是什麼」與「該輪為何重要」。⚠️ 機械檢查的範圍是「有名詞表的報告,其涵蓋必須完整」;「報告有沒有名詞表」判不了誰是該輪的 cycle,故列入 close-out checklist | tests/test_report_glossary.py | ✓ PASS |
SW-DOC-04 | 報告的圖表要能被讀懂(CLAUDE.md §6 item 4b):每張圖需有 aria-label;每張數據圖需有 caption,且 caption 要寫出結論而非重複標題(架構/流程圖屬 item 4,豁免)。⚠️ 規則不追溯——報告以「至少有一張帶 caption 的圖」表示採用此慣例,之後其圖才受檢;該輪之前無任何報告為圖加 caption | tests/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):報告引用的每一個 A/D/E/F/R 都要在表裡有自己一列——只定義「家族」與範圍不算數,那回答的是「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 |
| SW_id | 行為(規格) | 測試 | 結果 |
|---|---|---|---|
SW-W-06 | smart-turn 第二句開頭說喚醒詞不觸發喚醒 · 2026-08-17 廢止,與 SW-W-02 互斥,見 SW 規格的廢止紀錄。由 SW-W-13 取代 | — | — |
SW-B-04 | Prefix 出現在 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-04(UX§3.1 490/510 ms 斷句邊界),但 kitt-stt-sw-spec.md 從未登記此 ID;保留以滿足 SW-DOC-02 的累積 ledger。 | test_remaining_ux_gaps.py(live,docstring 引用) | —(未註冊) |
| —(已移除) | — |
| SW_id | 行為(規格) | 測試 | 結果 |
|---|---|---|---|
SW-HW-01 | from_sense_voice(decoding_method="modified_beam_search") 在未提供 hotwords_file 時必須正常建構並解碼,不得因無條件呼叫熱詞載入而 hard-exit | tests/test_sensevoice_hotword_python_api.py | ✓ PASS |
SW-HW-02 | 載入 hotwords_file 後,對已知因該詞彙被誤聽的音檔,解碼輸出必須改變並更接近正確答案 | tests/test_sensevoice_hotword_python_api.py | ✓ PASS |
SW-HW-03 | hotword_debug 預設 False,此時 hotword_debug_json 維持空字串 | tests/test_sensevoice_hotword_python_api.py | ✓ PASS |
SW-HW-04 | hotword_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-06 | add_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-08 | features.enable_hotwords 與 hotwords.{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 |
| 驗收 ID | 行為 | 測試(file::test) | 結果 |
|---|---|---|---|
| A1 | 第一次 interim 無前次延遲 → 用 initial_interval | test_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–A5 | margin≤1 / 地板≤0 / 初始值≤0 → ValidationError | test_audio_cfg::test_safety_margin_must_be_greater_than_one / test_min_interval_must_be_positive / test_initial_interval_must_be_positive | ✓ PASS |
| A6 | ASR_INTERIM_SAFETY_MARGIN 覆寫 / 未設時吃 YAML | test_audio_cfg::test_env_var_overrides_margin / test_no_env_var_keeps_yaml_default | ✓ PASS |
| 驗收 ID | 行為 | 測試(file::test) | 結果 |
|---|---|---|---|
| SW-PERF-01 | 連線無錯誤 | test_audio_stress::test_audio_stress_no_errors | ✓ PASS |
| SW-PERF-01 | 90 秒連續長句仍收到 final | test_audio_stress::test_audio_stress_final_received | ✓ PASS |
| SW-PERF-01 | time 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 |
| 驗收 ID | 行為 | 測試(file::test) | 結果 |
|---|---|---|---|
| B1 | interim 只套用 s2t(),不做數字正規化 | test_batch_asr_manager_itn_split::test_interim_inference_only_applies_s2t_not_numeral_normalization | ✓ PASS |
| B2 | final 仍套用完整 itn()(s2t + 數字),不受影響 | test_batch_asr_manager_itn_split::test_offline_inference_still_applies_full_itn | ✓ PASS |
| 驗收 ID | 行為 | 測試(file::test) | 結果 |
|---|---|---|---|
| B14-R1 | RTF 只計 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_task | test_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 |
| F_id / SW_id | Feature / 行為 | 歸屬 |
|---|---|---|
| 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.1/UX§7/UX§9/UX§10(除 11.3) | 問候語 UI/Guardrail/回復策略/顯示策略 | 下游 UI/LLM/TTS |
| 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 |
main 的現行需求補充2026-09-15 rebase 到 origin/main 3c0282d7 後,現行 SW 規格共有 159 個 SW_id。下表補入 baseline 之後由 main 新增的 39 個需求;行為、驗收準則、測試與 PRD 歸屬依 rebase 後規格,未在本次 rebase 驗證範圍內的結果一律標為 NOT-VERIFIED。
| SW_id | 預期行為(現行規格) | 驗收準則 | 測試 | PRD | 本次 rebase 結果 |
|---|---|---|---|---|---|
SW-DOC-10 | 報告檢查只能讀「散文」,不得讀進內嵌的二進位酬載:報告把音檔/圖片以 data: URI 內嵌(Artifact CSP 擋外部媒體,可播放的音檔只能隨檔案走),而 base64 是 64 種字元的任意字串——夠長就一定拼得出檢查要找的短樣式。2026-09-09 報告的音檔恰好拼出一個 follow-up 編號的樣式,被 SW-DOC-06 讀成「報告引用了這個 follow-up」而失敗——那個編號從來沒有任何人寫過(此處刻意不寫出該編號:寫出來就會讓引用它的文件真的引用到一個不存在的 ID)。八個 test_report_*.py 一律改走 tests/report_text.read,由該處單一決定「散文到哪裡為止」。⚠️ 真正危險的失敗方向是過度剝除:樣式若貪婪到吃掉整份文件,八個檢查會同時在空字串上通過,報告從此可以無聲地掉掉半個 ledger——故兩個方向都要釘 | 酬載(含跨行、圖片)被移除;其周圍散文逐位元組保留;<audio> 元素本身仍可見;非 base64 的 data URL 不動;剝除後文件不得被吞掉 | tests/test_report_text.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-FT-09 | 外部音檔注入訓練語料(非單一 TTS 供應商):build_corpus.py 之外,可把預先產好的 {speaker,text,wav,lang} manifest(如 ZipVoice 多語者 zero-shot clone)附加進 Kaldi 式 corpus/{train,val}/{wav.scp,text.txt},讓下游(build_training_set.py→sensevoice2jsonl→增強→訓練)一視同仁。分流依語者(比照 build_corpus.py 的 by-voice leakage rule:held-out 語者只進 val/);uid 為穩定 hash,重跑冪等(已存在的 uid 跳過);manifest 指到的 wav 缺一個就硬失敗(缺檔時語料會悄悄變小,訓練 log 卻看不出差別,所以不允許)。空/不存在的 manifest 是 no-op(純 Google TTS 語料不受影響)。退化 render 過濾(--min-dur-secs)+語者排除(--exclude-speakers),2026-09-14-ux-spec-model-ft-exit-word:zero-shot TTS clone 對極短目標(單一國語音節如「滾」)會產出 ~0.1s 近乎靜音的 clip,拿它訓練等於教模型聽到靜音就寫出這個詞。低於 --min-dur-secs 的 clip 跳過但大聲(stderr WARNING + 逐詞 tally),非硬失敗;--min-dur-secs 0(預設)為關閉。整個 reference 語者渲染品質差時(如「林志玲」的短詞出現 退下→S。/滾蛋→Goodbye),--exclude-speakers 在最前面就把該語者所有列丟掉(stale manifest 的防護網;生成端也應已排除)。驗收聽測者永遠 held-out(2026-09-14-ux-spec-model-ft-exit-word,使用者裁定):跑驗收聽測的真人(現為 john/patty/parker)也在 ZipVoice zero-shot clone 的語者池裡。拿他們聲音的 clone 訓練,模型會記住他們的音色,再找同一批人驗收,分數自然虛高(train/test 洩漏)。這些名字必在 run_pipeline.sh 的 ZIPVOICE_HELD_OUT(clip 只進 val/、永不進 train/,仍可驗證);換驗收人時清單與釘住它的測試一起改。與 --exclude-speakers 的差別:held-out =留著驗證、不訓練;exclude =渲染太差,訓練與驗證都不用。2026-09-14 擴充:除了時長門檻(抓退化渲染)外,另接受一份人工試聽裁定檔(--rejects,JSON:整語者 + 逐「語者×句子」)。裁定檔的每一項都必須在 manifest 裡對得到,對不到就硬失敗。語者 id 打錯(kelly 寫成 Kelly)時如果默默略過,人判定不能用的 clip 會照樣進訓練,而且完全看不出來 | 外部音檔注入測試全過(含 min-dur 過濾/預設關閉/無法解析的 wav 保留/驗收聽測者在 held-out 預設集內) | tests/test_external_audio.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-FT-10 | 喚醒詞/退出詞訓練列的雙語言標籤(B1):sensevoice2jsonl 用 base 模型自動標語言 token,「Hi Foxtron」「Dismiss」單說會被標成 <|en|>,服務卻預設用 <|zh|> 解碼。所以訓練框架要在 auto-label 之後,替這些列補上另一種 prefix 的複本。喚醒詞(嗨鴻華/嗨鸿华/你好鴻華/Hi Foxtron)與整句退出詞 positive(templates.yaml 的 exit_alone,key 前綴 exit_alone)都雙向翻:<|zh|>↔<|en|> 各補一份,key 加 __dl,重跑冪等。退出是喚醒契約的另一半,卡在待命出不來和喚不醒一樣糟,所以兩種 prefix 下都要穩(2026-09-14 使用者裁定)。⚠️ 這個方向量到過代價:短的國語退出詞(滾/退下)在 <|en|> prefix 下訓練,會讓模型在音近「hi」的「嗨」後面插一個拉丁 i(嗨鴻華→嗨i鴻華)。這種迴歸出現在中文喚醒詞的 MODEL 軸,不在退出詞上。出貨前一定要先看這個數字。不翻的列:其他語言的列、一般語句、bypass、退出詞夾在句中的 negative,以及產品語彙詞(TermKind.VOCAB)。語彙詞 auto-label 給的語言就是它該訓練的語言;它在某個 prefix 下寫錯只賠一個詞,喚醒或退出失敗卻賠掉整個 turn(2026-09-14 使用者裁定) | 雙語言標籤測試全過(喚醒詞與整句退出詞 positive 雙向/句中退出詞 negative 不翻/語彙詞不翻) | tests/test_dual_lang_label.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-FT-11 | 多語者 ZipVoice 生成器的退出詞模式:gen_zipvoice_wake.py --mode exit 產出 UX§2.1 的 13 個退出詞單說(6 zh + 7 en)+少量「退出詞 + 鴻華/Foxtron」tier-2 種子(matcher 尚未消費,僅預先備料)。每個英文退出詞(Dismiss/Goodbye/Shut up…)都不帶 Foxtron 標記,故 _lang_of 必須以顯式對照表判為 en——否則會 fall through 成 zh、被 Mandarin 音色念成「Dmiss」而污染語料;退出詞集合內 slug 不得碰撞 | 生成器離線單元全過 | tests/test_gen_zipvoice_wake.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-FT-12 | 英 | 6,000 | 300 | 英文一般語音不忘 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-FT-13 | 階段續跑的判斷必須認「做完」而不是「開始做」:run_pipeline.sh 每個階段有產物就跳過,die() 也明講「重跑同一個指令即可,已完成的階段會自動跳過」。但 stage 3 的 guard 原本看 model.pt——funasr 每次 validation 就重寫 model.pt,兩小時的訓練跑不到一分鐘該檔就存在。於是「中止後照指示重跑」會跳過訓練,stage 4-9 照樣評分/merge/匯出一顆只跑了一個 epoch 的模型,而且任何一行 log 都不會說訓練沒跑:stage 5 對 model.pt.ep* 一律評分,結果不是半成品僥倖過三軸而出貨,就是全 NO-GO 後 stage 7 把原因歸給語料(「這通常是語料問題」)。改法:sentinel 改成 ${OUT_DIR}/.train-complete,只在 trainer 回傳 0 之後才寫,且把產生它的設定寫進檔案內容(epochs/lora/rank/alpha/lr/data/aug)。設定相同才跳過;設定不同一律停下報錯,不得靜默沿用舊模型(同名重跑換參數會讓兩次看起來像在比較,實際上只訓練過一次);沒有 sentinel 但有殘留 checkpoint 時要大聲說(步數對不上的不會被覆蓋,stage 5 會混在一起評)。2026-09-14 於 cycle 2026-09-14-ux-spec-model-ft-exit-word 實際踩到 | 續跑 guard 測項全過(中止後重訓/設定相同才跳過/設定不同硬失敗/殘留 checkpoint 有告警/sentinel 只寫在成功分支) | tests/test_run_pipeline_guards.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-HR-01 | 公司名「鴻華先進」由 HR 同音規則寫對:src/hr/build_hr_fst.py 詞表含 鴻華先進 ← hong2hua2xian1jin4(套用模糊聲調/n-ng/l-n),出貨的 fuzzy_srdc_replace.fst 必須由這份詞表重建。規則要四個音節連在一起才會觸發,所以單說「仙境」「先進」不受影響。已知且接受的限制:「鴻華」後面緊接真的「仙境」(如「嗨鴻華,仙境谷怎麼走」)聽起來就是同樣四個音節,會被寫成「鴻華先進谷」——HR 和熱詞都分不開(2026-09-15 實測兩者皆 13/13) | 兩位非驗收語者(ZipVoice)12 句正例全部寫出「鴻華先進」;「仙境」「先進」對照句 4 句保留原字、不被改寫;提交的 FST 與詞表重建結果相同〔預設測試環境 NOT-ENFORCED(需 hr-build group 的 pynini,沒有就 skip)〕。前兩項要先 dvc pull 出貨模型,模型不在本機時 skip | tests/test_company_name_recognition.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-HW-09 | 出貨熱詞含公司名 鴻華先進 :2.0,和 SW-HR-01 的 HR 規則一起用(使用者裁定兩層都做)。它是只需要寫對、不觸發任何行為的詞,不列入喚醒/退出詞的比對。權重停在 2.0:只用熱詞時 2.0 已經幾乎全對,3.0 開始改動無關的喚醒詞句子 | hotwords.txt 恰好含這一行;完整鏈路下正例全部寫對 | tests/test_hotwords_default_file.py、tests/test_company_name_recognition.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-HW-10 | 出貨熱詞含車控詞區塊(2026-09-10 corpus-hotword-enhance 那一輪,2026-09-11 起由使用者設計;2026-09-15 使用者要求補進 cycle 2026-09-14-ux-spec-model-ft-exit-word)。內容是使用者主觀決策,依領域分組;每組先放 2026-09-10 那一輪量過的詞條,再放 # manual 下使用者人工加的(依 sup_single_turn_0.2.0 conversation set 測試結果,如 前擋除霧、主駕駛位、後車箱,沒有量測);air conditioner 是 2026-09-15 為 live 中英夾雜句加的,不帶權重,只抽查過那支音檔。核心詞和產品實際的長說法並列(如 除霧 :3.5 和 打開除霧/除霧關閉),因為長說法只在真的講那句指令時才會整條命中,加分更多、誤觸發範圍也更窄;英文照模型實際寫出的樣子列(AC、SYNC模式、MAX模式)。可以帶逐詞權重,也可以高於喚醒詞(使用者 2026-09-11 裁定)。清單由使用者設計,測試不釘內容,只守看不出來的錯:不得重複、喚醒/退出安全網與 config-basic.yaml 一致、來源檔全繁體。評測工具:corpus_hotword_eval.py 比出貨前後兩份熱詞檔(各分 HR 開/關),只讀 TTS 快取,兩份熱詞一樣就拒跑;paper_bench.py 的熱詞/HR 旗標預設關閉,歷史數字的量法不變 | 60 條那一輪在 …-kitt-wake-lora-v2+當時 9 行安全網上量,使用者人工加的沒有量測,本輪出貨模型都沒有重量(使用者要求補上、不重跑):live 除霧、小憩模式 各 5 次全對,同一映像換回出貨前的熱詞檔就回到 除物/小氣;AC :2.0/MAX :2.0 讓 LibriSpeech WER +0.040/+0.063 個百分點,使用者接受。離線:評測語料自洽、與 vc_core_corpus_0824 零重疊、語料的喚醒/退出組跟 config 的喚醒詞+13 個退出詞一致(2026-09-15 補合成);paper_bench.build_recognizer() 不給熱詞參數時完全不傳熱詞相關參數 | tests/test_hotwords_default_file.py、tests/test_hotword_corpus_fixture.py、tests/test_paper_bench_hotwords.py、tests/functional/live/test_hotwords_vehicle_terms.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-03 | 잘 해요。 被刪成 。,一個什麼都不做的指令)。字元層判定的程式碼(src/lang/script_policy.py)保留,但<b>只供量測工具使用</b>,不在服務路徑上 | 第三語言的 final <b>原樣送出、逐字元不變</b>;不得刪字;不得因違規而重新解碼(以<b>解碼次數</b>斷言,只看文字看不出來);<b>不得產生任何語言政策相關的 log</b> | tests/test_batch_asr_manager_lang_policy.py · tests/test_script_policy.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-04 | 語言政策研究工具的判讀邏輯:分層(等數量的句長分帶,餘數不得掉句)、外洩統計(以句為單位計 incidence、以字元計語言別)、以及 oracle 對照值——每句取誤差最小的那個做法,用以界定「任何以語言選擇為手段的方案」(釘 zh/釘 auto/投票後釘定/任何未來的語言分類器)最多能改善到哪裡。⚠️ 這條守的是量測而非產品行為,故 PRD 欄為 STT_EXTRA;它之所以要被測,是因為 oracle 若悄悄退化成「總是回傳基準做法」,整輪的結論會在數字看起來完全合理的情況下變成空話 | 24 項判讀測試全過;oracle 在「所有做法輸出相同」時必須等於 baseline,在「各做法互補」時必須取到零誤差,且永不劣於最好的單一做法 | tests/test_lang_policy_eval.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-05 | model.language 是組態值,且它的預設必須等於出貨值。(a) model.language(auto/zh/en/ja/ko/yue)取代 BatchASRManager._build() 裡寫死的 "zh",使其成為組態值;出貨預設為 zh——它是一句話的起始語言而非完整政策,英文由 SW-LANG-06 的 per-stream 閂鎖達成。⚠️ 2026-09-09 曾改為 auto 並於 2026-09-10 改回:auto 的用處只有一個——判斷這句是什麼語言。但釘 zh 解碼時模型照樣認得出來,而且從不回報第三語言(早期用密集時間點量到 98.9%,auto 只有 92.2%;改用服務真實節奏重測,英文是 458/493 句 92.9%,見本輪報告 §4.2)。既然判斷語言不需要 auto,它造成的 interim 抖動就沒有東西可以抵銷;無效值必須在建構時拋錯,因為 sherpa-onnx 只會記一行 Unknown language 然後靜默退回 auto,組態打錯字會以「服務正常但設定不是你以為的那個」出貨。(b) final 文字經 ITN 之後再過語言政策守衛:含政策外字元時剔除該字元並記 WARNING(不是 DEBUG——它響代表聲學模型跑出產品的兩種語言之外,其發生率就是模型/組態變壞的訊號)。守衛只作用於 final,interim 刻意不擋(UX§11 允許句子還短時暫時猜錯語言)。⚠️ 動作優先為「以 zh 重解」,刪字僅作為最後手段(2026-09-09 改判:刪字會把 '잘 해요。' 掏空成 ' 。',使用者對此零容忍且其優先度高於 CER)。重解之所以可行,是因為本輪為 vendored sherpa-onnx 補上了 per-stream language(SW-LANG-07);在那之前 1.13.0 沒有安全的 runtime 切換語言途徑——OfflineStream::SetOption("language",…) 對 SenseVoice 是靜默 no-op(呼叫成功、無人讀取),OfflineRecognizer::SetConfig 即使把組態原封不動推回去也會改變共用 recognizer 的行為(量測:auto→zh→auto 後輸出既非 auto 亦非 zh),在單一共用 recognizer 上會污染其後每一次解碼;而重解相對刪字的實測優勢僅 0.4 個百分點(相對 CER,且只在車控語料上有差) ⚠️ 2026-09-10 起本 ID 不再涵蓋任何文字處理(見 SW-LANG-24):服務端不刪字、不重解、不偵測、不記警告,final 原樣送出。另補一條先前漏掉的檢查:ModelCfg.language dataclass 的預設值本身——先前只測了 BatchASRManager 的預設,dataclass 因此一度停在被否決的 auto,任何省略該鍵的 override 檔都會靜默取得它。 | language 預設為 zh、傳入值會到達 from_sense_voice()、無效值拋 ValueError;ModelCfg 的 dataclass 預設 = config-basic.yaml 的值 = 合法語言值(三者一致);乾淨 final 逐位元組不變;含第三語言的 final 亦逐位元組不變(不得刪字、不得重解、不得記警告) | tests/test_batch_asr_manager_lang_policy.py · tests/test_config_language_default.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-06 | 一句話單向「閂鎖」到英文,中文則是預設(UX§11)。recognizer 以 zh 解碼,所以中文不需要偵測——它已經是預設,interim 行為與加入本功能前完全相同。英文是唯一的狀態轉移:interim 連續 KittSttService._EN_LATCH_INTERIMS(=2)次回報 en 時,該句 UserSession.pinned_language 鎖定為 en,同時對現行 stream 下 set_option,使該句剩餘的 interim 與 final 都改用英文。鎖定後不可解除:一句話不會中途換語言,可反轉的 pin 會把 interim 改寫放回畫面上。可行的前提是 釘 zh 的解碼本身就會回報它聽到的語言。按服務真實節奏實測:英文 493 句裡有 458 句(92.9%)至少有一次 interim 回報 en;反過來,中文跑了 5997 次 interim 只誤報 2 次英文,而且從未回報第三語言。(⚠️ 早期用密集時間點量到英文 97.9%;辨識次數變多,語言就比實際更早判出來,所以那個誤差只往一個方向偏。)既然如此,判斷語言就不必去付 auto 那筆 interim 抖動的帳。至於為什麼要連續 2 次而不是 1 次——車上 88% 是中文,判錯一次就等於拿錯的語言把中文重解一遍;實測要連續 2 次才鎖定時,1359 句中文一次都沒有觸發 | 一次 en 不鎖定;連續兩次才鎖定;中間被 zh 或第三語言打斷則重新計數;鎖定後 zh/第三語言都不得解除;中文 interim 不得產生任何 pin(預設就是 zh);第三語言(ja/ko/yue/nospeech)永不被 pin;鎖定時須對 stream 下 set_option 讓剩餘 interim 跟著切換;stream 不支援 set_option 時 final 仍須鎖定;無 interim/stream 不可讀時不得拋錯;新的一句話必須從未鎖定狀態開始 | tests/functional/scenarios/test_language_pinning.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-07 | vendored sherpa-onnx 支援 per-stream language:OfflineStream::SetOption("language", …) 須被 SenseVoice recognizer 讀取(ResolveLanguage()/ResolveTextNorm(),兩處:batch 的 DecodeStreams 與 kitt-stt 實際走的 DecodeOneStream),未設定時回落到建構時的 model_config。⚠️ 必須是 per-stream 而非改 recognizer 狀態:kitt-stt 全服務共用一個 recognizer,改共用狀態會污染其後每一次解碼。實測 OfflineRecognizer::SetConfig 即使把組態原封不動推回去也會改變輸出(晚咗→马饮九),故不可用。⚠️ 上游 1.13.7 只把 use_itn 接成 per-stream,language 沒有;且本地此檔已因熱詞 patch 與上游分歧 383 行,cherry-pick 不可行,本地依上游 use_itn 的樣板自行實作 | per-stream 設定須:①生效(輸出與未設定時不同)②與建構時就設該語言逐位元組等價 ③不污染共用 recognizer(其後未設定的 stream 回到預設)④auto/zh 交錯輪流穩定 | tests/functional/scenarios/test_language_pinning.py(服務層接線)· 建置驗證見該輪報告 §6 | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-08 | interim 側的語言指標(UX§11):kitt-stt 是 pseudo-streaming,每次 interim 都會推到 UI 當字幕,所以錯的語言 id 不是內部變數而是螢幕上的韓文。因此除了 final 準確度,另有兩個必須被量的指標:①定案時間(顯示語言不再變動的時刻,而非「第一次答對」——zh→en→zh 最終正確,但已讓使用者看過兩種語言);②政策外語言曝光(第三語言在字幕上停留的秒數與最後出現時刻)。UX§11 允許「前期剛講話時短暫出現其他語言」,那是一句關於數字的主張,故須量測而非宣稱。兩者皆以絕對時間計——使用者感受的是「開口後幾秒」,不是「一句話的百分之幾」。⚠️ 中英夾雜一律視為 zh(2026-09-10 產品決議):夾雜句以 zh 解碼兩邊都留得住(SW-LANG-02 已量到英文在 zh 解碼下存活),以 en 解碼則整段中文消失——兩種錯誤代價不對稱 | 定案時間須取「之後不再變動」而非「首次正確」;政策外曝光須自前一次 interim 起算;被釘定的策略須以其實際會顯示的序列計分(釘定前跟隨 auto,否則會為它從未顯示過的字幕受罰);空語料不得除以零 | tests/test_lid_timeline.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-09 | interim 串流的穩定度指標(UX§11):language 設定的代價有兩項,而只看 final 的量測只看得到一項。kitt-stt 會重解成長中的 buffer 並把每次結果推到 UI,所以「對每個前綴解出不同結果」的設定會在使用者眼前改寫字幕——auto 正是風險所在,因為它每次 interim 都重新判斷語言。三項指標分開量,因為它們會分開失敗:①改寫率(本次 interim 不是上一次的延伸=使用者已讀到的字被改掉,串流 ASR 的標準判準);②churn(整句累計被改寫的字元數 ÷ final 長度,即「使用者總共看到多少變動」);③收斂點(interim 何時起等於 final 且不再變動——穩定但穩定地錯,不算好串流)。⚠️ 比較的是使用者看到的文字,故僅去空白、不做計分正規化:標點與大小寫在螢幕上,改動它們就是使用者看到的改動 | 純附加的串流改寫率為 0;全句改寫的 churn 須大於等量附加;空白差異不算改寫、標點差異算;收斂點須取「之後不再變動」;空串流/空 final 不得拋錯或除以零 | tests/test_interim_stability.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-10 | 合成評估音檔的適用性檢查:車控研究的每一個英文數字都是 TTS 數字,所以壞掉的音檔<b>不會顯示為「音檔壞了」,而是顯示為「模型比較差」</b>——與真實發現無法區分。兩層獨立檢查:①訊號層(靜音/近靜音/短到不可能容納該句/多為填充靜音;不需模型);②內容層(解碼結果與應說內容差太遠)。內容層是必要的,因為它抓的是聽起來正常但其實壞掉的失敗——本輪首次 pilot 就發生兩種:跨語言 voice clone 把 prompt 的中文殘句接在英文指令前面(每句都以「23度」開頭),以及另兩個聲音把開頭數個字吃掉;兩者的 RMS 與長度都正常。⚠️ 內容層門檻刻意寬鬆(WER ≤ 0.5):它問的是「這個音檔能不能用來量」,不是「模型準不準」——把門檻收緊到足以評價模型,就會把語料存在的目的(難句)一起丟掉。⚠️ 修正做法:prompt text 必須是裁切後音檔的逐字稿,與交給模型的音訊完全一致,否則模型會用「發明或吞掉語音」來弭平落差 | 正常音檔通過;靜音/過短/多為填充者各自被對應理由擋下且所有失敗理由都要回報(非只回報第一個);立體聲須降混而非拒收;空音檔不得拋錯;輕微辨識錯誤不得判為壞音檔;prompt 殘句與開頭吞字須被內容層抓到;空參考須回報而非除以零 | tests/test_tts_audio_health.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-11 | 報告的逐例對照必須重現出貨的閂鎖,而不是它的轉述:範例是讀者比表格更信任的證據(它看起來像可以親眼看到的事),所以錯的範例特別貴——它會記錄一條服務並不遵守的規則,而套件裡沒有別的東西會發現。故範例產生器的閂鎖門檻直接對 KittSttService._EN_LATCH_INTERIMS 斷言,語意也對齊:連續 N 次 en 判定才釘、非 zh/en 判定會打斷連續、釘定後不可解除、切換自釘定的下一次 interim 起生效(判定讀自已經跑完的那次解碼)。另含每步編輯量與「改寫 vs 純附加」的區分——附加對使用者不可見,改寫才是畫面上會閃的東西 | 純中文永不觸發;連續兩次 en 觸發;單次 en 不觸發;ja 打斷連續;釘定不反轉;切換自下一次 interim 生效;純附加不計為改寫 | tests/test_lang_examples.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-12 | 逐 interim 準確度曲線的判讀契約:報告要回答「釘 zh 時英文的 interim 是不是整句都差、閂鎖後是不是變好」,而這個量測有一個會反轉結論的陷阱——一次 interim 只解了音訊前綴,拿它比整句參考會把「還沒講出口的字」全算成刪除,圖就只顯示音訊到了多少。折衷是比參考文字的前 N 詞(N=已吐出詞數),代價是什麼都不吐的解碼得 0% 錯。故契約規定:wer 必須與 emitted 成對回傳、所有彙總必須以詞數加權(否則吐 1 詞與吐 8 詞等重),且對齊到閂鎖點的重新編號只用於分離「閂鎖的功勞」與「音訊變多而自然變準」 | 正確前綴得 0;被截斷的詞按已吐詞數計;未講出口的字不計為刪除;空 interim 回報 emitted=0;大小寫標點不計為錯;彙總以詞數加權;未觸發的句子不進入對齊統計 | tests/test_interim_accuracy.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-13 | 量測用的辨識節奏必須由出貨設定推導,不得自訂:服務的過程辨識<b>不是固定週期</b>,而是 下一次等待 = max(上次解碼時間 × safety_margin, min_interval)(AudioCfg.next_interim_interval)。出貨值(起始 0.5s、下限 0.5s、係數 1.5)配上約 17ms 的實測解碼時間,代表<b>下限完全主導</b>,真實節奏就是每 0.5 秒一次。⚠️ 自訂一組較密的時間點(例如 0.3/0.5/0.7/0.9/1.2s)會量到一個不存在的服務,而且誤差只往一個方向偏:辨識次數變多 → 語言判定比實際更早到 → 「先聽再切換」這類做法看起來比實際有效。本輪早期即因此高估英文改善幅度,改用推導節奏後往下修正。節奏必須<b>讀 config-basic.yaml</b>而非寫死,否則服務被重新調校後量測會停在舊假設上 | 第一次等待等於起始值;解碼快時節奏等於下限;解碼慢時節奏只會變稀疏不會變密;句子結束後不再有辨識;短於第一次等待的句子沒有過程字幕;改讀不同設定檔會得到不同節奏;出貨的三個值仍與報告所述一致 | tests/test_interim_schedule.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-14 | 最終結果的語言判定要分「夾雜一個字」與「整句是別的語言」:需求收緊為「final 只能是中文、英文,或兩者夾雜的句子」,而它點名的失敗是模型把整句判成日文並全句照日文轉寫——這與「一個 token 被換成假名」是不同的缺陷,處理方式也不同:對前者刪字是修復,對後者刪字只會留下殘骸。故除了既有的字元層判定,另提供比例判定(政策外字元佔帶語言字元的比例;標點、空白、數字不計入分母,否則整句日文加一個句號就會被稀釋到門檻以下) | 乾淨文字比例為 0;單一被替換字元為小比例;整句外語為 1.0(含結尾標點);中英夾雜加一個雜字仍為低比例;空字串與純標點不得除以零 | tests/test_script_policy.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-15 | 動態切換語言之後,「整句被判成第三語言」的風險必須被量測,而不是被推論:固定 zh 時提示語言只有一個值,這個失敗在結構上不可能;改為動態切換後,它只因為程式碼拒絕釘定 en 以外的值而不可能——這是實作的性質,不是設定的性質,所以要有數據。量測三件事:①zh 解碼回報第三語言的頻率(_reported_language 這道過濾器實際擋掉多少);②出貨路徑的 final 依 SW-LANG-14 分類的結果;③en 釘定是否比 zh 帶來更高風險。⚠️ 已知量測空白:未餵入真正的日語/韓語音訊——本輪語料全是中文或英文語音,而「整句被判成日文」最可能發生在真正的非中英語音或高噪音輸入上 | 8114 次過程辨識中第三語言判定 0 次;出貨路徑 1852 句 final 全數乾淨(含「讓模型自己判斷」會洩漏的那 9 句);中文語料上 5997 次辨識僅 2 次誤報 en 且不連續(門檻 2 的餘裕可見) | tests/test_lid_risk_report.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-16 | ⚠️ 2026-09-10 起僅適用於量測工具,不再在服務端執行(見 SW-LANG-24)。剝除後只剩標點與空白時,回傳空字串:整句是別的語言時,逐字元剝除會留下原本夾在被移除詞語之間的標點與空白——實測真實日語/韓語語音,16 句中有 11 句剝完只剩 。、 . 或 .。這是一個非空但沒有意義的 final,下游對「什麼都沒聽到」已有處理路徑,對「只有一個句號的字串」沒有,它會被當成真的結果。⚠️ 僅定義全部都是殘骸這個情況;部分剝除(日文漢字會存活,温度を二度下げて 剝成可讀的 温度二度下)照原樣送出(2026-09-14 產品裁示:不特別處理) | 整句外語剝除後為空;夾在中間的空白與結尾標點不得單獨存活;有實質內容存活時標點保留原樣;本來就沒有違規的純標點文字不得被動到 | tests/test_script_policy.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-17 | 非中英文語音的手感測試:UX§11 的失敗情境是「模型把整句判成日文並照日文轉寫」,而它最可能發生在真的有人講該語言時。本輪語料全為中文或英文語音,故另以 TTS 產生少量日/韓/越/泰語車控指令實測。選這四種的理由是它們分屬兩種處境:日、韓 SenseVoice 有建模(有能力照樣轉寫),越、泰完全沒有建模(沒有正確輸出可選,看它退回什麼)。⚠️ 這是手感測試不是基準:每語言 8 句、單一語音,足以看出失敗往哪個方向倒,不足以給出發生率;此處乾淨不等於安全。TTS 依 (文字, 語音, 語速, 音高) 快取,重跑不再計費 | 四種語言各自的過程判定分布、出貨路徑違規數、守衛後結果與被清空數均產出;產出檔須記錄 TTS 計費字元數與快取命中數 | tests/test_foreign_probe_report.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-18 | 從解碼文字判斷語言的規則(切換依據的候選來源):現行做法讀 SenseVoice 自己的語言標記;此候選改讀解碼出來的字是什麼字集——<b>每個帶語言的字都是拉丁字母才算英文,只要出現一個漢字就算中文</b>。這等於把「中英夾雜一律當中文」寫成可執行的政策,而不是交給模型的分類頭去猜;不對稱是刻意的,因為昂貴的錯誤是把中文重解成英文(88% 流量),所以偵測器對 en 必須保守。⚠️ 兩種「說不出來」必須回傳 None 而非硬猜:①只有標點/數字/空白(多數早期過程字幕如此);②出現不服務的語言(假名、諺文)——回 zh 會把第三語言藏起來,回 en 會讓它把自己鎖定 | 全英文句判 en;純中文句判 zh;一個漢字即讓拉丁句變 zh;數字不影響判定;純標點/空字串/純數字回 None;假名與諺文<b>不得</b>判為 en,且不得判為 zh | tests/test_script_policy.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-19 | 十種語言政策必須在同一批解碼上比較:每個時間點的音訊分別以 zh/en/auto 各解一次後,任何切換規則都只是在這些結果上做選擇,因此十種做法(固定 zh、全程 auto、固定 en、依模型標記切換、依解碼文字切換、依最終解碼文字重判、早切換+最終重判、加起判時間與比例門檻+最終重判、起判時間與比例門檻+final 沿用、起判時間與比例門檻+收尾一票+final 沿用〔出貨〕)與任意門檻都是純函式,全部落在逐位元組相同的解碼上——差異只可能來自規則本身。⚠️ 因為是純函式,這裡的錯誤不會當掉,只會<b>無聲改寫比較表</b>,故重放語意須直接釘住:各方案顯示哪一組文字、切換自觸發的下一次過程辨識起生效、以及什麼會重置英文連續計數 | 固定型方案永不切換且顯示自己的解碼;auto 顯示自己的解碼;切換自觸發後一次生效;切換過的句子回傳英文最終結果、未切換的回傳中文最終結果;zh 與 None 判定都會重置連續計數;整句外語與夾雜雜字分開計數;門檻必須與結果一併回報 | tests/test_detector_compare.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-20 | 固定時點判定與事件驅動判定是兩種不同的問法,須分開量:出貨規則是<b>事件驅動</b>(連續 N 次判定一致就切換),每句實際切換時間不同;要回答「第幾秒判定最準」必須改用<b>固定時點</b>(在第 T 秒取該時點以前最後一次過程字幕的判定),兩者最佳點不保證相同,且只有後者畫得出隨時間變化的曲線。⚠️ 語意須釘住:固定時點只讀<b>一次</b>判定而非一段歷史,所以較晚的中文判定會蓋過較早的英文判定;第一次過程字幕之前不判定;無法判斷(純標點)維持起始的中文 | 取該時點以前最後一次過程字幕;較晚的中文判定覆蓋較早的英文判定;首次過程字幕前不切換;無法判斷時維持中文;兩種偵測器都能驅動同一組掃描 | tests/test_detector_compare.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-21 | 最終結果的語言可由「最終解碼自己的文字」重判:所有提早判定的做法都有同一個結構弱點——「中英夾雜算中文」在<b>完整句子</b>上定義清楚,在<b>句子前半段</b>上不成立,所以以英文開頭、中文結尾的句子在中文出現前一律看起來像英文。此方案不提早判定:過程字幕全程維持中文,只在最終解碼完成後看那段文字是否全為英文,是才用英文再解一次。它<b>放棄過程字幕的改善</b>換取<b>最好的最終結果</b>,代價是判成英文的句子多一次最終解碼 | 全英文的最終結果會改用英文重解;中英夾雜的最終結果維持中文(規則套用在完整句子上);過程字幕不得被切換;須列入可比較方案集合 | tests/test_detector_compare.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-22 | 語言判定改用「中英文字比例」而非「有沒有漢字」:原規則等於拉丁字母比例門檻 1.0(出現一個漢字即判中文);門檻可調後,較低的門檻容許句中有少量漢字仍判英文,用來救回因一個雜訊漢字而未被認出的英文句。⚠️ 反直覺且必須守住的事實:英文字母比中文字佔位多——幫我打開 air conditioner 這種一句中文帶一個英文名詞的句子,按字元數已有 77.8% 是拉丁字母,故門檻低於 0.8 會把正常的中英夾雜句判成英文。比例的分母只計帶語言字元;出現不服務的語言時回傳 None 而非比例 | 比例只計帶語言字元;純英文為 1.0、純中文為 0.0;無可計字元回 None;含第三語言回 None(不得回傳比例);預設門檻維持嚴格(一個漢字即中文);放寬後可容忍雜訊漢字;放寬過頭會誤判真夾雜句(此代價須被釘住);門檻不得凌駕第三語言判定 | tests/test_script_policy.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-23 | 過程字幕與最終結果是兩個獨立決定,時機相反:量測顯示過程字幕要盡早決定(使用者當下在看),最終結果要盡晚決定(「中英夾雜算中文」只在完整句子上成立)。單一切換動作無法同時滿足兩者,故此方案讓兩者各自決定:過程字幕依早期文字判定切換,最終結果忽略該切換、以完整句子的文字重新判斷 | 過程字幕依早期切換;最終結果不受該切換影響(過程判英文但完整句是中文時,最終仍為中文);過程未切換時最終仍可切換(英文較晚才出現的情況);須列入可比較方案集合 | tests/test_detector_compare.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-24 | 語言政策的作用範圍:只能選語言,不能改文字(2026-09-10 產品決議)。允許的手段只有一種——決定模型用哪個語言解碼(model.language、以及選擇它的切換規則)。不允許的手段包括:刪除違規字元、因違規而重新解碼、以及任何其他在解碼完成後改寫文字的動作;連偵測與記錄警告也一併移除(2026-09-10 追加裁示),服務端對 final 的字集不再有任何意見。⚠️ 這推翻了本輪先前建立的守衛(重解+刪字兜底),理由是刪字才是使用者真正受害的那一步,而「模型聽出別的語言」是可接受的結果。⚠️ 注意 ITN(簡繁轉換/數字正規化)不在此限——它早於本輪存在,且不是語言政策 | final 原樣送出;違規不觸發第二次解碼(以解碼次數斷言,只看文字看不出來);<b>不得產生任何語言政策相關的 log</b> | tests/test_batch_asr_manager_lang_policy.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-25 | 過程字幕的語言判定要有起判時間:實測顯示釘 zh 解碼英文語音時,模型的語言標記在 0.5s 只有 2.4% 說 <en>、1.5s 才到 94.9%;而解碼文字在 0.5s 就已有 89.5% 是全英文——文字是更早的訊號,但也正好在那裡最不可信:中文起音被誤解成短拉丁 token(H。、Yes。)與英文無法分辨。故起判時間之前一律不判、維持中文,且被跳過的那一格會重置連續計數(「還沒開始聽」不等於「同意」)。⚠️ 起判時間的真正刻度是辨識節奏而非秒數——辨識每 0.5s 一次,同一個刻度區間內的值是同一個政策。出貨值見 SW-LANG-27:0.8 s,從 VAD 起點算;重放時與 1.0 s 同屬 (0.5, 1.0],量測完全相同。曾試下一格 1.5 s:中文誤切同為 2/1197,英文會切換的句子卻由 68.7% 降為 55.7%,故不採用 | 起判時間之前的英文判定不計入;被跳過的一格重置連續計數;同一刻度區間內各值行為相同、跨區間是不同政策;比例門檻可容忍一個雜訊漢字 | tests/test_detector_compare.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-26 | 喚醒詞不得參與語言判定(UX§11)。喚醒詞的字集和它後面那句指令的語言無關(Hi Foxtron 是全拉丁、後面常接中文;嗨鴻華 是全漢字、後面常接英文),所以語言判定讀的是剝掉完整喚醒詞之後的文字;還沒講完的喚醒詞(Hi F、嗨鴻,WakeBypassMatcher.is_wake_prefix)不投票並重置連續計數——match_wake 只認得完整的詞,半截的既剝不掉也不能信。這同時涵蓋「已喚醒時又喊一次喚醒詞」:Hi Foxtron 打 拉丁佔比 0.90,不剝就會被判英文。待機起始且不含喚醒詞的語音不判(免喚醒指令的中文前綴可能被英文解碼弄壞,其餘語音將被丟棄);讀 was_in_standby_at_start 而非當下喚醒狀態,因為喚醒是整車共用的。manual 不受限 | 半截喚醒詞不閂鎖;只有喚醒詞不閂鎖;拉丁喚醒詞+中文指令不閂鎖;拉丁或中文喚醒詞+英文指令會閂鎖;已喚醒時重喊喚醒詞+中文不閂鎖;待機的免喚醒指令與無喚醒詞語音不判;另一座位喚醒不得打開本句;manual 照常判定;is_wake_prefix 對半截詞為真、對完整詞與一般語音為假 | tests/functional/scenarios/test_language_pinning.py · tests/test_wake_matcher.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-27 | 過程字幕的語言判定有起判時間,且依文字的拉丁字母比例。①起判時間 KittSttService._LANGUAGE_GATE_SECS = 0.8 s,從這句話的 VAD 起點算(_utterance_elapsed_secs),不是緩衝區裡的音訊長度——緩衝區一開始就放了 preroll,第一次過程辨識就已經有約 1 秒音訊,以緩衝秒數當門檻會讓第一次辨識直接投票。0.8 s 起的第一次辨識就投票。辨識每 0.5 s 一次(實機從解碼開始計時),所以投票的是 ≈1.0 s 那次,0.8 s 與 1.0 s 行為相同;只有單次解碼超過約 0.3 s 時,≈0.5 s 那次才會跨過 0.8 s;之前的一律不投票並重置計數。②判定依據是解碼文字的拉丁比例 ≥ _LATIN_SHARE_THRESHOLD(0.9),不讀模型的語言標記。③連續 2 次判英文才閂鎖,之後不再改回。⚠️ 量測(重放):0.8 s 時(與 1.0 s 相同)中文誤切 2/1197、英文會切換的句子 68.7%;曾試 1.5 s,誤切數不變但英文切換降為 55.7%;也曾改為 1.0 s,依使用者指示改回 0.8 s | 0.8 s 之前(含 0.5、0.7 s)不投票且重置計數、0.8 s 起投票;門檻讀 VAD 起點起算的時間,不受 preroll 影響;夾雜句不閂鎖;單一雜訊漢字不否決英文句;空字串/純標點/第三語言不閂鎖 | tests/functional/scenarios/test_language_pinning.py · tests/test_detector_compare.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-28 | 最終結果沿用過程字幕判定的語言,只解一次。final 以 session.pinned_language 解碼,不另行重判、不做第二次解碼。代價是過程字幕的誤切會帶進 final。收尾時還差一票的情況見 SW-LANG-30;兩者合起來的量測(重放,起判 0.8 s):英文 WER −8.65%、中文 CER −4.28%、1197 句中文參考有 3 句被誤切;若 final 另行重判為 −9.29%/−5.62%,但每個英文 final 多一次解碼 | final 只解碼一次;鎖定為英文的句子 final 以 en 解;未鎖定且沒有待定英文票時用預設語言;誤切的中文句 final 亦以 en 解(已知代價) | tests/functional/scenarios/test_language_pinning.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-29 | 同一個 EOT turn 內,語言一旦決定,後面每一段 VAD 都沿用(UX§11)。一個語意 turn 可以跨好幾段 VAD——講到一半停頓只會結束這一段,不會結束 turn。先前語言鎖定掛在 VAD 段上,第二段一開始就回到 zh 重新判,短短的尾巴常常判不回英文,同一句指令就變成前半英文、後半中文。改由 turn 持有:TurnState.language 只在新 turn 開頭(start())與 turn 結束(end():commit/cancel/EndFrame)時清除,續接段(mark_continuation())沿用;續接段新建的 interim stream 一建立就帶上該語言(KittSttService._create_interim_stream),final 也以該語言解碼。連續投票的計數仍以 VAD 段為單位——跨過停頓的是決定,不是票數。⚠️ 只有英文算「決定」;中文是尚未決定時的預設,第一段沒鎖定的話,後面的段仍可判成英文 | 續接段沿用 turn 已決定的 en;續接段的 final 以 en 解;續接段新 interim stream 帶 language=en;未決定的 turn 新 stream 不帶提示;新 turn 開頭與 turn 結束都清除語言;新 VAD 段不清除 turn 的語言;第一段是中文不會鎖住 turn | tests/functional/scenarios/test_language_pinning.py · tests/test_eot_state.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-LANG-30 | 收尾時還差一票的英文判定也決定 final 的語言(UX§11)。過程辨識約每 0.55 s 一次、0.8 s 起才投票,第二票最快在 VAD 起點後約 1.7 s,短指令講完前來不及。所以 VAD 段結束時若尚未鎖定、但最近一次投票的過程辨識判為英文(en_verdict_run > 0),final 以 en 解,並把整個 turn 鎖成英文(KittSttService._final_language)。final 本來就解一次,不多花解碼。量測(重放):英文 WER −8.65%(兩票版 −7.37%)、中文 CER −4.28%(兩票版 −1.74%);英文句有切換的比例 68.7% → 85.6%;1197 句中文參考的誤切 2 → 3 句(新增「马英九」,過程辨識解成 H9。);非中英文 32 句中有 1 句 final 改以 en 解(泰語,zh 解出來本來就是拉丁字母) | 收尾一票英文 → final 以 en 解且只解一次;票後又出現中文、起判前的票、只有喚醒詞 → 維持預設語言;鎖定延續到同一 turn 的下一段 | tests/functional/scenarios/test_language_pinning.py · tests/test_detector_compare.py | UX§11 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-OPS-08 | shipped 的 sherpa-onnx build tree 不得指向 repo 以外的路徑:Dockerfile 直接 COPY sherpa-onnx/build/,所以 git 裡是什麼,image 就拿到什麼。匯入 src/kitt_stt_service.py 會把其中四個 tracked 函式庫換成指向當下那個 venv 的絕對路徑 symlink(/home/<某人>/…/onnxruntime/capi/…);一旦被 commit,別人 build 時 COPY 跟著斷掉的連結走,image 就少了 CUDA execution provider,而且建置不會報錯。已發生兩次,故改以測試釘住 | build tree 內的 tracked symlink 只能指向同目錄的兄弟檔(libonnxruntime.so → .so.1 → .so.1.23.2 這種版本鏈可以);絕對路徑或以 .. 爬出 repo 的一律失敗 | tests/test_build_artifacts.py | STT_EXTRA | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
SW-PERF-07 | 送進辨識器的音訊範圍不得取決於傳輸時機:VAD start 之前要一併帶進去的 preroll,其長度只能由「收到多少音訊資料」與 VAD 自己回報的 start_secs 決定,不得由 chunk 的到達時間決定。並且不得往回跨越前一句的結尾(zero overlap) | 同一段音訊、同樣的順序,不論被切成 20ms、200ms 還是不對齊取樣點的碎塊,選出來的 preroll 逐位元組相同;長度等於 (start_secs + 0.8s) × 取樣率 × 2,受緩衝區容量與前一句邊界夾制 | tests/test_preroll_selection.py(10) | STT_EXTRA。原本以 perf_counter 到達時間戳比較,隱含「音訊即時等速到達」——WS 突發交付或 event loop 忙碌時不成立,導致同一句話的解碼範圍會浮動,邊界音檔的喚醒判定因此不可重現。SW-PERF-06 已於 2026-08-31 廢止,故編號跳號 | NOT-VERIFIED rebase 後未重跑此需求的登記測試 |
| 範圍 | 結果 | 說明 |
|---|---|---|
| Smart Turn/precedence focused | 44 passed | Smart Turn/TurnSense 共用 reason=model、VAD 使用 reason=vad;涵蓋 detector、runtime error、KWS 與 force precedence。 |
| Status/config focused | 60 passed | 設定與狀態頁讓三種 backend 接受 0.1–3.0 秒,並涵蓋空 VAD summary、VAD 模式 wake/wait 儲存與 reset、WAV 180 秒 hard cap/逐筆與全部 ZIP 下載,以及 runtime backend controls。 |
| Backend preload focused | 3 passed | VAD/Smart Turn 跳過 TurnSense runtime;TurnSense preload failure 仍中止啟動。 |
| Report contract | 142 passed, 2 skipped | 2026-09-15 branch integration 後重跑 HTML 結構、連結、SW_id、圖表與測試引用檢查。 |
| Offline full | 1035 passed, 3 skipped, 16 failed | 7 個既有 UX 缺口;9 個 hotword API 測試缺 gitignored WAV corpus。EOT model symlink 可用,原 CX1 model 缺檔不再失敗。 |
| Live WebSocket/CUDA | 3 passed | 隔離 container 的 test_forced_stop.py:max_duration、mic_off、audio_stall 三種 forced VAD stop 均立即 commit final。下方 88 筆保留為歷史 ledger。 |
資料狀態:本輪另有 test_forced_stop.py 的 3 筆 CUDA/WebSocket PASS;以下 88 筆仍是先前 cycle 的 live 執行快照,不納入本輪的通用 live regression 結論。
下表逐一保留 STT live 的執行結果;序號供上表對應,不是新的需求 ID。
| 序號 | 測項 | 結果 |
|---|---|---|
| 1 | tests/functional/live/test_active.py::test_active_wake_filter | PASSED |
| 2 | tests/functional/live/test_active.py::test_active_smart_turn_wake_head | PASSED |
| 3 | tests/functional/live/test_active.py::test_active_smart_turn | FAILED |
| 4 | tests/functional/live/test_active_greeting.py::test_ux_1_3_2_first_greeting_is_sent_within_500ms | PASSED |
| 5 | tests/functional/live/test_active_greeting.py::test_ux_1_3_2_speaking_inside_the_window_suppresses_the_first_greeting[50ms] | FAILED |
| 6 | tests/functional/live/test_active_greeting.py::test_ux_1_3_2_speaking_inside_the_window_suppresses_the_first_greeting[200ms] | FAILED |
| 7 | tests/functional/live/test_active_greeting.py::test_ux_1_3_2_speaking_inside_the_window_suppresses_the_first_greeting[350ms] | FAILED |
| 8 | tests/functional/live/test_active_greeting.py::test_ux_1_3_2_speaking_inside_the_window_suppresses_the_first_greeting[500ms] | FAILED |
| 9 | tests/functional/live/test_active_greeting.py::test_ux_1_3_one_shot_wake_emits_active_like_a_bare_wake | PASSED |
| 10 | tests/functional/live/test_active_greeting.py::test_ux_1_3_2_one_shot_wake_suppresses_the_first_greeting | PASSED |
| 11 | tests/functional/live/test_active_greeting.py::test_ux_1_3_3_second_greeting_fires_at_10s_when_nobody_speaks | FAILED |
| 12 | tests/functional/live/test_active_greeting.py::test_ux_1_3_3_speaking_within_10s_suppresses_the_second_greeting | PASSED |
| 13 | tests/functional/live/test_active_greeting.py::test_ux_1_3_4_exits_5s_after_the_second_greeting | PASSED |
| 14 | tests/functional/live/test_audio_stress.py::test_audio_stress_no_errors | PASSED |
| 15 | tests/functional/live/test_audio_stress.py::test_audio_stress_final_received | PASSED |
| 16 | tests/functional/live/test_audio_stress.py::test_audio_stress_time_to_final_bounded | PASSED |
| 17 | tests/functional/live/test_audio_stress.py::test_audio_stress_interim_gap_bounded | PASSED |
| 18 | tests/functional/live/test_bilingual.py::test_code_switching_within_one_command | FAILED |
| 19 | tests/functional/live/test_bilingual.py::test_english_command_after_chinese_wake_word | PASSED |
| 20 | tests/functional/live/test_btn_control.py::test_btn_activate | PASSED |
| 21 | tests/functional/live/test_btn_control.py::test_btn_rapid_toggle_active | PASSED |
| 22 | tests/functional/live/test_btn_control.py::test_btn_rapid_toggle_wake | PASSED |
| 23 | tests/functional/live/test_btn_control.py::test_btn_deactivate | PASSED |
| 24 | tests/functional/live/test_btn_control.py::test_btn_standby_voice | PASSED |
| 25 | tests/functional/live/test_btn_control.py::test_btn_standby_mid | PASSED |
| 26 | tests/functional/live/test_bypass_cmd.py::test_cmd_trigger | PASSED |
| 27 | tests/functional/live/test_bypass_cmd.py::test_cmd_not_at_start | PASSED |
| 28 | tests/functional/live/test_bypass_cmd.py::test_cmd_extra_words | PASSED |
| 29 | tests/functional/live/test_bypass_cmd.py::test_standby_cmd_smart_turn | PASSED |
| 30 | tests/functional/live/test_bypass_prefix.py::test_prefix_trigger | PASSED |
| 31 | tests/functional/live/test_bypass_prefix.py::test_prefix_not_at_start | PASSED |
| 32 | tests/functional/live/test_bypass_prefix.py::test_prefix_smart_turn | FAILED |
| 33 | tests/functional/live/test_debug_logs.py::test_trace_id_scopes_retrieved_logs_to_its_own_connection | PASSED |
| 34 | tests/functional/live/test_debug_logs.py::test_missing_trace_id_query_param_returns_a_404_when_the_route_itself_is_missing | PASSED |
| 35 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[\u8b1d\u8b1d] | FAILED |
| 36 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[\u518d\u898b] | FAILED |
| 37 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[\u9000\u51fa] | FAILED |
| 38 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[\u9000\u4e0b] | PASSED |
| 39 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[\u6efe\u86cb] | FAILED |
| 40 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[\u6efe] | FAILED |
| 41 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[Thank you] | FAILED |
| 42 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[Thanks] | FAILED |
| 43 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[Goodbye] | FAILED |
| 44 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[ByeBye] | FAILED |
| 45 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[Quiet] | FAILED |
| 46 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[Shut up] | FAILED |
| 47 | tests/functional/live/test_exit_words.py::test_ux_2_1_tier1_exit_word_returns_to_standby[Dismiss] | FAILED |
| 48 | tests/functional/live/test_forced_stop.py::test_forced_vad_stop_commits_immediately[max_duration] | PASSED |
| 49 | tests/functional/live/test_forced_stop.py::test_forced_vad_stop_commits_immediately[mic_off] | PASSED |
| 50 | tests/functional/live/test_forced_stop.py::test_forced_vad_stop_commits_immediately[audio_stall] | PASSED |
| 51 | tests/functional/live/test_kws_method_manual.py::test_wake_transcript_does_not_activate_manual_mode | SKIPPED |
| 52 | tests/functional/live/test_kws_method_manual.py::test_web_ui_active_allows_speech_without_wake_stripping | SKIPPED |
| 53 | tests/functional/live/test_kws_method_manual.py::test_web_ui_standby_blocks_speech_again | SKIPPED |
| 54 | tests/functional/live/test_kws_method_manual.py::test_bypass_still_emits_in_manual_standby | SKIPPED |
| 55 | tests/functional/live/test_multiuser_concurrency.py::test_multiuser_no_errors | PASSED |
| 56 | tests/functional/live/test_multiuser_concurrency.py::test_multiuser_no_stray_user_ids | PASSED |
| 57 | tests/functional/live/test_multiuser_concurrency.py::test_multiuser_every_seat_gets_a_final | PASSED |
| 58 | tests/functional/live/test_multiuser_concurrency.py::test_multiuser_time_to_final_bounded | PASSED |
| 59 | tests/functional/live/test_multiuser_concurrency.py::test_multiuser_interim_gap_bounded | PASSED |
| 60 | tests/functional/live/test_rejection_thresholds.py::test_ux_8_2_speech_longer_than_15s_is_not_forwarded | FAILED |
| 61 | tests/functional/live/test_rejection_thresholds.py::test_ux_8_2_speech_longer_than_50_chars_is_not_forwarded | FAILED |
| 62 | tests/functional/live/test_rejection_thresholds.py::test_ux_8_2_speech_under_both_thresholds_is_forwarded_normally | PASSED |
| 63 | tests/functional/live/test_rejection_thresholds.py::test_ux_10_2_rejection_keeps_the_room_listening[over-15s] | FAILED |
| 64 | tests/functional/live/test_rejection_thresholds.py::test_ux_10_2_rejection_keeps_the_room_listening[over-50-chars] | FAILED |
| 65 | tests/functional/live/test_remaining_ux_gaps.py::test_non_wake_speech_never_activates[multiuser-10s.wav] | PASSED |
| 66 | tests/functional/live/test_remaining_ux_gaps.py::test_non_wake_speech_never_activates[stress-90s.wav] | PASSED |
| 67 | tests/functional/live/test_remaining_ux_gaps.py::test_ten_second_prompt_stage_exists_end_to_end | FAILED |
| 68 | tests/functional/live/test_remaining_ux_gaps.py::test_command_wait_timeout_boundary[490ms-one-turn] | FAILED |
| 69 | tests/functional/live/test_remaining_ux_gaps.py::test_command_wait_timeout_boundary[510ms-two-turns] | PASSED |
| 70 | tests/functional/live/test_smart_turn_bypass.py::test_standby_smart_turn_prefix_2nd | PASSED |
| 71 | tests/functional/live/test_smart_turn_bypass.py::test_standby_smart_turn_cmd_2nd | PASSED |
| 72 | tests/functional/live/test_smart_turn_wake.py::test_standby_smart_turn_wake_2nd | PASSED |
| 73 | tests/functional/live/test_smart_turn_wake.py::test_active_smart_turn_wake_2nd | PASSED |
| 74 | tests/functional/live/test_standby.py::test_standby_rejection | PASSED |
| 75 | tests/functional/live/test_standby.py::test_standby_wake_not_at_start | PASSED |
| 76 | tests/functional/live/test_stt_config.py::test_custom_stt_config_active | PASSED |
| 77 | tests/functional/live/test_stt_config.py::test_custom_stt_config_deleted | PASSED |
| 78 | tests/functional/live/test_timeout.py::test_timeout_deactivate | PASSED |
| 79 | tests/functional/live/test_timeout.py::test_timeout_reset | PASSED |
| 80 | tests/functional/live/test_wake.py::test_wake_immediate | PASSED |
| 81 | tests/functional/live/test_wake.py::test_wake_pause | PASSED |
| 82 | tests/functional/live/test_wake_factory_words.py::test_wake_hi_foxtron | PASSED |
| 83 | tests/functional/live/test_wake_latency.py::test_wake_to_agent_active_fits_the_ui_budget | FAILED |
| 84 | tests/functional/live/test_wake_latency.py::test_wake_only_greeting_fits_the_response_budget | FAILED |
| 85 | tests/functional/live/test_wake_rate.py::test_ux_1_1_wake_rate_meets_the_90_percent_floor[\u55e8\u9d3b\u83ef] | PASSED |
| 86 | tests/functional/live/test_wake_rate.py::test_ux_1_1_wake_rate_meets_the_90_percent_floor[Hi Foxtron] | PASSED |
| 87 | tests/functional/live/test_wake_rate.py::test_ux_1_1_false_wake_floor_is_zero_over_repeated_replays[multiuser-10s] | PASSED |
| 88 | tests/functional/live/test_wake_rate.py::test_ux_1_1_false_wake_floor_is_zero_over_repeated_replays[say-wake-midsentence] | PASSED |
| 驗證範圍 | 命令 |
|---|---|
| pre-default-adjustment branch integration focused(184 passed, 12 skipped) | PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -q -rs tests/test_turn_backend_config.py tests/test_smart_turn_detector.py tests/test_status_page_settings.py tests/test_eot_kws_precedence.py tests/test_eot_state.py tests/turnsense/test_policy.py tests/turnsense/test_runtime.py tests/test_kws_method_config.py tests/test_wake_scan.py tests/test_max_utterance_audio.py tests/test_greeting_settings.py |
| pre-default-adjustment Live EOT forced VAD stop(3 passed) | 在 DVC materialize 的 models/turnsense-1.1 下:HOST_PORT=9307 LIVE_TEST_TARGET=tests/functional/live/test_forced_stop.py ./tests/docker-test.sh --build;本輪外部 symlink 以等效唯讀 model context 建 image。 |
| Reason mapping(3 passed) | UV_CACHE_DIR=/tmp/kitt-stt-uv-cache uv run --frozen --group test pytest -q tests/test_smart_turn_detector.py -k reason |
| 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 設為 9308:PYTHONPATH=./sherpa-onnx/build ASR_CONFIG_FILE=/tmp/<overlay>.yaml .venv/bin/python -m src.server;再以 curl --fail http://127.0.0.1:9308/stt/sensevoice 確認 data-current="vad"。 |
| Smart Turn integration(NOT-VERIFIED) | UV_CACHE_DIR=/tmp/kitt-stt-uv-cache uv run --frozen --group test pytest -q tests/test_eot_kws_precedence.py::test_smart_turn_complete_commits_with_reason_and_p_complete_only · 完成斷言後程序未退出,人工中止 |
| Offline full(1035 passed, 3 skipped, 16 failed) | ../kitt-stt/.venv/bin/python -m pytest -q --tb=no tests --ignore=tests/functional/live --ignore=tests/sherpa-onnx |
| 2026-09-15 report contract(142 passed, 2 skipped) | PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -q -rs -p no:cacheprovider tests/test_report_glossary.py tests/test_report_test_refs.py tests/test_report_integrity.py tests/test_report_crossrefs.py tests/test_report_highlighting.py tests/test_report_text.py tests/test_report_ids.py tests/test_report_structure.py tests/test_spec_traceability.py tests/test_report_charts.py |
| Status page controls(21 passed) | UV_CACHE_DIR=/tmp/kitt-stt-uv-cache uv run --frozen --group test pytest -q tests/test_status_page_settings.py |
| Post-change selected checks(129 passed, 2 skipped) | UV_CACHE_DIR=/tmp/kitt-stt-uv-cache uv run --frozen --group test pytest -q tests/test_status_page_settings.py tests/test_audio_cfg.py tests/test_report_integrity.py tests/test_report_test_refs.py tests/test_report_highlighting.py tests/test_report_glossary.py tests/test_report_structure.py tests/test_report_charts.py tests/test_report_crossrefs.py tests/test_report_ids.py |
| Post-change full offline(NOT-VERIFIED) | UV_CACHE_DIR=/tmp/kitt-stt-uv-cache ./tests/test.sh · collected 1105 / 1 skipped;卡在 tests/test_batch_asr_manager_incremental_prep.py 後中止 |
| Static | git diff --check · bash -n deploy.sh |
vad。Smart Turn 仍非出廠值。wait_ms 以 0.1 秒精度顯示。滑桿不是 mode switch,也不編輯 complete threshold。喚醒/免喚醒詞仍依 ?car=;wait 滑桿套用目前 backend,且全行程共用。data-current=vad;本次未重跑 WebSocket 音訊重放,88 筆歷史 live rows 僅供追溯。