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")},
    }