Spaces:
Paused
Paused
File size: 8,336 Bytes
308d297 9f39c70 308d297 9f39c70 308d297 9f39c70 | 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 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | """
app.py — tracer scan en tant que mini-app Gradio, à héberger sur un HuggingFace
Space (SDK: gradio). Permet d'uploader n'importe quel .jsonl compatible TRACER,
de choisir l'embedder / le target / le mode force, et de récupérer le rapport
HTML interactif + un résumé terminal-like du balayage de targets.
Déploiement :
1. huggingface.co/new-space -> SDK "Gradio" -> hardware "CPU basic" suffit
2. Uploader ce fichier (app.py) + requirements.txt à la racine du Space
3. Le Space build automatiquement et expose l'UI
Aucun souci réseau/SSL attendu ici : le Space tourne sur l'infra HuggingFace,
donc le téléchargement du modèle sentence-transformers se fait en local au
Hub, sans proxy ni certificat tiers à valider.
"""
from __future__ import annotations
import tempfile
from pathlib import Path
import gradio as gr
import numpy as np
import tracer
from tracer.scanner import ThinDataError, load_scan_traces, scan_html
from tracer.embeddings.index import embed_texts
EMBED_MODEL_CHOICES = [
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", # défaut fit_tracer.py
"BAAI/bge-m3", # exigé par la spec
"all-MiniLM-L6-v2", # défaut CLI tracer (EN only)
]
# Cache mémoire (process du Space) pour éviter de ré-embruter le même
# fichier+modèle entre deux clics (target/force/viz-layout différents).
_EMBED_CACHE: dict[str, np.ndarray] = {}
def _cache_key(path: Path, model: str) -> str:
import hashlib
h = hashlib.sha256()
h.update(path.read_bytes())
h.update(model.encode("utf-8"))
return h.hexdigest()
def _get_embeddings(path: Path, model: str, inputs: list[str], progress: gr.Progress | None = None) -> np.ndarray:
key = _cache_key(path, model)
if key in _EMBED_CACHE:
return _EMBED_CACHE[key]
if progress:
progress(0.1, desc=f"Téléchargement / chargement de {model}...")
X = embed_texts(inputs, model=model, show_progress=False)
_EMBED_CACHE[key] = X
return X
def run_scan(
files,
embed_model: str,
primary_target: float,
sweep_targets_str: str,
force_mode: str,
price: float,
monthly_calls: int,
viz_layout: str,
progress: gr.Progress = gr.Progress(),
):
"""Callback principal du bouton 'Lancer le scan'."""
if not files:
return "⚠ Aucun fichier fourni.", "", []
try:
sweep_targets = sorted(
{float(t.strip()) for t in sweep_targets_str.split(",") if t.strip()}
| {primary_target}
)
except ValueError:
return "⚠ Targets de balayage invalides (attendu : ex. '0.8, 0.85, 0.9, 0.95').", "", []
summary_lines: list[str] = []
html_reports: list[str] = []
html_files: list[str] = []
for i, file_obj in enumerate(files):
path = Path(file_obj.name if hasattr(file_obj, "name") else file_obj)
progress((i) / len(files), desc=f"Lecture de {path.name}...")
try:
inputs, labels = load_scan_traces(path)
except Exception as exc:
summary_lines.append(f"=== {path.name} ===\n⚠ Erreur de lecture : {exc}\n")
continue
n = len(inputs)
n_classes = len(set(labels))
summary_lines.append(f"=== {path.name} ===")
summary_lines.append(f"{n} traces utilisables · {n_classes} labels distincts")
if force_mode == "auto":
effective_force = n < 1000
if effective_force:
summary_lines.append(f"⚠ {n} < 1000 traces : force appliqué automatiquement (résultat best-effort).")
elif force_mode == "force":
effective_force = True
else:
effective_force = False
progress((i + 0.3) / len(files), desc=f"Embeddings ({embed_model})...")
try:
X = _get_embeddings(path, embed_model, inputs, progress=progress)
except Exception as exc:
summary_lines.append(f"⚠ Échec du calcul des embeddings : {exc}\n")
continue
progress((i + 0.6) / len(files), desc="Balayage de targets...")
for t in sweep_targets:
try:
result = tracer.scan(path, target=t, embeddings=X, force=effective_force, viz_layout=viz_layout)
summary_lines.append(
f" target {t:.0%} certifiable = {result.certifiable_share:6.1%} "
f"({result.n_clusters} cellules{' [forced]' if result.forced else ''})"
)
except ThinDataError as exc:
summary_lines.append(f" target {t:.0%} {exc}")
progress((i + 0.9) / len(files), desc="Génération du rapport HTML...")
try:
result = tracer.scan(
path,
target=primary_target,
embeddings=X,
force=effective_force,
teacher_price_per_1k=(price or None),
monthly_calls=(monthly_calls or None),
viz_layout=viz_layout,
)
html = scan_html(result, source_name=path.name)
tmp = tempfile.NamedTemporaryFile(
delete=False, suffix=".html", prefix=f"{path.stem}_scan_"
)
tmp.write(html.encode("utf-8"))
tmp.close()
html_files.append(tmp.name)
html_reports.append(html)
summary_lines.append(f" → rapport HTML généré ({path.name})")
except ThinDataError as exc:
summary_lines.append(f" ⚠ Rapport HTML non généré : {exc}")
summary_lines.append("")
terminal_summary = "\n".join(summary_lines)
preview_html = html_reports[0] if html_reports else "<p>Aucun rapport généré.</p>"
return terminal_summary, preview_html, html_files
with gr.Blocks(title="TRACER scan — testeur de dataset") as demo:
gr.Markdown(
"# tracer scan — testeur de dataset\n"
"Upload un ou plusieurs `.jsonl` compatibles TRACER (`input`+`teacher`, "
"ou alias tolérés) pour voir leur routabilité probable **avant** de lancer "
"`fit`. Tourne sur l'infra HuggingFace : le modèle d'embedding se "
"télécharge directement depuis le Hub, sans souci réseau/proxy local."
)
with gr.Row():
with gr.Column(scale=1):
files_in = gr.File(
label="Fichier(s) .jsonl", file_count="multiple", file_types=[".jsonl"]
)
embed_model_in = gr.Dropdown(
label="Embedder",
choices=EMBED_MODEL_CHOICES,
value=EMBED_MODEL_CHOICES[0],
allow_custom_value=True,
)
target_in = gr.Slider(
label="Target primaire (rapport HTML détaillé)",
minimum=0.5, maximum=0.99, step=0.01, value=0.95,
)
sweep_in = gr.Textbox(
label="Targets de balayage (séparés par des virgules)",
value="0.80, 0.85, 0.90, 0.95",
)
force_in = gr.Radio(
label="Mode thin-data",
choices=[("auto (force si <1000 traces)", "auto"), ("forcer", "force"), ("refuser", "no_force")],
value="auto",
)
with gr.Accordion("Options économiques (facultatif)", open=False):
price_in = gr.Number(label="Prix teacher $/1k appels", value=None)
monthly_in = gr.Number(label="Volume mensuel d'appels", value=None, precision=0)
viz_in = gr.Radio(
label="Layout 3D du rapport", choices=["pca", "umap", "tsne", "auto"], value="pca"
)
run_btn = gr.Button("Lancer le scan", variant="primary")
with gr.Column(scale=2):
summary_out = gr.Textbox(label="Résumé (terminal-like)", lines=24)
html_preview = gr.HTML(label="Aperçu du rapport (1er fichier)")
html_download = gr.Files(label="Télécharger le(s) rapport(s) HTML")
run_btn.click(
run_scan,
inputs=[files_in, embed_model_in, target_in, sweep_in, force_in, price_in, monthly_in, viz_in],
outputs=[summary_out, html_preview, html_download],
)
if __name__ == "__main__":
demo.launch(show_api=False) |