This commit is contained in:
pporcheret
2026-10-30 22:10:03 +01:00
parent 7f26f9b015
commit 90ffa635a0
161 changed files with 19042 additions and 111 deletions
+78
View File
@@ -0,0 +1,78 @@
"""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