Spaces:
Sleeping
Sleeping
Commit ·
dd6ff83
1
Parent(s): 6c78660
emotion_update
Browse files- core/pipeline.py +44 -16
- features/mcp/agent_bridge.py +12 -0
- static/frontend/js/websocket.js +5 -11
core/pipeline.py
CHANGED
|
@@ -211,7 +211,12 @@ class ChatPipeline:
|
|
| 211 |
language: Optional[str] = None,
|
| 212 |
) -> PipelineResult:
|
| 213 |
if not user_message or not user_message.strip():
|
| 214 |
-
return PipelineResult(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
# language 參數保留以向後兼容,但不使用(GPT 自動判斷語言)
|
| 217 |
|
|
@@ -223,33 +228,36 @@ class ChatPipeline:
|
|
| 223 |
return detect_res
|
| 224 |
has_feature, intent_data = detect_res
|
| 225 |
|
| 226 |
-
#
|
| 227 |
text_emotion = intent_data.get("emotion", "neutral") if intent_data else "neutral"
|
|
|
|
| 228 |
|
| 229 |
-
#
|
| 230 |
-
|
| 231 |
-
|
| 232 |
|
| 233 |
-
#
|
| 234 |
emotion_confidence = 0.5 # 預設置信度
|
| 235 |
if audio_emotion and audio_emotion.get("success"):
|
| 236 |
audio_emotion_label = audio_emotion.get("emotion", "neutral")
|
| 237 |
audio_confidence = audio_emotion.get("confidence", 0.0)
|
| 238 |
|
| 239 |
-
# 【優化】
|
| 240 |
-
if audio_confidence >= 0.
|
| 241 |
emotion_value = audio_emotion_label
|
| 242 |
emotion_confidence = audio_confidence
|
| 243 |
-
logger.info(f"🎭
|
| 244 |
-
logger.info(f"📝 文字情緒: {text_emotion} (輔助)")
|
| 245 |
else:
|
| 246 |
emotion_value = text_emotion
|
| 247 |
emotion_confidence = 0.5 # 文字情緒預設置信度
|
| 248 |
-
logger.info(f"
|
| 249 |
else:
|
| 250 |
emotion_value = text_emotion
|
| 251 |
emotion_confidence = 0.5 # 文字情緒預設置信度
|
| 252 |
-
logger.info(f"📝 使用文字情緒: {emotion_value}
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
# 1) 檢查是否在關懷模式
|
| 255 |
if user_id and EmotionCareManager.is_in_care_mode(user_id, chat_id):
|
|
@@ -279,7 +287,12 @@ class ChatPipeline:
|
|
| 279 |
return ai_res
|
| 280 |
text = str(ai_res or "").strip()
|
| 281 |
if not text:
|
| 282 |
-
return PipelineResult(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
return PipelineResult(text=text, is_fallback=False, meta={"care_mode": True, "emotion": care_emotion})
|
| 284 |
|
| 285 |
# 2) 檢查是否需要進入關懷模式(傳遞置信度,用於連續性判斷)
|
|
@@ -332,7 +345,12 @@ class ChatPipeline:
|
|
| 332 |
tool_name = feat_res.get('tool_name')
|
| 333 |
tool_data = feat_res.get('tool_data')
|
| 334 |
if not text:
|
| 335 |
-
return PipelineResult(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
|
| 337 |
# 簡化翻譯:非中文用戶 → 翻譯工具卡片
|
| 338 |
is_chinese = self._is_chinese_message(user_message)
|
|
@@ -365,7 +383,12 @@ class ChatPipeline:
|
|
| 365 |
# 正常字串
|
| 366 |
text = str(feat_res or "").strip()
|
| 367 |
if not text:
|
| 368 |
-
return PipelineResult(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
# 不再翻譯工具回應,讓 GPT 自己處理並用對應語言描述
|
| 371 |
|
|
@@ -394,7 +417,12 @@ class ChatPipeline:
|
|
| 394 |
return ai_res
|
| 395 |
text = str(ai_res or "").strip()
|
| 396 |
if not text:
|
| 397 |
-
return PipelineResult(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
# 一般聊天也包含情緒資訊
|
| 400 |
meta_dict = {
|
|
|
|
| 211 |
language: Optional[str] = None,
|
| 212 |
) -> PipelineResult:
|
| 213 |
if not user_message or not user_message.strip():
|
| 214 |
+
return PipelineResult(
|
| 215 |
+
text="我沒有收到您的消息,請重新輸入。",
|
| 216 |
+
is_fallback=True,
|
| 217 |
+
reason="empty",
|
| 218 |
+
meta={"emotion": "neutral", "care_mode": False}
|
| 219 |
+
)
|
| 220 |
|
| 221 |
# language 參數保留以向後兼容,但不使用(GPT 自動判斷語言)
|
| 222 |
|
|
|
|
| 228 |
return detect_res
|
| 229 |
has_feature, intent_data = detect_res
|
| 230 |
|
| 231 |
+
# 【情緒融合】雙軌制:音頻情緒優先,文字情緒輔助
|
| 232 |
text_emotion = intent_data.get("emotion", "neutral") if intent_data else "neutral"
|
| 233 |
+
logger.info(f"🎭 [情緒流向-1] 文字情緒: {text_emotion}")
|
| 234 |
|
| 235 |
+
# 檢查音頻情緒
|
| 236 |
+
if audio_emotion:
|
| 237 |
+
logger.info(f"🎭 [情緒流向-2] 音頻情緒資料: success={audio_emotion.get('success')}, emotion={audio_emotion.get('emotion')}, confidence={audio_emotion.get('confidence')}")
|
| 238 |
|
| 239 |
+
# 情緒融合邏輯
|
| 240 |
emotion_confidence = 0.5 # 預設置信度
|
| 241 |
if audio_emotion and audio_emotion.get("success"):
|
| 242 |
audio_emotion_label = audio_emotion.get("emotion", "neutral")
|
| 243 |
audio_confidence = audio_emotion.get("confidence", 0.0)
|
| 244 |
|
| 245 |
+
# 【優化】提高門檻到 0.7,避免誤判(太敏感會導致錯誤情緒)
|
| 246 |
+
if audio_confidence >= 0.7:
|
| 247 |
emotion_value = audio_emotion_label
|
| 248 |
emotion_confidence = audio_confidence
|
| 249 |
+
logger.info(f"🎭 [情緒流向-3] ✅ 採用音頻情緒: {emotion_value} (置信度: {audio_confidence:.4f})")
|
|
|
|
| 250 |
else:
|
| 251 |
emotion_value = text_emotion
|
| 252 |
emotion_confidence = 0.5 # 文字情緒預設置信度
|
| 253 |
+
logger.info(f"🎭 [情緒流向-3] ⬇️ 音頻置信度過低 ({audio_confidence:.4f}),改用文字情緒: {emotion_value}")
|
| 254 |
else:
|
| 255 |
emotion_value = text_emotion
|
| 256 |
emotion_confidence = 0.5 # 文字情緒預設置信度
|
| 257 |
+
logger.info(f"🎭 [情緒流向-3] 📝 無音頻情緒,使用文字情緒: {emotion_value}")
|
| 258 |
+
|
| 259 |
+
# 【關鍵】記錄最終情緒
|
| 260 |
+
logger.info(f"🎭 [情緒流向-最終] emotion={emotion_value}, confidence={emotion_confidence:.2f}")
|
| 261 |
|
| 262 |
# 1) 檢查是否在關懷模式
|
| 263 |
if user_id and EmotionCareManager.is_in_care_mode(user_id, chat_id):
|
|
|
|
| 287 |
return ai_res
|
| 288 |
text = str(ai_res or "").strip()
|
| 289 |
if not text:
|
| 290 |
+
return PipelineResult(
|
| 291 |
+
text="我在這裡陪你,隨時可以聊聊。",
|
| 292 |
+
is_fallback=True,
|
| 293 |
+
reason="ai-care-empty",
|
| 294 |
+
meta={"care_mode": True, "emotion": care_emotion or "sad"}
|
| 295 |
+
)
|
| 296 |
return PipelineResult(text=text, is_fallback=False, meta={"care_mode": True, "emotion": care_emotion})
|
| 297 |
|
| 298 |
# 2) 檢查是否需要進入關懷模式(傳遞置信度,用於連續性判斷)
|
|
|
|
| 345 |
tool_name = feat_res.get('tool_name')
|
| 346 |
tool_data = feat_res.get('tool_data')
|
| 347 |
if not text:
|
| 348 |
+
return PipelineResult(
|
| 349 |
+
text="抱歉,功能處理沒有產出結果。",
|
| 350 |
+
is_fallback=True,
|
| 351 |
+
reason="feature-empty",
|
| 352 |
+
meta={"emotion": emotion_value, "care_mode": False}
|
| 353 |
+
)
|
| 354 |
|
| 355 |
# 簡化翻譯:非中文用戶 → 翻譯工具卡片
|
| 356 |
is_chinese = self._is_chinese_message(user_message)
|
|
|
|
| 383 |
# 正常字串
|
| 384 |
text = str(feat_res or "").strip()
|
| 385 |
if not text:
|
| 386 |
+
return PipelineResult(
|
| 387 |
+
text="抱歉,功能處理沒有產出結果。",
|
| 388 |
+
is_fallback=True,
|
| 389 |
+
reason="feature-empty",
|
| 390 |
+
meta={"emotion": emotion_value, "care_mode": False}
|
| 391 |
+
)
|
| 392 |
|
| 393 |
# 不再翻譯工具回應,讓 GPT 自己處理並用對應語言描述
|
| 394 |
|
|
|
|
| 417 |
return ai_res
|
| 418 |
text = str(ai_res or "").strip()
|
| 419 |
if not text:
|
| 420 |
+
return PipelineResult(
|
| 421 |
+
text="抱歉,我暫時沒有合適的回應。可以換個說法再試試嗎?",
|
| 422 |
+
is_fallback=True,
|
| 423 |
+
reason="ai-empty",
|
| 424 |
+
meta={"emotion": emotion_value, "care_mode": False}
|
| 425 |
+
)
|
| 426 |
|
| 427 |
# 一般聊天也包含情緒資訊
|
| 428 |
meta_dict = {
|
features/mcp/agent_bridge.py
CHANGED
|
@@ -511,6 +511,18 @@ class MCPAgentBridge:
|
|
| 511 |
has_feature, intent_data, cached_time = self._intent_cache[cache_key]
|
| 512 |
if time_module.time() - cached_time < self._intent_cache_ttl:
|
| 513 |
logger.debug(f"💾 意圖快取命中: {message[:50]}...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
return has_feature, intent_data
|
| 515 |
else:
|
| 516 |
del self._intent_cache[cache_key]
|
|
|
|
| 511 |
has_feature, intent_data, cached_time = self._intent_cache[cache_key]
|
| 512 |
if time_module.time() - cached_time < self._intent_cache_ttl:
|
| 513 |
logger.debug(f"💾 意圖快取命中: {message[:50]}...")
|
| 514 |
+
|
| 515 |
+
# 【關鍵修復】快取命中時,仍需重新偵測情緒(情緒是即時的)
|
| 516 |
+
# 因為同一句話在不同時間說可能帶有不同的情緒強度
|
| 517 |
+
try:
|
| 518 |
+
fresh_emotion = await self._analyze_emotion_from_message(message)
|
| 519 |
+
if fresh_emotion and intent_data:
|
| 520 |
+
intent_data = dict(intent_data) # 複製避免修改原快取
|
| 521 |
+
intent_data['emotion'] = fresh_emotion
|
| 522 |
+
logger.info(f"🎭 快取命中但重新偵測情緒: {fresh_emotion}")
|
| 523 |
+
except Exception as e:
|
| 524 |
+
logger.warning(f"快取命中時情緒分析失敗: {e}")
|
| 525 |
+
|
| 526 |
return has_feature, intent_data
|
| 527 |
else:
|
| 528 |
del self._intent_cache[cache_key]
|
static/frontend/js/websocket.js
CHANGED
|
@@ -418,10 +418,8 @@ function initializeWebSocket(token) {
|
|
| 418 |
break;
|
| 419 |
|
| 420 |
case 'bot_message':
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
applyEmotion(data.emotion);
|
| 424 |
-
}
|
| 425 |
|
| 426 |
if (data.care_mode && typeof hideToolCards === 'function') {
|
| 427 |
hideToolCards();
|
|
@@ -455,10 +453,7 @@ function initializeWebSocket(token) {
|
|
| 455 |
transcript.textContent = data.text;
|
| 456 |
transcript.className = 'voice-transcript final';
|
| 457 |
window.realtimeTranscript = '';
|
| 458 |
-
|
| 459 |
-
if (data.emotion && typeof applyEmotion === 'function') {
|
| 460 |
-
applyEmotion(data.emotion);
|
| 461 |
-
}
|
| 462 |
break;
|
| 463 |
|
| 464 |
case 'realtime_stt_status':
|
|
@@ -494,9 +489,8 @@ function initializeWebSocket(token) {
|
|
| 494 |
break;
|
| 495 |
|
| 496 |
case 'audio_emotion_detected':
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
}
|
| 500 |
break;
|
| 501 |
|
| 502 |
case 'env_ack':
|
|
|
|
| 418 |
break;
|
| 419 |
|
| 420 |
case 'bot_message':
|
| 421 |
+
// 【統一】不在此處套用情緒,只由 emotion_detected 事件控制
|
| 422 |
+
// 保留情緒資訊在 data 中供調試使用
|
|
|
|
|
|
|
| 423 |
|
| 424 |
if (data.care_mode && typeof hideToolCards === 'function') {
|
| 425 |
hideToolCards();
|
|
|
|
| 453 |
transcript.textContent = data.text;
|
| 454 |
transcript.className = 'voice-transcript final';
|
| 455 |
window.realtimeTranscript = '';
|
| 456 |
+
// 【統一】不在此處套用情緒,只由 emotion_detected 事件控制
|
|
|
|
|
|
|
|
|
|
| 457 |
break;
|
| 458 |
|
| 459 |
case 'realtime_stt_status':
|
|
|
|
| 489 |
break;
|
| 490 |
|
| 491 |
case 'audio_emotion_detected':
|
| 492 |
+
// 【統一】不在此處套用情緒,只由 emotion_detected 事件控制
|
| 493 |
+
// 後端會融合音頻和文字情緒後統一發送 emotion_detected
|
|
|
|
| 494 |
break;
|
| 495 |
|
| 496 |
case 'env_ack':
|