feat: 完成 O 群組 — 立繪子系統

後端(apps/api/src/tachie/):立繪 manifest(身體姿勢/服裝/表情眉眼口/
效果層/微動態的資產目錄,服裝目錄有 M-1 登錄時優先採用);表情解析依
情緒引擎六狀態對照眉/眼/口/效果/姿勢,依 RelationshipStage 分層解鎖
(陌生~認識/朋友/摯友/羈絆,哭臉限羈絆才會出現),依角色外顯度縮放
差分幅度(magnitude = expressiveness × 情緒強度);emotionTag(實際
套用)與 rawEmotionTag(情緒引擎真實判定)分離,讓文字生成與表情解析
維持兩條獨立資料流,「文字與立繪故意不一致」是這個設計的自然結果,
不需要額外邏輯;視線方向依思考/迴避/警戒切換。

前端(apps/web/components/TachieStage.tsx):以 PixiJS 實作分層合成
與眨眼/呼吸微動態,預留 TachieRenderer 介面供未來 Live2D Cubism SDK
實作(未引入任何 Live2D 相依或授權,純架構佔位);接上 ChatView 取代
H 群組的佔位立繪,聊天回應後先查詢表情、延遲約 220ms 才顯示文字,
在非串流架構下盡力模擬「表情先變化、文字後出現」。

以 scratchpad 暫裝 playwright-core 實測瀏覽器行為(本環境無
chromium-cli),確認情緒變化即時反映在立繪、微動態持續運作。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-08-13 17:46:50 +08:00
co-authored by Claude Sonnet 5
parent eccdcba45c
commit 0f3d16059c
13 changed files with 780 additions and 17 deletions
+25
View File
@@ -49,3 +49,28 @@ export async function endChatSession(sessionId: string, characterId: string): Pr
body: JSON.stringify({ characterId }),
});
}
export interface TachieExpressionResponse {
emotionTag: EmotionTag;
rawEmotionTag: EmotionTag;
eyebrow: string;
eye: string;
mouth: string;
effect: string | null;
pose: string;
magnitude: number;
gaze: "UP" | "AWAY" | "DIRECT" | "NEUTRAL";
availableCostumes: string[];
lockedFallbackApplied: boolean;
usedCryingVariant: boolean;
}
// O-2 表情演出:情緒引擎已經在對話文字生成之前更新完畢,這裡獨立查詢目前的表情狀態,
// 讓前端可以在文字訊息出現前先套用新的表情差分。
export async function fetchExpression(characterId: string, userId: string): Promise<TachieExpressionResponse> {
const res = await fetch(`${API_BASE_URL}/tachie/${characterId}/${userId}/expression`);
if (!res.ok) {
throw new Error(`表情查詢失敗(${res.status})`);
}
return res.json();
}