feat: 完成 P 群組 — 語音子系統

新增 apps/api/src/voice/:Voice Sheet(基礎音色/預設語速/音域幅度/口頭
聲響庫/禁則,依性格原型推導預設值,1:1 掛在 Character,同 EmotionState
的關聯模式);情緒→韻律對照(六狀態語速/音高/音量/句尾走向),音高變化
依角色音域幅度縮放(三無角色近乎單音),設計原則同 O-4 表情外顯度縮放;
非語言發聲與節奏(依情緒插入聲響、重大情緒延長停頓、禁則過濾候選、
低親密度不打斷、talkative 特徵+親密度門檻才可插話);TTSProvider 抽象
完全比照 F-1 LLMProvider(MockTTSProvider 輸出韻律標記+佔位音檔,
RealTTSProvider 待人工確認供應商後再接,TTS_PROVIDER=mock|real 切換);
語音輸入副語言分析(EmotionService 新增 paralinguisticSignal,優先序
排在作息基線之前,讓「文字說沒事但聲音在抖」判為負向)。

修正一個真實 bug:非語言發聲的停頓時長原本直接拿目前主導情緒的數值
當強度訊號,但平靜狀態下 calm 預設就是 100,導致「什麼事都沒發生」被
誤判成「強度最強的重大情緒事件」而觸發不該有的長停頓——修正為 CALM
一律視為強度 0(平靜是情緒事件的缺席,不是訊號)。

刻意簡化:副語言特徵(語速/音量/顫抖/停頓)採結構化輸入,不做真實音訊
分析,比照 M/N 群組「先用結構化輸入代替真實訊號處理」的一貫取捨。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-08-13 18:19:51 +08:00
co-authored by Claude Sonnet 5
parent 0f3d16059c
commit 0e9401b891
16 changed files with 702 additions and 7 deletions
+15 -6
View File
@@ -387,12 +387,21 @@ flowchart TB
### P. 語音子系統
- [ ] **P-1 Voice Sheet(S)**:定義角色音色設定(基礎音色、預設語速、音域幅度、口頭聲響庫、禁則)並掛到 `Character`。驗收:可為種子角色寫入完整 Voice Sheet。依據:§角色音色設定(Voice Sheet)。
- [ ] **P-2 非語言發聲與節奏(S)**:依情緒自動插入「嗯?」「唔……」嘆氣、輕笑等非語言發聲與停頓(重大話題前停 1~2 秒);親密度影響音量與氣音比例;低親密度不打斷使用者。驗收:不同情緒下插入的聲響與停頓不同。依據:§對話節奏與非語言發聲。
- [ ] **P-3 語音標記層(M)**:情緒狀態機輸出直接驅動韻律參數(語速、音高、音量、句尾走向),實作 wiki§情緒 → 韻律對照 的六種情緒設定。驗收:同一句話在不同情緒下產生不同韻律標記。依據:§情緒 → 韻律對照。
- [ ] **P-4 TTS Provider 抽象(M)**:比照 `LLMProvider` 定義 `TTSProvider`,先實作 Mock(輸出韻律標記與佔位音檔),真實供應商待人工確認後再接。驗收:切換 Provider 不動呼叫端。依據:技術選型「TTS 服務:情緒韻律語音」。
- [ ] **P-5 語音輸入與副語言分析(M)**:STT 轉文字之外,另抽取語速、音量、顫抖、停頓等副語言特徵送入情緒標記器(「文字說沒事但聲音在抖」判為負向)。驗收:同一段文字配不同副語言特徵得到不同情緒標記。依據:§語音管線架構「雙向都有語音」。
- [ ] **P-V 階段驗證(XS)**:`npm run restart && npm run smoke -- P`(P.mjs:韻律對照、非語言發聲插入、副語言特徵影響情緒標記)。
- [x] **P-1 Voice Sheet(S)**:定義角色音色設定(基礎音色、預設語速、音域幅度、口頭聲響庫、禁則)並掛到 `Character`。驗收:可為種子角色寫入完整 Voice Sheet。依據:§角色音色設定(Voice Sheet)。
- [x] **P-2 非語言發聲與節奏(S)**:依情緒自動插入「嗯?」「唔……」嘆氣、輕笑等非語言發聲與停頓(重大話題前停 1~2 秒);親密度影響音量與氣音比例;低親密度不打斷使用者。驗收:不同情緒下插入的聲響與停頓不同。依據:§對話節奏與非語言發聲。
- [x] **P-3 語音標記層(M)**:情緒狀態機輸出直接驅動韻律參數(語速、音高、音量、句尾走向),實作 wiki§情緒 → 韻律對照 的六種情緒設定。驗收:同一句話在不同情緒下產生不同韻律標記。依據:§情緒 → 韻律對照。
- [x] **P-4 TTS Provider 抽象(M)**:比照 `LLMProvider` 定義 `TTSProvider`,先實作 Mock(輸出韻律標記與佔位音檔),真實供應商待人工確認後再接。驗收:切換 Provider 不動呼叫端。依據:技術選型「TTS 服務:情緒韻律語音」。
- [x] **P-5 語音輸入與副語言分析(M)**:STT 轉文字之外,另抽取語速、音量、顫抖、停頓等副語言特徵送入情緒標記器(「文字說沒事但聲音在抖」判為負向)。驗收:同一段文字配不同副語言特徵得到不同情緒標記。依據:§語音管線架構「雙向都有語音」。
- [x] **P-V 階段驗證(XS)**:`npm run restart && npm run smoke -- P`(P.mjs:韻律對照、非語言發聲插入、副語言特徵影響情緒標記)。
> **實作記錄(P 群組)**:
> - 新模組放在 `apps/api/src/voice/`,架構完全比照 F 群組:`TTSProvider`/`TTS_PROVIDER` token/`MockTTSProvider`/`RealTTSProvider`(尚未實作、被呼叫才丟例外)/`TTS_PROVIDER=mock|real` 環境變數切換工廠函式,逐一對應 `LLMProvider`/`LLM_PROVIDER`/`MockProvider`/`ClaudeProvider`/`LLM_PROVIDER=mock|claude`。**這組抽象本身沒有新設計,純粹是同一個模式的第二次套用**,這正是 F-1 當初把 Provider 抽出介面的目的:之後任何生成式外部服務要接進來,都走同一套「先做 Mock、真的供應商待人工確認、環境變數切換」的節奏。
> - **P-1 Voice Sheet 用獨立資料表(同 EmotionState 的 1:1 關聯),不是塞進 `Character` 本體**——口頭聲響庫/禁則是字串陣列,但 SQLite 的 Prisma connector 不支援原生字串陣列欄位,改用 JSON 字串欄位(`verbalTicsJson`/`forbiddenSoundsJson`)+ service 層 `JSON.parse`/`JSON.stringify`,對外一律回傳/接收真正的 `string[]`,呼叫端不需要知道底層是 JSON 字串。預設值依性格原型推導(`defaultVoiceSheetFor`),跟 G-5「原型 → 種子預設」、I-1「原型 → 作息表種子」是同一個模式的第三次套用。
> - **P-3/P-4 的韻律縮放沿用 O-4 的設計原則,但縮放的依據換成 Voice Sheet 的音域幅度而不是外顯度**:`pitchShiftSemitones = 情緒對照表的基準音高變化 × (角色音域幅度 / 4)`——三無角色音域幅度只有 0.5(對照組的通用預設是 4),同樣的情緒事件換算出來的音高變化只有基準值的 1/8,具體實現「三無角色近乎單音」。**這裡刻意不重複使用 `expressiveness`**(那是表情/情緒外顯度,跟音域幅度是概念上不同的兩個原型參數,各自獨立設定,即使數值上有意的相關性)。
> - **P-2 停頓機制踩到一個實際的 bug**:一開始用「目前主導情緒的數值 / 100」當作停頓長度的訊號強度,但完全沒考慮到「平靜」狀態下 `calm` 欄位預設就是 100——導致角色明明什麼事都沒發生(平靜狀態),卻被誤判成「強度 1.0 的重大情緒事件」,觸發本該只留給強烈情緒的 1~2 秒長停頓。**平靜不是情緒事件,是情緒事件的缺席**,修正方式是讓 `emotionTag === "CALM"` 時強制把訊號強度視為 0,只有真正的六種「有事發生」情緒(愉悅/低落/警戒/害羞/彆扭)才會依強度觸發長停頓——這個坑跟 G-4 早期「情緒觸發閾值只縮放強度、沒縮放要不要觸發」是同一類錯誤(把「數值存在」跟「訊號有意義」搞混),值得記錄成通用提醒:**任何用「情緒維度數值」當強度訊號的地方,都要先排除 CALM 這個「預設/缺席」維度,不能直接套用同一個公式**。
> - **P-5 副語言訊號的優先序刻意排在 I-5 作息基線之前**:`EmotionService.processInput` 現在的訊號決定順序是「文字本身觸發 → 副語言訊號(P-5)→ 作息基線(I-5)→ 維持平靜」——副語言是「這一輪輸入本身」的直接訊號,理應比「這個時段通常會怎樣」的基線更優先,但兩者都只在文字沒有觸發任何訊號(CALM)時才會生效,不會蓋掉文字本身已經觸發的訊號。**手動測試時發現一個容易踩的陷阱**:如果角色當下已經有一個很強的主導情緒(例如 JOY 90),副語言訊號算出來的 SAD 不一定能立刻蓋過去(情緒狀態機本身的轉移規則決定要不要真的切換主導情緒,跟訊號有沒有正確算出來是兩件事)——寫測試時要先把情緒重置到 CALM 基準,才能乾淨地驗證副語言訊號本身有沒有被正確送進情緒標記器,不要跟情緒狀態機的轉移阻力混在一起判斷。
> - **P-5 沒有做真正的語速/音量/顫抖偵測**——副語言特徵目前是結構化輸入(呼叫端直接給數值),跟 M 群組「場景切分」、N 群組「互動正負權重」是同一種「先用結構化輸入代替真實訊號處理」的簡化,理由相同:真正的音訊特徵抽取需要處理實際音訊串流,超出目前系統的 Mock 階段範疇,機制(訊號合成規則、與文字訊號的優先序、餵進情緒標記器)先做對,之後接上真的語音辨識/音訊分析只需要替換這個輸入來源。
> - **全量回歸執行到 O 群組時出現一次 `fetch failed`,重新單獨執行 O 群組立刻全綠**——跟 J 群組踩過的「重啟瞬間 keep-alive socket 失效」是類似的環境性瞬斷(這次沒有服務重啟動作,單純是連續執行 15 個群組、主機負載升高時的暫態網路錯誤),不是 P 群組程式碼引入的迴歸;已重新單獨驗證 O 群組與完整 A~P 序列(除了這次瞬斷)皆為全綠,記錄於此供之後遇到類似狀況時參考,不需要當成真的 bug 去追。
### Q. APP(React Native + Expo)