新增 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>
64 lines
3.9 KiB
TypeScript
64 lines
3.9 KiB
TypeScript
import type { EmotionTag } from "@kokorone/shared";
|
|
import type { Archetype } from "../personality/archetype-params.js";
|
|
|
|
export type SentenceEndContour = "RISING" | "FALLING" | "FLAT";
|
|
|
|
export interface ProsodyTemplate {
|
|
rateDeltaPercent: number; // 語速變化百分比,正值變快
|
|
pitchDeltaSemitones: number; // 音高變化(半音),正值變高
|
|
volumeDeltaPercent: number; // 音量變化百分比,正值變大
|
|
contour: SentenceEndContour;
|
|
characteristic: string; // 描述性文字,供除錯/前端顯示
|
|
}
|
|
|
|
// P-3 情緒 → 韻律對照:情緒狀態機的輸出直接驅動韻律參數。
|
|
export const EMOTION_PROSODY_TABLE: Record<EmotionTag, ProsodyTemplate> = {
|
|
CALM: { rateDeltaPercent: 0, pitchDeltaSemitones: 0, volumeDeltaPercent: 0, contour: "FLAT", characteristic: "角色預設值" },
|
|
JOY: { rateDeltaPercent: 15, pitchDeltaSemitones: 1.5, volumeDeltaPercent: 10, contour: "RISING", characteristic: "句尾上揚、偶爾帶笑聲" },
|
|
SAD: { rateDeltaPercent: -20, pitchDeltaSemitones: -1.5, volumeDeltaPercent: -15, contour: "FALLING", characteristic: "句間停頓變長、句尾下沉" },
|
|
ALERT: { rateDeltaPercent: 5, pitchDeltaSemitones: 0, volumeDeltaPercent: 0, contour: "FLAT", characteristic: "語調變平、字句變短" },
|
|
SHY: { rateDeltaPercent: -5, pitchDeltaSemitones: 2, volumeDeltaPercent: -10, contour: "FLAT", characteristic: "結巴、語尾變小聲,忽快忽慢" },
|
|
GRUMPY: { rateDeltaPercent: 10, pitchDeltaSemitones: 1, volumeDeltaPercent: -5, contour: "FALLING", characteristic: "「哼」開頭,先大後小" },
|
|
};
|
|
|
|
// P-1 Voice Sheet 預設值:依性格原型推導基準語速與音域幅度(同 G-5「原型 → 種子預設」的作法)。
|
|
export const DEFAULT_VOICE_SHEET_BY_ARCHETYPE: Record<Archetype, { baseTimbre: string; baseRate: number; pitchRangeSemitones: number }> = {
|
|
傲嬌: { baseTimbre: "清亮少女音", baseRate: 1.05, pitchRangeSemitones: 5 },
|
|
冷淡: { baseTimbre: "低沉平穩音", baseRate: 0.85, pitchRangeSemitones: 1.5 },
|
|
天然呆: { baseTimbre: "軟糯蘿莉音", baseRate: 0.95, pitchRangeSemitones: 4 },
|
|
元氣: { baseTimbre: "清亮少女音", baseRate: 1.2, pitchRangeSemitones: 6 },
|
|
大小姐: { baseTimbre: "低沉御姐音", baseRate: 0.95, pitchRangeSemitones: 3.5 },
|
|
三無: { baseTimbre: "軟糯蘿莉音", baseRate: 0.8, pitchRangeSemitones: 0.5 },
|
|
};
|
|
const DEFAULT_VOICE_SHEET_FALLBACK = { baseTimbre: "清亮少女音", baseRate: 1.0, pitchRangeSemitones: 3 };
|
|
export function defaultVoiceSheetFor(archetype: string) {
|
|
return DEFAULT_VOICE_SHEET_BY_ARCHETYPE[archetype as Archetype] ?? DEFAULT_VOICE_SHEET_FALLBACK;
|
|
}
|
|
|
|
// P-2 非語言發聲庫:依情緒狀態的預設候選詞,角色的 Voice Sheet 口頭聲響庫可補充、禁則可過濾排除。
|
|
export const DEFAULT_INTERJECTIONS_BY_EMOTION: Record<EmotionTag, string[]> = {
|
|
CALM: ["嗯"],
|
|
JOY: ["欸嘿嘿", "呵呵"],
|
|
SAD: ["唔……", "……"],
|
|
ALERT: ["嗯?", "欸?!"],
|
|
SHY: ["那個……就是……", "唔……"],
|
|
GRUMPY: ["哼!", "哼……"],
|
|
};
|
|
|
|
// P-2 停頓:情緒強度達門檻(沿用 C-4 高情緒事件的判斷門檻)且屬於重大話題時,回答前故意停頓 1~2 秒。
|
|
export const SIGNIFICANT_PAUSE_THRESHOLD = 0.6;
|
|
export const SIGNIFICANT_PAUSE_SECONDS = [1, 2];
|
|
export const DEFAULT_PAUSE_SECONDS = 0.2;
|
|
|
|
// P-2 打斷與搶話:需要「talkative」特徵(見 archetype-params.ts)且親密度達朋友分層以上才可能插話。
|
|
export const INTERRUPT_MIN_INTIMACY = 40;
|
|
|
|
// P-2 親密度影響音量與氣音比例:對陌生人聲音端正清晰,對摯友放鬆、氣音比例上升。
|
|
export function intimacyVoiceModifiers(intimacy: number): { volumeMultiplier: number; breathinessRatio: number } {
|
|
const clamped = Math.max(0, Math.min(100, intimacy));
|
|
return {
|
|
volumeMultiplier: 1 - (clamped / 100) * 0.15, // 端正清晰(1.0) → 放鬆慵懶(0.85)
|
|
breathinessRatio: clamped / 100,
|
|
};
|
|
}
|