"""Reconnaissance faciale sur action utilisateur, via MCP Hermes existant. Aucun visage enregistré; capture temporaire effacée automatiquement. """ from __future__ import annotations from pathlib import Path import os import re import tempfile import threading from urllib.parse import urlsplit from flask import jsonify, request _FACE_BUSY = threading.Lock() _FACE_COMMAND = re.compile( r"(?:qui\s+est\s+(?:devant|sur)\s+(?:la\s+)?cam[eé]ra|" r"(?:reconnais|reconna[iî]tre|identifie|identifier)\s+(?:la|les|une|des)?\s*personne[s]?\s+(?:devant|sur)\s+(?:la\s+)?cam[eé]ra|" r"(?:reconnaissance|identification)\s+faciale)", re.I) def is_face_request(text: str) -> bool: return bool(_FACE_COMMAND.search(str(text or ''))) def recognize_once(ask_hermes): """Appelle le MCP via Hermes dans le même compte; ne suppose pas le résultat.""" from services.camera import snapshot if not _FACE_BUSY.acquire(blocking=False): return "Une reconnaissance faciale est déjà en cours." try: # Lecture à la demande; autre flux webcam => message clair. with tempfile.TemporaryDirectory(prefix='dona-face-') as work: path = Path(work) / 'capture.jpg' snapshot(str(path), index=int(os.getenv('CAMERA_INDEX', '0'))) if not path.is_file() or not path.stat().st_size: raise RuntimeError('Aucune image exploitable de la caméra.') prompt = ( 'Utilise le MCP face_recognition, outil identify_people, pour analyser ' f'le fichier image local {path}. Cette image vient de la caméra de Dona. ' 'Donne uniquement les identités effectivement reconnues par le résultat de l’outil. ' 'Si aucune correspondance fiable ou si l’outil n’est pas accessible, dis-le explicitement. ' 'N’invente jamais d’identité et ne crée pas de nouvelle fiche de personne.' ) response = ask_hermes(prompt) if isinstance(response, dict): response = response.get('speech') or response.get('text') or '' return str(response or 'La reconnaissance faciale n’a renvoyé aucun résultat.').strip() finally: _FACE_BUSY.release() def register_face(app, socketio, ask_hermes, conversation_lock): @app.post('/api/dona/face/identify') def dona_face_identify(): # Protège la capture contre un déclenchement intersite involontaire. if request.headers.get('X-Alfred-Request') != '1': return jsonify(error='Requête non autorisée.'), 403 origin = request.headers.get('Origin') if origin and urlsplit(origin).netloc != request.host: return jsonify(error='Origine non autorisée.'), 403 if not conversation_lock.acquire(blocking=False): return jsonify(error='Dona traite déjà une autre demande.'), 409 def job(): try: socketio.emit('dona_face_status', {'state': 'analyzing', 'message': 'Analyse du visage en cours…'}) text = recognize_once(ask_hermes) socketio.emit('dona_face_result', {'ok': True, 'text': text}) except Exception as exc: app.logger.warning('Reconnaissance faciale indisponible: %s', exc) socketio.emit('dona_face_result', {'ok': False, 'text': 'Impossible d’analyser la caméra. Ferme son aperçu si elle est déjà utilisée, puis réessaie.'}) finally: conversation_lock.release() threading.Thread(target=job, daemon=True, name='dona-face-manual').start() return jsonify(ok=True, started=True), 202