Spaces:
Sleeping
Sleeping
| """ | |
| benchmarks.py — Collecte ANONYME de métriques d'analyse (Système Schrödinger). | |
| Chaque analyse enregistre UNE ligne JSON (JSONL, append-only) contenant | |
| exclusivement des métriques techniques : type de contenu, score final, scores | |
| par analyseur, durée, résolution. AUCUNE donnée personnelle : pas de nom de | |
| fichier, pas d'IP, pas de surnom, pas de contenu d'image (conforme RGPD — | |
| mention dédiée dans la politique de confidentialité). | |
| Ces données servent à calculer les vrais benchmarks d'Unkor sur des cas réels | |
| et à piloter l'optimisation des poids de fusion (cycle collecte -> analyse -> | |
| amélioration -> réévaluation). | |
| Variables d'environnement : | |
| BENCHMARKS_PATH chemin du fichier JSONL (défaut backend/data/benchmarks.jsonl ; | |
| repli automatique vers le répertoire temporaire si non inscriptible) | |
| BENCHMARKS_OFF "1" pour désactiver toute collecte | |
| """ | |
| import os | |
| import json | |
| import tempfile | |
| import threading | |
| from collections import deque | |
| from datetime import datetime, timezone | |
| MODEL_VERSION = "schrodinger-v2" # V2 : fusion vidéo par confiance + mode video_frame | |
| SYSTEM_NAME = "Système Schrödinger" | |
| _BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| _DEFAULT_PATH = os.path.join(_BASE_DIR, "data", "benchmarks.jsonl") | |
| _LOCK = threading.Lock() | |
| _MAX_ENTRIES = 200_000 # borne mémoire pour l'agrégation | |
| _resolved_path = None | |
| def _off(): | |
| return os.getenv("BENCHMARKS_OFF", "0").lower() in ("1", "true", "yes") | |
| def _path(): | |
| """Chemin inscriptible du JSONL (mémoïsé) — repli /tmp si besoin (HF Spaces).""" | |
| global _resolved_path | |
| if _resolved_path: | |
| return _resolved_path | |
| cand = os.getenv("BENCHMARKS_PATH", _DEFAULT_PATH) | |
| try: | |
| os.makedirs(os.path.dirname(cand) or ".", exist_ok=True) | |
| with open(cand, "a", encoding="utf-8"): | |
| pass | |
| _resolved_path = cand | |
| except Exception: | |
| _resolved_path = os.path.join(tempfile.gettempdir(), "unkor_benchmarks.jsonl") | |
| print(f"[benchmarks] chemin {cand} non inscriptible -> repli {_resolved_path}", flush=True) | |
| return _resolved_path | |
| def _clean_scores(scores): | |
| """Ne conserve que des paires clé -> nombre (jamais de texte libre).""" | |
| out = {} | |
| for k, v in (scores or {}).items(): | |
| if isinstance(v, (int, float)) and not isinstance(v, bool): | |
| out[str(k)] = round(float(v), 4) | |
| return out | |
| def record(kind, final_score, scores=None, duration_ms=None, resolution=None): | |
| """Enregistre une analyse — anonyme, silencieux en cas d'échec (ne casse | |
| jamais une analyse).""" | |
| if _off(): | |
| return | |
| try: | |
| entry = { | |
| "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), | |
| "type": str(kind), | |
| "final_score": round(float(final_score), 4), | |
| "scores": _clean_scores(scores), | |
| "duration_ms": int(duration_ms) if duration_ms is not None else None, | |
| "resolution": str(resolution) if resolution else None, | |
| "model_version": MODEL_VERSION, | |
| } | |
| line = json.dumps(entry, ensure_ascii=False) | |
| with _LOCK: | |
| with open(_path(), "a", encoding="utf-8") as f: | |
| f.write(line + "\n") | |
| except Exception as e: | |
| print(f"[benchmarks] enregistrement ignoré : {e}", flush=True) | |
| def _load(): | |
| entries = deque(maxlen=_MAX_ENTRIES) | |
| try: | |
| with open(_path(), "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| try: | |
| entries.append(json.loads(line)) | |
| except Exception: | |
| continue | |
| except FileNotFoundError: | |
| pass | |
| except Exception as e: | |
| print(f"[benchmarks] lecture impossible : {e}", flush=True) | |
| return list(entries) | |
| def stats(): | |
| """Statistiques agrégées pour GET /benchmarks/stats.""" | |
| entries = _load() | |
| base = { | |
| "system": SYSTEM_NAME, | |
| "model_version": MODEL_VERSION, | |
| "total_analyses": len(entries), | |
| } | |
| if not entries: | |
| return {**base, "message": "Aucune analyse enregistrée pour le moment."} | |
| by_type, dist = {}, [0] * 10 | |
| sum_score = 0.0 | |
| durations, scores_acc = {}, {} | |
| for e in entries: | |
| kind = e.get("type", "?") | |
| s = float(e.get("final_score", 0.0)) | |
| sum_score += s | |
| dist[min(9, max(0, int(s * 10)))] += 1 | |
| t = by_type.setdefault(kind, {"count": 0, "sum_score": 0.0}) | |
| t["count"] += 1 | |
| t["sum_score"] += s | |
| d = e.get("duration_ms") | |
| if isinstance(d, (int, float)): | |
| durations.setdefault(kind, []).append(float(d)) | |
| for k, v in (e.get("scores") or {}).items(): | |
| if isinstance(v, (int, float)): | |
| acc = scores_acc.setdefault(k, [0.0, 0]) | |
| acc[0] += float(v) | |
| acc[1] += 1 | |
| all_durations = [d for lst in durations.values() for d in lst] | |
| return { | |
| **base, | |
| "avg_score": round(sum_score / len(entries), 4), | |
| "avg_duration_ms": round(sum(all_durations) / len(all_durations)) if all_durations else None, | |
| "by_type": { | |
| k: { | |
| "count": t["count"], | |
| "avg_score": round(t["sum_score"] / t["count"], 4), | |
| "avg_duration_ms": (round(sum(durations[k]) / len(durations[k])) | |
| if durations.get(k) else None), | |
| } | |
| for k, t in sorted(by_type.items()) | |
| }, | |
| "score_distribution": { | |
| f"{i / 10:.1f}-{(i + 1) / 10:.1f}": dist[i] for i in range(10) | |
| }, | |
| "per_analyzer_avg": { | |
| k: round(s / n, 4) for k, (s, n) in sorted(scores_acc.items()) if n > 0 | |
| }, | |
| "period": {"from": entries[0].get("ts"), "to": entries[-1].get("ts")}, | |
| } | |