"""Dona V9.5 : YOLOv8 local opt-in + vision ponctuelle Hermes. Pas d'enregistrement de photo, de vidéos, ni d'activation sans demande. Detection de personne = classe COCO person. Objet presente = heuristique : classe non-personne stable au centre (ce n'est pas une detection de geste). """ from __future__ import annotations import importlib.util import os from collections import deque from datetime import datetime from pathlib import Path import re import threading import time from flask import Blueprint, jsonify, request def _env_float(name, default, low, high): try: return max(low,min(high,float(os.getenv(name,default)))) except (ValueError,TypeError): return default def _env_int(name, default, low, high): try: return max(low,min(high,int(os.getenv(name,default)))) except (ValueError,TypeError): return default class EventTracker: """State machine de debouncing. Seuils en nombre d'images, sans images stockees.""" def __init__(self, person_frames=2, object_frames=3, absent_frames=2, cooldown=45): self.person_frames=person_frames self.object_frames=object_frames self.absent_frames=absent_frames self.cooldown=cooldown self.person_count=0 self.person_absences=0 self.person_active=False self.object_candidate=None self.object_count=0 self.object_absences=0 self.object_active=None self.last_event={} def update(self, detections, now=None, watch_person=True, watch_objects=True): now=time.monotonic() if now is None else now out=[] visible_people=[d for d in detections if d['label']=='person'] if visible_people: self.person_absences=0 self.person_count+=1 if not self.person_active and self.person_count>=self.person_frames: self.person_active=True if watch_person and self._can_emit('person',now): out.append({'type':'person','title':'Une personne est apparue','detail':'Personne détectée devant Dona (identité inconnue).'}) else: self.person_count=0 self.person_absences+=1 if self.person_absences>=self.absent_frames:self.person_active=False # Un objet est considere 'presente' s'il est suffisamment central et visible. centrally=[] for d in detections: if d['label']=='person':continue x1,y1,x2,y2=d['box'] cx=(x1+x2)/2; cy=(y1+y2)/2; area=(x2-x1)*(y2-y1) if 0.28<=cx<=0.72 and 0.20<=cy<=0.85 and area>=0.018: centrally.append((d, area)) if centrally: best=max(centrally,key=lambda x:(x[0]['confidence'],x[1]))[0] label=best['label'] self.object_absences=0 if label==self.object_candidate:self.object_count+=1 else:self.object_candidate=label;self.object_count=1 if self.object_count>=self.object_frames and self.object_active!=label: self.object_active=label if watch_objects and self._can_emit('object:'+label,now): out.append({'type':'object','title':'Objet présenté devant Dona', 'detail':f'Objet détecté au centre : {TRANSLATIONS.get(label,label)} (à confirmer).'}) else: self.object_absences+=1 if self.object_absences>=self.absent_frames: self.object_candidate=None;self.object_count=0;self.object_active=None return out def _can_emit(self,key,now): if now-self.last_event.get(key,float('-inf'))