IKRAMELHADI commited on
Commit
d874fd8
·
1 Parent(s): 0db8a8f
Files changed (2) hide show
  1. app.py +554 -278
  2. requirements.txt +14 -21
app.py CHANGED
@@ -1,5 +1,4 @@
1
  import os
2
- import glob
3
  import tempfile
4
  import numpy as np
5
  import pandas as pd
@@ -9,72 +8,52 @@ import joblib
9
  import soundfile as sf
10
  from pydub import AudioSegment
11
  import opensmile
 
12
  import freesound
13
  import xgboost as xgb
 
14
 
15
 
16
- # =========================
17
  # CONFIG
18
- # =========================
19
  MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
20
  MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
21
  SR_TARGET = 16000
22
 
 
23
  FREESOUND_TOKEN = os.getenv("FREESOUND_TOKEN", "").strip()
24
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
25
 
26
-
27
- # =========================
28
- # Helpers fichiers
29
- # =========================
30
  def p(*parts):
31
  return os.path.join(BASE_DIR, *parts)
32
 
33
- def list_local_files():
34
- files = []
35
- for root, _, fnames in os.walk(BASE_DIR):
36
- for f in fnames:
37
- if f.lower().endswith((".pkl", ".joblib", ".json", ".bin", ".txt")):
38
- rel = os.path.relpath(os.path.join(root, f), BASE_DIR)
39
- files.append(rel)
40
- return sorted(files)
41
-
42
- def exists(rel_path: str) -> bool:
43
- return os.path.exists(p(rel_path))
44
-
45
- def load_joblib_any(candidates):
46
- """
47
- Essaie une liste de chemins relatifs (ou patterns glob).
48
- Retourne (obj, chosen_path) ou (None, None).
49
- """
50
- for c in candidates:
51
- if any(ch in c for ch in ["*", "?", "["]):
52
- matches = sorted(glob.glob(p(c)))
53
- if not matches:
54
- continue
55
- chosen = matches[0]
56
- try:
57
- obj = joblib.load(chosen)
58
- return obj, os.path.relpath(chosen, BASE_DIR)
59
- except Exception:
60
- continue
61
- else:
62
- full = p(c)
63
- if os.path.exists(full):
64
- try:
65
- obj = joblib.load(full)
66
- return obj, c
67
- except Exception:
68
- continue
69
- return None, None
70
 
 
 
 
 
 
71
 
72
- # =========================
 
 
 
 
 
 
 
 
 
 
73
  # UI helpers
74
- # =========================
75
  CSS = """
76
  .card { border: 1px solid #e5e7eb; background: #ffffff; padding: 16px; border-radius: 16px; }
77
  .card-error{ border-color: #fca5a5; background: #fff1f2; }
 
78
  .card-title{ font-weight: 950; margin-bottom: 8px; }
79
  .badges{ display:flex; gap:10px; flex-wrap:wrap; margin-bottom:12px; }
80
  .badge{ padding:6px 10px; border-radius:999px; font-weight:900; font-size: 13px; border: 1px solid #e5e7eb; }
@@ -85,19 +64,30 @@ CSS = """
85
  .box-title{ font-weight:900; margin-bottom:4px; }
86
  .box-value{ font-size:18px; font-weight:800; }
87
  .hint{ margin-top:10px; color:#6b7280; font-size:12px; }
88
- pre{ white-space:pre-wrap; }
89
  #header-title { font-size: 28px; font-weight: 950; margin-bottom: 6px; }
90
  #header-sub { color:#6b7280; margin-top:0px; line-height:1.45; }
 
91
  """
92
 
93
- def html_error(title, body_html):
 
 
 
 
 
94
  return f"""
95
- <div class="card card-error">
96
- <div class="card-title">❌ {title}</div>
97
- <div>{body_html}</div>
98
  </div>
99
  """.strip()
100
 
 
 
 
 
 
 
101
  def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""):
102
  return f"""
103
  <div class="card">
@@ -122,7 +112,7 @@ def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""
122
 
123
  def interpret_results(avg_class: int, dl_class: int) -> str:
124
  if avg_class == 0:
125
- return "ℹ️ <b>Interprétation</b> :<br>Aucune évaluation possible (rating manquant)."
126
 
127
  if avg_class == 3 and dl_class == 2:
128
  potentiel, detail = "très fort", "contenu de haute qualité et très populaire."
@@ -159,30 +149,83 @@ def avg_label_to_class(avg_label: str) -> int:
159
  return 1
160
  return 0
161
 
162
- def safe_float(v):
163
- try:
164
- return float(v)
165
- except Exception:
166
- return 0.0
167
-
168
- def parse_sound_id(url: str):
169
- return int(url.rstrip("/").split("/")[-1])
170
-
171
 
172
- # =========================
173
  # FreeSound client
174
- # =========================
175
  def get_fs_client():
176
  if not FREESOUND_TOKEN:
177
- raise RuntimeError("Token FreeSound manquant. Ajoute le secret FREESOUND_TOKEN (Settings Secrets).")
178
  c = freesound.FreesoundClient()
179
  c.set_token(FREESOUND_TOKEN, "token")
180
  return c
181
 
182
 
183
  # ============================================================
184
- # PARTIE A — OpenSMILE (upload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  # ============================================================
 
 
 
 
 
 
186
  SMILE = opensmile.Smile(
187
  feature_set=opensmile.FeatureSet.eGeMAPSv02,
188
  feature_level=opensmile.FeatureLevel.Functionals,
@@ -191,16 +234,6 @@ SMILE = opensmile.Smile(
191
  RATING_DISPLAY_AUDIO = {0: "❌ Informations manquantes", 1: "⭐ Faible", 2: "⭐⭐ Moyen", 3: "⭐⭐⭐ Élevé"}
192
  DOWNLOADS_DISPLAY_AUDIO = {0: "⭐ Faible", 1: "⭐⭐ Moyen", 2: "⭐⭐⭐ Élevé"}
193
 
194
- MODEL_EFFECT_A, PATH_EFFECT_A = load_joblib_any([
195
- "xgb_model_EffectSound.pkl",
196
- "xgb_model_effectsound.pkl",
197
- "xgb_model_effectSound.pkl",
198
- ])
199
- MODEL_MUSIC_A, PATH_MUSIC_A = load_joblib_any([
200
- "xgb_model_Music.pkl",
201
- "xgb_model_music.pkl",
202
- ])
203
-
204
  def get_duration_seconds(filepath):
205
  ext = os.path.splitext(filepath)[1].lower()
206
  if ext == ".mp3":
@@ -218,6 +251,7 @@ def to_wav_16k_mono(filepath):
218
  return filepath
219
  except Exception:
220
  pass
 
221
  audio = AudioSegment.from_file(filepath)
222
  audio = audio.set_channels(1).set_frame_rate(SR_TARGET)
223
  tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
@@ -232,26 +266,12 @@ def extract_opensmile_features(filepath):
232
  return feats
233
 
234
  def predict_upload_with_dmatrix(model, X_df: pd.DataFrame):
235
- # sklearn wrapper or Booster
236
  booster = model.get_booster() if hasattr(model, "get_booster") else model
237
  dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
238
- p = booster.predict(dm)
239
- p = np.asarray(p)
240
- if p.ndim == 1:
241
- # si ton modèle renvoie 2 outputs concat, ça ne marche pas;
242
- # ton modèle A semble renvoyer 2 classes (avg, downloads) -> souvent shape (2,)
243
- # on force (1, -1)
244
- p = p.reshape(1, -1)
245
- return p
246
 
247
  def predict_opensmile_upload(audio_file):
248
- if MODEL_EFFECT_A is None or MODEL_MUSIC_A is None:
249
- return html_error(
250
- "Modèles OpenSMILE manquants",
251
- "Il faut fournir les deux modèles OpenSMILE (effect & music) à la racine, ex: "
252
- "<code>xgb_model_EffectSound.pkl</code> et <code>xgb_model_Music.pkl</code>."
253
- )
254
-
255
  if audio_file is None:
256
  return html_error("Aucun fichier", "Veuillez importer un fichier audio (wav, mp3, flac…).")
257
 
@@ -261,45 +281,46 @@ def predict_opensmile_upload(audio_file):
261
  return html_error("Audio illisible", f"Détail : <code>{e}</code>")
262
 
263
  if duration < MIN_EFFECT:
264
- return html_error("Audio trop court", f"Durée : <b>{duration:.2f}s</b> — attendu 0.5–3s ou 10–60s")
265
  if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
266
- return html_error("Audio hors plage", f"Durée : <b>{duration:.2f}s</b> — attendu 0.5–3s ou 10–60s")
 
 
 
 
 
 
267
 
268
  if duration <= MAX_EFFECT:
269
- badge, model = "🔊 OpenSMILE (upload) — EffectSound", MODEL_EFFECT_A
 
270
  else:
271
- badge, model = "🎵 OpenSMILE (upload) — Music", MODEL_MUSIC_A
 
272
 
273
  try:
274
  X = extract_opensmile_features(audio_file)
275
  except Exception as e:
276
  return html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>")
277
 
278
- # align features si possible
279
  try:
280
- if hasattr(model, "feature_names_in_"):
281
- expected = list(model.feature_names_in_)
282
- X = X.reindex(columns=expected, fill_value=0)
283
- except Exception:
284
- # pas bloquant
285
- pass
286
 
287
  try:
288
  y = predict_upload_with_dmatrix(model, X)
289
  except Exception as e:
290
  return html_error("Prédiction échouée", f"Détail : <code>{e}</code>")
291
 
292
- # Convention attendue : y[0,0]=avg_class, y[0,1]=dl_class
293
- try:
294
- avg_class = int(y[0, 0])
295
- dl_class = int(y[0, 1])
296
- except Exception:
297
- return html_error("Sortie modèle inattendue", f"Forme sortie: <code>{getattr(y,'shape',None)}</code>")
298
 
299
  rating_text = RATING_DISPLAY_AUDIO.get(avg_class, "Inconnu")
300
  downloads_text = DOWNLOADS_DISPLAY_AUDIO.get(dl_class, "Inconnu")
 
301
  extra = f"""
302
- <div class="hint">Modèles chargés: <code>{PATH_EFFECT_A}</code> · <code>{PATH_MUSIC_A}</code></div>
303
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
304
  {interpret_results(avg_class, dl_class)}
305
  </div>
@@ -308,94 +329,27 @@ def predict_opensmile_upload(audio_file):
308
 
309
 
310
  # ============================================================
311
- # PARTIE B — FreeSound Acoustic Features (API fields)
312
- # => c’est ici que tu as l’erreur de fichier manquant
313
  # ============================================================
314
- def load_feature_models_B():
315
- """
316
- Essaie de trouver les fichiers même si tu as des variantes de nom.
317
- Retourne dict + liste problèmes.
318
- """
319
- problems = []
320
- M = {}
321
-
322
- # MUSIC
323
- M["music_num_model"], M["music_num_model_path"] = load_joblib_any([
324
- "xgb_num_downloads_music_model.pkl",
325
- "*num*downloads*music*model*.pkl",
326
- "*num*downloads*music*model*.joblib",
327
- ])
328
- M["music_num_feats"], M["music_num_feats_path"] = load_joblib_any([
329
- "xgb_num_downloads_music_features.pkl",
330
- "*num*downloads*music*features*.pkl",
331
- "*num*downloads*music*features*.joblib",
332
- ])
333
- M["music_avg_model"], M["music_avg_model_path"] = load_joblib_any([
334
- "xgb_avg_rating_music_model.pkl",
335
- "*avg*rating*music*model*.pkl",
336
- "*avg*rating*music*model*.joblib",
337
- ])
338
- M["music_avg_feats"], M["music_avg_feats_path"] = load_joblib_any([
339
- "xgb_avg_rating_music_features.pkl",
340
- "*avg*rating*music*features*.pkl",
341
- "*avg*rating*music*features*.joblib",
342
- ])
343
- M["music_avg_le"], M["music_avg_le_path"] = load_joblib_any([
344
- "xgb_avg_rating_music_label_encoder.pkl",
345
- "*avg*rating*music*label*encoder*.pkl",
346
- "*avg*rating*music*label*encoder*.joblib",
347
- ])
348
-
349
- # EFFECTSOUND (variantes de nom)
350
- M["eff_num_model"], M["eff_num_model_path"] = load_joblib_any([
351
- "xgb_num_downloads_effectsound_model.pkl",
352
- "xgb_num_downloads_effectSound_model.pkl",
353
- "xgb_num_downloads_effect_sound_model.pkl",
354
- "*num*downloads*effect*model*.pkl",
355
- "*num*downloads*effect*model*.joblib",
356
- ])
357
- M["eff_num_feats"], M["eff_num_feats_path"] = load_joblib_any([
358
- "xgb_num_downloads_effectsound_features.pkl",
359
- "xgb_num_downloads_effectSound_features.pkl",
360
- "xgb_num_downloads_effect_sound_features.pkl",
361
- "*num*downloads*effect*features*.pkl",
362
- "*num*downloads*effect*features*.joblib",
363
- ])
364
- M["eff_avg_model"], M["eff_avg_model_path"] = load_joblib_any([
365
- "xgb_avg_rating_effectsound_model.pkl",
366
- "xgb_avg_rating_effectSound_model.pkl",
367
- "xgb_avg_rating_effect_sound_model.pkl",
368
- "*avg*rating*effect*model*.pkl",
369
- "*avg*rating*effect*model*.joblib",
370
- ])
371
- M["eff_avg_feats"], M["eff_avg_feats_path"] = load_joblib_any([
372
- # <-- c’est exactement celui qui manque chez toi, on met plein de variantes
373
- "xgb_avg_rating_effectsound_features.pkl",
374
- "xgb_avg_rating_effectSound_features.pkl",
375
- "xgb_avg_rating_effect_sound_features.pkl",
376
- "*avg*rating*effect*features*.pkl",
377
- "*avg*rating*effect*features*.joblib",
378
- ])
379
- M["eff_avg_le"], M["eff_avg_le_path"] = load_joblib_any([
380
- "xgb_avg_rating_effectsound_label_encoder.pkl",
381
- "xgb_avg_rating_effectSound_label_encoder.pkl",
382
- "xgb_avg_rating_effect_sound_label_encoder.pkl",
383
- "*avg*rating*effect*label*encoder*.pkl",
384
- "*avg*rating*effect*label*encoder*.joblib",
385
- ])
386
-
387
- required = [
388
- ("music_num_model", "music_num_feats", "music_avg_model", "music_avg_feats", "music_avg_le"),
389
- ("eff_num_model", "eff_num_feats", "eff_avg_model", "eff_avg_feats", "eff_avg_le"),
390
- ]
391
- for group in required:
392
- for k in group:
393
- if M.get(k) is None:
394
- problems.append(k)
395
-
396
- return M, problems
397
-
398
- MODELS_B, PROBLEMS_B = load_feature_models_B()
399
  NUM_DOWNLOADS_MAP_B = {0: "Faible", 1: "Moyen", 2: "Élevé"}
400
 
401
  def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
@@ -407,7 +361,7 @@ def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
407
  row.append(safe_float(val))
408
 
409
  X = pd.DataFrame([row], columns=feat_list)
410
- dmatrix = xgb.DMatrix(X.values, feature_names=list(feat_list))
411
 
412
  booster = model.get_booster() if hasattr(model, "get_booster") else model
413
  pred_int = int(booster.predict(dmatrix)[0])
@@ -417,15 +371,6 @@ def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
417
  return pred_int
418
 
419
  def predict_freesound_acoustic_features(url: str):
420
- if PROBLEMS_B:
421
- missing = ", ".join(f"<code>{k}</code>" for k in PROBLEMS_B)
422
- files = "<br>".join(list_local_files()[:200])
423
- return html_error(
424
- "Modèles Features API incomplets",
425
- f"Il manque des fichiers nécessaires au pipeline B :<br>{missing}<br><br>"
426
- f"<b>Fichiers détectés dans ton Space (aperçu)</b>:<br><pre>{files}</pre>"
427
- )
428
-
429
  if not url or not url.strip():
430
  return html_error("URL vide", "Colle une URL du type <code>https://freesound.org/s/123456/</code>")
431
 
@@ -439,12 +384,19 @@ def predict_freesound_acoustic_features(url: str):
439
  except Exception as e:
440
  return html_error("Token FreeSound", str(e))
441
 
442
- # champs à récupérer
443
- all_features = list(set(
444
- MODELS_B["music_num_feats"] + MODELS_B["music_avg_feats"] +
445
- MODELS_B["eff_num_feats"] + MODELS_B["eff_avg_feats"]
446
- ))
447
- fields = "duration," + ",".join(all_features)
 
 
 
 
 
 
 
448
 
449
  try:
450
  results = fs_client.search(query="", filter=f"id:{sound_id}", fields=fields)
@@ -457,107 +409,430 @@ def predict_freesound_acoustic_features(url: str):
457
  sound = results.results[0]
458
  duration = safe_float(sound.get("duration", 0))
459
 
460
- if MIN_EFFECT <= duration <= MAX_EFFECT:
461
- badge = "🔊 FreeSound (Features acoustiques API) EffectSound"
462
- dl_class = int(predict_with_model_fs(MODELS_B["eff_num_model"], sound, MODELS_B["eff_num_feats"]))
463
- avg_text = str(predict_with_model_fs(MODELS_B["eff_avg_model"], sound, MODELS_B["eff_avg_feats"], MODELS_B["eff_avg_le"]))
 
 
 
464
  dl_text = NUM_DOWNLOADS_MAP_B.get(dl_class, str(dl_class))
 
465
  avg_class = avg_label_to_class(avg_text)
466
 
467
  extra = f"""
468
- <div class="hint">ID: <b>{sound_id}</b></div>
469
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
470
  {interpret_results(avg_class, dl_class)}
471
  </div>
472
  """
473
  return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
474
 
475
- if MIN_MUSIC <= duration <= MAX_MUSIC:
476
- badge = "🎵 FreeSound (Features acoustiques API) — Music"
477
- dl_class = int(predict_with_model_fs(MODELS_B["music_num_model"], sound, MODELS_B["music_num_feats"]))
478
- avg_text = str(predict_with_model_fs(MODELS_B["music_avg_model"], sound, MODELS_B["music_avg_feats"], MODELS_B["music_avg_le"]))
479
- dl_text = NUM_DOWNLOADS_MAP_B.get(dl_class, str(dl_class))
480
- avg_class = avg_label_to_class(avg_text)
481
 
482
- extra = f"""
483
- <div class="hint">ID: <b>{sound_id}</b></div>
484
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
485
  {interpret_results(avg_class, dl_class)}
486
  </div>
487
  """
488
- return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
489
-
490
- return html_error("Durée non supportée", f"Durée : <b>{duration:.2f}s</b> — attendu 0.5–3s ou 10–60s")
491
 
492
 
493
  # ============================================================
494
- # PARTIE C — Metadata (désactivée si pas de dossiers/fichiers)
495
  # ============================================================
496
- def predict_freesound_metadata_stub(url: str):
497
- return html_error(
498
- "Pipeline Metadata non disponible",
499
- "Tu as dit ne pas avoir les dossiers <code>music/</code> et <code>effectSound/</code> "
500
- "et/ou les joblib de preprocessing. Donc je n’active pas ce pipeline pour éviter de crasher."
501
- "<br><br>Si tu veux l’activer : ajoute les joblib de preprocessing + les modèles metadata, "
502
- "ou dis-moi comment tu les as nommés/organisés."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
 
506
  # ============================================================
507
- # Page “diagnostic”
508
  # ============================================================
509
- def status_page():
510
- files = list_local_files()
511
- files_txt = "\n".join(files) if files else "(aucun fichier .pkl/.joblib détecté)"
512
- a_ok = (MODEL_EFFECT_A is not None and MODEL_MUSIC_A is not None)
513
- b_ok = (len(PROBLEMS_B) == 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
- return f"""
516
- <div class="card">
517
- <div class="card-title">📦 Diagnostic du Space</div>
518
- <div class="grid">
519
- <div class="box">
520
- <div class="box-title">OpenSMILE (A)</div>
521
- <div class="box-value">{'✅ OK' if a_ok else '❌ modèles manquants'}</div>
522
- <div class="hint">Effect: <code>{PATH_EFFECT_A or 'non chargé'}</code><br>Music: <code>{PATH_MUSIC_A or 'non chargé'}</code></div>
523
- </div>
524
- <div class="box">
525
- <div class="box-title">Features API (B)</div>
526
- <div class="box-value">{' OK' if b_ok else '❌ incomplet'}</div>
527
- <div class="hint">Manquants: <code>{', '.join(PROBLEMS_B) if PROBLEMS_B else 'aucun'}</code></div>
528
- </div>
529
- <div class="box">
530
- <div class="box-title">Metadata (C)</div>
531
- <div class="box-value">⚠️ désactivé si dossiers/joblib absents</div>
532
- <div class="hint">Activer seulement si preprocessing joblib présents.</div>
533
- </div>
534
- </div>
535
- <div class="hint" style="margin-top:12px"><b>Fichiers détectés</b> :</div>
536
- <pre>{files_txt}</pre>
537
- </div>
538
- """.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
 
540
 
541
  # ============================================================
542
- # GRADIO UI
543
  # ============================================================
544
- with gr.Blocks(title="Popularité FreeSound — Pipelines séparés", css=CSS, theme=gr.themes.Soft()) as demo:
 
 
545
  gr.HTML(f"""
546
- <div id="header-title">Popularité FreeSound — Pipelines séparés</div>
547
  <p id="header-sub">
548
- <b>A)</b> Upload → OpenSMILE<br>
549
- <b>B)</b> URL → Features acoustiques FreeSound (API fields)<br>
550
- <b>C)</b> URL → Metadata FreeSound (désactivé si fichiers absents)<br><br>
551
  <b>Durées acceptées :</b> 🔊 {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 {MIN_MUSIC}–{MAX_MUSIC}s
552
  </p>
553
  """)
554
 
555
- with gr.Tabs():
556
- with gr.Tab("📦 Diagnostic"):
557
- diag = gr.HTML(value=status_page())
558
- btn_refresh = gr.Button("Rafraîchir diagnostic")
559
- btn_refresh.click(lambda: status_page(), outputs=diag)
560
 
 
561
  with gr.Tab("A) Upload → OpenSMILE"):
562
  with gr.Row():
563
  with gr.Column():
@@ -580,9 +855,10 @@ with gr.Blocks(title="Popularité FreeSound — Pipelines séparés", css=CSS, t
580
  with gr.Row():
581
  with gr.Column():
582
  url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
 
583
  btn = gr.Button("🚀 Prédire (Metadata)", variant="primary")
584
  with gr.Column():
585
  out = gr.HTML()
586
- btn.click(predict_freesound_metadata_stub, inputs=url_in, outputs=out)
587
 
588
  demo.launch()
 
1
  import os
 
2
  import tempfile
3
  import numpy as np
4
  import pandas as pd
 
8
  import soundfile as sf
9
  from pydub import AudioSegment
10
  import opensmile
11
+
12
  import freesound
13
  import xgboost as xgb
14
+ from sklearn.feature_extraction.text import HashingVectorizer
15
 
16
 
17
+ # ============================================================
18
  # CONFIG
19
+ # ============================================================
20
  MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
21
  MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
22
  SR_TARGET = 16000
23
 
24
+ # HF Space Secret: FREESOUND_TOKEN
25
  FREESOUND_TOKEN = os.getenv("FREESOUND_TOKEN", "").strip()
26
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
27
 
 
 
 
 
28
  def p(*parts):
29
  return os.path.join(BASE_DIR, *parts)
30
 
31
+ def exists(relpath: str) -> bool:
32
+ return os.path.exists(p(relpath))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ def load_local(relpath: str):
35
+ full = p(relpath)
36
+ if not os.path.exists(full):
37
+ raise FileNotFoundError(f"Fichier introuvable: {relpath}")
38
+ return joblib.load(full)
39
 
40
+ def safe_float(v):
41
+ try:
42
+ return float(v)
43
+ except Exception:
44
+ return 0.0
45
+
46
+ def parse_sound_id(url: str) -> int:
47
+ return int(url.rstrip("/").split("/")[-1])
48
+
49
+
50
+ # ============================================================
51
  # UI helpers
52
+ # ============================================================
53
  CSS = """
54
  .card { border: 1px solid #e5e7eb; background: #ffffff; padding: 16px; border-radius: 16px; }
55
  .card-error{ border-color: #fca5a5; background: #fff1f2; }
56
+ .card-warn{ border-color: #fcd34d; background: #fffbeb; }
57
  .card-title{ font-weight: 950; margin-bottom: 8px; }
58
  .badges{ display:flex; gap:10px; flex-wrap:wrap; margin-bottom:12px; }
59
  .badge{ padding:6px 10px; border-radius:999px; font-weight:900; font-size: 13px; border: 1px solid #e5e7eb; }
 
64
  .box-title{ font-weight:900; margin-bottom:4px; }
65
  .box-value{ font-size:18px; font-weight:800; }
66
  .hint{ margin-top:10px; color:#6b7280; font-size:12px; }
 
67
  #header-title { font-size: 28px; font-weight: 950; margin-bottom: 6px; }
68
  #header-sub { color:#6b7280; margin-top:0px; line-height:1.45; }
69
+ pre{ white-space:pre-wrap; }
70
  """
71
 
72
+ def html_box(title, body, kind=""):
73
+ cls = "card"
74
+ if kind == "error":
75
+ cls += " card-error"
76
+ elif kind == "warn":
77
+ cls += " card-warn"
78
  return f"""
79
+ <div class="{cls}">
80
+ <div class="card-title">{title}</div>
81
+ <div>{body}</div>
82
  </div>
83
  """.strip()
84
 
85
+ def html_error(title, body_html):
86
+ return html_box(f"❌ {title}", body_html, kind="error")
87
+
88
+ def html_warn(title, body_html):
89
+ return html_box(f"⚠️ {title}", body_html, kind="warn")
90
+
91
  def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""):
92
  return f"""
93
  <div class="card">
 
112
 
113
  def interpret_results(avg_class: int, dl_class: int) -> str:
114
  if avg_class == 0:
115
+ return "ℹ️ <b>Interprétation</b> :<br>Aucune évaluation possible (rating manquant / indisponible)."
116
 
117
  if avg_class == 3 and dl_class == 2:
118
  potentiel, detail = "très fort", "contenu de haute qualité et très populaire."
 
149
  return 1
150
  return 0
151
 
 
 
 
 
 
 
 
 
 
152
 
153
+ # ============================================================
154
  # FreeSound client
155
+ # ============================================================
156
  def get_fs_client():
157
  if not FREESOUND_TOKEN:
158
+ raise RuntimeError("Token FreeSound manquant. Ajoute le secret FREESOUND_TOKEN dans le Space.")
159
  c = freesound.FreesoundClient()
160
  c.set_token(FREESOUND_TOKEN, "token")
161
  return c
162
 
163
 
164
  # ============================================================
165
+ # DIAGNOSTIC FILE LISTS
166
+ # ============================================================
167
+ FILES_A = [
168
+ "xgb_model_EffectSound.pkl",
169
+ "xgb_model_Music.pkl",
170
+ ]
171
+
172
+ FILES_B = [
173
+ "xgb_num_downloads_effectsound_model.pkl",
174
+ "xgb_num_downloads_effectsound_features.pkl",
175
+ "xgb_avg_rating_effectsound_model.pkl",
176
+ "xgb_avg_rating_effectsound_features.pkl",
177
+ "xgb_avg_rating_effectsound_label_encoder.pkl",
178
+ "xgb_num_downloads_music_model.pkl",
179
+ "xgb_num_downloads_music_features.pkl",
180
+ "xgb_avg_rating_music_model.pkl",
181
+ "xgb_avg_rating_music_features.pkl",
182
+ "xgb_avg_rating_music_label_encoder.pkl",
183
+ ]
184
+
185
+ FILES_C_ROOT = [
186
+ "effectSound_model_num_downloads.joblib",
187
+ "effectSound_xgb_avg_rating.joblib",
188
+ "effectSound_xgb_avg_rating_label_encoder.joblib",
189
+ "effect_model_features_list.joblib",
190
+ "music_model_num_downloads.joblib",
191
+ "music_xgb_avg_rating.joblib",
192
+ "music_xgb_avg_rating_label_encoder.joblib",
193
+ # feature list music: tu as les deux, on accepte l’un ou l’autre
194
+ # "music_model_features_list.joblib" OU "model_features_list.joblib"
195
+ ]
196
+
197
+ FILES_C_EFFECT_DIR = [
198
+ "effectSound/scaler_effectSamplerate.joblib",
199
+ "effectSound/scaler_effectSound_age_days_log.joblib",
200
+ "effectSound/username_freq_dict_effectSound.joblib",
201
+ "effectSound/est_num_downloads_effectSound.joblib",
202
+ "effectSound/avg_rating_transformer_effectSound.joblib",
203
+ "effectSound/effectSound_subcategory_cols.joblib",
204
+ "effectSound/effectSound_onehot_cols.joblib",
205
+ "effectSound/effect_onehot_tags.joblib",
206
+ ]
207
+
208
+ FILES_C_MUSIC_DIR = [
209
+ "music/scaler_music_samplerate.joblib",
210
+ "music/scaler_music_age_days_log.joblib",
211
+ "music/username_freq_dict_music.joblib",
212
+ "music/est_num_downloads_music.joblib",
213
+ "music/avg_rating_transformer_music.joblib",
214
+ "music/music_subcategory_cols.joblib",
215
+ "music/music_onehot_cols.joblib",
216
+ "music/music_onehot_tags.joblib",
217
+ ]
218
+
219
+
220
+ # ============================================================
221
+ # PARTIE A — OpenSMILE upload
222
  # ============================================================
223
+ A_MODELS = {}
224
+
225
+ def load_A_models():
226
+ A_MODELS["effect"] = load_local("xgb_model_EffectSound.pkl")
227
+ A_MODELS["music"] = load_local("xgb_model_Music.pkl")
228
+
229
  SMILE = opensmile.Smile(
230
  feature_set=opensmile.FeatureSet.eGeMAPSv02,
231
  feature_level=opensmile.FeatureLevel.Functionals,
 
234
  RATING_DISPLAY_AUDIO = {0: "❌ Informations manquantes", 1: "⭐ Faible", 2: "⭐⭐ Moyen", 3: "⭐⭐⭐ Élevé"}
235
  DOWNLOADS_DISPLAY_AUDIO = {0: "⭐ Faible", 1: "⭐⭐ Moyen", 2: "⭐⭐⭐ Élevé"}
236
 
 
 
 
 
 
 
 
 
 
 
237
  def get_duration_seconds(filepath):
238
  ext = os.path.splitext(filepath)[1].lower()
239
  if ext == ".mp3":
 
251
  return filepath
252
  except Exception:
253
  pass
254
+
255
  audio = AudioSegment.from_file(filepath)
256
  audio = audio.set_channels(1).set_frame_rate(SR_TARGET)
257
  tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
 
266
  return feats
267
 
268
  def predict_upload_with_dmatrix(model, X_df: pd.DataFrame):
 
269
  booster = model.get_booster() if hasattr(model, "get_booster") else model
270
  dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
271
+ p_ = booster.predict(dm)
272
+ return np.asarray(p_).reshape(1, -1)
 
 
 
 
 
 
273
 
274
  def predict_opensmile_upload(audio_file):
 
 
 
 
 
 
 
275
  if audio_file is None:
276
  return html_error("Aucun fichier", "Veuillez importer un fichier audio (wav, mp3, flac…).")
277
 
 
281
  return html_error("Audio illisible", f"Détail : <code>{e}</code>")
282
 
283
  if duration < MIN_EFFECT:
284
+ return html_error("Audio trop court", f"Durée : <b>{duration:.2f}s</b><br>Accepté: 0.5–3s ou 10–60s")
285
  if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
286
+ return html_error("Audio hors plage", f"Durée : <b>{duration:.2f}s</b><br>Accepté: 0.5–3s ou 10–60s")
287
+
288
+ try:
289
+ if not A_MODELS:
290
+ load_A_models()
291
+ except Exception as e:
292
+ return html_error("Modèles OpenSMILE manquants", f"Détail : <code>{e}</code>")
293
 
294
  if duration <= MAX_EFFECT:
295
+ badge = "🔊 OpenSMILE (upload) — EffectSound"
296
+ model = A_MODELS["effect"]
297
  else:
298
+ badge = "🎵 OpenSMILE (upload) — Music"
299
+ model = A_MODELS["music"]
300
 
301
  try:
302
  X = extract_opensmile_features(audio_file)
303
  except Exception as e:
304
  return html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>")
305
 
 
306
  try:
307
+ expected = model.feature_names_in_ if hasattr(model, "feature_names_in_") else list(X.columns)
308
+ X = X.reindex(columns=list(expected), fill_value=0)
309
+ except Exception as e:
310
+ return html_error("Alignement features échoué", f"Détail : <code>{e}</code>")
 
 
311
 
312
  try:
313
  y = predict_upload_with_dmatrix(model, X)
314
  except Exception as e:
315
  return html_error("Prédiction échouée", f"Détail : <code>{e}</code>")
316
 
317
+ avg_class = int(y[0, 0])
318
+ dl_class = int(y[0, 1])
 
 
 
 
319
 
320
  rating_text = RATING_DISPLAY_AUDIO.get(avg_class, "Inconnu")
321
  downloads_text = DOWNLOADS_DISPLAY_AUDIO.get(dl_class, "Inconnu")
322
+
323
  extra = f"""
 
324
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
325
  {interpret_results(avg_class, dl_class)}
326
  </div>
 
329
 
330
 
331
  # ============================================================
332
+ # PARTIE B — FreeSound API acoustic features
 
333
  # ============================================================
334
+ B_MODELS = {}
335
+
336
+ def load_B_models():
337
+ # downloads
338
+ B_MODELS["eff_num_model"] = load_local("xgb_num_downloads_effectsound_model.pkl")
339
+ B_MODELS["eff_num_feats"] = load_local("xgb_num_downloads_effectsound_features.pkl")
340
+
341
+ B_MODELS["mus_num_model"] = load_local("xgb_num_downloads_music_model.pkl")
342
+ B_MODELS["mus_num_feats"] = load_local("xgb_num_downloads_music_features.pkl")
343
+
344
+ # avg rating
345
+ B_MODELS["eff_avg_model"] = load_local("xgb_avg_rating_effectsound_model.pkl")
346
+ B_MODELS["eff_avg_feats"] = load_local("xgb_avg_rating_effectsound_features.pkl")
347
+ B_MODELS["eff_avg_le"] = load_local("xgb_avg_rating_effectsound_label_encoder.pkl")
348
+
349
+ B_MODELS["mus_avg_model"] = load_local("xgb_avg_rating_music_model.pkl")
350
+ B_MODELS["mus_avg_feats"] = load_local("xgb_avg_rating_music_features.pkl")
351
+ B_MODELS["mus_avg_le"] = load_local("xgb_avg_rating_music_label_encoder.pkl")
352
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  NUM_DOWNLOADS_MAP_B = {0: "Faible", 1: "Moyen", 2: "Élevé"}
354
 
355
  def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
 
361
  row.append(safe_float(val))
362
 
363
  X = pd.DataFrame([row], columns=feat_list)
364
+ dmatrix = xgb.DMatrix(X.values, feature_names=feat_list)
365
 
366
  booster = model.get_booster() if hasattr(model, "get_booster") else model
367
  pred_int = int(booster.predict(dmatrix)[0])
 
371
  return pred_int
372
 
373
  def predict_freesound_acoustic_features(url: str):
 
 
 
 
 
 
 
 
 
374
  if not url or not url.strip():
375
  return html_error("URL vide", "Colle une URL du type <code>https://freesound.org/s/123456/</code>")
376
 
 
384
  except Exception as e:
385
  return html_error("Token FreeSound", str(e))
386
 
387
+ try:
388
+ if not B_MODELS:
389
+ load_B_models()
390
+ except Exception as e:
391
+ return html_error("Modèles Features API manquants", f"Détail : <code>{e}</code>")
392
+
393
+ # champs API = union de toutes les features nécessaires (pour éviter de faire 2 appels)
394
+ all_feats = set()
395
+ all_feats.update(B_MODELS["eff_num_feats"])
396
+ all_feats.update(B_MODELS["mus_num_feats"])
397
+ all_feats.update(B_MODELS["eff_avg_feats"])
398
+ all_feats.update(B_MODELS["mus_avg_feats"])
399
+ fields = "duration," + ",".join(sorted(all_feats))
400
 
401
  try:
402
  results = fs_client.search(query="", filter=f"id:{sound_id}", fields=fields)
 
409
  sound = results.results[0]
410
  duration = safe_float(sound.get("duration", 0))
411
 
412
+ if duration < MIN_EFFECT or ((MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC):
413
+ return html_error("Durée non supportée", f"Durée : <b>{duration:.2f}s</b><br>Accepté: 0.5–3s ou 10–60s")
414
+
415
+ # EffectSound
416
+ if duration <= MAX_EFFECT:
417
+ badge = "🔊 FreeSound (API features acoustiques) — EffectSound"
418
+ dl_class = int(predict_with_model_fs(B_MODELS["eff_num_model"], sound, B_MODELS["eff_num_feats"]))
419
  dl_text = NUM_DOWNLOADS_MAP_B.get(dl_class, str(dl_class))
420
+ avg_text = str(predict_with_model_fs(B_MODELS["eff_avg_model"], sound, B_MODELS["eff_avg_feats"], B_MODELS["eff_avg_le"]))
421
  avg_class = avg_label_to_class(avg_text)
422
 
423
  extra = f"""
424
+ <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
425
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
426
  {interpret_results(avg_class, dl_class)}
427
  </div>
428
  """
429
  return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
430
 
431
+ # Music
432
+ badge = "🎵 FreeSound (API features acoustiques) — Music"
433
+ dl_class = int(predict_with_model_fs(B_MODELS["mus_num_model"], sound, B_MODELS["mus_num_feats"]))
434
+ dl_text = NUM_DOWNLOADS_MAP_B.get(dl_class, str(dl_class))
435
+ avg_text = str(predict_with_model_fs(B_MODELS["mus_avg_model"], sound, B_MODELS["mus_avg_feats"], B_MODELS["mus_avg_le"]))
436
+ avg_class = avg_label_to_class(avg_text)
437
 
438
+ extra = f"""
439
+ <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
440
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
441
  {interpret_results(avg_class, dl_class)}
442
  </div>
443
  """
444
+ return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
 
 
445
 
446
 
447
  # ============================================================
448
+ # PARTIE C — Metadata preprocessing + joblib
449
  # ============================================================
450
+ C_READY = False
451
+ C = {}
452
+ C_LOAD_ERRORS = []
453
+
454
+ def try_load_C():
455
+ global C_READY, C, C_LOAD_ERRORS
456
+ C_READY = False
457
+ C = {}
458
+ C_LOAD_ERRORS = []
459
+
460
+ def load_and_store(key, relpath):
461
+ try:
462
+ C[key] = load_local(relpath)
463
+ return True
464
+ except Exception as e:
465
+ C_LOAD_ERRORS.append(f"{relpath} -> {type(e).__name__}: {e}")
466
+ return False
467
+
468
+ ok = True
469
+
470
+ # preprocess music
471
+ ok &= load_and_store("scaler_samplerate_music", "music/scaler_music_samplerate.joblib")
472
+ ok &= load_and_store("scaler_age_days_music", "music/scaler_music_age_days_log.joblib")
473
+ ok &= load_and_store("username_freq_music", "music/username_freq_dict_music.joblib")
474
+ ok &= load_and_store("est_num_downloads_music", "music/est_num_downloads_music.joblib")
475
+ ok &= load_and_store("avg_rating_tr_music", "music/avg_rating_transformer_music.joblib")
476
+ ok &= load_and_store("music_subcat_cols", "music/music_subcategory_cols.joblib")
477
+ ok &= load_and_store("music_onehot_cols", "music/music_onehot_cols.joblib")
478
+ ok &= load_and_store("music_onehot_tags", "music/music_onehot_tags.joblib")
479
+
480
+ # preprocess effect
481
+ ok &= load_and_store("scaler_samplerate_effect", "effectSound/scaler_effectSamplerate.joblib")
482
+ ok &= load_and_store("scaler_age_days_effect", "effectSound/scaler_effectSound_age_days_log.joblib")
483
+ ok &= load_and_store("username_freq_effect", "effectSound/username_freq_dict_effectSound.joblib")
484
+ ok &= load_and_store("est_num_downloads_effect", "effectSound/est_num_downloads_effectSound.joblib")
485
+ ok &= load_and_store("avg_rating_tr_effect", "effectSound/avg_rating_transformer_effectSound.joblib")
486
+ ok &= load_and_store("effect_subcat_cols", "effectSound/effectSound_subcategory_cols.joblib")
487
+ ok &= load_and_store("effect_onehot_cols", "effectSound/effectSound_onehot_cols.joblib")
488
+ ok &= load_and_store("effect_onehot_tags", "effectSound/effect_onehot_tags.joblib")
489
+
490
+ # models root
491
+ ok &= load_and_store("music_nd_model", "music_model_num_downloads.joblib")
492
+ ok &= load_and_store("music_ar_model", "music_xgb_avg_rating.joblib")
493
+ ok &= load_and_store("music_ar_le", "music_xgb_avg_rating_label_encoder.joblib")
494
+
495
+ ok &= load_and_store("effect_nd_model", "effectSound_model_num_downloads.joblib")
496
+ ok &= load_and_store("effect_ar_model", "effectSound_xgb_avg_rating.joblib")
497
+ ok &= load_and_store("effect_ar_le", "effectSound_xgb_avg_rating_label_encoder.joblib")
498
+
499
+ # feature lists
500
+ if exists("music_model_features_list.joblib"):
501
+ ok &= load_and_store("music_features", "music_model_features_list.joblib")
502
+ elif exists("model_features_list.joblib"):
503
+ ok &= load_and_store("music_features", "model_features_list.joblib")
504
+ else:
505
+ ok = False
506
+ C_LOAD_ERRORS.append("music features list manquante: music_model_features_list.joblib OU model_features_list.joblib")
507
+
508
+ ok &= load_and_store("effect_features", "effect_model_features_list.joblib")
509
+
510
+ if ok:
511
+ C["music_features"] = list(dict.fromkeys(C["music_features"]))
512
+ C["effect_features"] = list(dict.fromkeys(C["effect_features"]))
513
+ C_READY = True
514
+ else:
515
+ C_READY = False
516
+
517
+ # run once at import
518
+ try_load_C()
519
+
520
+ def preprocess_name(df, vec_dim=8):
521
+ df = df.copy()
522
+ df["name_len"] = df["name_clean"].str.len()
523
+ vectorizer = HashingVectorizer(n_features=vec_dim, alternate_sign=False, norm=None)
524
+ name_vec_sparse = vectorizer.transform(df["name_clean"])
525
+ name_vec_df = pd.DataFrame(
526
+ name_vec_sparse.toarray(),
527
+ columns=[f"name_vec_{i}" for i in range(vec_dim)],
528
+ index=df.index
529
  )
530
+ return pd.concat([df, name_vec_df], axis=1)
531
+
532
+ def fetch_sound_metadata(fs_client, sound_url):
533
+ sound_id = parse_sound_id(sound_url)
534
+ sound = fs_client.get_sound(sound_id)
535
+ data = {
536
+ "id": sound_id,
537
+ "name": sound.name,
538
+ "num_ratings": getattr(sound, "num_ratings", 0),
539
+ "tags": ",".join(sound.tags) if getattr(sound, "tags", None) else "",
540
+ "username": getattr(sound, "username", ""),
541
+ "description": getattr(sound, "description", "") or "",
542
+ "created": getattr(sound, "created", ""),
543
+ "license": getattr(sound, "license", ""),
544
+ "num_downloads": getattr(sound, "num_downloads", 0),
545
+ "channels": getattr(sound, "channels", 0),
546
+ "filesize": getattr(sound, "filesize", 0),
547
+ "num_comments": getattr(sound, "num_comments", 0),
548
+ "category_is_user_provided": getattr(sound, "category_is_user_provided", 0),
549
+ "duration": getattr(sound, "duration", 0),
550
+ "avg_rating": getattr(sound, "avg_rating", 0),
551
+ "category": getattr(sound, "category", "Unknown"),
552
+ "subcategory": getattr(sound, "subcategory", "Other"),
553
+ "type": getattr(sound, "type", ""),
554
+ "samplerate": getattr(sound, "samplerate", 0),
555
+ }
556
+ return pd.DataFrame([data])
557
+
558
+ def preprocess_sound_metadata(df):
559
+ df = df.copy()
560
+ dur = float(df["duration"].iloc[0])
561
+
562
+ if MIN_EFFECT <= dur <= MAX_EFFECT:
563
+ dataset_type = "effectSound"
564
+ scaler_samplerate = C["scaler_samplerate_effect"]
565
+ scaler_age = C["scaler_age_days_effect"]
566
+ username_freq = C["username_freq_effect"]
567
+ est_num_downloads = C["est_num_downloads_effect"]
568
+ avg_rating_tr = C["avg_rating_tr_effect"]
569
+ subcat_cols = C["effect_subcat_cols"]
570
+ onehot_cols = C["effect_onehot_cols"]
571
+ onehot_tags = C["effect_onehot_tags"]
572
+ elif MIN_MUSIC <= dur <= MAX_MUSIC:
573
+ dataset_type = "music"
574
+ scaler_samplerate = C["scaler_samplerate_music"]
575
+ scaler_age = C["scaler_age_days_music"]
576
+ username_freq = C["username_freq_music"]
577
+ est_num_downloads = C["est_num_downloads_music"]
578
+ avg_rating_tr = C["avg_rating_tr_music"]
579
+ subcat_cols = C["music_subcat_cols"]
580
+ onehot_cols = C["music_onehot_cols"]
581
+ onehot_tags = C["music_onehot_tags"]
582
+ else:
583
+ return None, None, f"Durée hors plage ({dur:.2f}s)."
584
+
585
+ df["category_is_user_provided"] = df["category_is_user_provided"].astype(int)
586
+ df["username_freq"] = df["username"].map(username_freq).fillna(0)
587
+
588
+ for col in ["num_ratings", "num_comments", "filesize", "duration"]:
589
+ df[col] = np.log1p(df[col])
590
+
591
+ df["samplerate"] = scaler_samplerate.transform(df[["samplerate"]])
592
+
593
+ df["created"] = pd.to_datetime(df["created"], errors="coerce").dt.tz_localize(None)
594
+ df["age_days"] = (pd.Timestamp.now() - df["created"]).dt.days
595
+ df["age_days_log"] = np.log1p(df["age_days"])
596
+ df["age_days_log_scaled"] = scaler_age.transform(df[["age_days_log"]])
597
+ df = df.drop(columns=["created", "age_days", "age_days_log"], errors="ignore")
598
+
599
+ df["num_downloads_class"] = est_num_downloads.transform(df[["num_downloads"]])
600
+ df["avg_rating"] = avg_rating_tr.transform(df["avg_rating"].to_numpy())
601
+
602
+ for col in subcat_cols:
603
+ df[col] = 0
604
+ subcat_val = df["subcategory"].iloc[0]
605
+ for col in subcat_cols:
606
+ cat_name = col.replace("subcategory_", "")
607
+ if subcat_val == cat_name:
608
+ df[col] = 1
609
+ df.drop(columns=["subcategory"], inplace=True, errors="ignore")
610
+
611
+ for col in onehot_cols:
612
+ if col not in df.columns:
613
+ df[col] = 0
614
+
615
+ license_val = df.loc[0, "license"]
616
+ category_val = df.loc[0, "category"]
617
+ type_val = df.loc[0, "type"]
618
+
619
+ for col_name in [f"license_{license_val}", f"category_{category_val}", f"type_{type_val}"]:
620
+ if col_name in df.columns:
621
+ df[col_name] = 1
622
+
623
+ for col in onehot_tags:
624
+ if col not in df.columns:
625
+ df[col] = 0
626
+
627
+ tags_list = df["tags"].iloc[0].lower().split(",") if df["tags"].iloc[0] else []
628
+ for col in onehot_tags:
629
+ tag_name = col.replace("tag_", "").lower()
630
+ if tag_name in tags_list:
631
+ df[col] = 1
632
+ df.drop(columns=["tags"], inplace=True, errors="ignore")
633
+
634
+ df["name_clean"] = df["name"].astype(str).str.lower().str.rsplit(".", n=1).str[0]
635
+ df = preprocess_name(df, vec_dim=8)
636
+ df.drop(columns=["name", "name_clean"], inplace=True, errors="ignore")
637
+
638
+ # IMPORTANT: Pas de GloVe ici (ton modèle est piloté par model_features_list.joblib)
639
+ df.drop(columns=["description"], inplace=True, errors="ignore")
640
+
641
+ df.drop(columns=["license", "category", "type", "id", "num_downloads", "username"], inplace=True, errors="ignore")
642
+
643
+ return df, dataset_type, None
644
+
645
+ def predict_with_model_df(model, df_input):
646
+ booster_feats = model.get_booster().feature_names
647
+ X_aligned = df_input.reindex(columns=booster_feats, fill_value=0.0).astype(float)
648
+ dmatrix = xgb.DMatrix(X_aligned.values, feature_names=booster_feats)
649
+ pred = model.get_booster().predict(dmatrix)
650
+ pred_val = pred[0]
651
+ if hasattr(pred_val, "__len__") and np.size(pred_val) > 1:
652
+ return int(np.argmax(pred_val))
653
+ return int(round(float(pred_val)))
654
+
655
+ def predict_freesound_metadata(url: str, show_debug: bool):
656
+ if not C_READY:
657
+ body = "Le pipeline metadata n’a pas pu charger tous les joblib."
658
+ if C_LOAD_ERRORS:
659
+ body += "<br><br><details><summary><b>Voir erreurs</b></summary><pre>" + "\n".join(C_LOAD_ERRORS[:80]) + "</pre></details>"
660
+ return html_warn("Pipeline C désactivé", body)
661
+
662
+ if not url or not url.strip():
663
+ return html_error("URL vide", "Colle une URL du type <code>https://freesound.org/s/123456/</code>")
664
+
665
+ try:
666
+ sound_id = parse_sound_id(url)
667
+ except Exception:
668
+ return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
669
+
670
+ try:
671
+ fs_client = get_fs_client()
672
+ except Exception as e:
673
+ return html_error("Token FreeSound", str(e))
674
+
675
+ try:
676
+ df_raw = fetch_sound_metadata(fs_client, url)
677
+ except Exception as e:
678
+ return html_error("Erreur API FreeSound", f"Détail : <code>{e}</code>")
679
+
680
+ dur = float(df_raw["duration"].iloc[0])
681
+ if dur < MIN_EFFECT or ((MAX_EFFECT < dur < MIN_MUSIC) or dur > MAX_MUSIC):
682
+ return html_error("Durée non supportée", f"Durée : <b>{dur:.2f}s</b><br>Accepté: 0.5–3s ou 10–60s")
683
+
684
+ df_proc, dtype, err = preprocess_sound_metadata(df_raw)
685
+ if df_proc is None:
686
+ return html_error("Prétraitement metadata", err or "Erreur inconnue.")
687
+
688
+ if dtype == "effectSound":
689
+ badge = "🔊 FreeSound (metadata) — EffectSound"
690
+ nd_model = C["effect_nd_model"]
691
+ ar_model = C["effect_ar_model"]
692
+ ar_le = C["effect_ar_le"]
693
+ feats = C["effect_features"]
694
+ else:
695
+ badge = "🎵 FreeSound (metadata) — Music"
696
+ nd_model = C["music_nd_model"]
697
+ ar_model = C["music_ar_model"]
698
+ ar_le = C["music_ar_le"]
699
+ feats = C["music_features"]
700
+
701
+ df_for_model = df_proc.reindex(columns=feats, fill_value=0.0).astype(float)
702
+
703
+ dl_class = predict_with_model_df(nd_model, df_for_model)
704
+ dl_map = {0: "Low", 1: "Medium", 2: "High"}
705
+ dl_text = dl_map.get(dl_class, str(dl_class))
706
+
707
+ ar_class = predict_with_model_df(ar_model, df_for_model)
708
+ try:
709
+ avg_text = ar_le.inverse_transform([ar_class])[0]
710
+ except Exception:
711
+ avg_text = f"Classe {ar_class}"
712
+
713
+ avg_class_for_interp = avg_label_to_class(avg_text)
714
+ dl_class_for_interp = {"Low": 0, "Medium": 1, "High": 2}.get(dl_text, 1)
715
+
716
+ debug_html = ""
717
+ if show_debug:
718
+ raw_txt = "\n".join([f"{c}: {df_raw.loc[0,c]}" for c in df_raw.columns])
719
+ proc_cols = df_proc.columns.tolist()
720
+ proc_preview = proc_cols[:140]
721
+ proc_txt = "\n".join([f"{c}: {df_proc.loc[0,c]}" for c in proc_preview])
722
+ debug_html = f"""
723
+ <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
724
+ <details><summary><b>Debug</b> — métadonnées brutes</summary><pre>{raw_txt}</pre></details>
725
+ <details><summary><b>Debug</b> — features après preprocessing (aperçu)</summary><pre>{proc_txt}</pre></details>
726
+ </div>
727
+ """
728
+
729
+ extra = f"""
730
+ <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
731
+ <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
732
+ {interpret_results(avg_class_for_interp, dl_class_for_interp)}
733
+ </div>
734
+ {debug_html}
735
+ """
736
+ return html_result(badge, dur, str(avg_text), str(dl_text), extra_html=extra)
737
 
738
 
739
  # ============================================================
740
+ # DIAGNOSTIC HTML
741
  # ============================================================
742
+ def make_diagnostic_html():
743
+ # A
744
+ missing_a = [f for f in FILES_A if not exists(f)]
745
+ a_ok = (len(missing_a) == 0)
746
+
747
+ # B
748
+ missing_b = [f for f in FILES_B if not exists(f)]
749
+ b_ok = (len(missing_b) == 0)
750
+
751
+ # C presence (files) + runtime load status (C_READY)
752
+ missing_c = []
753
+ for f in FILES_C_ROOT + FILES_C_EFFECT_DIR + FILES_C_MUSIC_DIR:
754
+ if not exists(f):
755
+ missing_c.append(f)
756
+ # music features list special rule
757
+ if not (exists("music_model_features_list.joblib") or exists("model_features_list.joblib")):
758
+ missing_c.append("music_model_features_list.joblib OU model_features_list.joblib")
759
+ c_files_ok = (len(missing_c) == 0)
760
+
761
+ parts = []
762
+ parts.append("<b>📦 Diagnostic du Space</b><br><br>")
763
+
764
+ parts.append("<b>OpenSMILE (A)</b><br>")
765
+ if a_ok:
766
+ parts.append("✅ OK<br>")
767
+ parts.append("Effect: xgb_model_EffectSound.pkl<br>Music: xgb_model_Music.pkl<br><br>")
768
+ else:
769
+ parts.append("❌ incomplet<br>")
770
+ parts.append(f"Manquants: {', '.join(missing_a)}<br><br>")
771
 
772
+ parts.append("<b>Features API (B)</b><br>")
773
+ if b_ok:
774
+ parts.append(" OK<br><br>")
775
+ else:
776
+ parts.append("❌ incomplet<br>")
777
+ parts.append(f"Manquants: {', '.join(missing_b)}<br><br>")
778
+
779
+ parts.append("<b>Metadata (C)</b><br>")
780
+ if not c_files_ok:
781
+ parts.append("⚠️ désactivé si dossiers/joblib absents<br>")
782
+ parts.append("Activer seulement si preprocessing joblib présents.<br>")
783
+ parts.append(f"Manquants: {', '.join(missing_c)}<br><br>")
784
+ else:
785
+ # files are OK, but loading can still fail due to version mismatch
786
+ if C_READY:
787
+ parts.append(" OK (actif)<br><br>")
788
+ else:
789
+ parts.append("⚠️ fichiers présents mais chargement joblib a échoué (versions ?) <br>")
790
+ if C_LOAD_ERRORS:
791
+ parts.append("<details><summary><b>Voir erreurs de chargement</b></summary>")
792
+ parts.append("<pre>" + "\n".join(C_LOAD_ERRORS[:80]) + "</pre></details>")
793
+ parts.append("<br>")
794
+
795
+ # list detected files
796
+ detected = []
797
+ for root, _, files in os.walk(BASE_DIR):
798
+ for fn in files:
799
+ rel = os.path.relpath(os.path.join(root, fn), BASE_DIR)
800
+ detected.append(rel)
801
+ detected = sorted(detected)
802
+
803
+ parts.append("<details><summary><b>Fichiers détectés</b></summary>")
804
+ parts.append("<pre>" + "\n".join(detected) + "</pre></details>")
805
+
806
+ return html_box("Diagnostic", "".join(parts))
807
+
808
+
809
+ def refresh_diagnostic():
810
+ # reload C on refresh
811
+ try_load_C()
812
+ return make_diagnostic_html()
813
 
814
 
815
  # ============================================================
816
+ # GRADIO APP
817
  # ============================================================
818
+ diag_init = make_diagnostic_html()
819
+
820
+ with gr.Blocks(title="Popularité FreeSound — 3 pipelines", css=CSS, theme=gr.themes.Soft()) as demo:
821
  gr.HTML(f"""
822
+ <div id="header-title">Popularité FreeSound — 3 pipelines</div>
823
  <p id="header-sub">
824
+ <b>A)</b> Upload audio <b>OpenSMILE</b><br>
825
+ <b>B)</b> URL FreeSound <b>Features acoustiques via API fields</b><br>
826
+ <b>C)</b> URL FreeSound <b>Metadata + preprocessing (joblib)</b><br><br>
827
  <b>Durées acceptées :</b> 🔊 {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 {MIN_MUSIC}–{MAX_MUSIC}s
828
  </p>
829
  """)
830
 
831
+ diag_out = gr.HTML(value=diag_init)
832
+ btn_diag = gr.Button("🔄 Rafraîchir diagnostic")
833
+ btn_diag.click(refresh_diagnostic, outputs=diag_out)
 
 
834
 
835
+ with gr.Tabs():
836
  with gr.Tab("A) Upload → OpenSMILE"):
837
  with gr.Row():
838
  with gr.Column():
 
855
  with gr.Row():
856
  with gr.Column():
857
  url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
858
+ show_debug = gr.Checkbox(label="Afficher debug (brut + aperçu features)", value=False)
859
  btn = gr.Button("🚀 Prédire (Metadata)", variant="primary")
860
  with gr.Column():
861
  out = gr.HTML()
862
+ btn.click(predict_freesound_metadata, inputs=[url_in, show_debug], outputs=out)
863
 
864
  demo.launch()
requirements.txt CHANGED
@@ -1,22 +1,15 @@
1
- gradio
2
- pandas
3
- numpy
4
- scikit-learn
5
- joblib
6
- xgboost
7
- soundfile
8
- pydub
9
- opensmile
10
- requests
11
- pytz
12
- gradio
13
- pandas
14
- numpy
15
- joblib
16
- xgboost
17
- requests
18
- urllib3
19
- scikit-learn
20
- imblearn
21
- matplotlib
22
  git+https://github.com/MTG/freesound-python.git
 
1
+ gradio==4.44.1
2
+ pandas==2.2.2
3
+ numpy==1.26.4
4
+ scikit-learn==1.3.2
5
+ joblib==1.3.2
6
+ xgboost==2.0.3
7
+ soundfile==0.12.1
8
+ pydub==0.25.1
9
+ opensmile==2.5.0
10
+ requests==2.32.3
11
+ pytz==2024.1
12
+ urllib3==2.2.2
13
+ matplotlib==3.8.4
14
+ imbalanced-learn==0.11.0
 
 
 
 
 
 
 
15
  git+https://github.com/MTG/freesound-python.git