後端(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>
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import type { EmotionTag, RelationshipStage } from "@kokorone/shared";
|
||
|
||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
|
||
|
||
export interface ChatResponseStyle {
|
||
tone: string;
|
||
sentenceLength: "short" | "medium" | "long";
|
||
initiative: "low" | "medium" | "high";
|
||
}
|
||
|
||
export interface ChatContext {
|
||
emotion: { dominant: EmotionTag; style: ChatResponseStyle };
|
||
relationship: { intimacy: number; trust: number; stage: RelationshipStage };
|
||
}
|
||
|
||
export type ChatAvailability = "NORMAL" | "BRIEF" | "DELAYED" | "NO_REPLY";
|
||
|
||
export interface ChatResult {
|
||
text: string;
|
||
actions: string[];
|
||
isFastChannel: boolean;
|
||
availability: ChatAvailability;
|
||
exceptionType: string | null;
|
||
// I-2:作息為睡眠且未破例時不會組裝上下文,此時為 null。
|
||
context: ChatContext | null;
|
||
}
|
||
|
||
export async function sendChatMessage(
|
||
characterId: string,
|
||
userId: string,
|
||
sessionId: string,
|
||
text: string,
|
||
): Promise<ChatResult> {
|
||
const res = await fetch(`${API_BASE_URL}/chat/${characterId}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ userId, sessionId, text }),
|
||
});
|
||
if (!res.ok) {
|
||
throw new Error(`對話請求失敗(${res.status})`);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
export async function endChatSession(sessionId: string, characterId: string): Promise<void> {
|
||
await fetch(`${API_BASE_URL}/session/${sessionId}/end`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
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();
|
||
}
|