Spaces:
Sleeping
Sleeping
File size: 5,900 Bytes
9a200aa 6e4fc31 9a200aa | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | """
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")},
}
|