MusaedMusaedSadeqMusaedAl-Fareh225739 commited on
Commit
d924b09
Β·
1 Parent(s): 6cd0b2f

Backend_upgrade

Browse files
mrrrme/backend_new.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MrrrMe Backend Entry Point
3
+ This script runs the modular backend located in the 'backend/' folder.
4
+ """
5
+ import uvicorn
6
+ import os
7
+ import sys
8
+
9
+ # Ensure we can import from the backend folder
10
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
11
+
12
+ # Import the 'app' object from the new modular structure
13
+ # This triggers backend/app.py, which loads all your other modules
14
+ from backend.app import app
15
+
16
+ if __name__ == "__main__":
17
+ print("πŸš€ Starting MrrrMe Modular Backend...")
18
+ # This matches the logic from the bottom of your old backend_server.py
19
+ uvicorn.run(app, host="0.0.0.0", port=8000)
mrrrme/{backend_server.py β†’ backend_server_old.py} RENAMED
@@ -1,1123 +1,1123 @@
1
- """MrrrMe Backend WebSocket Server - ENHANCED LOGGING VERSION"""
2
- import os
3
- import sys
4
-
5
- # ===== SET CACHE DIRECTORIES FIRST =====
6
- os.environ['HF_HOME'] = '/tmp/huggingface'
7
- os.environ['TRANSFORMERS_CACHE'] = '/tmp/transformers'
8
- os.environ['HF_HUB_CACHE'] = '/tmp/huggingface/hub'
9
- os.environ['TORCH_HOME'] = '/tmp/torch'
10
- os.makedirs('/tmp/huggingface', exist_ok=True)
11
- os.makedirs('/tmp/transformers', exist_ok=True)
12
- os.makedirs('/tmp/huggingface/hub', exist_ok=True)
13
- os.makedirs('/tmp/torch', exist_ok=True)
14
-
15
- # ===== GPU FIX: Patch TensorBoard =====
16
- class DummySummaryWriter:
17
- def __init__(self, *args, **kwargs): pass
18
- def __getattr__(self, name): return lambda *args, **kwargs: None
19
-
20
- try:
21
- import tensorboardX
22
- tensorboardX.SummaryWriter = DummySummaryWriter
23
- except: pass
24
-
25
- # ===== GPU FIX: Patch Logging to redirect /work paths =====
26
- import logging
27
- _original_FileHandler = logging.FileHandler
28
-
29
- class RedirectingFileHandler(_original_FileHandler):
30
- def __init__(self, filename, mode='a', encoding=None, delay=False, errors=None):
31
- if isinstance(filename, str) and filename.startswith('/work'):
32
- filename = '/tmp/openface_log.txt'
33
- os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '/tmp', exist_ok=True)
34
- super().__init__(filename, mode, encoding, delay, errors)
35
-
36
- logging.FileHandler = RedirectingFileHandler
37
-
38
- # Now import everything else
39
- import asyncio
40
- import json
41
- import base64
42
- import numpy as np
43
- import cv2
44
- import io
45
- import torch
46
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
47
- from fastapi.middleware.cors import CORSMiddleware
48
- from pydantic import BaseModel
49
- import requests
50
- from PIL import Image
51
- from typing import Optional
52
- import sqlite3
53
- import secrets
54
- import hashlib
55
- from datetime import datetime
56
-
57
- # Check GPU
58
- if not torch.cuda.is_available():
59
- print("[Backend] ⚠️ No GPU detected - using CPU mode")
60
- else:
61
- print(f"[Backend] βœ… GPU available: {torch.cuda.get_device_name(0)}")
62
-
63
- app = FastAPI()
64
-
65
- # CORS for browser access
66
- app.add_middleware(
67
- CORSMiddleware,
68
- allow_origins=["*"],
69
- allow_credentials=True,
70
- allow_methods=["*"],
71
- allow_headers=["*"],
72
- )
73
-
74
- # Global model variables (will be loaded after startup)
75
- face_processor = None
76
- text_analyzer = None
77
- whisper_worker = None
78
- voice_worker = None
79
- llm_generator = None
80
- fusion_engine = None
81
- models_ready = False
82
-
83
- # Avatar backend URL - environment aware
84
- def get_avatar_api_url():
85
- """Get correct avatar API URL based on environment"""
86
- # For Hugging Face Spaces, use same host
87
- if os.path.exists('/.dockerenv') or os.environ.get('SPACE_ID'):
88
- # Running in Docker/HF Spaces - use internal networking
89
- return "http://127.0.0.1:8765"
90
- else:
91
- # Local development
92
- return "http://localhost:8765"
93
-
94
- AVATAR_API = get_avatar_api_url()
95
- print(f"[Backend] 🎭 Avatar API URL: {AVATAR_API}")
96
-
97
- # ===== AUTHENTICATION & DATABASE =====
98
- # Use /data for Hugging Face Spaces (persistent) or /tmp for local dev
99
- if os.path.exists('/data'):
100
- DB_PATH = "/data/mrrrme_users.db"
101
- print("[Backend] πŸ“ Using persistent storage: /data/mrrrme_users.db")
102
- else:
103
- DB_PATH = "/tmp/mrrrme_users.db"
104
- print("[Backend] ⚠️ Using ephemeral storage: /tmp/mrrrme_users.db (will reset on rebuild!)")
105
- print("[Backend] ⚠️ To persist data, enable persistent storage in HF Spaces settings")
106
-
107
- class SignupRequest(BaseModel):
108
- username: str
109
- password: str
110
-
111
- class LoginRequest(BaseModel):
112
- username: str
113
- password: str
114
-
115
- def init_db():
116
- conn = sqlite3.connect(DB_PATH)
117
- cursor = conn.cursor()
118
-
119
- cursor.execute("""
120
- CREATE TABLE IF NOT EXISTS users (
121
- user_id TEXT PRIMARY KEY,
122
- username TEXT UNIQUE NOT NULL,
123
- password_hash TEXT NOT NULL,
124
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
125
- )
126
- """)
127
-
128
- cursor.execute("""
129
- CREATE TABLE IF NOT EXISTS sessions (
130
- session_id TEXT PRIMARY KEY,
131
- user_id TEXT NOT NULL,
132
- token TEXT UNIQUE NOT NULL,
133
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
134
- is_active BOOLEAN DEFAULT 1
135
- )
136
- """)
137
-
138
- cursor.execute("""
139
- CREATE TABLE IF NOT EXISTS messages (
140
- message_id INTEGER PRIMARY KEY AUTOINCREMENT,
141
- session_id TEXT NOT NULL,
142
- role TEXT NOT NULL,
143
- content TEXT NOT NULL,
144
- emotion TEXT,
145
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
146
- )
147
- """)
148
-
149
- cursor.execute("""
150
- CREATE TABLE IF NOT EXISTS user_summaries (
151
- user_id TEXT PRIMARY KEY,
152
- summary_text TEXT NOT NULL,
153
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
154
- )
155
- """)
156
-
157
- conn.commit()
158
- conn.close()
159
-
160
- init_db()
161
-
162
- def hash_password(pw: str) -> str:
163
- return hashlib.sha256(pw.encode()).hexdigest()
164
-
165
- @app.post("/api/signup")
166
- async def signup(req: SignupRequest):
167
- conn = sqlite3.connect(DB_PATH)
168
- cursor = conn.cursor()
169
-
170
- try:
171
- user_id = secrets.token_urlsafe(16)
172
- cursor.execute(
173
- "INSERT INTO users (user_id, username, password_hash) VALUES (?, ?, ?)",
174
- (user_id, req.username, hash_password(req.password))
175
- )
176
- conn.commit()
177
- conn.close()
178
- return {"success": True, "message": "Account created!"}
179
- except sqlite3.IntegrityError:
180
- conn.close()
181
- raise HTTPException(status_code=400, detail="Username already exists")
182
-
183
- @app.post("/api/login")
184
- async def login(req: LoginRequest):
185
- conn = sqlite3.connect(DB_PATH)
186
- cursor = conn.cursor()
187
-
188
- cursor.execute(
189
- "SELECT user_id, username FROM users WHERE username = ? AND password_hash = ?",
190
- (req.username, hash_password(req.password))
191
- )
192
-
193
- result = cursor.fetchone()
194
-
195
- if not result:
196
- conn.close()
197
- raise HTTPException(status_code=401, detail="Invalid credentials")
198
-
199
- user_id, username = result
200
-
201
- session_id = secrets.token_urlsafe(16)
202
- token = secrets.token_urlsafe(32)
203
-
204
- cursor.execute(
205
- "INSERT INTO sessions (session_id, user_id, token) VALUES (?, ?, ?)",
206
- (session_id, user_id, token)
207
- )
208
-
209
- cursor.execute(
210
- "SELECT summary_text FROM user_summaries WHERE user_id = ?",
211
- (user_id,)
212
- )
213
- summary_row = cursor.fetchone()
214
- summary = summary_row[0] if summary_row else None
215
-
216
- conn.commit()
217
- conn.close()
218
-
219
- return {
220
- "success": True,
221
- "token": token,
222
- "username": username,
223
- "user_id": user_id,
224
- "summary": summary
225
- }
226
-
227
- class LogoutRequest(BaseModel):
228
- token: str
229
-
230
- @app.post("/api/logout")
231
- async def logout(req: LogoutRequest):
232
- conn = sqlite3.connect(DB_PATH)
233
- cursor = conn.cursor()
234
-
235
- # Get session info before closing
236
- cursor.execute(
237
- "SELECT session_id, user_id FROM sessions WHERE token = ? AND is_active = 1",
238
- (req.token,)
239
- )
240
- result = cursor.fetchone()
241
-
242
- if result:
243
- session_id, user_id = result
244
-
245
- # Mark session as inactive
246
- cursor.execute(
247
- "UPDATE sessions SET is_active = 0 WHERE token = ?",
248
- (req.token,)
249
- )
250
- conn.commit()
251
- conn.close()
252
-
253
- # Generate summary on explicit logout
254
- print(f"[Logout] πŸ“ Generating summary for user {user_id}...")
255
- summary = await generate_session_summary(session_id, user_id)
256
- if summary:
257
- print(f"[Logout] βœ… Summary generated")
258
-
259
- return {"success": True, "message": "Logged out successfully"}
260
- else:
261
- conn.close()
262
- return {"success": True, "message": "Session already closed"}
263
-
264
- async def generate_session_summary(session_id: str, user_id: str):
265
- """Generate AI summary of conversation for THIS specific user"""
266
- conn = sqlite3.connect(DB_PATH)
267
- cursor = conn.cursor()
268
-
269
- # Verify session belongs to user
270
- cursor.execute(
271
- "SELECT user_id FROM sessions WHERE session_id = ?",
272
- (session_id,)
273
- )
274
- session_owner = cursor.fetchone()
275
-
276
- if not session_owner or session_owner[0] != user_id:
277
- print(f"[Summary] ❌ Security error: session {session_id} doesn't belong to user {user_id}")
278
- conn.close()
279
- return None
280
-
281
- # Get messages from this session
282
- cursor.execute(
283
- "SELECT role, content, emotion FROM messages WHERE session_id = ? ORDER BY timestamp ASC",
284
- (session_id,)
285
- )
286
-
287
- messages = cursor.fetchall()
288
-
289
- # Get username for better logging
290
- cursor.execute("SELECT username FROM users WHERE user_id = ?", (user_id,))
291
- username_row = cursor.fetchone()
292
- username = username_row[0] if username_row else user_id
293
-
294
- conn.close()
295
-
296
- if len(messages) < 3:
297
- print(f"[Summary] ⏭️ Skipped for {username} (only {len(messages)} messages)")
298
- return None
299
-
300
- conversation = ""
301
- for role, content, emotion in messages:
302
- speaker = "User" if role == "user" else "AI"
303
- emo_tag = f" [{emotion}]" if emotion else ""
304
- conversation += f"{speaker}{emo_tag}: {content}\n"
305
-
306
- try:
307
- from groq import Groq
308
- groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
309
-
310
- prompt = f"""Analyze this conversation and create a 2-3 sentence summary about THIS SPECIFIC USER.
311
-
312
- DO NOT include information about other users or other conversations.
313
- ONLY summarize what THIS user said and their patterns.
314
-
315
- Conversation ({len(messages)} messages):
316
- {conversation}
317
-
318
- Create a concise summary including: topics this user discussed, their emotional patterns, personal details THEY mentioned, and their preferences."""
319
-
320
- response = groq_client.chat.completions.create(
321
- model="llama-3.1-8b-instant",
322
- messages=[{"role": "user", "content": prompt}],
323
- max_tokens=150,
324
- temperature=0.7
325
- )
326
-
327
- summary = response.choices[0].message.content.strip()
328
-
329
- # Save summary FOR THIS USER ONLY
330
- conn = sqlite3.connect(DB_PATH)
331
- cursor = conn.cursor()
332
-
333
- cursor.execute(
334
- "INSERT OR REPLACE INTO user_summaries (user_id, summary_text, updated_at) VALUES (?, ?, ?)",
335
- (user_id, summary, datetime.now())
336
- )
337
-
338
- conn.commit()
339
- conn.close()
340
-
341
- print(f"[Summary] βœ… Generated for {username} (user_id: {user_id})")
342
- print(f"[Summary] πŸ“ Content: {summary}")
343
- return summary
344
-
345
- except Exception as e:
346
- print(f"[Summary] ❌ Error for {username}: {e}")
347
- import traceback
348
- traceback.print_exc()
349
- return None
350
-
351
- @app.on_event("startup")
352
- async def startup_event():
353
- """Start loading models in background after server is ready"""
354
- print("[Backend] πŸš€ Starting up...")
355
-
356
- # Check if avatar service is running
357
- try:
358
- response = requests.get(f"{AVATAR_API}/", timeout=2)
359
- if response.status_code == 200:
360
- print(f"[Backend] βœ… Avatar TTS service available at {AVATAR_API}")
361
- else:
362
- print(f"[Backend] ⚠️ Avatar TTS service responded with {response.status_code}")
363
- except requests.exceptions.ConnectionError:
364
- print(f"[Backend] ⚠️ Avatar TTS service NOT available at {AVATAR_API}")
365
- print(f"[Backend] πŸ’‘ Text-only mode will be used (no avatar speech)")
366
- print(f"[Backend] πŸ“ To enable avatar:")
367
- print(f"[Backend] cd avatar && python speak_server.py")
368
- except Exception as e:
369
- print(f"[Backend] ⚠️ Error checking avatar service: {e}")
370
-
371
- asyncio.create_task(load_models())
372
-
373
- async def load_models():
374
- """Load all AI models asynchronously"""
375
- global face_processor, text_analyzer, whisper_worker, voice_worker
376
- global llm_generator, fusion_engine, models_ready
377
-
378
- print("[Backend] πŸš€ Initializing MrrrMe AI models in background...")
379
-
380
- try:
381
- # Import modules
382
- from mrrrme.vision.face_processor import FaceProcessor
383
- from mrrrme.audio.voice_emotion import VoiceEmotionWorker
384
- from mrrrme.audio.whisper_transcription import WhisperTranscriptionWorker
385
- from mrrrme.nlp.text_sentiment import TextSentimentAnalyzer
386
- from mrrrme.nlp.llm_generator_groq import LLMResponseGenerator
387
- from mrrrme.config import FUSE4
388
-
389
- # Load models
390
- print("[Backend] Loading FaceProcessor...")
391
- face_processor = FaceProcessor()
392
-
393
- print("[Backend] Loading TextSentiment...")
394
- text_analyzer = TextSentimentAnalyzer()
395
-
396
- print("[Backend] Loading Whisper...")
397
- whisper_worker = WhisperTranscriptionWorker(text_analyzer)
398
-
399
- print("[Backend] Loading VoiceEmotion...")
400
- voice_worker = VoiceEmotionWorker(whisper_worker=whisper_worker)
401
-
402
- print("[Backend] Initializing LLM...")
403
- groq_api_key = os.getenv("GROQ_API_KEY", "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")
404
- llm_generator = LLMResponseGenerator(api_key=groq_api_key)
405
-
406
- # Initialize fusion engine
407
- class FusionEngine:
408
- def __init__(self):
409
- self.alpha_face = 0.4
410
- self.alpha_voice = 0.3
411
- self.alpha_text = 0.3
412
-
413
- def fuse(self, face_probs, voice_probs, text_probs):
414
- fused = (
415
- self.alpha_face * face_probs +
416
- self.alpha_voice * voice_probs +
417
- self.alpha_text * text_probs
418
- )
419
- fused = fused / (np.sum(fused) + 1e-8)
420
- fused_idx = int(np.argmax(fused))
421
- fused_emotion = FUSE4[fused_idx]
422
- intensity = float(np.max(fused))
423
- return fused_emotion, intensity
424
-
425
- fusion_engine = FusionEngine()
426
- models_ready = True
427
-
428
- print("[Backend] βœ… All models loaded!")
429
-
430
- except Exception as e:
431
- print(f"[Backend] ❌ Error loading models: {e}")
432
- import traceback
433
- traceback.print_exc()
434
-
435
- @app.get("/")
436
- async def root():
437
- """Root endpoint"""
438
- return {
439
- "status": "running",
440
- "models_ready": models_ready,
441
- "message": "MrrrMe AI Backend"
442
- }
443
-
444
- @app.get("/health")
445
- async def health():
446
- """Health check - responds immediately"""
447
- return {
448
- "status": "healthy",
449
- "models_ready": models_ready
450
- }
451
-
452
- @app.get("/api/debug/users")
453
- async def debug_users():
454
- """Debug endpoint - view all users and their summaries"""
455
- conn = sqlite3.connect(DB_PATH)
456
- cursor = conn.cursor()
457
-
458
- cursor.execute("""
459
- SELECT u.username, u.user_id, s.summary_text, s.updated_at
460
- FROM users u
461
- LEFT JOIN user_summaries s ON u.user_id = s.user_id
462
- ORDER BY u.created_at DESC
463
- """)
464
-
465
- users = []
466
- for username, user_id, summary, updated in cursor.fetchall():
467
- users.append({
468
- "username": username,
469
- "user_id": user_id,
470
- "summary": summary,
471
- "summary_updated": updated
472
- })
473
-
474
- conn.close()
475
-
476
- return {"users": users, "database": DB_PATH}
477
-
478
- @app.get("/api/debug/sessions")
479
- async def debug_sessions():
480
- """Debug endpoint - view all active sessions"""
481
- conn = sqlite3.connect(DB_PATH)
482
- cursor = conn.cursor()
483
-
484
- cursor.execute("""
485
- SELECT s.session_id, s.token, u.username, s.is_active, s.created_at
486
- FROM sessions s
487
- JOIN users u ON s.user_id = u.user_id
488
- ORDER BY s.created_at DESC
489
- LIMIT 20
490
- """)
491
-
492
- sessions = []
493
- for session_id, token, username, is_active, created_at in cursor.fetchall():
494
- sessions.append({
495
- "session_id": session_id,
496
- "token_preview": token[:10] + "..." if token else None,
497
- "username": username,
498
- "is_active": bool(is_active),
499
- "created_at": created_at
500
- })
501
-
502
- conn.close()
503
-
504
- return {"sessions": sessions, "database": DB_PATH}
505
-
506
- @app.websocket("/ws")
507
- async def websocket_endpoint(websocket: WebSocket):
508
- await websocket.accept()
509
- print("[WebSocket] βœ… Client connected!")
510
-
511
- # ===== AUTHENTICATION =====
512
- session_data = None
513
- user_summary = None
514
- session_id = None
515
- user_id = None
516
- username = None
517
-
518
- try:
519
- auth_msg = await websocket.receive_json()
520
- print(f"[WebSocket] πŸ“¨ Auth message received: {auth_msg.get('type')}")
521
-
522
- if auth_msg.get("type") != "auth":
523
- print(f"[WebSocket] ❌ Wrong message type: {auth_msg.get('type')}")
524
- await websocket.send_json({"type": "error", "message": "Authentication required"})
525
- return
526
-
527
- token = auth_msg.get("token")
528
- print(f"[WebSocket] πŸ”‘ Validating token: {token[:10] if token else 'None'}...")
529
-
530
- if not token:
531
- print(f"[WebSocket] ❌ No token provided!")
532
- await websocket.send_json({"type": "error", "message": "No token provided"})
533
- return
534
-
535
- # Validate token
536
- conn = sqlite3.connect(DB_PATH)
537
- cursor = conn.cursor()
538
-
539
- cursor.execute(
540
- "SELECT s.session_id, s.user_id, u.username FROM sessions s JOIN users u ON s.user_id = u.user_id WHERE s.token = ? AND s.is_active = 1",
541
- (token,)
542
- )
543
-
544
- result = cursor.fetchone()
545
-
546
- if not result:
547
- # Debug: Check if token exists at all
548
- cursor.execute("SELECT session_id, user_id, is_active FROM sessions WHERE token = ?", (token,))
549
- debug_result = cursor.fetchone()
550
-
551
- if debug_result:
552
- print(f"[WebSocket] ⚠️ Token found but session inactive or invalid: {debug_result}")
553
- else:
554
- print(f"[WebSocket] ❌ Token not found in database!")
555
-
556
- await websocket.send_json({"type": "error", "message": "Invalid session - please login again"})
557
- conn.close()
558
- return
559
-
560
- session_id, user_id, username = result
561
- print(f"[WebSocket] βœ… Token validated for user: {username} (session: {session_id})")
562
-
563
- # Get user-specific summary
564
- cursor.execute(
565
- "SELECT summary_text FROM user_summaries WHERE user_id = ?",
566
- (user_id,)
567
- )
568
- summary_row = cursor.fetchone()
569
- user_summary = summary_row[0] if summary_row else None
570
-
571
- conn.close()
572
-
573
- session_data = {
574
- 'session_id': session_id,
575
- 'user_id': user_id,
576
- 'username': username
577
- }
578
-
579
- # Send authenticated confirmation
580
- await websocket.send_json({
581
- "type": "authenticated",
582
- "username": username,
583
- "summary": user_summary
584
- })
585
-
586
- print(f"[WebSocket] βœ… Authenticated: {username} (user_id: {user_id})")
587
- if user_summary:
588
- print(f"[WebSocket] πŸ“– Loaded summary: {user_summary[:60]}...")
589
-
590
- # Clear LLM's conversation history
591
- if llm_generator:
592
- llm_generator.clear_history()
593
- print(f"[LLM] πŸ—‘οΈ Conversation history cleared")
594
-
595
- # Load user's recent conversation history
596
- conn = sqlite3.connect(DB_PATH)
597
- cursor = conn.cursor()
598
- cursor.execute(
599
- """SELECT role, content FROM messages
600
- WHERE session_id IN (
601
- SELECT session_id FROM sessions WHERE user_id = ?
602
- )
603
- ORDER BY timestamp DESC
604
- LIMIT 10""",
605
- (user_id,)
606
- )
607
- user_history = cursor.fetchall()
608
- conn.close()
609
-
610
- # Load user-specific history into LLM
611
- for role, content in reversed(user_history):
612
- llm_generator.conversation_history.append({
613
- "role": role,
614
- "content": content
615
- })
616
-
617
- if user_history:
618
- print(f"[WebSocket] πŸ“š Loaded {len(user_history)} messages from {username}'s history")
619
-
620
- except Exception as auth_err:
621
- print(f"[WebSocket] ❌ Auth error: {auth_err}")
622
- return
623
-
624
- # Wait for models to load if needed
625
- if not models_ready:
626
- await websocket.send_json({
627
- "type": "status",
628
- "message": "AI models are loading, please wait..."
629
- })
630
-
631
- # Wait up to 15 minutes for models
632
- for _ in range(900):
633
- if models_ready:
634
- await websocket.send_json({
635
- "type": "status",
636
- "message": "Models loaded! Ready to chat."
637
- })
638
- break
639
- await asyncio.sleep(1)
640
-
641
- if not models_ready:
642
- await websocket.send_json({
643
- "type": "error",
644
- "message": "Models failed to load. Please refresh."
645
- })
646
- return
647
-
648
- # Session state
649
- audio_buffer = []
650
- user_preferences = {"voice": "female", "language": "en"}
651
-
652
- try:
653
- while True:
654
- data = await websocket.receive_json()
655
- msg_type = data.get("type")
656
-
657
- # ============ PREFERENCES UPDATE ============
658
- if msg_type == "preferences":
659
- if "voice" in data:
660
- user_preferences["voice"] = data.get("voice", "female")
661
- if "language" in data:
662
- user_preferences["language"] = data.get("language", "en")
663
- print(f"[Preferences] {username}: voice={user_preferences.get('voice')}, language={user_preferences.get('language')}")
664
- continue
665
-
666
- # ============ AUTO-GREETING REQUEST ============
667
- elif msg_type == "request_greeting":
668
- try:
669
- print(f"[WebSocket] πŸ€– Generating initial greeting for {username}...")
670
-
671
- # Determine greeting based on user context
672
- greeting_prompts = {
673
- "new": f"Hey {username}! I'm MrrrMe, your emotion AI companion. How are you feeling today?",
674
- "returning": f"Welcome back, {username}! It's great to see you again. How have you been?"
675
- }
676
-
677
- # Check if user has summary (returning user)
678
- greeting_text = greeting_prompts["returning"] if user_summary else greeting_prompts["new"]
679
-
680
- # Add language context
681
- if user_preferences.get("language") == "nl":
682
- if user_summary:
683
- greeting_text = f"Welkom terug, {username}! Fijn je weer te zien. Hoe gaat het met je?"
684
- else:
685
- greeting_text = f"Hoi {username}! Ik ben MrrrMe, jouw emotie AI-metgezel. Hoe voel je je vandaag?"
686
-
687
- print(f"[Greeting] πŸ‘‹ Sending: '{greeting_text}'")
688
-
689
- # Try to send to avatar for TTS
690
- audio_url = None
691
- visemes = None
692
-
693
- try:
694
- voice_preference = user_preferences.get("voice", "female")
695
- language_preference = user_preferences.get("language", "en")
696
-
697
- print(f"[Greeting] πŸ”Š Requesting TTS from avatar service...")
698
- avatar_response = requests.post(
699
- f"{AVATAR_API}/speak",
700
- data={
701
- "text": greeting_text,
702
- "voice": voice_preference,
703
- "language": language_preference
704
- },
705
- timeout=10
706
- )
707
-
708
- if avatar_response.status_code == 200:
709
- avatar_data = avatar_response.json()
710
- audio_url = avatar_data.get("audio_url")
711
- visemes = avatar_data.get("visemes")
712
- print(f"[Greeting] βœ… TTS generated successfully")
713
- else:
714
- print(f"[Greeting] ⚠️ TTS failed: {avatar_response.status_code}")
715
-
716
- except requests.exceptions.ConnectionError as conn_err:
717
- print(f"[Greeting] ⚠️ Avatar service not available (port 8765 not responding)")
718
- print(f"[Greeting] πŸ“ Sending text-only greeting (TTS will be skipped)")
719
-
720
- except Exception as tts_err:
721
- print(f"[Greeting] ⚠️ TTS error: {tts_err}")
722
- print(f"[Greeting] πŸ“ Sending text-only greeting")
723
-
724
- # Send greeting to client
725
- response_data = {
726
- "type": "llm_response",
727
- "text": greeting_text,
728
- "emotion": "Neutral",
729
- "intensity": 0.5,
730
- "is_greeting": True
731
- }
732
-
733
- # Add audio/visemes only if TTS succeeded
734
- if audio_url and visemes:
735
- response_data["audio_url"] = audio_url
736
- response_data["visemes"] = visemes
737
- else:
738
- response_data["text_only"] = True
739
- print(f"[Greeting] πŸ“ Sending text-only (no TTS)")
740
-
741
- await websocket.send_json(response_data)
742
-
743
- # Save greeting to history
744
- conn = sqlite3.connect(DB_PATH)
745
- cursor = conn.cursor()
746
- cursor.execute(
747
- "INSERT INTO messages (session_id, role, content, emotion) VALUES (?, ?, ?, ?)",
748
- (session_id, "assistant", greeting_text, "Neutral")
749
- )
750
- conn.commit()
751
- conn.close()
752
-
753
- print(f"[Greeting] βœ… Sent to {username}")
754
-
755
- except Exception as greeting_err:
756
- print(f"[Greeting] ❌ Error: {greeting_err}")
757
- import traceback
758
- traceback.print_exc()
759
-
760
- try:
761
- await websocket.send_json({
762
- "type": "error",
763
- "message": "Greeting failed - avatar service unavailable"
764
- })
765
- except:
766
- pass
767
-
768
- # ============ VIDEO FRAME - UPDATED WITH PROBABILITIES ============
769
- elif msg_type == "video_frame":
770
- try:
771
- # Decode base64 image
772
- img_data = base64.b64decode(data["frame"].split(",")[1])
773
- img = Image.open(io.BytesIO(img_data))
774
- frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
775
-
776
- # Process face emotion
777
- try:
778
- processed_frame, result = face_processor.process_frame(frame)
779
- face_emotion = face_processor.get_last_emotion() or "Neutral"
780
- face_confidence = face_processor.get_last_confidence() or 0.0
781
- face_probs = face_processor.get_last_probs()
782
- face_quality = face_processor.get_last_quality() if hasattr(face_processor, 'get_last_quality') else 0.5
783
- except Exception as proc_err:
784
- print(f"[FaceProcessor] Error: {proc_err}")
785
- face_emotion = "Neutral"
786
- face_confidence = 0.0
787
- face_probs = np.array([0.25, 0.25, 0.25, 0.25])
788
- face_quality = 0.0
789
-
790
- # Send face emotion to frontend with probabilities
791
- await websocket.send_json({
792
- "type": "face_emotion",
793
- "emotion": face_emotion,
794
- "confidence": face_confidence,
795
- "probabilities": face_probs.tolist(),
796
- "quality": face_quality
797
- })
798
-
799
- except Exception as e:
800
- print(f"[Video] Error: {e}")
801
-
802
- # ============ AUDIO CHUNK ============
803
- elif msg_type == "audio_chunk":
804
- try:
805
- audio_data = base64.b64decode(data["audio"])
806
- audio_buffer.append(audio_data)
807
-
808
- if len(audio_buffer) >= 5:
809
- voice_probs, voice_emotion = voice_worker.get_probs()
810
- await websocket.send_json({
811
- "type": "voice_emotion",
812
- "emotion": voice_emotion
813
- })
814
- audio_buffer = audio_buffer[-3:]
815
-
816
- except Exception as e:
817
- print(f"[Audio] Error: {e}")
818
-
819
- # ============ USER FINISHED SPEAKING (ENHANCED LOGGING) ============
820
- elif msg_type == "speech_end":
821
- transcription = data.get("text", "").strip()
822
-
823
- print(f"\n{'='*80}")
824
- print(f"[Speech End] 🎀 USER FINISHED SPEAKING: {username}")
825
- print(f"{'='*80}")
826
- print(f"[Transcription] '{transcription}'")
827
-
828
- # Filter short/meaningless
829
- if len(transcription) < 2:
830
- print(f"[Filter] ⏭️ Skipped: Too short ({len(transcription)} chars)")
831
- continue
832
-
833
- hallucinations = {"thank you", "thanks", "okay", "ok", "you", "yeah", "yep"}
834
- if transcription.lower().strip('.,!?') in hallucinations:
835
- print(f"[Filter] ⏭️ Skipped: Hallucination detected ('{transcription}')")
836
- continue
837
-
838
- # Save user message
839
- conn = sqlite3.connect(DB_PATH)
840
- cursor = conn.cursor()
841
- cursor.execute(
842
- "INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)",
843
- (session_id, "user", transcription)
844
- )
845
- conn.commit()
846
- conn.close()
847
-
848
- try:
849
- # ========== EMOTION DETECTION PIPELINE (ENHANCED LOGGING) ==========
850
- print(f"\n[Pipeline] πŸ” Starting emotion analysis pipeline...")
851
- print(f"{'─'*80}")
852
-
853
- # Step 1: Get face emotion
854
- print(f"[Step 1/4] πŸ“Έ FACIAL EXPRESSION ANALYSIS")
855
- face_emotion = face_processor.get_last_emotion()
856
- face_confidence = face_processor.get_last_confidence()
857
- face_quality = face_processor.get_last_quality() if hasattr(face_processor, 'get_last_quality') else 0.5
858
-
859
- # Create emotion probabilities
860
- emotion_map = {'Neutral': 0, 'Happy': 1, 'Sad': 2, 'Angry': 3}
861
- face_probs = np.array([0.25, 0.25, 0.25, 0.25], dtype=np.float32)
862
- if face_emotion in emotion_map:
863
- face_idx = emotion_map[face_emotion]
864
- face_probs[face_idx] = face_confidence
865
- face_probs = face_probs / face_probs.sum()
866
-
867
- print(f" Result: {face_emotion}")
868
- print(f" Confidence: {face_confidence:.3f}")
869
- print(f" Quality Score: {face_quality:.3f}")
870
- print(f" Distribution: Neutral={face_probs[0]:.3f} | Happy={face_probs[1]:.3f} | Sad={face_probs[2]:.3f} | Angry={face_probs[3]:.3f}")
871
-
872
- # Step 2: Get voice emotion
873
- print(f"\n[Step 2/4] 🎀 VOICE TONE ANALYSIS")
874
- voice_probs, voice_emotion = voice_worker.get_probs()
875
- voice_state = voice_worker.get_state()
876
- voice_active = voice_state.get('speech_active', False)
877
- voice_inferences = voice_state.get('inference_count', 0)
878
- voice_skipped = voice_state.get('skipped_inferences', 0)
879
-
880
- print(f" {'βœ… ACTIVELY PROCESSING' if voice_active else '⚠️ IDLE (no recent speech)'}")
881
- print(f" Result: {voice_emotion}")
882
- print(f" Distribution: Neutral={voice_probs[0]:.3f} | Happy={voice_probs[1]:.3f} | Sad={voice_probs[2]:.3f} | Angry={voice_probs[3]:.3f}")
883
- print(f" Inferences completed: {voice_inferences}")
884
- print(f" Skipped (silence optimization): {voice_skipped}")
885
- efficiency = (voice_inferences / (voice_inferences + voice_skipped) * 100) if (voice_inferences + voice_skipped) > 0 else 0
886
- print(f" Processing efficiency: {efficiency:.1f}%")
887
-
888
- # Step 3: Analyze text sentiment
889
- print(f"\n[Step 3/4] πŸ’¬ TEXT SENTIMENT ANALYSIS")
890
- print(f" βœ… Using Whisper transcription")
891
- text_analyzer.analyze(transcription)
892
- text_probs, _ = text_analyzer.get_probs()
893
- text_emotion = ['Neutral', 'Happy', 'Sad', 'Angry'][int(np.argmax(text_probs))]
894
-
895
- print(f" Result: {text_emotion}")
896
- print(f" Distribution: Neutral={text_probs[0]:.3f} | Happy={text_probs[1]:.3f} | Sad={text_probs[2]:.3f} | Angry={text_probs[3]:.3f}")
897
- print(f" Text length: {len(transcription)} characters")
898
-
899
- # Step 4: Calculate fusion weights
900
- print(f"\n[Step 4/4] βš–οΈ MULTI-MODAL FUSION")
901
- base_weights = {
902
- 'face': fusion_engine.alpha_face,
903
- 'voice': fusion_engine.alpha_voice,
904
- 'text': fusion_engine.alpha_text
905
- }
906
-
907
- # Adjust weights based on quality/confidence
908
- adjusted_weights = base_weights.copy()
909
- adjustments_made = []
910
-
911
- # Reduce face weight if quality is poor
912
- if face_quality < 0.5:
913
- adjusted_weights['face'] *= 0.7
914
- adjustments_made.append(f"Face weight reduced (low quality: {face_quality:.3f})")
915
-
916
- # Reduce voice weight if not active
917
- if not voice_active:
918
- adjusted_weights['voice'] *= 0.5
919
- adjustments_made.append(f"Voice weight reduced (no recent speech)")
920
-
921
- # Reduce text weight if very short
922
- if len(transcription) < 10:
923
- adjusted_weights['text'] *= 0.7
924
- adjustments_made.append(f"Text weight reduced (short input: {len(transcription)} chars)")
925
-
926
- # Normalize weights to sum to 1.0
927
- total_weight = sum(adjusted_weights.values())
928
- final_weights = {k: v/total_weight for k, v in adjusted_weights.items()}
929
-
930
- print(f" Base weights: Face={base_weights['face']:.3f} | Voice={base_weights['voice']:.3f} | Text={base_weights['text']:.3f}")
931
- if adjustments_made:
932
- print(f" Adjustments:")
933
- for adj in adjustments_made:
934
- print(f" - {adj}")
935
- print(f" Final weights: Face={final_weights['face']:.3f} | Voice={final_weights['voice']:.3f} | Text={final_weights['text']:.3f}")
936
-
937
- # Calculate weighted fusion
938
- fused_probs = (
939
- final_weights['face'] * face_probs +
940
- final_weights['voice'] * voice_probs +
941
- final_weights['text'] * text_probs
942
- )
943
- fused_probs = fused_probs / (np.sum(fused_probs) + 1e-8)
944
-
945
- fused_emotion, intensity = fusion_engine.fuse(face_probs, voice_probs, text_probs)
946
-
947
- # Calculate fusion accuracy metrics
948
- agreement_count = sum([
949
- face_emotion == fused_emotion,
950
- voice_emotion == fused_emotion,
951
- text_emotion == fused_emotion
952
- ])
953
- agreement_score = agreement_count / 3.0
954
-
955
- # Check for conflicts
956
- all_same = (face_emotion == voice_emotion == text_emotion)
957
- has_conflict = len({face_emotion, voice_emotion, text_emotion}) == 3
958
-
959
- print(f"\n {'─'*76}")
960
- print(f" FUSION RESULTS:")
961
- print(f" {'─'*76}")
962
- print(f" Input emotions:")
963
- print(f" Face: {face_emotion:7s} (confidence={face_probs[emotion_map.get(face_emotion, 0)]:.3f}, weight={final_weights['face']:.3f})")
964
- print(f" Voice: {voice_emotion:7s} (confidence={voice_probs[emotion_map.get(voice_emotion, 0)]:.3f}, weight={final_weights['voice']:.3f})")
965
- print(f" Text: {text_emotion:7s} (confidence={text_probs[emotion_map.get(text_emotion, 0)]:.3f}, weight={final_weights['text']:.3f})")
966
- print(f" {'─'*76}")
967
- print(f" FUSED EMOTION: {fused_emotion}")
968
- print(f" Intensity: {intensity:.3f}")
969
- print(f" Fused distribution: Neutral={fused_probs[0]:.3f} | Happy={fused_probs[1]:.3f} | Sad={fused_probs[2]:.3f} | Angry={fused_probs[3]:.3f}")
970
- print(f" {'─'*76}")
971
- print(f" Agreement: {agreement_count}/3 modalities ({agreement_score*100:.1f}%)")
972
-
973
- if all_same:
974
- print(f" Status: βœ… Perfect agreement - all modalities aligned")
975
- elif has_conflict:
976
- print(f" Status: ⚠️ Full conflict - weighted fusion resolved disagreement")
977
- else:
978
- print(f" Status: πŸ“Š Partial agreement - majority vote with confidence weighting")
979
-
980
- print(f" {'─'*76}")
981
-
982
- # ========== LLM INPUT PREPARATION ==========
983
- print(f"\n[LLM Input] 🧠 Preparing context for language model...")
984
-
985
- # Language instruction
986
- user_language = user_preferences.get("language", "en")
987
-
988
- context_prefix = ""
989
- if user_summary:
990
- context_prefix = f"[User context for {username}: {user_summary}]\n\n"
991
- print(f"[LLM Input] - User context: YES ({len(user_summary)} chars)")
992
- else:
993
- print(f"[LLM Input] - User context: NO (new user)")
994
-
995
- # Add language instruction
996
- if user_language == "nl":
997
- context_prefix += "[BELANGRIJK: Antwoord ALTIJD in het Nederlands!]\n\n"
998
- print(f"[LLM Input] - Language: Dutch (Nederlands)")
999
- else:
1000
- context_prefix += "[IMPORTANT: ALWAYS respond in English!]\n\n"
1001
- print(f"[LLM Input] - Language: English")
1002
-
1003
- full_llm_input = context_prefix + transcription
1004
-
1005
- print(f"[LLM Input] - Fused emotion: {fused_emotion}")
1006
- print(f"[LLM Input] - Face emotion: {face_emotion}")
1007
- print(f"[LLM Input] - Voice emotion: {voice_emotion}")
1008
- print(f"[LLM Input] - Intensity: {intensity:.3f}")
1009
- print(f"[LLM Input] - User text: '{transcription}'")
1010
- print(f"[LLM Input] - Full prompt length: {len(full_llm_input)} chars")
1011
-
1012
- if len(context_prefix) > 50:
1013
- print(f"[LLM Input] - Context preview: '{context_prefix[:100]}...'")
1014
-
1015
- # Generate LLM response
1016
- print(f"\n[LLM] πŸ€– Generating response...")
1017
- response_text = llm_generator.generate_response(
1018
- fused_emotion, face_emotion, voice_emotion,
1019
- full_llm_input, force=True, intensity=intensity
1020
- )
1021
-
1022
- print(f"[LLM] βœ… Response generated: '{response_text}'")
1023
-
1024
- # Save assistant message
1025
- conn = sqlite3.connect(DB_PATH)
1026
- cursor = conn.cursor()
1027
- cursor.execute(
1028
- "INSERT INTO messages (session_id, role, content, emotion) VALUES (?, ?, ?, ?)",
1029
- (session_id, "assistant", response_text, fused_emotion)
1030
- )
1031
- conn.commit()
1032
- conn.close()
1033
-
1034
- # ========== SEND TO AVATAR FOR TTS ==========
1035
- print(f"\n[TTS] 🎭 Sending to avatar backend...")
1036
-
1037
- try:
1038
- voice_preference = user_preferences.get("voice", "female")
1039
- language_preference = user_preferences.get("language", "en")
1040
-
1041
- print(f"[TTS] - Voice: {voice_preference}")
1042
- print(f"[TTS] - Language: {language_preference}")
1043
- print(f"[TTS] - Text: '{response_text}'")
1044
-
1045
- avatar_response = requests.post(
1046
- f"{AVATAR_API}/speak",
1047
- data={
1048
- "text": response_text,
1049
- "voice": voice_preference,
1050
- "language": language_preference
1051
- },
1052
- timeout=45
1053
- )
1054
- avatar_response.raise_for_status()
1055
- avatar_data = avatar_response.json()
1056
-
1057
- print(f"[TTS] βœ… Avatar TTS generated")
1058
- print(f"[TTS] - Audio URL: {avatar_data.get('audio_url', 'N/A')}")
1059
- print(f"[TTS] - Visemes: {len(avatar_data.get('visemes', []))} keyframes")
1060
-
1061
- await websocket.send_json({
1062
- "type": "llm_response",
1063
- "text": response_text,
1064
- "emotion": fused_emotion,
1065
- "intensity": intensity,
1066
- "audio_url": avatar_data.get("audio_url"),
1067
- "visemes": avatar_data.get("visemes")
1068
- })
1069
-
1070
- print(f"[Pipeline] βœ… Complete response sent to {username}")
1071
-
1072
- except requests.exceptions.ConnectionError:
1073
- print(f"[TTS] ⚠️ Avatar service not available - sending text-only")
1074
- await websocket.send_json({
1075
- "type": "llm_response",
1076
- "text": response_text,
1077
- "emotion": fused_emotion,
1078
- "intensity": intensity,
1079
- "text_only": True
1080
- })
1081
-
1082
- except Exception as avatar_err:
1083
- print(f"[TTS] ❌ Avatar error: {avatar_err}")
1084
- await websocket.send_json({
1085
- "type": "llm_response",
1086
- "text": response_text,
1087
- "emotion": fused_emotion,
1088
- "intensity": intensity,
1089
- "error": "Avatar TTS failed",
1090
- "text_only": True
1091
- })
1092
-
1093
- print(f"{'='*80}\n")
1094
-
1095
- except Exception as e:
1096
- print(f"[Pipeline] ❌ Error in emotion processing: {e}")
1097
- import traceback
1098
- traceback.print_exc()
1099
-
1100
- except WebSocketDisconnect:
1101
- print(f"[WebSocket] ❌ {username} disconnected (close/refresh)")
1102
-
1103
- except Exception as e:
1104
- print(f"[WebSocket] ❌ {username} error: {e}")
1105
- import traceback
1106
- traceback.print_exc()
1107
-
1108
- finally:
1109
- # Generate summary on disconnect
1110
- if session_data and session_id and user_id:
1111
- print(f"[WebSocket] πŸ“ Generating summary for {username} (session ended)...")
1112
- try:
1113
- summary = await generate_session_summary(session_id, user_id)
1114
- if summary:
1115
- print(f"[Summary] βœ… Saved for {username}")
1116
- else:
1117
- print(f"[Summary] ⏭️ Skipped (not enough messages)")
1118
- except Exception as summary_err:
1119
- print(f"[Summary] ❌ Error for {username}: {summary_err}")
1120
-
1121
- if __name__ == "__main__":
1122
- import uvicorn
1123
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ """MrrrMe Backend WebSocket Server - ENHANCED LOGGING VERSION"""
2
+ import os
3
+ import sys
4
+
5
+ # ===== SET CACHE DIRECTORIES FIRST =====
6
+ os.environ['HF_HOME'] = '/tmp/huggingface'
7
+ os.environ['TRANSFORMERS_CACHE'] = '/tmp/transformers'
8
+ os.environ['HF_HUB_CACHE'] = '/tmp/huggingface/hub'
9
+ os.environ['TORCH_HOME'] = '/tmp/torch'
10
+ os.makedirs('/tmp/huggingface', exist_ok=True)
11
+ os.makedirs('/tmp/transformers', exist_ok=True)
12
+ os.makedirs('/tmp/huggingface/hub', exist_ok=True)
13
+ os.makedirs('/tmp/torch', exist_ok=True)
14
+
15
+ # ===== GPU FIX: Patch TensorBoard =====
16
+ class DummySummaryWriter:
17
+ def __init__(self, *args, **kwargs): pass
18
+ def __getattr__(self, name): return lambda *args, **kwargs: None
19
+
20
+ try:
21
+ import tensorboardX
22
+ tensorboardX.SummaryWriter = DummySummaryWriter
23
+ except: pass
24
+
25
+ # ===== GPU FIX: Patch Logging to redirect /work paths =====
26
+ import logging
27
+ _original_FileHandler = logging.FileHandler
28
+
29
+ class RedirectingFileHandler(_original_FileHandler):
30
+ def __init__(self, filename, mode='a', encoding=None, delay=False, errors=None):
31
+ if isinstance(filename, str) and filename.startswith('/work'):
32
+ filename = '/tmp/openface_log.txt'
33
+ os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '/tmp', exist_ok=True)
34
+ super().__init__(filename, mode, encoding, delay, errors)
35
+
36
+ logging.FileHandler = RedirectingFileHandler
37
+
38
+ # Now import everything else
39
+ import asyncio
40
+ import json
41
+ import base64
42
+ import numpy as np
43
+ import cv2
44
+ import io
45
+ import torch
46
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
47
+ from fastapi.middleware.cors import CORSMiddleware
48
+ from pydantic import BaseModel
49
+ import requests
50
+ from PIL import Image
51
+ from typing import Optional
52
+ import sqlite3
53
+ import secrets
54
+ import hashlib
55
+ from datetime import datetime
56
+
57
+ # Check GPU
58
+ if not torch.cuda.is_available():
59
+ print("[Backend] ⚠️ No GPU detected - using CPU mode")
60
+ else:
61
+ print(f"[Backend] βœ… GPU available: {torch.cuda.get_device_name(0)}")
62
+
63
+ app = FastAPI()
64
+
65
+ # CORS for browser access
66
+ app.add_middleware(
67
+ CORSMiddleware,
68
+ allow_origins=["*"],
69
+ allow_credentials=True,
70
+ allow_methods=["*"],
71
+ allow_headers=["*"],
72
+ )
73
+
74
+ # Global model variables (will be loaded after startup)
75
+ face_processor = None
76
+ text_analyzer = None
77
+ whisper_worker = None
78
+ voice_worker = None
79
+ llm_generator = None
80
+ fusion_engine = None
81
+ models_ready = False
82
+
83
+ # Avatar backend URL - environment aware
84
+ def get_avatar_api_url():
85
+ """Get correct avatar API URL based on environment"""
86
+ # For Hugging Face Spaces, use same host
87
+ if os.path.exists('/.dockerenv') or os.environ.get('SPACE_ID'):
88
+ # Running in Docker/HF Spaces - use internal networking
89
+ return "http://127.0.0.1:8765"
90
+ else:
91
+ # Local development
92
+ return "http://localhost:8765"
93
+
94
+ AVATAR_API = get_avatar_api_url()
95
+ print(f"[Backend] 🎭 Avatar API URL: {AVATAR_API}")
96
+
97
+ # ===== AUTHENTICATION & DATABASE =====
98
+ # Use /data for Hugging Face Spaces (persistent) or /tmp for local dev
99
+ if os.path.exists('/data'):
100
+ DB_PATH = "/data/mrrrme_users.db"
101
+ print("[Backend] πŸ“ Using persistent storage: /data/mrrrme_users.db")
102
+ else:
103
+ DB_PATH = "/tmp/mrrrme_users.db"
104
+ print("[Backend] ⚠️ Using ephemeral storage: /tmp/mrrrme_users.db (will reset on rebuild!)")
105
+ print("[Backend] ⚠️ To persist data, enable persistent storage in HF Spaces settings")
106
+
107
+ class SignupRequest(BaseModel):
108
+ username: str
109
+ password: str
110
+
111
+ class LoginRequest(BaseModel):
112
+ username: str
113
+ password: str
114
+
115
+ def init_db():
116
+ conn = sqlite3.connect(DB_PATH)
117
+ cursor = conn.cursor()
118
+
119
+ cursor.execute("""
120
+ CREATE TABLE IF NOT EXISTS users (
121
+ user_id TEXT PRIMARY KEY,
122
+ username TEXT UNIQUE NOT NULL,
123
+ password_hash TEXT NOT NULL,
124
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
125
+ )
126
+ """)
127
+
128
+ cursor.execute("""
129
+ CREATE TABLE IF NOT EXISTS sessions (
130
+ session_id TEXT PRIMARY KEY,
131
+ user_id TEXT NOT NULL,
132
+ token TEXT UNIQUE NOT NULL,
133
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
134
+ is_active BOOLEAN DEFAULT 1
135
+ )
136
+ """)
137
+
138
+ cursor.execute("""
139
+ CREATE TABLE IF NOT EXISTS messages (
140
+ message_id INTEGER PRIMARY KEY AUTOINCREMENT,
141
+ session_id TEXT NOT NULL,
142
+ role TEXT NOT NULL,
143
+ content TEXT NOT NULL,
144
+ emotion TEXT,
145
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
146
+ )
147
+ """)
148
+
149
+ cursor.execute("""
150
+ CREATE TABLE IF NOT EXISTS user_summaries (
151
+ user_id TEXT PRIMARY KEY,
152
+ summary_text TEXT NOT NULL,
153
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
154
+ )
155
+ """)
156
+
157
+ conn.commit()
158
+ conn.close()
159
+
160
+ init_db()
161
+
162
+ def hash_password(pw: str) -> str:
163
+ return hashlib.sha256(pw.encode()).hexdigest()
164
+
165
+ @app.post("/api/signup")
166
+ async def signup(req: SignupRequest):
167
+ conn = sqlite3.connect(DB_PATH)
168
+ cursor = conn.cursor()
169
+
170
+ try:
171
+ user_id = secrets.token_urlsafe(16)
172
+ cursor.execute(
173
+ "INSERT INTO users (user_id, username, password_hash) VALUES (?, ?, ?)",
174
+ (user_id, req.username, hash_password(req.password))
175
+ )
176
+ conn.commit()
177
+ conn.close()
178
+ return {"success": True, "message": "Account created!"}
179
+ except sqlite3.IntegrityError:
180
+ conn.close()
181
+ raise HTTPException(status_code=400, detail="Username already exists")
182
+
183
+ @app.post("/api/login")
184
+ async def login(req: LoginRequest):
185
+ conn = sqlite3.connect(DB_PATH)
186
+ cursor = conn.cursor()
187
+
188
+ cursor.execute(
189
+ "SELECT user_id, username FROM users WHERE username = ? AND password_hash = ?",
190
+ (req.username, hash_password(req.password))
191
+ )
192
+
193
+ result = cursor.fetchone()
194
+
195
+ if not result:
196
+ conn.close()
197
+ raise HTTPException(status_code=401, detail="Invalid credentials")
198
+
199
+ user_id, username = result
200
+
201
+ session_id = secrets.token_urlsafe(16)
202
+ token = secrets.token_urlsafe(32)
203
+
204
+ cursor.execute(
205
+ "INSERT INTO sessions (session_id, user_id, token) VALUES (?, ?, ?)",
206
+ (session_id, user_id, token)
207
+ )
208
+
209
+ cursor.execute(
210
+ "SELECT summary_text FROM user_summaries WHERE user_id = ?",
211
+ (user_id,)
212
+ )
213
+ summary_row = cursor.fetchone()
214
+ summary = summary_row[0] if summary_row else None
215
+
216
+ conn.commit()
217
+ conn.close()
218
+
219
+ return {
220
+ "success": True,
221
+ "token": token,
222
+ "username": username,
223
+ "user_id": user_id,
224
+ "summary": summary
225
+ }
226
+
227
+ class LogoutRequest(BaseModel):
228
+ token: str
229
+
230
+ @app.post("/api/logout")
231
+ async def logout(req: LogoutRequest):
232
+ conn = sqlite3.connect(DB_PATH)
233
+ cursor = conn.cursor()
234
+
235
+ # Get session info before closing
236
+ cursor.execute(
237
+ "SELECT session_id, user_id FROM sessions WHERE token = ? AND is_active = 1",
238
+ (req.token,)
239
+ )
240
+ result = cursor.fetchone()
241
+
242
+ if result:
243
+ session_id, user_id = result
244
+
245
+ # Mark session as inactive
246
+ cursor.execute(
247
+ "UPDATE sessions SET is_active = 0 WHERE token = ?",
248
+ (req.token,)
249
+ )
250
+ conn.commit()
251
+ conn.close()
252
+
253
+ # Generate summary on explicit logout
254
+ print(f"[Logout] πŸ“ Generating summary for user {user_id}...")
255
+ summary = await generate_session_summary(session_id, user_id)
256
+ if summary:
257
+ print(f"[Logout] βœ… Summary generated")
258
+
259
+ return {"success": True, "message": "Logged out successfully"}
260
+ else:
261
+ conn.close()
262
+ return {"success": True, "message": "Session already closed"}
263
+
264
+ async def generate_session_summary(session_id: str, user_id: str):
265
+ """Generate AI summary of conversation for THIS specific user"""
266
+ conn = sqlite3.connect(DB_PATH)
267
+ cursor = conn.cursor()
268
+
269
+ # Verify session belongs to user
270
+ cursor.execute(
271
+ "SELECT user_id FROM sessions WHERE session_id = ?",
272
+ (session_id,)
273
+ )
274
+ session_owner = cursor.fetchone()
275
+
276
+ if not session_owner or session_owner[0] != user_id:
277
+ print(f"[Summary] ❌ Security error: session {session_id} doesn't belong to user {user_id}")
278
+ conn.close()
279
+ return None
280
+
281
+ # Get messages from this session
282
+ cursor.execute(
283
+ "SELECT role, content, emotion FROM messages WHERE session_id = ? ORDER BY timestamp ASC",
284
+ (session_id,)
285
+ )
286
+
287
+ messages = cursor.fetchall()
288
+
289
+ # Get username for better logging
290
+ cursor.execute("SELECT username FROM users WHERE user_id = ?", (user_id,))
291
+ username_row = cursor.fetchone()
292
+ username = username_row[0] if username_row else user_id
293
+
294
+ conn.close()
295
+
296
+ if len(messages) < 3:
297
+ print(f"[Summary] ⏭️ Skipped for {username} (only {len(messages)} messages)")
298
+ return None
299
+
300
+ conversation = ""
301
+ for role, content, emotion in messages:
302
+ speaker = "User" if role == "user" else "AI"
303
+ emo_tag = f" [{emotion}]" if emotion else ""
304
+ conversation += f"{speaker}{emo_tag}: {content}\n"
305
+
306
+ try:
307
+ from groq import Groq
308
+ groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
309
+
310
+ prompt = f"""Analyze this conversation and create a 2-3 sentence summary about THIS SPECIFIC USER.
311
+
312
+ DO NOT include information about other users or other conversations.
313
+ ONLY summarize what THIS user said and their patterns.
314
+
315
+ Conversation ({len(messages)} messages):
316
+ {conversation}
317
+
318
+ Create a concise summary including: topics this user discussed, their emotional patterns, personal details THEY mentioned, and their preferences."""
319
+
320
+ response = groq_client.chat.completions.create(
321
+ model="llama-3.1-8b-instant",
322
+ messages=[{"role": "user", "content": prompt}],
323
+ max_tokens=150,
324
+ temperature=0.7
325
+ )
326
+
327
+ summary = response.choices[0].message.content.strip()
328
+
329
+ # Save summary FOR THIS USER ONLY
330
+ conn = sqlite3.connect(DB_PATH)
331
+ cursor = conn.cursor()
332
+
333
+ cursor.execute(
334
+ "INSERT OR REPLACE INTO user_summaries (user_id, summary_text, updated_at) VALUES (?, ?, ?)",
335
+ (user_id, summary, datetime.now())
336
+ )
337
+
338
+ conn.commit()
339
+ conn.close()
340
+
341
+ print(f"[Summary] βœ… Generated for {username} (user_id: {user_id})")
342
+ print(f"[Summary] πŸ“ Content: {summary}")
343
+ return summary
344
+
345
+ except Exception as e:
346
+ print(f"[Summary] ❌ Error for {username}: {e}")
347
+ import traceback
348
+ traceback.print_exc()
349
+ return None
350
+
351
+ @app.on_event("startup")
352
+ async def startup_event():
353
+ """Start loading models in background after server is ready"""
354
+ print("[Backend] πŸš€ Starting up...")
355
+
356
+ # Check if avatar service is running
357
+ try:
358
+ response = requests.get(f"{AVATAR_API}/", timeout=2)
359
+ if response.status_code == 200:
360
+ print(f"[Backend] βœ… Avatar TTS service available at {AVATAR_API}")
361
+ else:
362
+ print(f"[Backend] ⚠️ Avatar TTS service responded with {response.status_code}")
363
+ except requests.exceptions.ConnectionError:
364
+ print(f"[Backend] ⚠️ Avatar TTS service NOT available at {AVATAR_API}")
365
+ print(f"[Backend] πŸ’‘ Text-only mode will be used (no avatar speech)")
366
+ print(f"[Backend] πŸ“ To enable avatar:")
367
+ print(f"[Backend] cd avatar && python speak_server.py")
368
+ except Exception as e:
369
+ print(f"[Backend] ⚠️ Error checking avatar service: {e}")
370
+
371
+ asyncio.create_task(load_models())
372
+
373
+ async def load_models():
374
+ """Load all AI models asynchronously"""
375
+ global face_processor, text_analyzer, whisper_worker, voice_worker
376
+ global llm_generator, fusion_engine, models_ready
377
+
378
+ print("[Backend] πŸš€ Initializing MrrrMe AI models in background...")
379
+
380
+ try:
381
+ # Import modules
382
+ from mrrrme.vision.face_processor import FaceProcessor
383
+ from mrrrme.audio.voice_emotion import VoiceEmotionWorker
384
+ from mrrrme.audio.whisper_transcription import WhisperTranscriptionWorker
385
+ from mrrrme.nlp.text_sentiment import TextSentimentAnalyzer
386
+ from mrrrme.nlp.llm_generator_groq import LLMResponseGenerator
387
+ from mrrrme.config import FUSE4
388
+
389
+ # Load models
390
+ print("[Backend] Loading FaceProcessor...")
391
+ face_processor = FaceProcessor()
392
+
393
+ print("[Backend] Loading TextSentiment...")
394
+ text_analyzer = TextSentimentAnalyzer()
395
+
396
+ print("[Backend] Loading Whisper...")
397
+ whisper_worker = WhisperTranscriptionWorker(text_analyzer)
398
+
399
+ print("[Backend] Loading VoiceEmotion...")
400
+ voice_worker = VoiceEmotionWorker(whisper_worker=whisper_worker)
401
+
402
+ print("[Backend] Initializing LLM...")
403
+ groq_api_key = os.getenv("GROQ_API_KEY", "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")
404
+ llm_generator = LLMResponseGenerator(api_key=groq_api_key)
405
+
406
+ # Initialize fusion engine
407
+ class FusionEngine:
408
+ def __init__(self):
409
+ self.alpha_face = 0.4
410
+ self.alpha_voice = 0.3
411
+ self.alpha_text = 0.3
412
+
413
+ def fuse(self, face_probs, voice_probs, text_probs):
414
+ fused = (
415
+ self.alpha_face * face_probs +
416
+ self.alpha_voice * voice_probs +
417
+ self.alpha_text * text_probs
418
+ )
419
+ fused = fused / (np.sum(fused) + 1e-8)
420
+ fused_idx = int(np.argmax(fused))
421
+ fused_emotion = FUSE4[fused_idx]
422
+ intensity = float(np.max(fused))
423
+ return fused_emotion, intensity
424
+
425
+ fusion_engine = FusionEngine()
426
+ models_ready = True
427
+
428
+ print("[Backend] βœ… All models loaded!")
429
+
430
+ except Exception as e:
431
+ print(f"[Backend] ❌ Error loading models: {e}")
432
+ import traceback
433
+ traceback.print_exc()
434
+
435
+ @app.get("/")
436
+ async def root():
437
+ """Root endpoint"""
438
+ return {
439
+ "status": "running",
440
+ "models_ready": models_ready,
441
+ "message": "MrrrMe AI Backend"
442
+ }
443
+
444
+ @app.get("/health")
445
+ async def health():
446
+ """Health check - responds immediately"""
447
+ return {
448
+ "status": "healthy",
449
+ "models_ready": models_ready
450
+ }
451
+
452
+ @app.get("/api/debug/users")
453
+ async def debug_users():
454
+ """Debug endpoint - view all users and their summaries"""
455
+ conn = sqlite3.connect(DB_PATH)
456
+ cursor = conn.cursor()
457
+
458
+ cursor.execute("""
459
+ SELECT u.username, u.user_id, s.summary_text, s.updated_at
460
+ FROM users u
461
+ LEFT JOIN user_summaries s ON u.user_id = s.user_id
462
+ ORDER BY u.created_at DESC
463
+ """)
464
+
465
+ users = []
466
+ for username, user_id, summary, updated in cursor.fetchall():
467
+ users.append({
468
+ "username": username,
469
+ "user_id": user_id,
470
+ "summary": summary,
471
+ "summary_updated": updated
472
+ })
473
+
474
+ conn.close()
475
+
476
+ return {"users": users, "database": DB_PATH}
477
+
478
+ @app.get("/api/debug/sessions")
479
+ async def debug_sessions():
480
+ """Debug endpoint - view all active sessions"""
481
+ conn = sqlite3.connect(DB_PATH)
482
+ cursor = conn.cursor()
483
+
484
+ cursor.execute("""
485
+ SELECT s.session_id, s.token, u.username, s.is_active, s.created_at
486
+ FROM sessions s
487
+ JOIN users u ON s.user_id = u.user_id
488
+ ORDER BY s.created_at DESC
489
+ LIMIT 20
490
+ """)
491
+
492
+ sessions = []
493
+ for session_id, token, username, is_active, created_at in cursor.fetchall():
494
+ sessions.append({
495
+ "session_id": session_id,
496
+ "token_preview": token[:10] + "..." if token else None,
497
+ "username": username,
498
+ "is_active": bool(is_active),
499
+ "created_at": created_at
500
+ })
501
+
502
+ conn.close()
503
+
504
+ return {"sessions": sessions, "database": DB_PATH}
505
+
506
+ @app.websocket("/ws")
507
+ async def websocket_endpoint(websocket: WebSocket):
508
+ await websocket.accept()
509
+ print("[WebSocket] βœ… Client connected!")
510
+
511
+ # ===== AUTHENTICATION =====
512
+ session_data = None
513
+ user_summary = None
514
+ session_id = None
515
+ user_id = None
516
+ username = None
517
+
518
+ try:
519
+ auth_msg = await websocket.receive_json()
520
+ print(f"[WebSocket] πŸ“¨ Auth message received: {auth_msg.get('type')}")
521
+
522
+ if auth_msg.get("type") != "auth":
523
+ print(f"[WebSocket] ❌ Wrong message type: {auth_msg.get('type')}")
524
+ await websocket.send_json({"type": "error", "message": "Authentication required"})
525
+ return
526
+
527
+ token = auth_msg.get("token")
528
+ print(f"[WebSocket] πŸ”‘ Validating token: {token[:10] if token else 'None'}...")
529
+
530
+ if not token:
531
+ print(f"[WebSocket] ❌ No token provided!")
532
+ await websocket.send_json({"type": "error", "message": "No token provided"})
533
+ return
534
+
535
+ # Validate token
536
+ conn = sqlite3.connect(DB_PATH)
537
+ cursor = conn.cursor()
538
+
539
+ cursor.execute(
540
+ "SELECT s.session_id, s.user_id, u.username FROM sessions s JOIN users u ON s.user_id = u.user_id WHERE s.token = ? AND s.is_active = 1",
541
+ (token,)
542
+ )
543
+
544
+ result = cursor.fetchone()
545
+
546
+ if not result:
547
+ # Debug: Check if token exists at all
548
+ cursor.execute("SELECT session_id, user_id, is_active FROM sessions WHERE token = ?", (token,))
549
+ debug_result = cursor.fetchone()
550
+
551
+ if debug_result:
552
+ print(f"[WebSocket] ⚠️ Token found but session inactive or invalid: {debug_result}")
553
+ else:
554
+ print(f"[WebSocket] ❌ Token not found in database!")
555
+
556
+ await websocket.send_json({"type": "error", "message": "Invalid session - please login again"})
557
+ conn.close()
558
+ return
559
+
560
+ session_id, user_id, username = result
561
+ print(f"[WebSocket] βœ… Token validated for user: {username} (session: {session_id})")
562
+
563
+ # Get user-specific summary
564
+ cursor.execute(
565
+ "SELECT summary_text FROM user_summaries WHERE user_id = ?",
566
+ (user_id,)
567
+ )
568
+ summary_row = cursor.fetchone()
569
+ user_summary = summary_row[0] if summary_row else None
570
+
571
+ conn.close()
572
+
573
+ session_data = {
574
+ 'session_id': session_id,
575
+ 'user_id': user_id,
576
+ 'username': username
577
+ }
578
+
579
+ # Send authenticated confirmation
580
+ await websocket.send_json({
581
+ "type": "authenticated",
582
+ "username": username,
583
+ "summary": user_summary
584
+ })
585
+
586
+ print(f"[WebSocket] βœ… Authenticated: {username} (user_id: {user_id})")
587
+ if user_summary:
588
+ print(f"[WebSocket] πŸ“– Loaded summary: {user_summary[:60]}...")
589
+
590
+ # Clear LLM's conversation history
591
+ if llm_generator:
592
+ llm_generator.clear_history()
593
+ print(f"[LLM] πŸ—‘οΈ Conversation history cleared")
594
+
595
+ # Load user's recent conversation history
596
+ conn = sqlite3.connect(DB_PATH)
597
+ cursor = conn.cursor()
598
+ cursor.execute(
599
+ """SELECT role, content FROM messages
600
+ WHERE session_id IN (
601
+ SELECT session_id FROM sessions WHERE user_id = ?
602
+ )
603
+ ORDER BY timestamp DESC
604
+ LIMIT 10""",
605
+ (user_id,)
606
+ )
607
+ user_history = cursor.fetchall()
608
+ conn.close()
609
+
610
+ # Load user-specific history into LLM
611
+ for role, content in reversed(user_history):
612
+ llm_generator.conversation_history.append({
613
+ "role": role,
614
+ "content": content
615
+ })
616
+
617
+ if user_history:
618
+ print(f"[WebSocket] πŸ“š Loaded {len(user_history)} messages from {username}'s history")
619
+
620
+ except Exception as auth_err:
621
+ print(f"[WebSocket] ❌ Auth error: {auth_err}")
622
+ return
623
+
624
+ # Wait for models to load if needed
625
+ if not models_ready:
626
+ await websocket.send_json({
627
+ "type": "status",
628
+ "message": "AI models are loading, please wait..."
629
+ })
630
+
631
+ # Wait up to 15 minutes for models
632
+ for _ in range(900):
633
+ if models_ready:
634
+ await websocket.send_json({
635
+ "type": "status",
636
+ "message": "Models loaded! Ready to chat."
637
+ })
638
+ break
639
+ await asyncio.sleep(1)
640
+
641
+ if not models_ready:
642
+ await websocket.send_json({
643
+ "type": "error",
644
+ "message": "Models failed to load. Please refresh."
645
+ })
646
+ return
647
+
648
+ # Session state
649
+ audio_buffer = []
650
+ user_preferences = {"voice": "female", "language": "en"}
651
+
652
+ try:
653
+ while True:
654
+ data = await websocket.receive_json()
655
+ msg_type = data.get("type")
656
+
657
+ # ============ PREFERENCES UPDATE ============
658
+ if msg_type == "preferences":
659
+ if "voice" in data:
660
+ user_preferences["voice"] = data.get("voice", "female")
661
+ if "language" in data:
662
+ user_preferences["language"] = data.get("language", "en")
663
+ print(f"[Preferences] {username}: voice={user_preferences.get('voice')}, language={user_preferences.get('language')}")
664
+ continue
665
+
666
+ # ============ AUTO-GREETING REQUEST ============
667
+ elif msg_type == "request_greeting":
668
+ try:
669
+ print(f"[WebSocket] πŸ€– Generating initial greeting for {username}...")
670
+
671
+ # Determine greeting based on user context
672
+ greeting_prompts = {
673
+ "new": f"Hey {username}! I'm MrrrMe, your emotion AI companion. How are you feeling today?",
674
+ "returning": f"Welcome back, {username}! It's great to see you again. How have you been?"
675
+ }
676
+
677
+ # Check if user has summary (returning user)
678
+ greeting_text = greeting_prompts["returning"] if user_summary else greeting_prompts["new"]
679
+
680
+ # Add language context
681
+ if user_preferences.get("language") == "nl":
682
+ if user_summary:
683
+ greeting_text = f"Welkom terug, {username}! Fijn je weer te zien. Hoe gaat het met je?"
684
+ else:
685
+ greeting_text = f"Hoi {username}! Ik ben MrrrMe, jouw emotie AI-metgezel. Hoe voel je je vandaag?"
686
+
687
+ print(f"[Greeting] πŸ‘‹ Sending: '{greeting_text}'")
688
+
689
+ # Try to send to avatar for TTS
690
+ audio_url = None
691
+ visemes = None
692
+
693
+ try:
694
+ voice_preference = user_preferences.get("voice", "female")
695
+ language_preference = user_preferences.get("language", "en")
696
+
697
+ print(f"[Greeting] πŸ”Š Requesting TTS from avatar service...")
698
+ avatar_response = requests.post(
699
+ f"{AVATAR_API}/speak",
700
+ data={
701
+ "text": greeting_text,
702
+ "voice": voice_preference,
703
+ "language": language_preference
704
+ },
705
+ timeout=10
706
+ )
707
+
708
+ if avatar_response.status_code == 200:
709
+ avatar_data = avatar_response.json()
710
+ audio_url = avatar_data.get("audio_url")
711
+ visemes = avatar_data.get("visemes")
712
+ print(f"[Greeting] βœ… TTS generated successfully")
713
+ else:
714
+ print(f"[Greeting] ⚠️ TTS failed: {avatar_response.status_code}")
715
+
716
+ except requests.exceptions.ConnectionError as conn_err:
717
+ print(f"[Greeting] ⚠️ Avatar service not available (port 8765 not responding)")
718
+ print(f"[Greeting] πŸ“ Sending text-only greeting (TTS will be skipped)")
719
+
720
+ except Exception as tts_err:
721
+ print(f"[Greeting] ⚠️ TTS error: {tts_err}")
722
+ print(f"[Greeting] πŸ“ Sending text-only greeting")
723
+
724
+ # Send greeting to client
725
+ response_data = {
726
+ "type": "llm_response",
727
+ "text": greeting_text,
728
+ "emotion": "Neutral",
729
+ "intensity": 0.5,
730
+ "is_greeting": True
731
+ }
732
+
733
+ # Add audio/visemes only if TTS succeeded
734
+ if audio_url and visemes:
735
+ response_data["audio_url"] = audio_url
736
+ response_data["visemes"] = visemes
737
+ else:
738
+ response_data["text_only"] = True
739
+ print(f"[Greeting] πŸ“ Sending text-only (no TTS)")
740
+
741
+ await websocket.send_json(response_data)
742
+
743
+ # Save greeting to history
744
+ conn = sqlite3.connect(DB_PATH)
745
+ cursor = conn.cursor()
746
+ cursor.execute(
747
+ "INSERT INTO messages (session_id, role, content, emotion) VALUES (?, ?, ?, ?)",
748
+ (session_id, "assistant", greeting_text, "Neutral")
749
+ )
750
+ conn.commit()
751
+ conn.close()
752
+
753
+ print(f"[Greeting] βœ… Sent to {username}")
754
+
755
+ except Exception as greeting_err:
756
+ print(f"[Greeting] ❌ Error: {greeting_err}")
757
+ import traceback
758
+ traceback.print_exc()
759
+
760
+ try:
761
+ await websocket.send_json({
762
+ "type": "error",
763
+ "message": "Greeting failed - avatar service unavailable"
764
+ })
765
+ except:
766
+ pass
767
+
768
+ # ============ VIDEO FRAME - UPDATED WITH PROBABILITIES ============
769
+ elif msg_type == "video_frame":
770
+ try:
771
+ # Decode base64 image
772
+ img_data = base64.b64decode(data["frame"].split(",")[1])
773
+ img = Image.open(io.BytesIO(img_data))
774
+ frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
775
+
776
+ # Process face emotion
777
+ try:
778
+ processed_frame, result = face_processor.process_frame(frame)
779
+ face_emotion = face_processor.get_last_emotion() or "Neutral"
780
+ face_confidence = face_processor.get_last_confidence() or 0.0
781
+ face_probs = face_processor.get_last_probs()
782
+ face_quality = face_processor.get_last_quality() if hasattr(face_processor, 'get_last_quality') else 0.5
783
+ except Exception as proc_err:
784
+ print(f"[FaceProcessor] Error: {proc_err}")
785
+ face_emotion = "Neutral"
786
+ face_confidence = 0.0
787
+ face_probs = np.array([0.25, 0.25, 0.25, 0.25])
788
+ face_quality = 0.0
789
+
790
+ # Send face emotion to frontend with probabilities
791
+ await websocket.send_json({
792
+ "type": "face_emotion",
793
+ "emotion": face_emotion,
794
+ "confidence": face_confidence,
795
+ "probabilities": face_probs.tolist(),
796
+ "quality": face_quality
797
+ })
798
+
799
+ except Exception as e:
800
+ print(f"[Video] Error: {e}")
801
+
802
+ # ============ AUDIO CHUNK ============
803
+ elif msg_type == "audio_chunk":
804
+ try:
805
+ audio_data = base64.b64decode(data["audio"])
806
+ audio_buffer.append(audio_data)
807
+
808
+ if len(audio_buffer) >= 5:
809
+ voice_probs, voice_emotion = voice_worker.get_probs()
810
+ await websocket.send_json({
811
+ "type": "voice_emotion",
812
+ "emotion": voice_emotion
813
+ })
814
+ audio_buffer = audio_buffer[-3:]
815
+
816
+ except Exception as e:
817
+ print(f"[Audio] Error: {e}")
818
+
819
+ # ============ USER FINISHED SPEAKING (ENHANCED LOGGING) ============
820
+ elif msg_type == "speech_end":
821
+ transcription = data.get("text", "").strip()
822
+
823
+ print(f"\n{'='*80}")
824
+ print(f"[Speech End] 🎀 USER FINISHED SPEAKING: {username}")
825
+ print(f"{'='*80}")
826
+ print(f"[Transcription] '{transcription}'")
827
+
828
+ # Filter short/meaningless
829
+ if len(transcription) < 2:
830
+ print(f"[Filter] ⏭️ Skipped: Too short ({len(transcription)} chars)")
831
+ continue
832
+
833
+ hallucinations = {"thank you", "thanks", "okay", "ok", "you", "yeah", "yep"}
834
+ if transcription.lower().strip('.,!?') in hallucinations:
835
+ print(f"[Filter] ⏭️ Skipped: Hallucination detected ('{transcription}')")
836
+ continue
837
+
838
+ # Save user message
839
+ conn = sqlite3.connect(DB_PATH)
840
+ cursor = conn.cursor()
841
+ cursor.execute(
842
+ "INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)",
843
+ (session_id, "user", transcription)
844
+ )
845
+ conn.commit()
846
+ conn.close()
847
+
848
+ try:
849
+ # ========== EMOTION DETECTION PIPELINE (ENHANCED LOGGING) ==========
850
+ print(f"\n[Pipeline] πŸ” Starting emotion analysis pipeline...")
851
+ print(f"{'─'*80}")
852
+
853
+ # Step 1: Get face emotion
854
+ print(f"[Step 1/4] πŸ“Έ FACIAL EXPRESSION ANALYSIS")
855
+ face_emotion = face_processor.get_last_emotion()
856
+ face_confidence = face_processor.get_last_confidence()
857
+ face_quality = face_processor.get_last_quality() if hasattr(face_processor, 'get_last_quality') else 0.5
858
+
859
+ # Create emotion probabilities
860
+ emotion_map = {'Neutral': 0, 'Happy': 1, 'Sad': 2, 'Angry': 3}
861
+ face_probs = np.array([0.25, 0.25, 0.25, 0.25], dtype=np.float32)
862
+ if face_emotion in emotion_map:
863
+ face_idx = emotion_map[face_emotion]
864
+ face_probs[face_idx] = face_confidence
865
+ face_probs = face_probs / face_probs.sum()
866
+
867
+ print(f" Result: {face_emotion}")
868
+ print(f" Confidence: {face_confidence:.3f}")
869
+ print(f" Quality Score: {face_quality:.3f}")
870
+ print(f" Distribution: Neutral={face_probs[0]:.3f} | Happy={face_probs[1]:.3f} | Sad={face_probs[2]:.3f} | Angry={face_probs[3]:.3f}")
871
+
872
+ # Step 2: Get voice emotion
873
+ print(f"\n[Step 2/4] 🎀 VOICE TONE ANALYSIS")
874
+ voice_probs, voice_emotion = voice_worker.get_probs()
875
+ voice_state = voice_worker.get_state()
876
+ voice_active = voice_state.get('speech_active', False)
877
+ voice_inferences = voice_state.get('inference_count', 0)
878
+ voice_skipped = voice_state.get('skipped_inferences', 0)
879
+
880
+ print(f" {'βœ… ACTIVELY PROCESSING' if voice_active else '⚠️ IDLE (no recent speech)'}")
881
+ print(f" Result: {voice_emotion}")
882
+ print(f" Distribution: Neutral={voice_probs[0]:.3f} | Happy={voice_probs[1]:.3f} | Sad={voice_probs[2]:.3f} | Angry={voice_probs[3]:.3f}")
883
+ print(f" Inferences completed: {voice_inferences}")
884
+ print(f" Skipped (silence optimization): {voice_skipped}")
885
+ efficiency = (voice_inferences / (voice_inferences + voice_skipped) * 100) if (voice_inferences + voice_skipped) > 0 else 0
886
+ print(f" Processing efficiency: {efficiency:.1f}%")
887
+
888
+ # Step 3: Analyze text sentiment
889
+ print(f"\n[Step 3/4] πŸ’¬ TEXT SENTIMENT ANALYSIS")
890
+ print(f" βœ… Using Whisper transcription")
891
+ text_analyzer.analyze(transcription)
892
+ text_probs, _ = text_analyzer.get_probs()
893
+ text_emotion = ['Neutral', 'Happy', 'Sad', 'Angry'][int(np.argmax(text_probs))]
894
+
895
+ print(f" Result: {text_emotion}")
896
+ print(f" Distribution: Neutral={text_probs[0]:.3f} | Happy={text_probs[1]:.3f} | Sad={text_probs[2]:.3f} | Angry={text_probs[3]:.3f}")
897
+ print(f" Text length: {len(transcription)} characters")
898
+
899
+ # Step 4: Calculate fusion weights
900
+ print(f"\n[Step 4/4] βš–οΈ MULTI-MODAL FUSION")
901
+ base_weights = {
902
+ 'face': fusion_engine.alpha_face,
903
+ 'voice': fusion_engine.alpha_voice,
904
+ 'text': fusion_engine.alpha_text
905
+ }
906
+
907
+ # Adjust weights based on quality/confidence
908
+ adjusted_weights = base_weights.copy()
909
+ adjustments_made = []
910
+
911
+ # Reduce face weight if quality is poor
912
+ if face_quality < 0.5:
913
+ adjusted_weights['face'] *= 0.7
914
+ adjustments_made.append(f"Face weight reduced (low quality: {face_quality:.3f})")
915
+
916
+ # Reduce voice weight if not active
917
+ if not voice_active:
918
+ adjusted_weights['voice'] *= 0.5
919
+ adjustments_made.append(f"Voice weight reduced (no recent speech)")
920
+
921
+ # Reduce text weight if very short
922
+ if len(transcription) < 10:
923
+ adjusted_weights['text'] *= 0.7
924
+ adjustments_made.append(f"Text weight reduced (short input: {len(transcription)} chars)")
925
+
926
+ # Normalize weights to sum to 1.0
927
+ total_weight = sum(adjusted_weights.values())
928
+ final_weights = {k: v/total_weight for k, v in adjusted_weights.items()}
929
+
930
+ print(f" Base weights: Face={base_weights['face']:.3f} | Voice={base_weights['voice']:.3f} | Text={base_weights['text']:.3f}")
931
+ if adjustments_made:
932
+ print(f" Adjustments:")
933
+ for adj in adjustments_made:
934
+ print(f" - {adj}")
935
+ print(f" Final weights: Face={final_weights['face']:.3f} | Voice={final_weights['voice']:.3f} | Text={final_weights['text']:.3f}")
936
+
937
+ # Calculate weighted fusion
938
+ fused_probs = (
939
+ final_weights['face'] * face_probs +
940
+ final_weights['voice'] * voice_probs +
941
+ final_weights['text'] * text_probs
942
+ )
943
+ fused_probs = fused_probs / (np.sum(fused_probs) + 1e-8)
944
+
945
+ fused_emotion, intensity = fusion_engine.fuse(face_probs, voice_probs, text_probs)
946
+
947
+ # Calculate fusion accuracy metrics
948
+ agreement_count = sum([
949
+ face_emotion == fused_emotion,
950
+ voice_emotion == fused_emotion,
951
+ text_emotion == fused_emotion
952
+ ])
953
+ agreement_score = agreement_count / 3.0
954
+
955
+ # Check for conflicts
956
+ all_same = (face_emotion == voice_emotion == text_emotion)
957
+ has_conflict = len({face_emotion, voice_emotion, text_emotion}) == 3
958
+
959
+ print(f"\n {'─'*76}")
960
+ print(f" FUSION RESULTS:")
961
+ print(f" {'─'*76}")
962
+ print(f" Input emotions:")
963
+ print(f" Face: {face_emotion:7s} (confidence={face_probs[emotion_map.get(face_emotion, 0)]:.3f}, weight={final_weights['face']:.3f})")
964
+ print(f" Voice: {voice_emotion:7s} (confidence={voice_probs[emotion_map.get(voice_emotion, 0)]:.3f}, weight={final_weights['voice']:.3f})")
965
+ print(f" Text: {text_emotion:7s} (confidence={text_probs[emotion_map.get(text_emotion, 0)]:.3f}, weight={final_weights['text']:.3f})")
966
+ print(f" {'─'*76}")
967
+ print(f" FUSED EMOTION: {fused_emotion}")
968
+ print(f" Intensity: {intensity:.3f}")
969
+ print(f" Fused distribution: Neutral={fused_probs[0]:.3f} | Happy={fused_probs[1]:.3f} | Sad={fused_probs[2]:.3f} | Angry={fused_probs[3]:.3f}")
970
+ print(f" {'─'*76}")
971
+ print(f" Agreement: {agreement_count}/3 modalities ({agreement_score*100:.1f}%)")
972
+
973
+ if all_same:
974
+ print(f" Status: βœ… Perfect agreement - all modalities aligned")
975
+ elif has_conflict:
976
+ print(f" Status: ⚠️ Full conflict - weighted fusion resolved disagreement")
977
+ else:
978
+ print(f" Status: πŸ“Š Partial agreement - majority vote with confidence weighting")
979
+
980
+ print(f" {'─'*76}")
981
+
982
+ # ========== LLM INPUT PREPARATION ==========
983
+ print(f"\n[LLM Input] 🧠 Preparing context for language model...")
984
+
985
+ # Language instruction
986
+ user_language = user_preferences.get("language", "en")
987
+
988
+ context_prefix = ""
989
+ if user_summary:
990
+ context_prefix = f"[User context for {username}: {user_summary}]\n\n"
991
+ print(f"[LLM Input] - User context: YES ({len(user_summary)} chars)")
992
+ else:
993
+ print(f"[LLM Input] - User context: NO (new user)")
994
+
995
+ # Add language instruction
996
+ if user_language == "nl":
997
+ context_prefix += "[BELANGRIJK: Antwoord ALTIJD in het Nederlands!]\n\n"
998
+ print(f"[LLM Input] - Language: Dutch (Nederlands)")
999
+ else:
1000
+ context_prefix += "[IMPORTANT: ALWAYS respond in English!]\n\n"
1001
+ print(f"[LLM Input] - Language: English")
1002
+
1003
+ full_llm_input = context_prefix + transcription
1004
+
1005
+ print(f"[LLM Input] - Fused emotion: {fused_emotion}")
1006
+ print(f"[LLM Input] - Face emotion: {face_emotion}")
1007
+ print(f"[LLM Input] - Voice emotion: {voice_emotion}")
1008
+ print(f"[LLM Input] - Intensity: {intensity:.3f}")
1009
+ print(f"[LLM Input] - User text: '{transcription}'")
1010
+ print(f"[LLM Input] - Full prompt length: {len(full_llm_input)} chars")
1011
+
1012
+ if len(context_prefix) > 50:
1013
+ print(f"[LLM Input] - Context preview: '{context_prefix[:100]}...'")
1014
+
1015
+ # Generate LLM response
1016
+ print(f"\n[LLM] πŸ€– Generating response...")
1017
+ response_text = llm_generator.generate_response(
1018
+ fused_emotion, face_emotion, voice_emotion,
1019
+ full_llm_input, force=True, intensity=intensity
1020
+ )
1021
+
1022
+ print(f"[LLM] βœ… Response generated: '{response_text}'")
1023
+
1024
+ # Save assistant message
1025
+ conn = sqlite3.connect(DB_PATH)
1026
+ cursor = conn.cursor()
1027
+ cursor.execute(
1028
+ "INSERT INTO messages (session_id, role, content, emotion) VALUES (?, ?, ?, ?)",
1029
+ (session_id, "assistant", response_text, fused_emotion)
1030
+ )
1031
+ conn.commit()
1032
+ conn.close()
1033
+
1034
+ # ========== SEND TO AVATAR FOR TTS ==========
1035
+ print(f"\n[TTS] 🎭 Sending to avatar backend...")
1036
+
1037
+ try:
1038
+ voice_preference = user_preferences.get("voice", "female")
1039
+ language_preference = user_preferences.get("language", "en")
1040
+
1041
+ print(f"[TTS] - Voice: {voice_preference}")
1042
+ print(f"[TTS] - Language: {language_preference}")
1043
+ print(f"[TTS] - Text: '{response_text}'")
1044
+
1045
+ avatar_response = requests.post(
1046
+ f"{AVATAR_API}/speak",
1047
+ data={
1048
+ "text": response_text,
1049
+ "voice": voice_preference,
1050
+ "language": language_preference
1051
+ },
1052
+ timeout=45
1053
+ )
1054
+ avatar_response.raise_for_status()
1055
+ avatar_data = avatar_response.json()
1056
+
1057
+ print(f"[TTS] βœ… Avatar TTS generated")
1058
+ print(f"[TTS] - Audio URL: {avatar_data.get('audio_url', 'N/A')}")
1059
+ print(f"[TTS] - Visemes: {len(avatar_data.get('visemes', []))} keyframes")
1060
+
1061
+ await websocket.send_json({
1062
+ "type": "llm_response",
1063
+ "text": response_text,
1064
+ "emotion": fused_emotion,
1065
+ "intensity": intensity,
1066
+ "audio_url": avatar_data.get("audio_url"),
1067
+ "visemes": avatar_data.get("visemes")
1068
+ })
1069
+
1070
+ print(f"[Pipeline] βœ… Complete response sent to {username}")
1071
+
1072
+ except requests.exceptions.ConnectionError:
1073
+ print(f"[TTS] ⚠️ Avatar service not available - sending text-only")
1074
+ await websocket.send_json({
1075
+ "type": "llm_response",
1076
+ "text": response_text,
1077
+ "emotion": fused_emotion,
1078
+ "intensity": intensity,
1079
+ "text_only": True
1080
+ })
1081
+
1082
+ except Exception as avatar_err:
1083
+ print(f"[TTS] ❌ Avatar error: {avatar_err}")
1084
+ await websocket.send_json({
1085
+ "type": "llm_response",
1086
+ "text": response_text,
1087
+ "emotion": fused_emotion,
1088
+ "intensity": intensity,
1089
+ "error": "Avatar TTS failed",
1090
+ "text_only": True
1091
+ })
1092
+
1093
+ print(f"{'='*80}\n")
1094
+
1095
+ except Exception as e:
1096
+ print(f"[Pipeline] ❌ Error in emotion processing: {e}")
1097
+ import traceback
1098
+ traceback.print_exc()
1099
+
1100
+ except WebSocketDisconnect:
1101
+ print(f"[WebSocket] ❌ {username} disconnected (close/refresh)")
1102
+
1103
+ except Exception as e:
1104
+ print(f"[WebSocket] ❌ {username} error: {e}")
1105
+ import traceback
1106
+ traceback.print_exc()
1107
+
1108
+ finally:
1109
+ # Generate summary on disconnect
1110
+ if session_data and session_id and user_id:
1111
+ print(f"[WebSocket] πŸ“ Generating summary for {username} (session ended)...")
1112
+ try:
1113
+ summary = await generate_session_summary(session_id, user_id)
1114
+ if summary:
1115
+ print(f"[Summary] βœ… Saved for {username}")
1116
+ else:
1117
+ print(f"[Summary] ⏭️ Skipped (not enough messages)")
1118
+ except Exception as summary_err:
1119
+ print(f"[Summary] ❌ Error for {username}: {summary_err}")
1120
+
1121
+ if __name__ == "__main__":
1122
+ import uvicorn
1123
  uvicorn.run(app, host="0.0.0.0", port=8000)
mrrrme/database/db_tool.py CHANGED
@@ -136,7 +136,7 @@ def reset_database():
136
  conn.close()
137
 
138
  # Recreate tables
139
- from backend_server import init_db
140
  init_db()
141
 
142
  print("βœ… Database reset complete")
 
136
  conn.close()
137
 
138
  # Recreate tables
139
+ from mrrrme.backend_server_old import init_db
140
  init_db()
141
 
142
  print("βœ… Database reset complete")