IKRAMELHADI commited on
Commit
d32e0d8
·
1 Parent(s): d874fd8

modif interpretation results

Browse files
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  import tempfile
3
  import numpy as np
@@ -11,82 +12,107 @@ 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; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  .badge-type{ background:#eef2ff; color:#3730a3;}
61
  .badge-time{ background:#ecfeff; color:#155e75;}
62
- .grid{ display:grid; grid-template-columns: 1fr; gap:10px; }
63
- .box{ border:1px solid #e5e7eb; border-radius:14px; padding:12px; background:#fafafa; }
 
 
 
 
 
 
 
 
 
 
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"""
@@ -95,6 +121,7 @@ def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""
95
  <span class="badge badge-type">{badge_text}</span>
96
  <span class="badge badge-time">⏱️ {duration:.2f} s</span>
97
  </div>
 
98
  <div class="grid">
99
  <div class="box">
100
  <div class="box-title">📈 Popularité de la note moyenne</div>
@@ -105,37 +132,70 @@ def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""
105
  <div class="box-value">{downloads_text}</div>
106
  </div>
107
  </div>
 
108
  {extra_html}
109
- <div class="hint">Résultats en <b>niveaux</b> (faible / moyen / élevé), pas en valeurs exactes.</div>
 
 
 
110
  </div>
111
  """.strip()
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."
 
119
  elif avg_class == 3 and dl_class == 1:
120
- potentiel, detail = "fort", "contenu bien apprécié, en croissance."
 
121
  elif avg_class == 3 and dl_class == 0:
122
- potentiel, detail = "prometteur", "bonne qualité mais faible visibilité."
 
123
  elif avg_class == 2 and dl_class == 2:
124
- potentiel, detail = "modéré à fort", "populaire mais qualité perçue moyenne."
 
125
  elif avg_class == 2 and dl_class == 1:
126
- potentiel, detail = "modéré", "profil standard, popularité stable."
 
127
  elif avg_class == 2 and dl_class == 0:
128
- potentiel, detail = "limité", "engagement faible, diffusion limitée."
 
129
  elif avg_class == 1 and dl_class == 2:
130
- potentiel, detail = "contradictoire", "très téléchargé mais peu apprécié."
 
131
  elif avg_class == 1 and dl_class == 1:
132
- potentiel, detail = "faible", "peu attractif."
 
133
  else:
134
- potentiel, detail = "très faible", "faible intérêt global."
 
 
 
 
 
 
135
 
136
- return f"<b>Interprétation</b> :<br>Potentiel estimé : <b>{potentiel}</b> — {detail}"
137
 
138
  def avg_label_to_class(avg_label: str) -> int:
 
 
 
 
 
139
  if avg_label is None:
140
  return 0
141
  s = str(avg_label).strip().lower()
@@ -150,89 +210,38 @@ def avg_label_to_class(avg_label: str) -> int:
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,
232
  )
233
 
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()
@@ -242,6 +251,7 @@ def get_duration_seconds(filepath):
242
  with sf.SoundFile(filepath) as f:
243
  return len(f) / f.samplerate
244
 
 
245
  def to_wav_16k_mono(filepath):
246
  ext = os.path.splitext(filepath)[1].lower()
247
  if ext == ".wav":
@@ -254,103 +264,136 @@ def to_wav_16k_mono(filepath):
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)
258
  tmp.close()
259
  audio.export(tmp.name, format="wav")
260
  return tmp.name
261
 
 
262
  def extract_opensmile_features(filepath):
263
  wav_path = to_wav_16k_mono(filepath)
264
  feats = SMILE.process_file(wav_path)
265
  feats = feats.select_dtypes(include=[np.number]).reset_index(drop=True)
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
 
 
278
  try:
279
  duration = get_duration_seconds(audio_file)
280
  except Exception as e:
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>
327
  """
328
  return html_result(badge, duration, rating_text, downloads_text, extra_html=extra)
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):
356
  row = []
@@ -363,40 +406,33 @@ def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
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])
368
 
369
  if label_encoder is not None:
370
  return label_encoder.inverse_transform([pred_int])[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
 
 
377
  try:
378
- sound_id = parse_sound_id(url)
379
  except Exception:
380
  return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
381
 
382
- try:
383
- fs_client = get_fs_client()
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,134 +445,165 @@ def predict_freesound_acoustic_features(url: str):
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 CMetadata 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", ""),
@@ -555,50 +622,62 @@ def fetch_sound_metadata(fs_client, sound_url):
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]
@@ -606,8 +685,9 @@ def preprocess_sound_metadata(df):
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
@@ -620,6 +700,16 @@ def preprocess_sound_metadata(df):
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
@@ -629,236 +719,180 @@ def preprocess_sound_metadata(df):
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():
 
839
  audio_in = gr.Audio(type="filepath", label="Fichier audio")
840
- btn = gr.Button("🚀 Prédire (OpenSMILE)", variant="primary")
841
- with gr.Column():
842
- out = gr.HTML()
843
- btn.click(predict_opensmile_upload, inputs=audio_in, outputs=out)
844
-
845
- with gr.Tab("B) URL → Features acoustiques (API)"):
 
 
846
  with gr.Row():
847
- with gr.Column():
 
848
  url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
849
- btn = gr.Button("🚀 Prédire (Features API)", variant="primary")
850
- with gr.Column():
851
- out = gr.HTML()
852
- btn.click(predict_freesound_acoustic_features, inputs=url_in, outputs=out)
853
-
854
- with gr.Tab("C) URL → Metadata (prétraitement)"):
 
 
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()
 
 
1
+ # app.py
2
  import os
3
  import tempfile
4
  import numpy as np
 
12
 
13
  import freesound
14
  import xgboost as xgb
 
15
 
16
+ # (Optionnel) GloVe via gensim (si dispo / autorisé)
17
+ try:
18
+ import gensim.downloader as api
19
+ _GENSIM_OK = True
20
+ except Exception:
21
+ _GENSIM_OK = False
22
 
23
+
24
+ # =========================
25
+ # RÈGLES DURÉE
26
+ # =========================
27
  MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
28
  MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
29
  SR_TARGET = 16000
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
+ # =========================
33
+ # HELPERS LOAD
34
+ # =========================
35
+ def load_artifact(*candidate_paths: str):
36
+ """
37
+ Charge un artifact joblib/pkl depuis la racine ou chemins candidats.
38
+ Essaie tous les chemins donnés, puis lève une erreur claire.
39
+ """
40
+ for p in candidate_paths:
41
+ if p and os.path.exists(p):
42
+ return joblib.load(p)
43
+ tried = "\n".join([f"- {p}" for p in candidate_paths if p])
44
+ raise FileNotFoundError(
45
+ "Artifact introuvable. J'ai essayé :\n" + (tried if tried else "(aucun chemin)")
46
+ )
47
 
48
 
49
+ # =========================
50
+ # UI (CSS)
51
+ # =========================
52
  CSS = """
53
+ .card {
54
+ border: 1px solid #e5e7eb;
55
+ background: #ffffff;
56
+ padding: 16px;
57
+ border-radius: 16px;
58
+ }
59
+ .card-error{
60
+ border-color: #fca5a5;
61
+ background: #fff1f2;
62
+ }
63
+ .card-title{
64
+ font-weight: 950;
65
+ margin-bottom: 8px;
66
+ }
67
+ .badges{
68
+ display:flex;
69
+ gap:10px;
70
+ flex-wrap:wrap;
71
+ margin-bottom:12px;
72
+ }
73
+ .badge{
74
+ padding:6px 10px;
75
+ border-radius:999px;
76
+ font-weight:900;
77
+ font-size: 13px;
78
+ border: 1px solid #e5e7eb;
79
+ }
80
  .badge-type{ background:#eef2ff; color:#3730a3;}
81
  .badge-time{ background:#ecfeff; color:#155e75;}
82
+
83
+ .grid{
84
+ display:grid;
85
+ grid-template-columns: 1fr;
86
+ gap:10px;
87
+ }
88
+ .box{
89
+ border:1px solid #e5e7eb;
90
+ border-radius:14px;
91
+ padding:12px;
92
+ background:#fafafa;
93
+ }
94
  .box-title{ font-weight:900; margin-bottom:4px; }
95
  .box-value{ font-size:18px; font-weight:800; }
96
+
97
+ .hint{
98
+ margin-top:10px;
99
+ color:#6b7280;
100
+ font-size:12px;
101
+ }
102
+
103
  #header-title { font-size: 28px; font-weight: 950; margin-bottom: 6px; }
104
  #header-sub { color:#6b7280; margin-top:0px; line-height:1.45; }
 
105
  """
106
 
107
+
108
+ def html_error(title, body_html):
 
 
 
 
109
  return f"""
110
+ <div class="card card-error">
111
+ <div class="card-title">❌ {title}</div>
112
+ <div>{body_html}</div>
113
  </div>
114
  """.strip()
115
 
 
 
 
 
 
116
 
117
  def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""):
118
  return f"""
 
121
  <span class="badge badge-type">{badge_text}</span>
122
  <span class="badge badge-time">⏱️ {duration:.2f} s</span>
123
  </div>
124
+
125
  <div class="grid">
126
  <div class="box">
127
  <div class="box-title">📈 Popularité de la note moyenne</div>
 
132
  <div class="box-value">{downloads_text}</div>
133
  </div>
134
  </div>
135
+
136
  {extra_html}
137
+
138
+ <div class="hint">
139
+ Résultats affichés en <b>niveaux</b> (faible / moyen / élevé), pas en valeurs exactes.
140
+ </div>
141
  </div>
142
  """.strip()
143
 
144
+
145
+ # =========================
146
+ # INTERPRETATION (COMMUNE)
147
+ # =========================
148
  def interpret_results(avg_class: int, dl_class: int) -> str:
149
+ """
150
+ avg_class: 0=Missed info, 1=Low, 2=Medium, 3=High
151
+ dl_class: 0=Low, 1=Medium, 2=High
152
+ """
153
  if avg_class == 0:
154
+ return (
155
+ "ℹ️ <b>Interprétation</b> :<br>"
156
+ "Aucune évaluation possible (rating manquant)."
157
+ )
158
 
159
  if avg_class == 3 and dl_class == 2:
160
+ potentiel = "très fort"
161
+ detail = "contenu de haute qualité et très populaire."
162
  elif avg_class == 3 and dl_class == 1:
163
+ potentiel = "fort"
164
+ detail = "contenu bien apprécié, en croissance."
165
  elif avg_class == 3 and dl_class == 0:
166
+ potentiel = "prometteur"
167
+ detail = "bonne qualité mais faible visibilité (peut gagner en popularité)."
168
  elif avg_class == 2 and dl_class == 2:
169
+ potentiel = "modéré à fort"
170
+ detail = "populaire mais qualité perçue moyenne."
171
  elif avg_class == 2 and dl_class == 1:
172
+ potentiel = "modéré"
173
+ detail = "profil standard, popularité stable."
174
  elif avg_class == 2 and dl_class == 0:
175
+ potentiel = "limité"
176
+ detail = "engagement faible, diffusion limitée."
177
  elif avg_class == 1 and dl_class == 2:
178
+ potentiel = "contradictoire"
179
+ detail = "très téléchargé mais peu apprécié (usage pratique possible)."
180
  elif avg_class == 1 and dl_class == 1:
181
+ potentiel = "faible"
182
+ detail = "peu attractif pour les utilisateurs."
183
  else:
184
+ potentiel = "très faible"
185
+ detail = "faible intérêt global."
186
+
187
+ return (
188
+ "<b>Interprétation</b> :<br>"
189
+ f"Potentiel estimé : <b>{potentiel}</b> — {detail}"
190
+ )
191
 
 
192
 
193
  def avg_label_to_class(avg_label: str) -> int:
194
+ """
195
+ Convertit un label texte (LabelEncoder) en classe 0..3 :
196
+ 0=Missed info, 1=Low, 2=Medium, 3=High
197
+ Robuste aux variantes.
198
+ """
199
  if avg_label is None:
200
  return 0
201
  s = str(avg_label).strip().lower()
 
210
  return 0
211
 
212
 
213
+ # =========================
214
+ # FreeSound client (commun)
215
+ # =========================
216
+ API_TOKEN = os.getenv("FREESOUND_TOKEN", "").strip()
217
+ fs_client = freesound.FreesoundClient()
218
+ if API_TOKEN:
219
+ fs_client.set_token(API_TOKEN, "token")
 
 
220
 
221
 
222
  # ============================================================
223
+ # ONGLET 1 — Upload audio → openSMILE → modèle local
224
  # ============================================================
225
+ MODEL_EFFECT = load_artifact("xgb_model_EffectSound.pkl")
226
+ MODEL_MUSIC = load_artifact("xgb_model_Music.pkl")
227
+
228
+ RATING_DISPLAY_AUDIO = {
229
+ 0: "❌ Informations manquantes",
230
+ 1: "⭐ Faible",
231
+ 2: "⭐⭐ Moyen",
232
+ 3: "⭐⭐⭐ Élevé",
233
+ }
234
+ DOWNLOADS_DISPLAY_AUDIO = {
235
+ 0: "⭐ Faible",
236
+ 1: "⭐⭐ Moyen",
237
+ 2: "⭐⭐⭐ Élevé",
238
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  SMILE = opensmile.Smile(
241
  feature_set=opensmile.FeatureSet.eGeMAPSv02,
242
  feature_level=opensmile.FeatureLevel.Functionals,
243
  )
244
 
 
 
245
 
246
  def get_duration_seconds(filepath):
247
  ext = os.path.splitext(filepath)[1].lower()
 
251
  with sf.SoundFile(filepath) as f:
252
  return len(f) / f.samplerate
253
 
254
+
255
  def to_wav_16k_mono(filepath):
256
  ext = os.path.splitext(filepath)[1].lower()
257
  if ext == ".wav":
 
264
 
265
  audio = AudioSegment.from_file(filepath)
266
  audio = audio.set_channels(1).set_frame_rate(SR_TARGET)
267
+
268
  tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
269
  tmp.close()
270
  audio.export(tmp.name, format="wav")
271
  return tmp.name
272
 
273
+
274
  def extract_opensmile_features(filepath):
275
  wav_path = to_wav_16k_mono(filepath)
276
  feats = SMILE.process_file(wav_path)
277
  feats = feats.select_dtypes(include=[np.number]).reset_index(drop=True)
278
  return feats
279
 
280
+
281
  def predict_upload_with_dmatrix(model, X_df: pd.DataFrame):
282
+ """
283
+ Résout 'data did not contain feature names' en passant via Booster + DMatrix(feature_names=...).
284
+ Retour: array shape (1, n_outputs)
285
+ """
286
+ if hasattr(model, "estimators_"):
287
+ preds = []
288
+ for est in model.estimators_:
289
+ booster = est.get_booster() if hasattr(est, "get_booster") else est
290
+ dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
291
+ p = booster.predict(dm)
292
+ preds.append(np.asarray(p).reshape(-1))
293
+ return np.column_stack(preds)
294
+
295
  booster = model.get_booster() if hasattr(model, "get_booster") else model
296
  dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
297
+ p = booster.predict(dm)
298
+ return np.asarray(p).reshape(1, -1)
299
 
300
+
301
+ def predict_from_uploaded_audio(audio_file):
302
  if audio_file is None:
303
  return html_error("Aucun fichier", "Veuillez importer un fichier audio (wav, mp3, flac…).")
304
 
305
+ # Durée
306
  try:
307
  duration = get_duration_seconds(audio_file)
308
  except Exception as e:
309
+ return html_error("Audio illisible", f"Impossible de lire l'audio.<br>Détail : <code>{e}</code>")
310
 
311
+ # Vérif durées
312
  if duration < MIN_EFFECT:
313
+ return html_error(
314
+ "Audio trop court",
315
+ f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
316
+ f"Plages acceptées :<br>"
317
+ f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
318
+ f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
319
+ )
 
 
320
 
321
+ if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
322
+ return html_error(
323
+ "Audio hors plage",
324
+ f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
325
+ f"Plages acceptées :<br>"
326
+ f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
327
+ f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
328
+ )
329
+
330
+ # Type + modèle
331
  if duration <= MAX_EFFECT:
332
+ badge = "🔊 Effet sonore (upload)"
333
+ model = MODEL_EFFECT
334
  else:
335
+ badge = "🎵 Musique (upload)"
336
+ model = MODEL_MUSIC
337
 
338
+ # openSMILE
339
  try:
340
  X = extract_opensmile_features(audio_file)
341
  except Exception as e:
342
  return html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>")
343
 
344
+ # Align features
345
  try:
346
+ expected = model.estimators_[0].feature_names_in_ if hasattr(model, "estimators_") else model.feature_names_in_
347
  X = X.reindex(columns=list(expected), fill_value=0)
348
  except Exception as e:
349
+ return html_error("Alignement des features échoué", f"Détail : <code>{e}</code>")
350
 
351
+ # Predict
352
  try:
353
  y = predict_upload_with_dmatrix(model, X)
354
  except Exception as e:
355
  return html_error("Prédiction échouée", f"Détail : <code>{e}</code>")
356
 
357
+ y = np.array(y)
358
  avg_class = int(y[0, 0])
359
  dl_class = int(y[0, 1])
360
 
361
  rating_text = RATING_DISPLAY_AUDIO.get(avg_class, "Inconnu")
362
  downloads_text = DOWNLOADS_DISPLAY_AUDIO.get(dl_class, "Inconnu")
363
 
364
+ conclusion = interpret_results(avg_class, dl_class)
365
  extra = f"""
366
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
367
+ {conclusion}
368
  </div>
369
  """
370
  return html_result(badge, duration, rating_text, downloads_text, extra_html=extra)
371
 
372
 
373
  # ============================================================
374
+ # ONGLET 2URL FreeSound → features API modèles locaux
375
  # ============================================================
376
+ xgb_music_num = load_artifact("xgb_num_downloads_music_model.pkl")
377
+ xgb_music_feat_num = load_artifact("xgb_num_downloads_music_features.pkl")
378
+ xgb_music_avg = load_artifact("xgb_avg_rating_music_model.pkl")
379
+ xgb_music_feat_avg = load_artifact("xgb_avg_rating_music_features.pkl")
380
+ le_music_avg = load_artifact("xgb_avg_rating_music_label_encoder.pkl")
381
 
382
+ xgb_effect_num = load_artifact("xgb_num_downloads_effectsound_model.pkl")
383
+ xgb_effect_feat_num = load_artifact("xgb_num_downloads_effectsound_features.pkl")
384
+ xgb_effect_avg = load_artifact("xgb_avg_rating_effectsound_model.pkl")
385
+ xgb_effect_feat_avg = load_artifact("xgb_avg_rating_effectsound_features.pkl")
386
+ le_effect_avg = load_artifact("xgb_avg_rating_effectsound_label_encoder.pkl")
387
 
388
+ NUM_DOWNLOADS_MAP_FR = {0: "Faible", 1: "Moyen", 2: "Élevé"}
 
389
 
 
 
 
 
390
 
391
+ def safe_float(v):
392
+ try:
393
+ return float(v)
394
+ except Exception:
395
+ return 0.0
396
 
 
397
 
398
  def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
399
  row = []
 
406
  X = pd.DataFrame([row], columns=feat_list)
407
  dmatrix = xgb.DMatrix(X.values, feature_names=feat_list)
408
 
409
+ pred_int = int(model.get_booster().predict(dmatrix)[0])
 
410
 
411
  if label_encoder is not None:
412
  return label_encoder.inverse_transform([pred_int])[0]
413
  return pred_int
414
 
415
+
416
+ def predict_from_freesound_url(url: str):
417
+ if not API_TOKEN:
418
+ return html_error(
419
+ "Token FreeSound manquant",
420
+ "Ajoute la variable d’environnement <code>FREESOUND_TOKEN</code> pour activer cet onglet."
421
+ )
422
+
423
  if not url or not url.strip():
424
+ return html_error("URL vide", "Collez une URL FreeSound du type <code>https://freesound.org/s/123456/</code>")
425
 
426
+ # ID
427
  try:
428
+ sound_id = int(url.rstrip("/").split("/")[-1])
429
  except Exception:
430
  return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
431
 
432
+ all_features = list(set(
433
+ list(xgb_music_feat_num) + list(xgb_music_feat_avg) + list(xgb_effect_feat_num) + list(xgb_effect_feat_avg)
434
+ ))
435
+ fields = "duration," + ",".join(all_features)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
 
437
  try:
438
  results = fs_client.search(query="", filter=f"id:{sound_id}", fields=fields)
 
445
  sound = results.results[0]
446
  duration = safe_float(sound.get("duration", 0))
447
 
448
+ # Effect Sound
449
+ if MIN_EFFECT <= duration <= MAX_EFFECT:
450
+ badge = "🔊 Effet sonore (URL → features API)"
451
+ dl_class = int(predict_with_model_fs(xgb_effect_num, sound, xgb_effect_feat_num))
452
+ avg_text = str(predict_with_model_fs(xgb_effect_avg, sound, xgb_effect_feat_avg, le_effect_avg))
453
+ dl_text = NUM_DOWNLOADS_MAP_FR.get(dl_class, str(dl_class))
454
 
 
 
 
 
 
 
455
  avg_class = avg_label_to_class(avg_text)
456
+ conclusion = interpret_results(avg_class, dl_class)
457
 
458
  extra = f"""
459
  <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
460
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
461
+ {conclusion}
462
  </div>
463
  """
464
  return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
465
 
466
  # Music
467
+ if MIN_MUSIC <= duration <= MAX_MUSIC:
468
+ badge = "🎵 Musique (URL → features API)"
469
+ dl_class = int(predict_with_model_fs(xgb_music_num, sound, xgb_music_feat_num))
470
+ avg_text = str(predict_with_model_fs(xgb_music_avg, sound, xgb_music_feat_avg, le_music_avg))
471
+ dl_text = NUM_DOWNLOADS_MAP_FR.get(dl_class, str(dl_class))
472
 
473
+ avg_class = avg_label_to_class(avg_text)
474
+ conclusion = interpret_results(avg_class, dl_class)
475
+
476
+ extra = f"""
477
  <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
478
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
479
+ {conclusion}
480
  </div>
481
  """
482
+ return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
483
+
484
+ return html_error(
485
+ "Durée non supportée",
486
+ f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
487
+ f"Plages acceptées :<br>"
488
+ f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
489
+ f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
490
+ )
491
 
492
 
493
  # ============================================================
494
+ # ONGLET 3URL FreeSound → METADATA → preprocessing complet → modèles
495
+ # (reprend la logique du script metadata, mais sans HF hub obligatoire)
496
  # ============================================================
 
 
 
497
 
498
+ class AvgRatingTransformer:
499
+ def __init__(self, est, class_mapping=None):
500
+ self.est = est
501
+ if class_mapping is None:
502
+ self.class_mapping = {0: "MissedInfo", 1: "Low", 2: "Medium", 3: "High"}
503
+ else:
504
+ self.class_mapping = class_mapping
505
+
506
+ def transform(self, X):
507
+ X = np.asarray(X)
508
+ mask_non_zero = X != 0
509
+ Xt = np.zeros_like(X, dtype=int)
510
+ if mask_non_zero.any():
511
+ Xt[mask_non_zero] = self.est.transform(X[mask_non_zero].reshape(-1, 1)).flatten() + 1
512
+ return np.array([self.class_mapping.get(v, "MissedInfo") for v in Xt])
513
+
514
+
515
+ # ---- Artifacts preprocessing (music/effect) ----
516
+ # Supporte soit "à la racine", soit encore dans music/ et effectSound/
517
+ scaler_samplerate_music = load_artifact("scaler_music_samplerate.joblib", "music/scaler_music_samplerate.joblib")
518
+ scaler_age_days_music = load_artifact("scaler_music_age_days_log.joblib", "music/scaler_music_age_days_log.joblib")
519
+ username_freq_music = load_artifact("username_freq_dict_music.joblib", "music/username_freq_dict_music.joblib")
520
+ est_num_downloads_music = load_artifact("est_num_downloads_music.joblib", "music/est_num_downloads_music.joblib")
521
+ avg_rating_transformer_music = load_artifact("avg_rating_transformer_music.joblib", "music/avg_rating_transformer_music.joblib")
522
+ music_subcategory_cols = load_artifact("music_subcategory_cols.joblib", "music/music_subcategory_cols.joblib")
523
+ music_onehot_cols = load_artifact("music_onehot_cols.joblib", "music/music_onehot_cols.joblib")
524
+ music_onehot_tags = load_artifact("music_onehot_tags.joblib", "music/music_onehot_tags.joblib")
525
+
526
+ scaler_samplerate_effect = load_artifact("scaler_effectSamplerate.joblib", "effectSound/scaler_effectSamplerate.joblib")
527
+ scaler_age_days_effect = load_artifact("scaler_effectSound_age_days_log.joblib", "effectSound/scaler_effectSound_age_days_log.joblib")
528
+ username_freq_effect = load_artifact("username_freq_dict_effectSound.joblib", "effectSound/username_freq_dict_effectSound.joblib")
529
+ est_num_downloads_effect = load_artifact("est_num_downloads_effectSound.joblib", "effectSound/est_num_downloads_effectSound.joblib")
530
+ avg_rating_transformer_effect = load_artifact("avg_rating_transformer_effectSound.joblib", "effectSound/avg_rating_transformer_effectSound.joblib")
531
+ effect_subcategory_cols = load_artifact("effectSound_subcategory_cols.joblib", "effectSound/effectSound_subcategory_cols.joblib")
532
+ effect_onehot_cols = load_artifact("effectSound_onehot_cols.joblib", "effectSound/effectSound_onehot_cols.joblib")
533
+ effect_onehot_tags = load_artifact("effect_onehot_tags.joblib", "effectSound/effect_onehot_tags.joblib")
534
+
535
+ # ---- Modèles metadata (num_downloads + avg_rating + features list) ----
536
+ # mettre idéalement à la racine)
537
+ music_model_num_downloads = load_artifact("music_model_num_downloads.joblib")
538
+ music_model_avg_rating = load_artifact("music_xgb_avg_rating.joblib")
539
+ music_avg_rating_le_meta = load_artifact("music_xgb_avg_rating_label_encoder.joblib")
540
+ music_model_features = load_artifact("music_model_features_list.joblib")
541
+
542
+ effect_model_num_downloads = load_artifact("effectSound_model_num_downloads.joblib")
543
+ effect_model_avg_rating = load_artifact("effectSound_xgb_avg_rating.joblib")
544
+ effect_avg_rating_le_meta = load_artifact("effectSound_xgb_avg_rating_label_encoder.joblib")
545
+ effect_model_features = load_artifact("effect_model_features_list.joblib")
546
+
547
+ # Nettoyage doublons (comme ta collègue)
548
+ music_model_features = list(dict.fromkeys(list(music_model_features)))
549
+ effect_model_features = list(dict.fromkeys(list(effect_model_features)))
550
+
551
+ # GloVe (optionnel)
552
+ if _GENSIM_OK:
553
+ try:
554
+ glove_model = api.load("glove-wiki-gigaword-100")
555
+ except Exception:
556
+ glove_model = None
557
+ else:
558
+ glove_model = None
 
559
 
 
 
560
 
561
  def preprocess_name(df, vec_dim=8):
562
+ # Version simple: hashing via sklearn n'est pas importé ici pour rester léger.
563
+ # Pour rester fidèle au code collègue, on refait le hashing "à la main" avec pandas+numpy.
564
+ # (Si tu veux EXACTEMENT HashingVectorizer, dis-moi et je te le remets.)
565
  df = df.copy()
566
+ name = df["name_clean"].fillna("").astype(str)
567
+ df["name_len"] = name.str.len()
568
+ # hashing rudimentaire en vec_dim dimensions
569
+ vec = np.zeros((len(df), vec_dim), dtype=float)
570
+ for i, s in enumerate(name.tolist()):
571
+ h = abs(hash(s))
572
+ for k in range(vec_dim):
573
+ vec[i, k] = ((h >> (k * 3)) & 0x7) # petit pattern stable
574
+ for k in range(vec_dim):
575
+ df[f"name_vec_{k}"] = vec[:, k]
576
+ return df
577
+
578
+
579
+ def description_to_vec(text, model, dim=100):
580
+ if model is None:
581
+ return np.zeros(dim)
582
+ if not text:
583
+ return np.zeros(dim)
584
+ words = str(text).lower().split()
585
+ vecs = [model[w] for w in words if w in model]
586
+ if len(vecs) == 0:
587
+ return np.zeros(dim)
588
+ return np.mean(vecs, axis=0)
589
+
590
+
591
+ def fetch_sound_metadata(sound_url: str) -> pd.DataFrame:
592
+ """
593
+ Récupère les metadata FreeSound (sans télécharger l'audio).
594
+ """
595
+ if not API_TOKEN:
596
+ raise RuntimeError("Token FreeSound manquant (FREESOUND_TOKEN).")
597
+
598
+ sound_id = int(sound_url.rstrip("/").split("/")[-1])
599
  sound = fs_client.get_sound(sound_id)
600
+
601
  data = {
602
  "id": sound_id,
603
+ "file_path": None,
604
+ "name": getattr(sound, "name", ""),
605
  "num_ratings": getattr(sound, "num_ratings", 0),
606
+ "tags": ",".join(getattr(sound, "tags", []) or []),
607
  "username": getattr(sound, "username", ""),
608
  "description": getattr(sound, "description", "") or "",
609
  "created": getattr(sound, "created", ""),
 
622
  }
623
  return pd.DataFrame([data])
624
 
625
+
626
+ def preprocess_sound(df: pd.DataFrame):
627
+ """
628
+ Preprocessing complet basé sur la durée pour choisir Music vs EffectSound.
629
+ """
630
  df = df.copy()
631
  dur = float(df["duration"].iloc[0])
632
 
633
  if MIN_EFFECT <= dur <= MAX_EFFECT:
634
+ scaler_samplerate = scaler_samplerate_effect
635
+ scaler_age = scaler_age_days_effect
636
+ username_freq = username_freq_effect
637
+ est_num_downloads = est_num_downloads_effect
638
+ avg_rating_transformer = avg_rating_transformer_effect
639
+ subcat_cols = effect_subcategory_cols
640
+ onehot_cols = effect_onehot_cols
641
+ onehot_tags = effect_onehot_tags
 
642
  elif MIN_MUSIC <= dur <= MAX_MUSIC:
643
+ scaler_samplerate = scaler_samplerate_music
644
+ scaler_age = scaler_age_days_music
645
+ username_freq = username_freq_music
646
+ est_num_downloads = est_num_downloads_music
647
+ avg_rating_transformer = avg_rating_transformer_music
648
+ subcat_cols = music_subcategory_cols
649
+ onehot_cols = music_onehot_cols
650
+ onehot_tags = music_onehot_tags
 
651
  else:
652
+ return f" Son trop court ou trop long ({dur} sec)"
653
 
654
+ # Category bool
655
  df["category_is_user_provided"] = df["category_is_user_provided"].astype(int)
656
+
657
+ # Username frequency
658
  df["username_freq"] = df["username"].map(username_freq).fillna(0)
659
 
660
+ # Numeric features log1p
661
  for col in ["num_ratings", "num_comments", "filesize", "duration"]:
662
  df[col] = np.log1p(df[col])
663
 
664
+ # samplerate scaled
665
  df["samplerate"] = scaler_samplerate.transform(df[["samplerate"]])
666
 
667
+ # Age_days
668
  df["created"] = pd.to_datetime(df["created"], errors="coerce").dt.tz_localize(None)
669
  df["age_days"] = (pd.Timestamp.now() - df["created"]).dt.days
670
  df["age_days_log"] = np.log1p(df["age_days"])
671
  df["age_days_log_scaled"] = scaler_age.transform(df[["age_days_log"]])
672
+ df = df.drop(columns=["created", "age_days", "age_days_log"])
673
 
674
+ # num_downloads_class (binned)
675
  df["num_downloads_class"] = est_num_downloads.transform(df[["num_downloads"]])
 
676
 
677
+ # avg_rating discretized via transformer
678
+ df["avg_rating"] = avg_rating_transformer.transform(df["avg_rating"].to_numpy())
679
+
680
+ # Subcategory onehot
681
  for col in subcat_cols:
682
  df[col] = 0
683
  subcat_val = df["subcategory"].iloc[0]
 
685
  cat_name = col.replace("subcategory_", "")
686
  if subcat_val == cat_name:
687
  df[col] = 1
688
+ df.drop(columns=["subcategory"], inplace=True)
689
 
690
+ # One-hot cols (license/category/type)
691
  for col in onehot_cols:
692
  if col not in df.columns:
693
  df[col] = 0
 
700
  if col_name in df.columns:
701
  df[col_name] = 1
702
 
703
+ # Tags
704
+ for col in ["name", "tags", "description"]:
705
+ if col not in df.columns:
706
+ df[col] = ""
707
+
708
+ df["tags_list"] = df["tags"].fillna("").astype(str).str.lower().str.split(",")
709
+
710
+ if not df["tags_list"].iloc[0] or df["tags_list"].iloc[0] == [""]:
711
+ df["tags_list"] = [["Other"]]
712
+
713
  for col in onehot_tags:
714
  if col not in df.columns:
715
  df[col] = 0
 
719
  tag_name = col.replace("tag_", "").lower()
720
  if tag_name in tags_list:
721
  df[col] = 1
 
722
 
723
+ df.drop(columns=["tags"], inplace=True)
724
+
725
+ # Name hashing
726
  df["name_clean"] = df["name"].astype(str).str.lower().str.rsplit(".", n=1).str[0]
727
  df = preprocess_name(df, vec_dim=8)
728
+ df.drop(columns=["name", "name_clean"], inplace=True)
729
+
730
+ # Description glove mean (si glove non dispo: zeros)
731
+ desc_vec = description_to_vec(df["description"].iloc[0], glove_model)
732
+ for i in range(100):
733
+ df[f"description_glove_{i}"] = float(desc_vec[i])
734
+ df.drop(columns=["description"], inplace=True)
735
+
736
+ # Drop non-features
737
+ df.drop(
738
+ columns=[
739
+ "license", "category", "type", "subcategory", "id",
740
+ "num_downloads", "file_path", "username", "tags_list"
741
+ ],
742
+ inplace=True,
743
+ errors="ignore"
744
+ )
745
 
746
+ return df
747
 
 
748
 
749
+ def predict_with_model_meta(model, df_input: pd.DataFrame, le=None):
750
  booster_feats = model.get_booster().feature_names
751
  X_aligned = df_input.reindex(columns=booster_feats, fill_value=0.0).astype(float)
752
+ dmatrix = xgb.DMatrix(X_aligned.values, feature_names=list(booster_feats))
753
+ preds = model.get_booster().predict(dmatrix)
754
+ pred_val = preds[0]
755
+ pred_int = int(round(float(pred_val)))
 
 
 
 
 
 
 
 
 
756
 
757
+ if le is not None:
758
+ try:
759
+ return le.inverse_transform([pred_int])[0]
760
+ except Exception:
761
+ return f"Classe inconnue ({pred_int})"
762
+ return pred_int
763
 
 
 
 
 
764
 
765
+ def predict_from_metadata_url(url: str):
766
+ if not API_TOKEN:
767
+ return "❌ Token FreeSound manquant. Ajoute FREESOUND_TOKEN (env / secret)."
768
+
769
+ if not url or not url.strip():
770
+ return "❌ Veuillez entrer une URL FreeSound."
771
 
772
+ # 1) metadata brute
773
  try:
774
+ df_raw = fetch_sound_metadata(url)
775
  except Exception as e:
776
+ return f"Erreur API FreeSound: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
 
778
+ raw_lines = ["=== Métadonnées brutes ==="]
779
+ for col in df_raw.columns:
780
+ raw_lines.append(f"{col}: {df_raw[col].iloc[0]}")
781
+ raw_str = "\n".join(raw_lines)
782
 
783
+ # 2) durée
784
+ dur = float(df_raw["duration"].iloc[0])
785
+ if dur < MIN_EFFECT:
786
+ return raw_str + f"\n\n❌ Son trop court ({dur} sec). Plage acceptée: {MIN_EFFECT}-{MAX_EFFECT} ou {MIN_MUSIC}-{MAX_MUSIC} sec"
787
+ if (MAX_EFFECT < dur < MIN_MUSIC) or dur > MAX_MUSIC:
788
+ return raw_str + f"\n\n❌ Son hors plage ({dur} sec). Plage acceptée: {MIN_EFFECT}-{MAX_EFFECT} ou {MIN_MUSIC}-{MAX_MUSIC} sec"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
789
 
790
+ # 3) preprocessing complet
791
+ df_processed = preprocess_sound(df_raw)
792
+ if isinstance(df_processed, str):
793
+ return raw_str + "\n\n" + df_processed
794
 
795
+ cols_to_remove = ["avg_rating", "num_downloads_class"]
796
+ df_for_model = df_processed.drop(columns=[c for c in cols_to_remove if c in df_processed.columns])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
 
798
+ # 4) choisir modèles metadata
799
+ if MIN_EFFECT <= dur <= MAX_EFFECT:
800
+ model_nd = effect_model_num_downloads
801
+ model_ar = effect_model_avg_rating
802
+ model_features = effect_model_features
803
+ sound_type = "EffectSound"
804
+ current_le = effect_avg_rating_le_meta
 
 
 
 
 
805
  else:
806
+ model_nd = music_model_num_downloads
807
+ model_ar = music_model_avg_rating
808
+ model_features = music_model_features
809
+ sound_type = "Music"
810
+ current_le = music_avg_rating_le_meta
811
+
812
+ # 5) forcer colonnes exactes
813
+ df_for_model = df_for_model.reindex(columns=model_features, fill_value=0.0).astype(float)
814
+
815
+ # 6) prédictions
816
+ pred_num_downloads_val = predict_with_model_meta(model_nd, df_for_model, le=None)
817
+ NUM_DOWNLOADS_MAP = {0: "Low", 1: "Medium", 2: "High"}
818
+ pred_num_downloads = NUM_DOWNLOADS_MAP.get(int(pred_num_downloads_val), str(pred_num_downloads_val))
819
+
820
+ pred_avg_rating = predict_with_model_meta(model_ar, df_for_model, le=current_le)
821
+
822
+ # 7) afficher features après preprocessing
823
+ processed_lines = ["\n=== Features après preprocessing ==="]
824
+ for col in df_processed.columns:
825
+ processed_lines.append(f"{col}: {df_processed[col].iloc[0]}")
826
+ processed_str = "\n".join(processed_lines)
827
+
828
+ # 8) résultat
829
+ prediction_lines = [
830
+ "\n=== Prédictions ===",
831
+ f"Type détecté : {sound_type}",
832
+ f"📥 Num downloads prédit : {pred_num_downloads}",
833
+ f"⭐ Avg rating prédit : {pred_avg_rating}",
834
+ ]
835
+ prediction_str = "\n".join(prediction_lines)
836
+
837
+ return raw_str + processed_str + prediction_str
838
+
839
+
840
+ # =========================
841
+ # APP UI (3 onglets)
842
+ # =========================
843
+ theme = gr.themes.Soft()
844
+
845
+ with gr.Blocks(title="Démo — Popularité Audio", css=CSS) as demo:
846
+ gr.HTML(
847
+ f"""
848
+ <div id="header-title">Démo — Prédiction de popularité audio</div>
849
  <p id="header-sub">
850
+ Trois modes : <b>Upload audio</b> (openSMILE), <b>URL FreeSound</b> (features API), <b>URL FreeSound</b> (metadata + preprocessing complet).<br><br>
851
+ <b>Durées acceptées :</b> 🔊 Effet sonore {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}–{MAX_MUSIC}s
 
 
852
  </p>
853
+ """
854
+ )
855
 
856
+ if not API_TOKEN:
857
+ gr.Markdown(
858
+ "⚠️ **FREESOUND_TOKEN non défini** : les onglets URL (2 et 3) ne fonctionneront pas tant que tu ne l’ajoutes pas."
859
+ )
860
 
861
  with gr.Tabs():
862
+ # -------- TAB 1 --------
863
+ with gr.Tab("1) Upload audio (openSMILE)"):
864
  with gr.Row():
865
+ with gr.Column(scale=1):
866
+ gr.Markdown("### Importer un fichier")
867
  audio_in = gr.Audio(type="filepath", label="Fichier audio")
868
+ btn_audio = gr.Button("🚀 Prédire (upload)", variant="primary")
869
+ with gr.Column(scale=1):
870
+ gr.Markdown("### Résultat")
871
+ out_audio = gr.HTML()
872
+ btn_audio.click(predict_from_uploaded_audio, inputs=audio_in, outputs=out_audio)
873
+
874
+ # -------- TAB 2 --------
875
+ with gr.Tab("2) URL FreeSound (features API)"):
876
  with gr.Row():
877
+ with gr.Column(scale=1):
878
+ gr.Markdown("### Coller une URL FreeSound")
879
  url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
880
+ btn_url = gr.Button("🚀 Prédire (URL → features API)", variant="primary")
881
+ with gr.Column(scale=1):
882
+ gr.Markdown("### Résultat")
883
+ out_url = gr.HTML()
884
+ btn_url.click(predict_from_freesound_url, inputs=url_in, outputs=out_url)
885
+
886
+ # -------- TAB 3 --------
887
+ with gr.Tab("3) URL FreeSound (metadata + preprocessing complet)"):
888
  with gr.Row():
889
+ with gr.Column(scale=1):
890
+ gr.Markdown("### Coller une URL FreeSound")
891
+ url_meta = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
892
+ btn_meta = gr.Button("📊 Prétraiter + prédire (metadata)", variant="primary")
893
+ with gr.Column(scale=1):
894
+ gr.Markdown("### Sortie détaillée (brut + features + prédictions)")
895
+ out_meta = gr.Textbox(label="Résultat", lines=22)
896
+ btn_meta.click(predict_from_metadata_url, inputs=url_meta, outputs=out_meta)
897
+
898
+ demo.launch(theme=theme)
avg_rating_transformer_effectSound.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:37b2862f96050ad72ab3964d30f4f2d3908dd6b5e746f150c1baea5d2cdb2bbf
3
+ size 944
avg_rating_transformer_music.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:29e054a504fd9193b232384f5a2799cad18e54903f03cbfc999a77547feff2d2
3
+ size 944
effectSound_onehot_cols.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3de47c718ff02e366470f28167a8e5736829fa84b0d34531ac046ceaec5371fa
3
+ size 761
effectSound_subcategory_cols.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6796b185bc36b2a0961c0a0b22f813f473eec2962cfa5c20a013f0f328ae8021
3
+ size 418
effect_onehot_tags.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9504d82fd7b4691fdc61b00f2e8ae15e28665fce17c60cf44655ccd60cf09f36
3
+ size 69808
est_num_downloads_effectSound.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cd69b4b945f61331c7778a7ff3366a856191beff40ed439ed78705c1f94440ef
3
+ size 831
est_num_downloads_music.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:435f88fd8e8f46a970b39b2f255920c298c20e41cf558276dae8b09a40bd56be
3
+ size 831
music_onehot_cols.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:554cb8135c47967de9f480942f6d09c79b2ac8440adecebd1dd8c013444d195a
3
+ size 771
music_onehot_tags.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b8d510dc14604d2d69333e144cc3212ecb3b446d5192f15940347d65610e6eb1
3
+ size 36877
music_subcategory_cols.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a51f89fa69f26d5785cd8518fc594ceffbc959493572ac9b06162bfd4f509247
3
+ size 377
music_xgb_avg_rating (1).joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:528b63dab12f2d20b07086f7d7b1a8747fbc09798d5c6a199185cec57bda823d
3
+ size 7961465
music_xgb_model_smote_balanced_avg_rating.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f9266eb3b73bbe34dcbbf84a5cefd758b8dae561f1ee7abd11ea9e79dcb9a756
3
+ size 4144472
music_xgb_model_smote_balanced_num_downloads.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fca062c7c044eeb44c0acaaad0f1ee91ff79b733d877d7893795580c74b68f87
3
+ size 5322685
requirements.txt CHANGED
@@ -1,15 +1,14 @@
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
 
1
+ gradio==6.5.0
2
+ pandas
3
+ numpy
4
+ scikit-learn
5
+ joblib
6
+ xgboost
7
+ soundfile
8
+ pydub
9
+ opensmile
10
+ requests
11
+ pytz
12
+ imblearn
13
+ matplotlib
14
+ git+https://github.com/MTG/freesound-python
 
scaler_effectSamplerate.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5ac8d3018ca0d1477592952a1aa6b9d582ad589c46314854efd56b607d175b3a
3
+ size 879
scaler_effectSound_age_days_log.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:332ee96e7bca4c412bc0d5ac20c0876d5bf8304142d4fd57d4d5524e03228e61
3
+ size 895
scaler_music_age_days_log.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1941f5f48e21243a939080d9d7a1cedc677e2b0b813a451a50f64d00ce149588
3
+ size 895
scaler_music_samplerate.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0339152b44141d57f6be072c67c50c074d7b6e12280a57f0434520188af83483
3
+ size 879
username_freq_dict_effectSound.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:457517d900b3c05061f398d37b00f8087ae9edb1a4776c7cbc2fc77fa60a4036
3
+ size 209269
username_freq_dict_music.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6ca2078e0e0c5c0d5f871362bba1e787c6860fb547dd1f9f3c4f0f3c366b447
3
+ size 214933