Files
Kokorone/apps/web/lib/api.ts
T
JefferyandClaude Sonnet 5 0f3d16059c 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>
2026-08-13 17:46:50 +08:00

77 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}