Spaces:
Running
Running
Update backend/main.py
Browse files- backend/main.py +126 -27
backend/main.py
CHANGED
|
@@ -11,7 +11,7 @@ import random
|
|
| 11 |
from deepface import DeepFace
|
| 12 |
from supabase import create_client, Client
|
| 13 |
|
| 14 |
-
# --- CONFIGURATION SUPABASE ---
|
| 15 |
SUPABASE_URL = os.getenv("SUPABASE_URL", "https://gwjrwejdjpctizolfkcz.supabase.co")
|
| 16 |
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
| 17 |
|
|
@@ -20,57 +20,156 @@ try:
|
|
| 20 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 21 |
print("☁️ Connecté à Supabase")
|
| 22 |
else:
|
| 23 |
-
print("⚠️
|
| 24 |
except:
|
| 25 |
pass
|
| 26 |
|
| 27 |
-
# ---
|
| 28 |
-
# Cette fonction force l'acceptation et imprime l'origine
|
| 29 |
def force_allow_origins(origin, environ, **kwargs):
|
| 30 |
-
|
| 31 |
-
return True # Force YES
|
| 32 |
|
| 33 |
sio = socketio.AsyncServer(
|
| 34 |
async_mode='asgi',
|
| 35 |
-
cors_allowed_origins=force_allow_origins,
|
| 36 |
-
logger=
|
| 37 |
-
engineio_logger=
|
| 38 |
always_connect=True
|
| 39 |
)
|
| 40 |
|
| 41 |
app = FastAPI()
|
| 42 |
app.add_middleware(
|
| 43 |
-
CORSMiddleware,
|
| 44 |
-
allow_origins=["*"],
|
| 45 |
-
allow_credentials=True,
|
| 46 |
-
allow_methods=["*"],
|
| 47 |
-
allow_headers=["*"],
|
| 48 |
)
|
| 49 |
socket_app = socketio.ASGIApp(sio, app)
|
| 50 |
|
| 51 |
-
# ---
|
| 52 |
-
|
| 53 |
-
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
try:
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
# --- SOCKET EVENTS ---
|
| 62 |
@sio.event
|
| 63 |
async def connect(sid, environ):
|
| 64 |
-
print(f"✅
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
@sio.event
|
| 67 |
-
async def
|
| 68 |
-
|
| 69 |
-
pass
|
| 70 |
|
| 71 |
@sio.event
|
| 72 |
async def start_session(sid, data):
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
| 76 |
uvicorn.run(socket_app, host="0.0.0.0", port=7860)
|
|
|
|
| 11 |
from deepface import DeepFace
|
| 12 |
from supabase import create_client, Client
|
| 13 |
|
| 14 |
+
# --- 1. CONFIGURATION SUPABASE ---
|
| 15 |
SUPABASE_URL = os.getenv("SUPABASE_URL", "https://gwjrwejdjpctizolfkcz.supabase.co")
|
| 16 |
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
| 17 |
|
|
|
|
| 20 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 21 |
print("☁️ Connecté à Supabase")
|
| 22 |
else:
|
| 23 |
+
print("⚠️ ATTENTION: SUPABASE_KEY manquante !")
|
| 24 |
except:
|
| 25 |
pass
|
| 26 |
|
| 27 |
+
# --- 2. SERVER SETUP ---
|
|
|
|
| 28 |
def force_allow_origins(origin, environ, **kwargs):
|
| 29 |
+
return True
|
|
|
|
| 30 |
|
| 31 |
sio = socketio.AsyncServer(
|
| 32 |
async_mode='asgi',
|
| 33 |
+
cors_allowed_origins=force_allow_origins,
|
| 34 |
+
logger=False, # On réduit les logs pour la performance
|
| 35 |
+
engineio_logger=False,
|
| 36 |
always_connect=True
|
| 37 |
)
|
| 38 |
|
| 39 |
app = FastAPI()
|
| 40 |
app.add_middleware(
|
| 41 |
+
CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
)
|
| 43 |
socket_app = socketio.ASGIApp(sio, app)
|
| 44 |
|
| 45 |
+
# --- 3. VARIABLES GLOBALES ---
|
| 46 |
+
# On stocke l'état pour chaque client connecté (sid)
|
| 47 |
+
sessions_data = {}
|
| 48 |
|
| 49 |
+
def get_default_kpis():
|
| 50 |
+
return {
|
| 51 |
+
"engagement": 0, "satisfaction": 50, "trust": 50, "loyalty": 50, "opinion": 50,
|
| 52 |
+
"lbl_eng": "En attente...", "lbl_sat": "Analyse..."
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# --- 4. LOGIQUE MÉTIER ---
|
| 56 |
+
def analyze_image_task(frame):
|
| 57 |
+
# Cette fonction sera exécutée dans un thread séparé pour ne pas bloquer
|
| 58 |
try:
|
| 59 |
+
result = DeepFace.analyze(frame, actions=['emotion'], enforce_detection=False, silent=True)
|
| 60 |
+
data = result[0] if isinstance(result, list) else result
|
| 61 |
+
emotion = data['dominant_emotion']
|
| 62 |
+
region = data['region']
|
| 63 |
+
face_coords = {'x': region['x'], 'y': region['y'], 'w': region['w'], 'h': region['h']} if region['w'] > 0 else None
|
| 64 |
+
|
| 65 |
+
# Calcul KPI (Algorithme)
|
| 66 |
+
valence = 0.0; arousal = 0.0
|
| 67 |
+
if emotion == "happy": valence = 0.8; arousal = 0.6
|
| 68 |
+
elif emotion == "sad": valence = -0.6; arousal = 0.2
|
| 69 |
+
elif emotion == "angry": valence = -0.7; arousal = 0.8
|
| 70 |
+
elif emotion == "fear": valence = -0.7; arousal = 0.8
|
| 71 |
+
elif emotion == "surprise": valence = 0.2; arousal = 0.9
|
| 72 |
+
else: valence = 0.0; arousal = 0.3 # neutral
|
| 73 |
+
|
| 74 |
+
def clamp(n): return max(0, min(100, int(n)))
|
| 75 |
+
kpis = {
|
| 76 |
+
"engagement": clamp((arousal * 100) + random.uniform(0, 10)),
|
| 77 |
+
"satisfaction": clamp(((valence + 1) / 2) * 100),
|
| 78 |
+
"trust": clamp(50 + (valence * 20)),
|
| 79 |
+
"loyalty": clamp(50 + (valence * 10)),
|
| 80 |
+
"opinion": clamp(((valence + 1) / 2) * 100),
|
| 81 |
+
"lbl_eng": "Fort 🔥" if arousal > 0.6 else "Moyen",
|
| 82 |
+
"lbl_sat": "Positif 😃" if valence > 0.2 else ("Négatif 😡" if valence < -0.2 else "Neutre 😐")
|
| 83 |
+
}
|
| 84 |
+
return {"emotion": emotion, "face_coords": face_coords, "kpis": kpis}
|
| 85 |
+
except Exception as e:
|
| 86 |
+
print(f"DeepFace Error: {e}")
|
| 87 |
+
return None
|
| 88 |
|
| 89 |
+
# --- 5. SOCKET EVENTS ---
|
| 90 |
@sio.event
|
| 91 |
async def connect(sid, environ):
|
| 92 |
+
print(f"✅ Client CONNECTÉ: {sid}")
|
| 93 |
+
sessions_data[sid] = {
|
| 94 |
+
"is_recording": False, "session_time": 0, "db_id": None,
|
| 95 |
+
"last_data": {"emotion": "neutral", "face_coords": None, "kpis": get_default_kpis()},
|
| 96 |
+
"is_processing": False # Verrou pour éviter la surcharge
|
| 97 |
+
}
|
| 98 |
|
| 99 |
@sio.event
|
| 100 |
+
async def disconnect(sid):
|
| 101 |
+
if sid in sessions_data: del sessions_data[sid]
|
|
|
|
| 102 |
|
| 103 |
@sio.event
|
| 104 |
async def start_session(sid, data):
|
| 105 |
+
if sid in sessions_data:
|
| 106 |
+
sessions_data[sid]["is_recording"] = True
|
| 107 |
+
sessions_data[sid]["session_time"] = 0
|
| 108 |
+
print(f"▶️ SESSION START pour {sid}")
|
| 109 |
+
# Optionnel: Création DB ici
|
| 110 |
+
|
| 111 |
+
@sio.event
|
| 112 |
+
async def stop_session(sid):
|
| 113 |
+
if sid in sessions_data:
|
| 114 |
+
sessions_data[sid]["is_recording"] = False
|
| 115 |
+
print(f"⏹️ SESSION STOP pour {sid}")
|
| 116 |
+
|
| 117 |
+
@sio.event
|
| 118 |
+
async def process_frame(sid, data_uri):
|
| 119 |
+
# Si le client n'existe pas ou si le serveur est déjà occupé à traiter une image pour ce client : ON IGNORE
|
| 120 |
+
if sid not in sessions_data: return
|
| 121 |
+
if sessions_data[sid]["is_processing"]: return
|
| 122 |
+
|
| 123 |
+
sessions_data[sid]["is_processing"] = True # On verrouille
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
# 1. Décodage Image
|
| 127 |
+
encoded_data = data_uri.split(',')[1]
|
| 128 |
+
nparr = np.frombuffer(base64.b64decode(encoded_data), np.uint8)
|
| 129 |
+
frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 130 |
+
|
| 131 |
+
# 2. Analyse dans un THREAD SÉPARÉ (Magie Asyncio)
|
| 132 |
+
# Cela empêche le serveur de se figer
|
| 133 |
+
result = await asyncio.to_thread(analyze_image_task, frame)
|
| 134 |
+
|
| 135 |
+
# 3. Mise à jour des données si succès
|
| 136 |
+
if result:
|
| 137 |
+
sessions_data[sid]["last_data"]["emotion"] = result["emotion"]
|
| 138 |
+
sessions_data[sid]["last_data"]["face_coords"] = result["face_coords"]
|
| 139 |
+
sessions_data[sid]["last_data"]["kpis"] = result["kpis"]
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
print(f"Erreur Frame: {e}")
|
| 143 |
+
finally:
|
| 144 |
+
sessions_data[sid]["is_processing"] = False # On déverrouille
|
| 145 |
+
|
| 146 |
+
# --- 6. BOUCLE D'ENVOI (Le Coeur) ---
|
| 147 |
+
async def broadcast_loop():
|
| 148 |
+
while True:
|
| 149 |
+
await asyncio.sleep(1) # 1 Tick par seconde pour l'horloge
|
| 150 |
+
|
| 151 |
+
for sid, session in list(sessions_data.items()):
|
| 152 |
+
# Gestion du chrono
|
| 153 |
+
if session["is_recording"]:
|
| 154 |
+
session["session_time"] += 1
|
| 155 |
+
|
| 156 |
+
# Préparation du paquet
|
| 157 |
+
payload = {
|
| 158 |
+
"emotion": session["last_data"]["emotion"],
|
| 159 |
+
"face_coords": session["last_data"]["face_coords"],
|
| 160 |
+
"metrics": session["last_data"]["kpis"],
|
| 161 |
+
"session_time": session["session_time"],
|
| 162 |
+
"is_recording": session["is_recording"]
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
# Envoi au Frontend
|
| 166 |
+
try:
|
| 167 |
+
await sio.emit('metrics_update', payload, room=sid)
|
| 168 |
+
except:
|
| 169 |
+
pass
|
| 170 |
|
| 171 |
if __name__ == "__main__":
|
| 172 |
+
@app.on_event("startup")
|
| 173 |
+
async def startup():
|
| 174 |
+
asyncio.create_task(broadcast_loop())
|
| 175 |
uvicorn.run(socket_app, host="0.0.0.0", port=7860)
|