IKRAMELHADI commited on
Commit
c019996
·
1 Parent(s): d469b87
Files changed (1) hide show
  1. app.py +179 -294
app.py CHANGED
@@ -1,343 +1,228 @@
1
  import os
2
  import time
3
- import gradio as gr
4
  import pandas as pd
5
- import numpy as np
6
  import joblib
7
- import xgboost as xgb
8
- import requests
9
- from requests.adapters import HTTPAdapter
10
- from urllib3.util.retry import Retry
11
-
12
 
13
  # =========================
14
  # CONFIG
15
  # =========================
16
- API_TOKEN = "A ECRIRE" # <-- remplace ici
17
-
18
- MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
19
- MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
20
-
21
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
22
-
23
  FREESOUND_API_BASE = "https://freesound.org/apiv2"
 
 
 
 
24
 
 
 
 
 
 
25
 
26
  # =========================
27
- # UI (CSS)
28
  # =========================
29
- CSS = """
30
- .card { border:1px solid #e5e7eb; background:#fff; padding:16px; border-radius:16px; }
31
- .card-error{ border-color:#fca5a5; background:#fff1f2; }
32
- .card-title{ font-weight:950; margin-bottom:8px; }
33
- .badges{ display:flex; gap:10px; flex-wrap:wrap; margin-bottom:12px; }
34
- .badge{ padding:6px 10px; border-radius:999px; font-weight:900; font-size:13px; border:1px solid #e5e7eb; }
35
- .badge-type{ background:#eef2ff; color:#3730a3; }
36
- .badge-time{ background:#ecfeff; color:#155e75; }
37
- .grid{ display:grid; grid-template-columns:1fr; gap:10px; }
38
- .box{ border:1px solid #e5e7eb; border-radius:14px; padding:12px; background:#fafafa; }
39
- .box-title{ font-weight:900; margin-bottom:4px; }
40
- .box-value{ font-size:18px; font-weight:800; }
41
- .hint{ margin-top:10px; color:#6b7280; font-size:12px; }
42
- #header-title{ font-size:28px; font-weight:950; margin-bottom:6px; }
43
- #header-sub{ color:#6b7280; margin-top:0px; line-height:1.45; }
44
- """
45
-
46
-
47
- def html_error(title, body_html):
48
- return f"""
49
- <div class="card card-error">
50
- <div class="card-title">❌ {title}</div>
51
- <div>{body_html}</div>
52
- </div>
53
- """.strip()
54
-
55
-
56
- def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""):
57
- return f"""
58
- <div class="card">
59
- <div class="badges">
60
- <span class="badge badge-type">{badge_text}</span>
61
- <span class="badge badge-time">⏱️ {duration:.2f} s</span>
62
- </div>
63
-
64
- <div class="grid">
65
- <div class="box">
66
- <div class="box-title">📈 Popularité de la note moyenne</div>
67
- <div class="box-value">{rating_text}</div>
68
- </div>
69
- <div class="box">
70
- <div class="box-title">⬇️ Popularité des téléchargements</div>
71
- <div class="box-value">{downloads_text}</div>
72
- </div>
73
- </div>
74
-
75
- {extra_html}
76
-
77
- <div class="hint">
78
- Résultats affichés en <b>niveaux</b> (faible / moyen / élevé), pas en valeurs exactes.
79
- </div>
80
- </div>
81
- """.strip()
82
 
 
 
 
83
 
84
- # =========================
85
- # INTERPRETATION
86
- # =========================
87
- def interpret_results(avg_class: int, dl_class: int) -> str:
88
- if avg_class == 0:
89
- return (
90
- "ℹ️ <b>Interprétation</b> :<br>"
91
- "Aucune/peu d'évaluations utilisateurs (rating manquant).<br>"
92
- "La popularité est donc probablement liée à l'usage (téléchargements) plutôt qu'à la qualité perçue."
93
- )
94
-
95
- rating_txt = {1: "faible", 2: "moyenne", 3: "élevée"}.get(avg_class, "inconnue")
96
- downloads_txt = {0: "faible", 1: "modérée", 2: "élevée"}.get(dl_class, "inconnue")
97
-
98
- if avg_class == 3 and dl_class == 2:
99
- potentiel, detail = "très fort", "contenu de haute qualité et très populaire."
100
- elif avg_class == 3 and dl_class == 1:
101
- potentiel, detail = "fort", "contenu bien apprécié, en croissance."
102
- elif avg_class == 3 and dl_class == 0:
103
- potentiel, detail = "prometteur", "bonne qualité mais faible visibilité."
104
- elif avg_class == 2 and dl_class == 2:
105
- potentiel, detail = "modéré à fort", "populaire mais qualité perçue moyenne."
106
- elif avg_class == 2 and dl_class == 1:
107
- potentiel, detail = "modéré", "profil standard, popularité stable."
108
- elif avg_class == 2 and dl_class == 0:
109
- potentiel, detail = "limité", "engagement faible, diffusion limitée."
110
- elif avg_class == 1 and dl_class == 2:
111
- potentiel, detail = "contradictoire", "très téléchargé mais peu apprécié."
112
- elif avg_class == 1 and dl_class == 1:
113
- potentiel, detail = "faible", "peu attractif pour les utilisateurs."
114
- else:
115
- potentiel, detail = "très faible", "faible intérêt global."
116
-
117
- return (
118
- "🧠 <b>Interprétation</b> :<br>"
119
- f"- Qualité perçue : <b>{rating_txt}</b><br>"
120
- f"- Popularité : <b>{downloads_txt}</b><br><br>"
121
- f"👉 Potentiel estimé : <b>{potentiel}</b> — {detail}"
122
- )
123
-
124
-
125
- def avg_label_to_class(avg_label: str) -> int:
126
- if avg_label is None:
127
- return 0
128
- s = str(avg_label).strip().lower()
129
- if "miss" in s or "missing" in s or "none" in s or "no" in s:
130
- return 0
131
- if "high" in s or "élev" in s or "eleve" in s:
132
- return 3
133
- if "medium" in s or "moy" in s:
134
- return 2
135
- if "low" in s or "faibl" in s:
136
- return 1
137
- return 0
138
 
139
 
140
  # =========================
141
- # HTTP SESSION (retries)
142
  # =========================
143
- def make_session():
144
- session = requests.Session()
145
- retry = Retry(
146
- total=5,
147
- backoff_factor=0.8,
148
- status_forcelist=[429, 500, 502, 503, 504],
149
- allowed_methods=["GET"],
150
- raise_on_status=False,
151
- )
152
- adapter = HTTPAdapter(max_retries=retry)
153
- session.mount("https://", adapter)
154
- session.mount("http://", adapter)
155
- return session
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- SESSION = make_session()
 
 
 
159
 
160
 
161
- def fetch_sound_metadata_by_id(sound_id: int, fields: str) -> dict:
162
  """
163
- Appel API FreeSound directement (plus stable) + retries + timeout.
164
  """
165
- url = f"{FREESOUND_API_BASE}/search/text/"
166
- headers = {"Authorization": f"Token {API_TOKEN}"}
 
 
 
 
 
 
167
 
168
- params = {
169
- "query": "",
170
- "filter": f"id:{sound_id}",
171
- "fields": fields,
172
- "page_size": 1,
173
- }
174
 
175
- # timeout séparé (connect, read)
176
- resp = SESSION.get(url, headers=headers, params=params, timeout=(6, 20))
177
- if resp.status_code == 401:
178
- raise RuntimeError("Token invalide ou non autorisé (401).")
179
- if resp.status_code >= 400:
180
- raise RuntimeError(f"Erreur HTTP {resp.status_code}: {resp.text[:200]}")
181
 
182
- data = resp.json()
183
- results = data.get("results", [])
184
- if not results:
185
- raise RuntimeError("Sound not found (aucun résultat pour cet ID).")
186
- return results[0]
 
 
 
 
 
 
 
187
 
188
 
189
  # =========================
190
- # Charger modèles (NOMS EXACTS)
191
  # =========================
192
- music_num_model = joblib.load(os.path.join(BASE_DIR, "music_model_num_downloads.joblib"))
193
- music_feat_list = joblib.load(os.path.join(BASE_DIR, "music_model_features_list.joblib"))
194
- music_avg_model = joblib.load(os.path.join(BASE_DIR, "music_xgb_avg_rating.joblib"))
195
- music_avg_le = joblib.load(os.path.join(BASE_DIR, "music_xgb_avg_rating_label_encoder.joblib"))
196
 
197
- effect_num_model = joblib.load(os.path.join(BASE_DIR, "effectSound_model_num_downloads.joblib"))
198
- effect_feat_list = joblib.load(os.path.join(BASE_DIR, "effect_model_features_list.joblib"))
199
- effect_avg_model = joblib.load(os.path.join(BASE_DIR, "effectSound_xgb_avg_rating.joblib"))
200
- effect_avg_le = joblib.load(os.path.join(BASE_DIR, "effectSound_xgb_avg_rating_label_encoder.joblib"))
201
 
202
- NUM_DOWNLOADS_MAP = {0: "Faible", 1: "Moyen", 2: "Élevé"}
203
 
 
204
 
205
- def safe_float(v):
206
- try:
207
- return float(v)
208
- except Exception:
209
- return 0.0
210
 
 
 
211
 
212
- def build_feature_df(sound: dict, feat_list: list) -> pd.DataFrame:
213
- """
214
- Tableau lisible des features utilisées (valeur API + NaN si absent).
215
- """
216
- rows = []
217
- for col in feat_list:
218
- val = sound.get(col, np.nan)
219
- if val is None or isinstance(val, (list, dict)):
220
- val = np.nan
221
- rows.append({"feature": col, "value": val})
222
- return pd.DataFrame(rows)
223
 
 
 
 
 
 
 
224
 
225
- def predict_with_model(model, sound: dict, feat_list: list, le=None):
226
- row = []
227
- for col in feat_list:
228
- val = sound.get(col, 0)
229
- if val is None or isinstance(val, (list, dict)):
230
- val = 0
231
- row.append(safe_float(val))
232
-
233
- X = pd.DataFrame([row], columns=feat_list)
234
- dm = xgb.DMatrix(X.values, feature_names=feat_list)
235
- pred_int = int(model.get_booster().predict(dm)[0])
236
-
237
- if le is not None:
238
- return le.inverse_transform([pred_int])[0]
239
- return pred_int
240
-
241
-
242
- def extract_and_predict(url: str):
243
- if not url or not url.strip():
244
- return html_error("URL vide", "Collez une URL FreeSound du type <code>https://freesound.org/s/123456/</code>"), pd.DataFrame()
245
-
246
- # Parse ID
247
- try:
248
- sound_id = int(url.rstrip("/").split("/")[-1])
249
- except Exception:
250
- return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL."), pd.DataFrame()
251
-
252
- # Fields nécessaires : union music/effect + duration
253
- all_features = sorted(list(set(music_feat_list + effect_feat_list)))
254
- fields = "duration," + ",".join(all_features)
255
-
256
- # Fetch API (avec retries)
257
- try:
258
- sound = fetch_sound_metadata_by_id(sound_id, fields=fields)
259
- except Exception as e:
260
- return html_error(
261
- "Erreur API FreeSound",
262
- f"Détail : <code>{e}</code><br><br>"
263
- "Astuce : si ça arrive aléatoirement, c'est souvent un souci réseau/rate limit → réessayez."
264
- ), pd.DataFrame()
265
-
266
- duration = safe_float(sound.get("duration", 0))
267
-
268
- # Vérif durées
269
- if duration < MIN_EFFECT:
270
- return html_error(
271
- "Audio trop court",
272
- f"Durée : <b>{duration:.2f}s</b><br><br>"
273
- f"Plages : Effet sonore <b>{MIN_EFFECT}-{MAX_EFFECT}s</b> | Musique <b>{MIN_MUSIC}-{MAX_MUSIC}s</b>"
274
- ), pd.DataFrame()
275
-
276
- if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
277
- return html_error(
278
- "Audio hors plage",
279
- f"Durée : <b>{duration:.2f}s</b><br><br>"
280
- f"Plages : Effet sonore <b>{MIN_EFFECT}-{MAX_EFFECT}s</b> | Musique <b>{MIN_MUSIC}-{MAX_MUSIC}s</b>"
281
- ), pd.DataFrame()
282
-
283
- # Effect
284
- if MIN_EFFECT <= duration <= MAX_EFFECT:
285
- badge = "🔊 Effet sonore (metadata FreeSound)"
286
- dl_class = int(predict_with_model(effect_num_model, sound, effect_feat_list))
287
- avg_text = str(predict_with_model(effect_avg_model, sound, effect_feat_list, effect_avg_le))
288
- dl_text = NUM_DOWNLOADS_MAP.get(dl_class, str(dl_class))
289
-
290
- avg_class = avg_label_to_class(avg_text)
291
- conclusion = interpret_results(avg_class, dl_class)
292
-
293
- extra = f"""
294
- <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
295
- <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">{conclusion}</div>
296
- """
297
- df_feat = build_feature_df(sound, effect_feat_list)
298
- return html_result(badge, duration, avg_text, dl_text, extra_html=extra), df_feat
299
-
300
- # Music
301
- badge = "🎵 Musique (metadata FreeSound)"
302
- dl_class = int(predict_with_model(music_num_model, sound, music_feat_list))
303
- avg_text = str(predict_with_model(music_avg_model, sound, music_feat_list, music_avg_le))
304
- dl_text = NUM_DOWNLOADS_MAP.get(dl_class, str(dl_class))
305
-
306
- avg_class = avg_label_to_class(avg_text)
307
- conclusion = interpret_results(avg_class, dl_class)
308
-
309
- extra = f"""
310
- <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
311
- <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">{conclusion}</div>
312
- """
313
- df_feat = build_feature_df(sound, music_feat_list)
314
- return html_result(badge, duration, avg_text, dl_text, extra_html=extra), df_feat
315
 
316
 
317
  # =========================
318
  # UI
319
  # =========================
320
- theme = gr.themes.Soft()
321
-
322
- with gr.Blocks(title="Test — Metadata FreeSound", css=CSS) as demo:
323
- gr.HTML(
324
- f"""
325
- <div id="header-title">🔎 Test — Prédiction via Metadata FreeSound</div>
326
- <p id="header-sub">
327
- Collez une URL FreeSound. L'app récupère les <b>metadata</b> via l'API et prédit la popularité (avg_rating, num_downloads).
328
- <br><br>
329
- <b>Durées acceptées :</b> 🔊 Effet sonore {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}–{MAX_MUSIC}s
330
- </p>
331
- """
332
- )
333
-
334
- url = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
335
- btn = gr.Button("🚀 Tester la prédiction", variant="primary")
336
 
337
  with gr.Row():
338
- out_html = gr.HTML(label="Résultat")
339
- out_df = gr.Dataframe(label="Features utilisées (metadata)", interactive=False)
 
 
 
 
340
 
341
- btn.click(extract_and_predict, inputs=url, outputs=[out_html, out_df])
 
342
 
343
- demo.launch(theme=theme)
 
 
1
  import os
2
  import time
3
+ import requests
4
  import pandas as pd
5
+ import gradio as gr
6
  import joblib
 
 
 
 
 
7
 
8
  # =========================
9
  # CONFIG
10
  # =========================
 
 
 
 
 
 
 
11
  FREESOUND_API_BASE = "https://freesound.org/apiv2"
12
+ API_TOKEN = os.getenv("FREESOUND_API_TOKEN", "").strip()
13
+
14
+ # Timeout: (connect, read)
15
+ TIMEOUT = (6, 20)
16
 
17
+ # Session HTTP réutilisable
18
+ SESSION = requests.Session()
19
+ ADAPTER = requests.adapters.HTTPAdapter(pool_connections=20, pool_maxsize=20, max_retries=0)
20
+ SESSION.mount("https://", ADAPTER)
21
+ SESSION.headers.update({"User-Agent": "freesound-gradio-metadata/1.0"})
22
 
23
  # =========================
24
+ # CHARGE TON MODELE + FEATURES
25
  # =========================
26
+ # Adapte ces chemins à ton projet
27
+ MODEL_PATH = "model.joblib"
28
+ FEATURES_PATH = "features.txt" # un fichier avec 1 feature par ligne (ordre = ordre du training)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ if not os.path.exists(MODEL_PATH):
31
+ raise FileNotFoundError(f"Modèle introuvable: {MODEL_PATH}")
32
+ model = joblib.load(MODEL_PATH)
33
 
34
+ if not os.path.exists(FEATURES_PATH):
35
+ raise FileNotFoundError(f"Liste de features introuvable: {FEATURES_PATH}")
36
+ with open(FEATURES_PATH, "r", encoding="utf-8") as f:
37
+ FEATURE_NAMES = [line.strip() for line in f if line.strip()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
 
40
  # =========================
41
+ # OUTILS
42
  # =========================
43
+ def safe_get_json(url, headers=None, params=None, attempts=5, backoff=1.7):
44
+ """
45
+ GET JSON robuste : retries sur erreurs réseau/5xx/429.
46
+ """
47
+ last_err = None
48
+ for i in range(attempts):
49
+ try:
50
+ resp = SESSION.get(url, headers=headers, params=params, timeout=TIMEOUT)
51
+
52
+ # Rate limit
53
+ if resp.status_code == 429:
54
+ retry_after = resp.headers.get("Retry-After")
55
+ wait = float(retry_after) if retry_after and retry_after.isdigit() else (backoff ** i)
56
+ time.sleep(wait)
57
+ continue
58
+
59
+ # Server errors
60
+ if 500 <= resp.status_code < 600:
61
+ time.sleep(backoff ** i)
62
+ continue
63
+
64
+ # Auth / Not found / autres erreurs client
65
+ if resp.status_code == 401:
66
+ raise RuntimeError("❌ Token FreeSound invalide ou non autorisé (401).")
67
+ if resp.status_code == 404:
68
+ raise RuntimeError("❌ Sound introuvable (404).")
69
+ if resp.status_code >= 400:
70
+ raise RuntimeError(f"❌ Erreur HTTP {resp.status_code}: {resp.text[:200]}")
71
+
72
+ return resp.json()
73
+
74
+ except (requests.exceptions.ConnectionError,
75
+ requests.exceptions.Timeout,
76
+ requests.exceptions.ChunkedEncodingError) as e:
77
+ last_err = e
78
+ time.sleep(backoff ** i)
79
+ continue
80
+ except Exception as e:
81
+ # autre exception : on remonte direct
82
+ raise
83
+
84
+ raise RuntimeError(f"❌ Échec après {attempts} tentatives. Dernière erreur: {repr(last_err)}")
85
+
86
+
87
+ def fetch_sound_by_id(sound_id: int, fields: str) -> dict:
88
+ """
89
+ ✅ Endpoint stable : /sounds/{id}/
90
+ """
91
+ if not API_TOKEN:
92
+ raise RuntimeError("❌ FREESOUND_API_TOKEN manquant (variable d'environnement).")
93
+
94
+ url = f"{FREESOUND_API_BASE}/sounds/{int(sound_id)}/"
95
+ headers = {"Authorization": f"Token {API_TOKEN}"}
96
+ params = {"fields": fields}
97
+ return safe_get_json(url, headers=headers, params=params)
98
+
99
 
100
+ def flatten_features(ac_analysis: dict) -> dict:
101
+ """
102
+ FreeSound renvoie souvent un dict de features (ac_analysis).
103
+ Ici on aplatit en {feature_name: value} en gardant uniquement
104
+ les clés directes (et on ignore les structures trop imbriquées).
105
+ """
106
+ flat = {}
107
+ if not isinstance(ac_analysis, dict):
108
+ return flat
109
+
110
+ for k, v in ac_analysis.items():
111
+ # garde les nombres simples / bool / str courts
112
+ if isinstance(v, (int, float, bool)):
113
+ flat[k] = float(v) if isinstance(v, bool) else v
114
+ elif isinstance(v, str):
115
+ # éviter d'injecter des textes énormes
116
+ flat[k] = v[:200]
117
+ # si liste/dict: on ignore (ou tu peux custom)
118
+ return flat
119
+
120
+
121
+ def build_feature_df(sound_json: dict, wanted_features: list[str]) -> pd.DataFrame:
122
+ """
123
+ Construit un DataFrame avec les features réellement utilisées par ton modèle.
124
+ """
125
+ ac = sound_json.get("ac_analysis", {}) or {}
126
+ flat = flatten_features(ac)
127
 
128
+ rows = []
129
+ for feat in wanted_features:
130
+ rows.append({"feature": feat, "value": flat.get(feat, None)})
131
+ return pd.DataFrame(rows)
132
 
133
 
134
+ def build_model_vector(sound_json: dict, feature_names: list[str]) -> pd.DataFrame:
135
  """
136
+ Construit un X (1 ligne) dans le bon ordre de features.
137
  """
138
+ ac = sound_json.get("ac_analysis", {}) or {}
139
+ flat = flatten_features(ac)
140
+
141
+ x = {feat: flat.get(feat, None) for feat in feature_names}
142
+ X = pd.DataFrame([x])
143
+
144
+ # Option: fillna(0) si ton training le faisait (sinon enlève)
145
+ X = X.fillna(0)
146
 
147
+ return X
 
 
 
 
 
148
 
 
 
 
 
 
 
149
 
150
+ def predict_label(sound_json: dict):
151
+ X = build_model_vector(sound_json, FEATURE_NAMES)
152
+
153
+ # proba si dispo
154
+ label = model.predict(X)[0]
155
+ proba = None
156
+ if hasattr(model, "predict_proba"):
157
+ try:
158
+ proba = float(model.predict_proba(X).max())
159
+ except Exception:
160
+ proba = None
161
+ return label, proba, X
162
 
163
 
164
  # =========================
165
+ # GRADIO LOGIC
166
  # =========================
167
+ DEFAULT_FIELDS = "id,name,username,license,tags,previews,ac_analysis"
 
 
 
168
 
169
+ def run(sound_id: str):
170
+ sound_id = str(sound_id).strip()
171
+ if not sound_id.isdigit():
172
+ raise gr.Error("Entre un ID numérique (ex: 123456).")
173
 
174
+ sid = int(sound_id)
175
 
176
+ sound = fetch_sound_by_id(sid, fields=DEFAULT_FIELDS)
177
 
178
+ # Tableau des features utilisées
179
+ df_features = build_feature_df(sound, FEATURE_NAMES)
 
 
 
180
 
181
+ # Prediction
182
+ label, proba, X = predict_label(sound)
183
 
184
+ # Infos utiles à afficher
185
+ title = sound.get("name", "")
186
+ user = sound.get("username", "")
187
+ tags = sound.get("tags", [])
188
+ preview_url = (sound.get("previews", {}) or {}).get("preview-hq-mp3") or (sound.get("previews", {}) or {}).get("preview-lq-mp3")
 
 
 
 
 
 
189
 
190
+ info_md = f"""
191
+ ### 🎧 Sound
192
+ - **ID**: `{sid}`
193
+ - **Nom**: {title}
194
+ - **Auteur**: {user}
195
+ - **Tags**: {", ".join(tags[:25])}{' …' if len(tags) > 25 else ''}
196
 
197
+ ### 🔮 Prédiction
198
+ - **Classe prédite**: **{label}**
199
+ """ + (f"- **Confiance (max proba)**: `{proba:.3f}`\n" if proba is not None else "")
200
+
201
+ audio = preview_url if preview_url else None
202
+
203
+ # Option: montrer aussi le vecteur X (1 ligne) si tu veux
204
+ # df_x = X.T.reset_index().rename(columns={"index": "feature", 0: "value"})
205
+ # return info_md, audio, df_features, df_x
206
+
207
+ return info_md, audio, df_features
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
 
210
  # =========================
211
  # UI
212
  # =========================
213
+ with gr.Blocks(title="FreeSound ID → Metadata + Prediction") as demo:
214
+ gr.Markdown("# FreeSound : Métadonnées → Features → Prédiction")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  with gr.Row():
217
+ sound_id_in = gr.Textbox(label="Sound ID", placeholder="ex: 123456", scale=2)
218
+ btn = gr.Button("Récupérer & prédire", scale=1)
219
+
220
+ info_out = gr.Markdown()
221
+ audio_out = gr.Audio(label="Preview (si dispo)", interactive=False)
222
+ features_out = gr.Dataframe(label="Features utilisées (valeurs FreeSound)", interactive=False)
223
 
224
+ btn.click(fn=run, inputs=[sound_id_in], outputs=[info_out, audio_out, features_out])
225
+ sound_id_in.submit(fn=run, inputs=[sound_id_in], outputs=[info_out, audio_out, features_out])
226
 
227
+ if __name__ == "__main__":
228
+ demo.launch()