Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 65 additions & 27 deletions Main/example/AIChat.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class AIChat {
private final String MEMORY_FILE = "memory.json";

// 1. 替换成你申请的 API Key
private static final String AI_API_KEY = "sk-****";
private static final String AI_API_KEY = "sk-";
private static final String AI_URL = "https://api.deepseek.com/chat/completions";
private final EmotionSmoother smoother = new EmotionSmoother(0.7);

Expand Down Expand Up @@ -83,45 +83,60 @@ private void addMessageToHistory(String role, String content) {

public void chatWithAI(String userMessage) {
try {
// 记录用户消息并持久化
addMessageToHistory("user", userMessage);

// 1. 获取 AI 回复全文 (这里内部会自动带上所有历史)
String aiReply = fetchAIReply();

// 记录AI消息并持久化
addMessageToHistory("assistant", aiReply);
saveMemoryToFile();

System.out.println("\n🤖 AI 全文: " + aiReply);

// 1. 【预处理】先切割所有片段
String[] segments = aiReply.split("(?<=[,。!?,;!?])");
List<EmotionFrame> emotionQueue = new ArrayList<>();

// 2. 【一口气分析完】不要在播放线程里临时分析
for (String segment : segments) {
String clean = segment.trim();
if (clean.isEmpty()) continue;
Map<String, Double> raw = analyzeEmotionToMap(clean);
if (raw != null) {
// 计算这一句该读多久
long duration = Math.max(1500, clean.length() * 200L);
emotionQueue.add(new EmotionFrame(clean, raw, duration));
}
}

// 3. 【平稳播放】启动播放线程
new Thread(() -> {
try {
for (String segment : segments) {
String clean = segment.trim();
if (clean.isEmpty()) continue;

Map<String, Double> raw = analyzeEmotionToMap(clean);

if (raw != null) {
updateDisplay(clean, raw);
for (EmotionFrame frame : emotionQueue) {
System.out.println("\n💬 正在说: " + frame.text);

long startTime = System.currentTimeMillis();
// 在这一句的持续时间内,进行高频平滑更新(每 50ms 一次)
while (System.currentTimeMillis() - startTime < frame.duration) {
updateDisplay(frame.text, frame.emotions);
Thread.sleep(50); // 20Hz 的刷新率,足以让表情丝滑
}

long sleepTime = Math.max(1500, clean.length() * 200L);
Thread.sleep(sleepTime);
}

// 归零回归
System.out.println("\n[表演结束,情绪回归中...]");
for (int i = 0; i < 5; i++) {
updateDisplay("...", null);
Thread.sleep(300);

// AI回答结束后,复原所有表情
if (Main.emmaProcessor != null) {
Main.emmaProcessor.resetExpressions();
}

Map<String, Double> neutral = new HashMap<>();
// 假设你的情感 key 包含 happiness 等,这里全部给 0
for (int i = 0; i < 10; i++) {
updateDisplay("...", neutral);
Thread.sleep(50);
}
System.out.print("\n👤 你: ");
} catch (Exception e) {
System.err.println("表演线程崩溃: " + e.getMessage());
System.err.println("播放线程异常: " + e.getMessage());
}
}).start();

Expand All @@ -130,21 +145,44 @@ public void chatWithAI(String userMessage) {
}
}

// 辅助类:记录每一段的话语、情感和持续时间
private static class EmotionFrame {
String text;
Map<String, Double> emotions;
long duration;
EmotionFrame(String t, Map<String, Double> e, long d) {
this.text = t; this.emotions = e; this.duration = d;
}
}

private void updateDisplay(String text, Map<String, Double> raw) {
// 1. 获取平滑后的数据(保留你原有的逻辑)
Map<String, Double> smoothed = smoother.smooth(raw);

// --- 发送 UDP 信号给 3D 软件 ---
// 2. 原有的 VMC UDP 逻辑保持不动
double happiness = smoothed.getOrDefault("happiness", 0.0);
vmcSender.sendMessage("Joy:" + happiness);

System.out.println("\n------------------------------------");
System.out.println("💬 正在说: " + text);
// 3. 【新增核心驱动】将数据喂给 Emma,驱动 Unity 里的爱音
if (Main.emmaProcessor != null && raw != null) {
// 我们需要把 Map 转回符合 Emma 要求的 JSONObject 格式,或者直接让 Emma 处理 Map
// 为了最稳妥且不改动 Emma 类,我们将 Map 转为 JSON 字符串
JSONObject mockJson = new JSONObject();
JSONObject probs = new JSONObject();
smoothed.forEach(probs::put);
mockJson.put("emotion_probabilities", probs);

// 核心下发
Main.emmaProcessor.processPythonJson(mockJson.toString());
}

// 4. 修改为只显示情感数字
System.out.println("\n💬 正在说: " + text);

smoothed.forEach((k, v) -> {
int barLen = (int) (v * 20);
String bar = "█".repeat(Math.max(0, barLen)) + "░".repeat(Math.max(0, 20 - barLen));
System.out.printf("%-10s [%s] %.2f%n", k, bar, v);
System.out.printf("%s: %.2f ", k, v);
});
System.out.println(); // 换行
}

/**
Expand Down
160 changes: 160 additions & 0 deletions Main/example/EmmaEmotionProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package example;

import org.json.JSONObject;

public class EmmaEmotionProcessor {
private UnityExpressionServer server;
private static final float THRESHOLD = 0.1f; // 降低阈值以提高灵敏性

// 添加自动回复表情功能
private long lastExpressionTime = 0;
private static final long AUTO_REPLY_INTERVAL = 3000; // 3秒间隔

// 添加缓存值以实现渐变效果
private float currentMouthSmile = 0.0f;
private float currentMouthSmirk = 0.0f;
private float currentEyeSqint = 0.0f;
private float currentFaceBlush = 0.0f;
private float currentBrowSad = 0.0f;
private float currentMouthHappy = 0.0f;
private float currentBrowAngry = 0.0f;
private float currentFaceSerious = 0.0f;
private float currentEyeWide = 0.0f;
private float currentMouthWide = 0.0f;

// 渐变速度控制 - 进一步降低以减小衰减率
private static final float SMOOTH_FACTOR = 0.1f;

public EmmaEmotionProcessor(UnityExpressionServer server) {
this.server = server;
}

/**
* 渐变辅助方法 - 使目标值平滑过渡到当前值
*/
private float smoothTransition(float currentValue, float targetValue) {
if (Math.abs(targetValue - currentValue) < 0.01f) { // 如果差异很小,直接设置
return targetValue;
}

if (targetValue > currentValue) {
return Math.min(currentValue + SMOOTH_FACTOR, targetValue);
} else {
return Math.max(currentValue - SMOOTH_FACTOR, targetValue);
}
}

/**
* 核心解析与映射逻辑 - 适配 Anon Chihaya 完整映射表
*/
public void processPythonJson(String jsonString) {
try {
JSONObject root = new JSONObject(jsonString);
JSONObject probs = root.getJSONObject("emotion_probabilities");

// 1. 提取原始概率
float happiness = probs.optFloat("happiness", 0f);
float sadness = probs.optFloat("sadness", 0f);
float anger = probs.optFloat("anger", 0f);
float surprise = probs.optFloat("surprise", 0f);
float like = probs.optFloat("like", 0f);
float fear = probs.optFloat("fear", 0f);
float disgust = probs.optFloat("disgust", 0f);

// 2. 详细映射逻辑

// --- [A. 积极/喜爱/愉悦] ---
float joy = Math.max(happiness, like);
// 增强输出数值:提高积极情绪的表达强度
float enhancedJoy = Math.min(1.0f, joy * 1.5f);
float targetMouthSmirk = enhancedJoy > THRESHOLD ? enhancedJoy * 0.8f : 0f;
float targetMouthHappy = enhancedJoy > THRESHOLD ? (enhancedJoy > 0.4f ? (enhancedJoy - 0.3f) * 2.5f : 0f) : 0f;
float targetMouthWide = enhancedJoy > THRESHOLD ? (enhancedJoy > 0.4f ? enhancedJoy - 0.3f : 0f) : 0f;
// 应用渐变效果
float targetMouthSmile = enhancedJoy > THRESHOLD ? enhancedJoy * 0.8f : 0f;
currentMouthSmile = smoothTransition(currentMouthSmile, targetMouthSmile);
currentMouthSmirk = smoothTransition(currentMouthSmirk, targetMouthSmirk);
currentMouthWide = smoothTransition(currentMouthWide, targetMouthWide);
currentMouthHappy = smoothTransition(currentMouthHappy, targetMouthHappy);

server.sendExpression("Mouth_Smile", 1.0f);
server.sendExpression("Mouth_Smirk", currentMouthSmirk);
server.sendExpression("Mouth_Wide", currentMouthWide);
server.sendExpression("Mouth_Happy", currentMouthHappy);


// --- [B. 悲伤/忧郁] ---
// 增强输出数值:提高悲伤情绪的表达强度
float enhancedSadness = Math.min(1.0f, sadness * 1.5f);
float targetBrowSad = enhancedSadness > THRESHOLD ? enhancedSadness : 0f;
targetMouthHappy = 0f; // 总是关闭快乐嘴型
float targetEyeSqintB = enhancedSadness > THRESHOLD ? enhancedSadness * 0.5f : currentEyeSqint; // 悲伤时的额外眯眼
currentBrowSad = smoothTransition(currentBrowSad, targetBrowSad);
currentMouthHappy = smoothTransition(currentMouthHappy, targetMouthHappy);
float finalEyeSqint = Math.max(currentEyeSqint, targetEyeSqintB);
server.sendExpression("Brow_Sad", currentBrowSad);
server.sendExpression("Mouth_Happy", currentMouthHappy);
server.sendExpression("Eye_Squint", finalEyeSqint);
float irritation = Math.max(anger, disgust);
// 增强愤怒/反感的表达强度
float enhancedIrritation = Math.min(1.0f, irritation * 1.5f);
float targetBrowAngry = enhancedIrritation > THRESHOLD ? enhancedIrritation : 0f;
float targetFaceSerious = enhancedIrritation > THRESHOLD ? 1.0f : 0f;
float targetMouthSmirkC = (enhancedIrritation > THRESHOLD && disgust > 0.5f) ? 0.8f : currentMouthSmirk;
currentBrowAngry = smoothTransition(currentBrowAngry, targetBrowAngry);
currentFaceSerious = smoothTransition(currentFaceSerious, targetFaceSerious);
float finalMouthSmirk = Math.max(currentMouthSmirk, targetMouthSmirkC);
server.sendExpression("Brow_Angry", currentBrowAngry);
server.sendExpression("Face_Serious", currentFaceSerious);
server.sendExpression("Mouth_Smirk", finalMouthSmirk);
float shock = Math.max(surprise, fear);
// 增强惊讶/恐惧的表达强度
float enhancedShock = Math.min(1.0f, shock * 1.5f);
float targetEyeWide = enhancedShock > THRESHOLD ? enhancedShock : 0f;
targetMouthWide = enhancedShock > THRESHOLD ? enhancedShock * 0.8f : 0f;
currentEyeWide = smoothTransition(currentEyeWide, targetEyeWide);
currentMouthWide = smoothTransition(currentMouthWide, targetMouthWide);

server.sendExpression("Eye_Wide", currentEyeWide);
server.sendExpression("Mouth_Wide", currentMouthWide);

// --- [E. 细节修正(互斥处理)] ---
// 如果正在大笑(Mouth_Wide),就不应该同时有小微笑(Smirk)
if (surprise > 0.3f) { // 降低阈值以增强响应
// 渐变关闭微笑
currentMouthSmile = smoothTransition(currentMouthSmile, 0f);
server.sendExpression("Mouth_Smile", currentMouthSmile);
}
} catch (Exception e) {
System.err.println("Emma 解析错误: " + e.getMessage());
}
}

/**
* 重置所有表情到默认值
*/
public void resetExpressions() {
currentMouthSmile = smoothTransition(currentMouthSmile, 0f);
currentMouthSmirk = smoothTransition(currentMouthSmirk, 0f);
currentEyeSqint = smoothTransition(currentEyeSqint, 0f);
currentFaceBlush = smoothTransition(currentFaceBlush, 0f);
currentBrowSad = smoothTransition(currentBrowSad, 0f);
currentMouthHappy = smoothTransition(currentMouthHappy, 0f);
currentBrowAngry = smoothTransition(currentBrowAngry, 0f);
currentFaceSerious = smoothTransition(currentFaceSerious, 0f);
currentEyeWide = smoothTransition(currentEyeWide, 0f);
currentMouthWide = smoothTransition(currentMouthWide, 0f);

// 发送所有重置值到服务器
server.sendExpression("Mouth_Smile", currentMouthSmile);
server.sendExpression("Mouth_Smirk", currentMouthSmirk);
server.sendExpression("Eye_Squint", currentEyeSqint);
server.sendExpression("Face_Blush", currentFaceBlush);
server.sendExpression("Brow_Sad", currentBrowSad);
server.sendExpression("Mouth_Happy", currentMouthHappy);
server.sendExpression("Brow_Angry", currentBrowAngry);
server.sendExpression("Face_Serious", currentFaceSerious);
server.sendExpression("Eye_Wide", currentEyeWide);
server.sendExpression("Mouth_Wide", currentMouthWide);
}
}
16 changes: 6 additions & 10 deletions Main/example/EmotionAnalyzer.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package example;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.EmotionResponse;
import example.EmotionResponse;

import java.net.URI;
import java.net.http.HttpClient;
Expand All @@ -12,23 +12,19 @@

public class EmotionAnalyzer {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final HttpClient CLIENT = HttpClient.newHttpClient();
private static final HttpClient CLIENT = HttpClient.newHttpClient();
// 💡 确保此路径与 Python 中的 @app.post("/predict") 对应
private static final String API_URL = "http://127.0.0.1:8000/predict";
private static final EmotionSmoother SMOOTHER = new EmotionSmoother(1);

private static final EmotionSmoother SMOOTHER = new EmotionSmoother(0.5);
public static void execute(String text) throws Exception {
long startTime = System.nanoTime();

// 1. 发送请求
String requestBody = MAPPER.writeValueAsString(Map.of("text", text));

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.version(HttpClient.Version.HTTP_1_1)
.header("Content-Type", "application/json; charset=UTF-8") // 显式指定字符集
.version(HttpClient.Version.HTTP_1_1) //
.header("Content-Type", "application/json; charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();

HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());

if (response.statusCode() != 200) {
Expand Down
2 changes: 1 addition & 1 deletion Main/example/EmotionResponse.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.example;
package example;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down
7 changes: 6 additions & 1 deletion Main/example/EmotionSmoother.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ public Map<String, Double> smooth(Map<String, Double> current) {
lastEmotions.put(key, smoothedVal);
});
return new HashMap<>(lastEmotions);
}
}

/**
* 【新增功能】自然衰减:让表情在没有对话时慢慢恢复自然
* 这样 Live2D 模型不会一直卡在某个激动的表情上
*/
public void applyDecay() {
lastEmotions.keySet().forEach(key -> {
double lastVal = lastEmotions.get(key);
Expand Down
Loading