IKRAMELHADI commited on
Commit
bb09077
·
1 Parent(s): 97483f5
Files changed (1) hide show
  1. app.py +602 -284
app.py CHANGED
@@ -1,179 +1,169 @@
1
  import os
2
- import re
3
- import time
4
  import tempfile
5
- import joblib
6
  import numpy as np
7
  import pandas as pd
8
  import gradio as gr
9
 
10
- import opensmile
11
- import xgboost as xgb
12
  import soundfile as sf
13
  from pydub import AudioSegment
 
14
 
15
  import freesound
 
16
 
 
17
 
18
- # =========================
 
19
  # CONFIG
20
- # =========================
21
- MIN_EFFECT = 1
22
- MAX_EFFECT = 30
23
- MIN_MUSIC = 31
24
- MAX_MUSIC = 600
25
  SR_TARGET = 16000
26
 
27
- # Mets ton token FreeSound dans une variable d'environnement :
28
- # export FREESOUND_API_TOKEN="xxxxx"
29
- API_TOKEN = os.getenv("FREESOUND_API_TOKEN", "").strip()
30
-
31
- # Modèles openSMILE (les tiens)
32
- MODEL_EFFECT_PATH = "xgb_model_EffectSound.pkl"
33
- MODEL_MUSIC_PATH = "xgb_model_Music.pkl"
34
-
35
- MODEL_EFFECT = joblib.load(MODEL_EFFECT_PATH)
36
- MODEL_MUSIC = joblib.load(MODEL_MUSIC_PATH)
37
-
38
- RATING_DISPLAY_AUDIO = {
39
- 0: "❌ Informations manquantes",
40
- 1: "⭐ Faible",
41
- 2: "⭐⭐ Moyen",
42
- 3: "⭐⭐⭐ Élevé",
43
- }
44
- DOWNLOADS_DISPLAY_AUDIO = {
45
- 0: "⭐ Faible",
46
- 1: "⭐⭐ Moyen",
47
- 2: "⭐⭐⭐ Élevé",
48
- }
49
 
50
- SMILE = opensmile.Smile(
51
- feature_set=opensmile.FeatureSet.eGeMAPSv02,
52
- feature_level=opensmile.FeatureLevel.Functionals,
53
- )
54
 
 
 
55
 
56
- # =========================
57
- # UI helpers
58
- # =========================
 
 
 
 
 
 
 
59
  CSS = """
60
- #header-title { font-size: 28px; font-weight: 800; margin-bottom: 6px; }
61
- #header-sub { color:#444; margin-top:0; }
62
- .card {
63
- border: 1px solid #e5e7eb; border-radius: 14px; padding: 14px 14px;
64
- background: #fff; box-shadow: 0 3px 10px rgba(0,0,0,0.04);
65
- }
66
- .badge { display:inline-block; padding:6px 10px; border-radius:999px; font-weight:700; font-size:12px; }
67
- .badge.music { background:#eef2ff; color:#3730a3; }
68
- .badge.fx { background:#ecfeff; color:#155e75; }
69
- .kv { margin:6px 0; }
70
- .k { font-weight:700; }
71
- .hint { color:#6b7280; font-size:12px; margin-top:8px; }
72
- .err { color:#991b1b; font-weight:700; }
 
 
73
  """
74
 
75
- def html_error(title: str, msg: str) -> str:
76
  return f"""
77
- <div class="card">
78
- <div class="err">❌ {title}</div>
79
- <div style="margin-top:8px">{msg}</div>
80
  </div>
81
- """
82
 
83
- def html_result(badge: str, duration: float, rating_text: str, downloads_text: str, extra_html: str = "") -> str:
84
- klass = "music" if "Musique" in badge else "fx"
85
  return f"""
86
  <div class="card">
87
- <div class="badge {klass}">{badge}</div>
88
- <div class="kv"><span class="k">Durée :</span> {duration:.2f}s</div>
89
- <div class="kv"><span class="k">Rating (classe) :</span> {rating_text}</div>
90
- <div class="kv"><span class="k">Downloads (classe) :</span> {downloads_text}</div>
 
 
 
 
 
 
 
 
 
 
91
  {extra_html}
 
92
  </div>
93
- """
94
 
95
  def interpret_results(avg_class: int, dl_class: int) -> str:
96
  if avg_class == 0:
97
- return (
98
- "ℹ️ <b>Interprétation</b> :<br>"
99
- "Aucune évaluation possible (rating manquant)."
100
- )
101
-
102
- rating_txt = {1: "faible", 2: "moyenne", 3: "élevée"}.get(avg_class, "inconnue")
103
- downloads_txt = {0: "faible", 1: "modérée", 2: "élevée"}.get(dl_class, "inconnue")
104
 
105
  if avg_class == 3 and dl_class == 2:
106
- potentiel = "très fort"; detail = "contenu de haute qualité et très populaire."
107
  elif avg_class == 3 and dl_class == 1:
108
- potentiel = "fort"; detail = "contenu bien apprécié, en croissance."
109
  elif avg_class == 3 and dl_class == 0:
110
- potentiel = "prometteur"; detail = "bonne qualité mais faible visibilité (peut gagner en popularité)."
111
  elif avg_class == 2 and dl_class == 2:
112
- potentiel = "modéré à fort"; detail = "populaire mais qualité perçue moyenne."
113
  elif avg_class == 2 and dl_class == 1:
114
- potentiel = "modéré"; detail = "profil standard, popularité stable."
115
  elif avg_class == 2 and dl_class == 0:
116
- potentiel = "limité"; detail = "engagement faible, diffusion limitée."
117
  elif avg_class == 1 and dl_class == 2:
118
- potentiel = "contradictoire"; detail = "très téléchargé mais peu apprécié (usage pratique possible)."
119
  elif avg_class == 1 and dl_class == 1:
120
- potentiel = "faible"; detail = "peu attractif pour les utilisateurs."
121
  else:
122
- potentiel = "très faible"; detail = "faible intérêt global."
123
 
124
  return f"<b>Interprétation</b> :<br>Potentiel estimé : <b>{potentiel}</b> — {detail}"
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- # =========================
128
- # FreeSound helpers
129
- # =========================
130
- def extract_freesound_id(url: str) -> int:
131
- if not url or not url.strip():
132
- raise ValueError("URL vide")
133
-
134
- # accepte: https://freesound.org/s/123456/
135
- m = re.search(r"/s/(\d+)", url)
136
- if not m:
137
- # fallback: dernier segment numérique
138
- parts = [p for p in url.strip().rstrip("/").split("/") if p]
139
- if not parts or not parts[-1].isdigit():
140
- raise ValueError("Impossible d'extraire l'ID depuis l'URL")
141
- return int(parts[-1])
142
- return int(m.group(1))
143
-
144
- def get_fs_client() -> freesound.FreesoundClient:
145
- if not API_TOKEN:
146
- raise RuntimeError(
147
- "Token FreeSound manquant. Mets-le dans FREESOUND_API_TOKEN (variable d'environnement)."
148
- )
149
  c = freesound.FreesoundClient()
150
- c.set_token(API_TOKEN, "token")
151
  return c
152
 
153
- def download_preview_with_retry(client: freesound.FreesoundClient, sound_id: int, tries: int = 4, sleep_base: float = 1.0):
154
- """
155
- Télécharge le preview FreeSound dans un fichier temporaire.
156
- Retry simple (souvent utile quand FreeSound coupe / rate-limit).
157
- """
158
- last_err = None
159
- for i in range(tries):
160
- try:
161
- snd = client.get_sound(sound_id)
162
- # on force un mp3 (preview) -> pydub sait le lire (si ffmpeg dispo)
163
- tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
164
- tmp.close()
165
- snd.retrieve_preview(tmp.name)
166
- return tmp.name, snd
167
- except Exception as e:
168
- last_err = e
169
- time.sleep(sleep_base * (2 ** i))
170
- raise RuntimeError(f"Échec téléchargement preview après {tries} essais: {last_err}")
171
-
172
-
173
- # =========================
174
- # Audio helpers
175
- # =========================
176
- def get_duration_seconds(filepath: str) -> float:
177
  ext = os.path.splitext(filepath)[1].lower()
178
  if ext == ".mp3":
179
  audio = AudioSegment.from_file(filepath)
@@ -181,7 +171,7 @@ def get_duration_seconds(filepath: str) -> float:
181
  with sf.SoundFile(filepath) as f:
182
  return len(f) / f.samplerate
183
 
184
- def to_wav_16k_mono(filepath: str) -> str:
185
  ext = os.path.splitext(filepath)[1].lower()
186
  if ext == ".wav":
187
  try:
@@ -190,7 +180,6 @@ def to_wav_16k_mono(filepath: str) -> str:
190
  return filepath
191
  except Exception:
192
  pass
193
-
194
  audio = AudioSegment.from_file(filepath)
195
  audio = audio.set_channels(1).set_frame_rate(SR_TARGET)
196
  tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
@@ -198,35 +187,13 @@ def to_wav_16k_mono(filepath: str) -> str:
198
  audio.export(tmp.name, format="wav")
199
  return tmp.name
200
 
201
- def extract_opensmile_features(filepath: str) -> pd.DataFrame:
202
  wav_path = to_wav_16k_mono(filepath)
203
  feats = SMILE.process_file(wav_path)
204
  feats = feats.select_dtypes(include=[np.number]).reset_index(drop=True)
205
  return feats
206
 
207
- def expected_feature_names(model) -> list[str]:
208
- if hasattr(model, "estimators_"): # multioutput wrapper
209
- base = model.estimators_[0]
210
- if hasattr(base, "feature_names_in_"):
211
- return list(base.feature_names_in_)
212
- # fallback xgb
213
- if hasattr(base, "get_booster"):
214
- bn = base.get_booster().feature_names
215
- if bn:
216
- return list(bn)
217
- if hasattr(model, "feature_names_in_"):
218
- return list(model.feature_names_in_)
219
- if hasattr(model, "get_booster"):
220
- bn = model.get_booster().feature_names
221
- if bn:
222
- return list(bn)
223
- raise RuntimeError("Impossible de récupérer la liste des features attendues par le modèle.")
224
-
225
- def predict_with_dmatrix(model, X_df: pd.DataFrame) -> np.ndarray:
226
- """
227
- Robust contre: 'data did not contain feature names'
228
- Supporte MultiOutput (estimators_)
229
- """
230
  if hasattr(model, "estimators_"):
231
  preds = []
232
  for est in model.estimators_:
@@ -241,170 +208,521 @@ def predict_with_dmatrix(model, X_df: pd.DataFrame) -> np.ndarray:
241
  p = booster.predict(dm)
242
  return np.asarray(p).reshape(1, -1)
243
 
 
 
 
244
 
245
- # =========================
246
- # Main pipeline (URL -> download -> features -> align -> predict)
247
- # =========================
248
- def predict_from_freesound_url(url: str):
249
- # 1) parse URL
250
- try:
251
- sound_id = extract_freesound_id(url)
252
- except Exception as e:
253
- return (
254
- html_error("URL invalide", f"{e}"),
255
- pd.DataFrame(),
256
- pd.DataFrame(),
257
- pd.DataFrame()
258
- )
259
-
260
- # 2) API + download preview
261
  try:
262
- client = get_fs_client()
263
- audio_path, snd = download_preview_with_retry(client, sound_id)
264
  except Exception as e:
265
- return (
266
- html_error("Erreur FreeSound", f"Détail : <code>{e}</code>"),
267
- pd.DataFrame(),
268
- pd.DataFrame(),
269
- pd.DataFrame()
270
- )
271
-
272
- # 3) duration + model select
273
- try:
274
- duration = float(getattr(snd, "duration", None) or 0.0)
275
- if duration <= 0:
276
- duration = get_duration_seconds(audio_path)
277
- except Exception as e:
278
- return (
279
- html_error("Audio illisible", f"Impossible de lire la durée.<br>Détail : <code>{e}</code>"),
280
- pd.DataFrame(),
281
- pd.DataFrame(),
282
- pd.DataFrame()
283
- )
284
 
285
  if duration < MIN_EFFECT:
286
- return (
287
- html_error(
288
- "Audio trop court",
289
- f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
290
- f"Plages acceptées :<br>"
291
- f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
292
- f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
293
- ),
294
- pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
295
- )
296
-
297
  if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
298
- return (
299
- html_error(
300
- "Audio hors plage",
301
- f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
302
- f"Plages acceptées :<br>"
303
- f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
304
- f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
305
- ),
306
- pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
307
- )
308
 
309
  if duration <= MAX_EFFECT:
310
- badge = "🔊 Effet sonore (URL FreeSound → openSMILE)"
311
- model = MODEL_EFFECT
312
  else:
313
- badge = "🎵 Musique (URL FreeSound → openSMILE)"
314
- model = MODEL_MUSIC
315
 
316
- # 4) extract openSMILE features (AVANT)
317
  try:
318
- X_before = extract_opensmile_features(audio_path)
319
  except Exception as e:
320
- return (
321
- html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>"),
322
- pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
323
- )
324
 
325
- # 5) align features (APRÈS)
326
  try:
327
- expected = expected_feature_names(model)
328
- before_cols = list(X_before.columns)
329
- X_after = X_before.reindex(columns=expected, fill_value=0)
330
-
331
- missing_added = [c for c in expected if c not in before_cols]
332
- extras_dropped = [c for c in before_cols if c not in expected]
333
-
334
- diff_df = pd.DataFrame({
335
- "missing_added_(filled_0)": pd.Series(missing_added, dtype="object"),
336
- "extras_dropped": pd.Series(extras_dropped, dtype="object"),
337
- })
338
  except Exception as e:
339
- return (
340
- html_error("Alignement des features échoué", f"Détail : <code>{e}</code>"),
341
- pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
342
- )
343
 
344
- # 6) predict
345
  try:
346
- y = predict_with_dmatrix(model, X_after)
347
- y = np.array(y)
348
- avg_class = int(y[0, 0])
349
- dl_class = int(y[0, 1])
350
  except Exception as e:
351
- return (
352
- html_error("Prédiction échouée", f"Détail : <code>{e}</code>"),
353
- X_before, X_after, diff_df
354
- )
355
 
356
- rating_text = RATING_DISPLAY_AUDIO.get(avg_class, str(avg_class))
357
- downloads_text = DOWNLOADS_DISPLAY_AUDIO.get(dl_class, str(dl_class))
 
 
 
 
358
 
359
- conclusion = interpret_results(avg_class, dl_class)
360
  extra = f"""
361
- <div class="hint">ID FreeSound : <b>{sound_id}</b> · Preview téléchargé automatiquement</div>
362
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
363
- {conclusion}
364
  </div>
365
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
- return html_result(badge, duration, rating_text, downloads_text, extra_html=extra), X_before, X_after, diff_df
 
368
 
 
 
 
369
 
370
- # =========================
371
- # UI (fusion: 1 seule entrée URL)
372
- # =========================
373
- theme = gr.themes.Soft()
374
 
375
- with gr.Blocks(title="Prédiction popularité — URL FreeSound", css=CSS, theme=theme) as demo:
376
- gr.HTML(
377
- f"""
378
- <div id="header-title">Prédiction de popularité URL FreeSound</div>
379
- <p id="header-sub">
380
- ✅ Entrée = URL FreeSound → téléchargement preview → openSMILE → sélection auto du modèle → prédiction<br>
381
- <b>Durées acceptées :</b> 🔊 Effet sonore {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}–{MAX_MUSIC}s
382
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
 
386
- with gr.Row():
387
- with gr.Column(scale=1):
388
- url_in = gr.Textbox(
389
- label="URL FreeSound",
390
- placeholder="https://freesound.org/s/123456/",
391
- )
392
- btn = gr.Button("🚀 Prédire depuis l’URL", variant="primary")
393
 
394
- with gr.Column(scale=1):
395
- out_html = gr.HTML(label="Résultat")
 
 
396
 
397
- gr.Markdown("## Features")
398
- with gr.Row():
399
- feat_before = gr.Dataframe(label="Features AVANT (openSMILE raw)", wrap=True, max_rows=20)
400
- feat_after = gr.Dataframe(label="Features APRÈS (alignées modèle)", wrap=True, max_rows=20)
401
 
402
- diff_out = gr.Dataframe(label="Diff (manquantes ajoutées / extras supprimées)", wrap=True, max_rows=50)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
- btn.click(
405
- predict_from_freesound_url,
406
- inputs=[url_in],
407
- outputs=[out_html, feat_before, feat_after, diff_out],
408
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
  demo.launch()
 
1
  import os
 
 
2
  import tempfile
 
3
  import numpy as np
4
  import pandas as pd
5
  import gradio as gr
6
 
7
+ import joblib
 
8
  import soundfile as sf
9
  from pydub import AudioSegment
10
+ import opensmile
11
 
12
  import freesound
13
+ import xgboost as xgb
14
 
15
+ from sklearn.feature_extraction.text import HashingVectorizer
16
 
17
+
18
+ # ============================================================
19
  # CONFIG
20
+ # ============================================================
21
+ MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
22
+ MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
 
 
23
  SR_TARGET = 16000
24
 
25
+ # HF Space Secret: FREESOUND_TOKEN
26
+ FREESOUND_TOKEN = os.getenv("FREESOUND_TOKEN", "").strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 
29
 
30
+ def p(*parts):
31
+ return os.path.join(BASE_DIR, *parts)
32
 
33
+ def load_local(path_rel: str):
34
+ full = p(path_rel)
35
+ if not os.path.exists(full):
36
+ raise FileNotFoundError(f"Fichier introuvable: {path_rel}")
37
+ return joblib.load(full)
38
+
39
+
40
+ # ============================================================
41
+ # UI
42
+ # ============================================================
43
  CSS = """
44
+ .card { border: 1px solid #e5e7eb; background: #ffffff; padding: 16px; border-radius: 16px; }
45
+ .card-error{ border-color: #fca5a5; background: #fff1f2; }
46
+ .card-title{ font-weight: 950; margin-bottom: 8px; }
47
+ .badges{ display:flex; gap:10px; flex-wrap:wrap; margin-bottom:12px; }
48
+ .badge{ padding:6px 10px; border-radius:999px; font-weight:900; font-size: 13px; border: 1px solid #e5e7eb; }
49
+ .badge-type{ background:#eef2ff; color:#3730a3;}
50
+ .badge-time{ background:#ecfeff; color:#155e75;}
51
+ .grid{ display:grid; grid-template-columns: 1fr; gap:10px; }
52
+ .box{ border:1px solid #e5e7eb; border-radius:14px; padding:12px; background:#fafafa; }
53
+ .box-title{ font-weight:900; margin-bottom:4px; }
54
+ .box-value{ font-size:18px; font-weight:800; }
55
+ .hint{ margin-top:10px; color:#6b7280; font-size:12px; }
56
+ #header-title { font-size: 28px; font-weight: 950; margin-bottom: 6px; }
57
+ #header-sub { color:#6b7280; margin-top:0px; line-height:1.45; }
58
+ pre{ white-space:pre-wrap; }
59
  """
60
 
61
+ def html_error(title, body_html):
62
  return f"""
63
+ <div class="card card-error">
64
+ <div class="card-title">❌ {title}</div>
65
+ <div>{body_html}</div>
66
  </div>
67
+ """.strip()
68
 
69
+ def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""):
 
70
  return f"""
71
  <div class="card">
72
+ <div class="badges">
73
+ <span class="badge badge-type">{badge_text}</span>
74
+ <span class="badge badge-time">⏱️ {duration:.2f} s</span>
75
+ </div>
76
+ <div class="grid">
77
+ <div class="box">
78
+ <div class="box-title">📈 Popularité de la note moyenne</div>
79
+ <div class="box-value">{rating_text}</div>
80
+ </div>
81
+ <div class="box">
82
+ <div class="box-title">⬇️ Popularité des téléchargements</div>
83
+ <div class="box-value">{downloads_text}</div>
84
+ </div>
85
+ </div>
86
  {extra_html}
87
+ <div class="hint">Résultats en <b>niveaux</b> (faible / moyen / élevé), pas en valeurs exactes.</div>
88
  </div>
89
+ """.strip()
90
 
91
  def interpret_results(avg_class: int, dl_class: int) -> str:
92
  if avg_class == 0:
93
+ return "ℹ️ <b>Interprétation</b> :<br>Aucune évaluation possible (rating manquant)."
 
 
 
 
 
 
94
 
95
  if avg_class == 3 and dl_class == 2:
96
+ potentiel, detail = "très fort", "contenu de haute qualité et très populaire."
97
  elif avg_class == 3 and dl_class == 1:
98
+ potentiel, detail = "fort", "contenu bien apprécié, en croissance."
99
  elif avg_class == 3 and dl_class == 0:
100
+ potentiel, detail = "prometteur", "bonne qualité mais faible visibilité."
101
  elif avg_class == 2 and dl_class == 2:
102
+ potentiel, detail = "modéré à fort", "populaire mais qualité perçue moyenne."
103
  elif avg_class == 2 and dl_class == 1:
104
+ potentiel, detail = "modéré", "profil standard, popularité stable."
105
  elif avg_class == 2 and dl_class == 0:
106
+ potentiel, detail = "limité", "engagement faible, diffusion limitée."
107
  elif avg_class == 1 and dl_class == 2:
108
+ potentiel, detail = "contradictoire", "très téléchargé mais peu apprécié."
109
  elif avg_class == 1 and dl_class == 1:
110
+ potentiel, detail = "faible", "peu attractif."
111
  else:
112
+ potentiel, detail = "très faible", "faible intérêt global."
113
 
114
  return f"<b>Interprétation</b> :<br>Potentiel estimé : <b>{potentiel}</b> — {detail}"
115
 
116
+ def avg_label_to_class(avg_label: str) -> int:
117
+ if avg_label is None:
118
+ return 0
119
+ s = str(avg_label).strip().lower()
120
+ if "miss" in s or "missing" in s or "none" in s or "no" in s:
121
+ return 0
122
+ if "high" in s or "élev" in s or "eleve" in s:
123
+ return 3
124
+ if "medium" in s or "moy" in s:
125
+ return 2
126
+ if "low" in s or "faibl" in s:
127
+ return 1
128
+ return 0
129
+
130
+ def safe_float(v):
131
+ try:
132
+ return float(v)
133
+ except Exception:
134
+ return 0.0
135
 
136
+ def parse_sound_id(url: str):
137
+ return int(url.rstrip("/").split("/")[-1])
138
+
139
+
140
+ # ============================================================
141
+ # FREESOUND CLIENT
142
+ # ============================================================
143
+ def get_fs_client():
144
+ if not FREESOUND_TOKEN:
145
+ raise RuntimeError("Token FreeSound manquant. Ajoute le secret FREESOUND_TOKEN dans le Space.")
 
 
 
 
 
 
 
 
 
 
 
 
146
  c = freesound.FreesoundClient()
147
+ c.set_token(FREESOUND_TOKEN, "token")
148
  return c
149
 
150
+
151
+ # ============================================================
152
+ # PARTIE A Upload audio openSMILE → modèles
153
+ # (depuis app (2).py)
154
+ # ============================================================
155
+ MODEL_EFFECT_A = load_local("xgb_model_EffectSound.pkl")
156
+ MODEL_MUSIC_A = load_local("xgb_model_Music.pkl")
157
+
158
+ RATING_DISPLAY_AUDIO = {0: "❌ Informations manquantes", 1: "⭐ Faible", 2: "⭐⭐ Moyen", 3: "⭐⭐⭐ Élevé"}
159
+ DOWNLOADS_DISPLAY_AUDIO = {0: "⭐ Faible", 1: "⭐⭐ Moyen", 2: "⭐⭐⭐ Élevé"}
160
+
161
+ SMILE = opensmile.Smile(
162
+ feature_set=opensmile.FeatureSet.eGeMAPSv02,
163
+ feature_level=opensmile.FeatureLevel.Functionals,
164
+ )
165
+
166
+ def get_duration_seconds(filepath):
 
 
 
 
 
 
 
167
  ext = os.path.splitext(filepath)[1].lower()
168
  if ext == ".mp3":
169
  audio = AudioSegment.from_file(filepath)
 
171
  with sf.SoundFile(filepath) as f:
172
  return len(f) / f.samplerate
173
 
174
+ def to_wav_16k_mono(filepath):
175
  ext = os.path.splitext(filepath)[1].lower()
176
  if ext == ".wav":
177
  try:
 
180
  return filepath
181
  except Exception:
182
  pass
 
183
  audio = AudioSegment.from_file(filepath)
184
  audio = audio.set_channels(1).set_frame_rate(SR_TARGET)
185
  tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
 
187
  audio.export(tmp.name, format="wav")
188
  return tmp.name
189
 
190
+ def extract_opensmile_features(filepath):
191
  wav_path = to_wav_16k_mono(filepath)
192
  feats = SMILE.process_file(wav_path)
193
  feats = feats.select_dtypes(include=[np.number]).reset_index(drop=True)
194
  return feats
195
 
196
+ def predict_upload_with_dmatrix(model, X_df: pd.DataFrame):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  if hasattr(model, "estimators_"):
198
  preds = []
199
  for est in model.estimators_:
 
208
  p = booster.predict(dm)
209
  return np.asarray(p).reshape(1, -1)
210
 
211
+ def predict_opensmile_upload(audio_file):
212
+ if audio_file is None:
213
+ return html_error("Aucun fichier", "Veuillez importer un fichier audio (wav, mp3, flac…).")
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  try:
216
+ duration = get_duration_seconds(audio_file)
 
217
  except Exception as e:
218
+ return html_error("Audio illisible", f"Impossible de lire l'audio.<br>Détail : <code>{e}</code>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  if duration < MIN_EFFECT:
221
+ return html_error("Audio trop court",
222
+ f"Durée : <b>{duration:.2f}s</b><br>Accepté: 0.5–3s ou 10–60s")
 
 
 
 
 
 
 
 
 
223
  if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
224
+ return html_error("Audio hors plage",
225
+ f"Durée : <b>{duration:.2f}s</b><br>Accepté: 0.5–3s ou 10–60s")
 
 
 
 
 
 
 
 
226
 
227
  if duration <= MAX_EFFECT:
228
+ badge = "🔊 OpenSMILE (upload) EffectSound"
229
+ model = MODEL_EFFECT_A
230
  else:
231
+ badge = "🎵 OpenSMILE (upload) Music"
232
+ model = MODEL_MUSIC_A
233
 
 
234
  try:
235
+ X = extract_opensmile_features(audio_file)
236
  except Exception as e:
237
+ return html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>")
 
 
 
238
 
239
+ # Align features
240
  try:
241
+ expected = model.estimators_[0].feature_names_in_ if hasattr(model, "estimators_") else model.feature_names_in_
242
+ X = X.reindex(columns=list(expected), fill_value=0)
 
 
 
 
 
 
 
 
 
243
  except Exception as e:
244
+ return html_error("Alignement features échoué", f"Détail : <code>{e}</code>")
 
 
 
245
 
 
246
  try:
247
+ y = predict_upload_with_dmatrix(model, X)
 
 
 
248
  except Exception as e:
249
+ return html_error("Prédiction échouée", f"Détail : <code>{e}</code>")
 
 
 
250
 
251
+ y = np.array(y)
252
+ avg_class = int(y[0, 0])
253
+ dl_class = int(y[0, 1])
254
+
255
+ rating_text = RATING_DISPLAY_AUDIO.get(avg_class, "Inconnu")
256
+ downloads_text = DOWNLOADS_DISPLAY_AUDIO.get(dl_class, "Inconnu")
257
 
 
258
  extra = f"""
 
259
  <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
260
+ {interpret_results(avg_class, dl_class)}
261
  </div>
262
  """
263
+ return html_result(badge, duration, rating_text, downloads_text, extra_html=extra)
264
+
265
+
266
+ # ============================================================
267
+ # PARTIE B — FreeSound URL → “Acoustic features API fields” → modèles
268
+ # (depuis app (2).py / PARTIE B)
269
+ # ============================================================
270
+ xgb_music_num_B = load_local("xgb_num_downloads_music_model.pkl")
271
+ xgb_music_feat_num_B = load_local("xgb_num_downloads_music_features.pkl")
272
+ xgb_music_avg_B = load_local("xgb_avg_rating_music_model.pkl")
273
+ xgb_music_feat_avg_B = load_local("xgb_avg_rating_music_features.pkl")
274
+ le_music_avg_B = load_local("xgb_avg_rating_music_label_encoder.pkl")
275
+
276
+ xgb_effect_num_B = load_local("xgb_num_downloads_effectsound_model.pkl")
277
+ xgb_effect_feat_num_B = load_local("xgb_num_downloads_effectsound_features.pkl")
278
+ xgb_effect_avg_B = load_local("xgb_avg_rating_effectsound_model.pkl")
279
+ xgb_effect_feat_avg_B = load_local("xgb_avg_rating_effectsound_features.pkl")
280
+ le_effect_avg_B = load_local("xgb_avg_rating_effectsound_label_encoder.pkl")
281
+
282
+ NUM_DOWNLOADS_MAP_B = {0: "Faible", 1: "Moyen", 2: "Élevé"}
283
+
284
+ def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
285
+ row = []
286
+ for col in feat_list:
287
+ val = features_dict.get(col, 0)
288
+ if val is None or isinstance(val, (list, dict)):
289
+ val = 0
290
+ row.append(safe_float(val))
291
+
292
+ X = pd.DataFrame([row], columns=feat_list)
293
+ dmatrix = xgb.DMatrix(X.values, feature_names=feat_list)
294
 
295
+ booster = model.get_booster() if hasattr(model, "get_booster") else model
296
+ pred_int = int(booster.predict(dmatrix)[0])
297
 
298
+ if label_encoder is not None:
299
+ return label_encoder.inverse_transform([pred_int])[0]
300
+ return pred_int
301
 
302
+ def predict_freesound_acoustic_features(url: str):
303
+ if not url or not url.strip():
304
+ return html_error("URL vide", "Colle une URL du type <code>https://freesound.org/s/123456/</code>")
 
305
 
306
+ try:
307
+ sound_id = parse_sound_id(url)
308
+ except Exception:
309
+ return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
310
+
311
+ try:
312
+ fs_client = get_fs_client()
313
+ except Exception as e:
314
+ return html_error("Token FreeSound", str(e))
315
+
316
+ all_features = list(set(
317
+ xgb_music_feat_num_B + xgb_music_feat_avg_B + xgb_effect_feat_num_B + xgb_effect_feat_avg_B
318
+ ))
319
+ fields = "duration," + ",".join(all_features)
320
+
321
+ try:
322
+ results = fs_client.search(query="", filter=f"id:{sound_id}", fields=fields)
323
+ except Exception as e:
324
+ return html_error("Erreur API FreeSound", f"Détail : <code>{e}</code>")
325
+
326
+ if len(results.results) == 0:
327
+ return html_error("Son introuvable", "Aucun résultat pour cet ID.")
328
+
329
+ sound = results.results[0]
330
+ duration = safe_float(sound.get("duration", 0))
331
+
332
+ if MIN_EFFECT <= duration <= MAX_EFFECT:
333
+ badge = "🔊 FreeSound (API features acoustiques) — EffectSound"
334
+ dl_class = int(predict_with_model_fs(xgb_effect_num_B, sound, xgb_effect_feat_num_B))
335
+ avg_text = str(predict_with_model_fs(xgb_effect_avg_B, sound, xgb_effect_feat_avg_B, le_effect_avg_B))
336
+ dl_text = NUM_DOWNLOADS_MAP_B.get(dl_class, str(dl_class))
337
+
338
+ avg_class = avg_label_to_class(avg_text)
339
+ extra = f"""
340
+ <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
341
+ <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
342
+ {interpret_results(avg_class, dl_class)}
343
+ </div>
344
  """
345
+ return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
346
+
347
+ if MIN_MUSIC <= duration <= MAX_MUSIC:
348
+ badge = "🎵 FreeSound (API features acoustiques) — Music"
349
+ dl_class = int(predict_with_model_fs(xgb_music_num_B, sound, xgb_music_feat_num_B))
350
+ avg_text = str(predict_with_model_fs(xgb_music_avg_B, sound, xgb_music_feat_avg_B, le_music_avg_B))
351
+ dl_text = NUM_DOWNLOADS_MAP_B.get(dl_class, str(dl_class))
352
+
353
+ avg_class = avg_label_to_class(avg_text)
354
+ extra = f"""
355
+ <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
356
+ <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
357
+ {interpret_results(avg_class, dl_class)}
358
+ </div>
359
+ """
360
+ return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
361
+
362
+ return html_error("Durée non supportée",
363
+ f"Durée : <b>{duration:.2f}s</b><br>Accepté: 0.5–3s ou 10–60s")
364
+
365
+
366
+ # ============================================================
367
+ # PARTIE C — FreeSound URL → Metadata preprocessing lourd → modèles
368
+ # (depuis app (3).py)
369
+ # ============================================================
370
+
371
+ # ---- objets de preprocessing (local) ----
372
+ # Music
373
+ scaler_samplerate_music = load_local("music/scaler_music_samplerate.joblib")
374
+ scaler_age_days_music = load_local("music/scaler_music_age_days_log.joblib")
375
+ username_freq_music = load_local("music/username_freq_dict_music.joblib")
376
+ est_num_downloads_music = load_local("music/est_num_downloads_music.joblib")
377
+ avg_rating_transformer_music = load_local("music/avg_rating_transformer_music.joblib")
378
+ music_subcategory_cols = load_local("music/music_subcategory_cols.joblib")
379
+ music_onehot_cols = load_local("music/music_onehot_cols.joblib")
380
+ music_onehot_tags = load_local("music/music_onehot_tags.joblib")
381
+
382
+ # EffectSound
383
+ scaler_samplerate_effect = load_local("effectSound/scaler_effectSamplerate.joblib")
384
+ scaler_age_days_effect = load_local("effectSound/scaler_effectSound_age_days_log.joblib")
385
+ username_freq_effect = load_local("effectSound/username_freq_dict_effectSound.joblib")
386
+ est_num_downloads_effect = load_local("effectSound/est_num_downloads_effectSound.joblib")
387
+ avg_rating_transformer_effect = load_local("effectSound/avg_rating_transformer_effectSound.joblib")
388
+ effect_subcategory_cols = load_local("effectSound/effectSound_subcategory_cols.joblib")
389
+ effect_onehot_cols = load_local("effectSound/effectSound_onehot_cols.joblib")
390
+ effect_onehot_tags = load_local("effectSound/effect_onehot_tags.joblib")
391
+
392
+ # ---- modèles metadata (local) ----
393
+ music_model_num_downloads_C = load_local("music_model_num_downloads.joblib")
394
+ music_model_avg_rating_C = load_local("music_xgb_avg_rating.joblib")
395
+ music_avg_rating_le_C = load_local("music_xgb_avg_rating_label_encoder.joblib")
396
+ music_model_features_C = load_local("music_model_features_list.joblib")
397
+
398
+ effect_model_num_downloads_C = load_local("effectSound_model_num_downloads.joblib")
399
+ effect_model_avg_rating_C = load_local("effectSound_xgb_avg_rating.joblib")
400
+ effect_avg_rating_le_C = load_local("effectSound_xgb_avg_rating_label_encoder.joblib")
401
+ effect_model_features_C = load_local("effect_model_features_list.joblib")
402
+
403
+ # Dedup des listes (comme ton script)
404
+ music_model_features_C = list(dict.fromkeys(music_model_features_C))
405
+ effect_model_features_C = list(dict.fromkeys(effect_model_features_C))
406
+
407
+ # ---- GloVe local (optionnel) ----
408
+ # Mets un fichier local et indique son chemin via GLOVE_PATH si tu veux.
409
+ # Exemple: GLOVE_PATH="models/glove.kv"
410
+ GLOVE_PATH = os.getenv("GLOVE_PATH", "").strip()
411
+ glove_model = None
412
+
413
+ def try_load_glove():
414
+ global glove_model
415
+ if not GLOVE_PATH:
416
+ glove_model = None
417
+ return
418
+ full = p(GLOVE_PATH)
419
+ if not os.path.exists(full):
420
+ glove_model = None
421
+ return
422
+ try:
423
+ import gensim
424
+ from gensim.models import KeyedVectors
425
+ glove_model = KeyedVectors.load(full, mmap="r")
426
+ except Exception:
427
+ glove_model = None
428
+
429
+ try_load_glove()
430
+
431
+ def description_to_vec(text, model, dim=100):
432
+ if model is None or not text:
433
+ return np.zeros(dim, dtype=float)
434
+ words = text.lower().split()
435
+ vecs = [model[w] for w in words if w in model]
436
+ if len(vecs) == 0:
437
+ return np.zeros(dim, dtype=float)
438
+ return np.mean(vecs, axis=0)
439
+
440
+ def preprocess_name(df, vec_dim=8):
441
+ df = df.copy()
442
+ df["name_len"] = df["name_clean"].str.len()
443
+ vectorizer = HashingVectorizer(n_features=vec_dim, alternate_sign=False, norm=None)
444
+ name_vec_sparse = vectorizer.transform(df["name_clean"])
445
+ name_vec_df = pd.DataFrame(
446
+ name_vec_sparse.toarray(),
447
+ columns=[f"name_vec_{i}" for i in range(vec_dim)],
448
+ index=df.index
449
  )
450
+ df = pd.concat([df, name_vec_df], axis=1)
451
+ return df
452
+
453
+ def fetch_sound_metadata(fs_client, sound_url):
454
+ sound_id = parse_sound_id(sound_url)
455
+ sound = fs_client.get_sound(sound_id)
456
+ data = {
457
+ "id": sound_id,
458
+ "name": sound.name,
459
+ "num_ratings": getattr(sound, "num_ratings", 0),
460
+ "tags": ",".join(sound.tags) if getattr(sound, "tags", None) else "",
461
+ "username": getattr(sound, "username", ""),
462
+ "description": getattr(sound, "description", "") or "",
463
+ "created": getattr(sound, "created", ""),
464
+ "license": getattr(sound, "license", ""),
465
+ "num_downloads": getattr(sound, "num_downloads", 0),
466
+ "channels": getattr(sound, "channels", 0),
467
+ "filesize": getattr(sound, "filesize", 0),
468
+ "num_comments": getattr(sound, "num_comments", 0),
469
+ "category_is_user_provided": getattr(sound, "category_is_user_provided", 0),
470
+ "duration": getattr(sound, "duration", 0),
471
+ "avg_rating": getattr(sound, "avg_rating", 0),
472
+ "category": getattr(sound, "category", "Unknown"),
473
+ "subcategory": getattr(sound, "subcategory", "Other"),
474
+ "type": getattr(sound, "type", ""),
475
+ "samplerate": getattr(sound, "samplerate", 0)
476
+ }
477
+ return pd.DataFrame([data])
478
+
479
+ def preprocess_sound_metadata(df):
480
+ df = df.copy()
481
+ dur = float(df["duration"].iloc[0])
482
+
483
+ if MIN_EFFECT <= dur <= MAX_EFFECT:
484
+ dataset_type = "effectSound"
485
+ scaler_samplerate = scaler_samplerate_effect
486
+ scaler_age = scaler_age_days_effect
487
+ username_freq = username_freq_effect
488
+ est_num_downloads = est_num_downloads_effect
489
+ avg_rating_transformer = avg_rating_transformer_effect
490
+ subcat_cols = effect_subcategory_cols
491
+ onehot_cols = effect_onehot_cols
492
+ onehot_tags = effect_onehot_tags
493
+ elif MIN_MUSIC <= dur <= MAX_MUSIC:
494
+ dataset_type = "music"
495
+ scaler_samplerate = scaler_samplerate_music
496
+ scaler_age = scaler_age_days_music
497
+ username_freq = username_freq_music
498
+ est_num_downloads = est_num_downloads_music
499
+ avg_rating_transformer = avg_rating_transformer_music
500
+ subcat_cols = music_subcategory_cols
501
+ onehot_cols = music_onehot_cols
502
+ onehot_tags = music_onehot_tags
503
+ else:
504
+ return None, f"Durée hors plage ({dur:.2f}s)."
505
+
506
+ # Category bool
507
+ df["category_is_user_provided"] = df["category_is_user_provided"].astype(int)
508
+
509
+ # Username frequency
510
+ df["username_freq"] = df["username"].map(username_freq).fillna(0)
511
+
512
+ # Numeric log1p
513
+ for col in ["num_ratings", "num_comments", "filesize", "duration"]:
514
+ df[col] = np.log1p(df[col])
515
+
516
+ # samplerate scaled
517
+ df["samplerate"] = scaler_samplerate.transform(df[["samplerate"]])
518
+
519
+ # age_days
520
+ df["created"] = pd.to_datetime(df["created"], errors="coerce").dt.tz_localize(None)
521
+ df["age_days"] = (pd.Timestamp.now() - df["created"]).dt.days
522
+ df["age_days_log"] = np.log1p(df["age_days"])
523
+ df["age_days_log_scaled"] = scaler_age.transform(df[["age_days_log"]])
524
+ df = df.drop(columns=["created", "age_days", "age_days_log"], errors="ignore")
525
+
526
+ # num_downloads_class
527
+ df["num_downloads_class"] = est_num_downloads.transform(df[["num_downloads"]])
528
+
529
+ # avg_rating transform
530
+ df["avg_rating"] = avg_rating_transformer.transform(df["avg_rating"].to_numpy())
531
+
532
+ # Subcategory one-hot
533
+ for col in subcat_cols:
534
+ df[col] = 0
535
+ subcat_val = df["subcategory"].iloc[0]
536
+ for col in subcat_cols:
537
+ cat_name = col.replace("subcategory_", "")
538
+ if subcat_val == cat_name:
539
+ df[col] = 1
540
+ df.drop(columns=["subcategory"], inplace=True, errors="ignore")
541
+
542
+ # onehot fixed columns
543
+ for col in onehot_cols:
544
+ if col not in df.columns:
545
+ df[col] = 0
546
+
547
+ license_val = df.loc[0, "license"]
548
+ category_val = df.loc[0, "category"]
549
+ type_val = df.loc[0, "type"]
550
+
551
+ for col_name in [f"license_{license_val}", f"category_{category_val}", f"type_{type_val}"]:
552
+ if col_name in df.columns:
553
+ df[col_name] = 1
554
+
555
+ # Tags one-hot
556
+ for col in ["name", "tags", "description"]:
557
+ if col not in df.columns:
558
+ df[col] = ""
559
+ for col in onehot_tags:
560
+ if col not in df.columns:
561
+ df[col] = 0
562
+
563
+ tags_list = df["tags"].iloc[0].lower().split(",") if df["tags"].iloc[0] else []
564
+ for col in onehot_tags:
565
+ tag_name = col.replace("tag_", "").lower()
566
+ if tag_name in tags_list:
567
+ df[col] = 1
568
+ df.drop(columns=["tags"], inplace=True, errors="ignore")
569
+
570
+ # Name hashing
571
+ df["name_clean"] = df["name"].astype(str).str.lower().str.rsplit(".", n=1).str[0]
572
+ df = preprocess_name(df, vec_dim=8)
573
+ df.drop(columns=["name", "name_clean"], inplace=True, errors="ignore")
574
+
575
+ # Description vectors (GloVe local si dispo, sinon zeros)
576
+ desc_vec = description_to_vec(df["description"].iloc[0], glove_model, dim=100)
577
+ for i in range(100):
578
+ df[f"description_glove_{i}"] = float(desc_vec[i])
579
+ df.drop(columns=["description"], inplace=True, errors="ignore")
580
+
581
+ # drop unused raw cols
582
+ df.drop(columns=["license","category","type","subcategory","id","num_downloads","file_path","username"],
583
+ inplace=True, errors="ignore")
584
+
585
+ return df, dataset_type
586
+
587
+ def predict_with_model_df(model, df_input, model_features, le=None):
588
+ booster_feats = model.get_booster().feature_names
589
+ X_aligned = df_input.reindex(columns=booster_feats, fill_value=0.0).astype(float)
590
+ dmatrix = xgb.DMatrix(X_aligned.values, feature_names=booster_feats)
591
+ preds = model.get_booster().predict(dmatrix)
592
+ pred_val = preds[0]
593
+ if len(preds.shape) > 1 and preds.shape[1] > 1:
594
+ pred_int = int(np.argmax(pred_val))
595
+ else:
596
+ pred_int = int(round(float(pred_val)))
597
+ if le is not None:
598
+ try:
599
+ return le.inverse_transform([pred_int])[0]
600
+ except Exception:
601
+ return f"Classe inconnue ({pred_int})"
602
+ return pred_int
603
 
604
+ def predict_freesound_metadata(url: str, show_debug: bool):
605
+ if not url or not url.strip():
606
+ return html_error("URL vide", "Colle une URL du type <code>https://freesound.org/s/123456/</code>")
 
 
 
 
607
 
608
+ try:
609
+ sound_id = parse_sound_id(url)
610
+ except Exception:
611
+ return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
612
 
613
+ try:
614
+ fs_client = get_fs_client()
615
+ except Exception as e:
616
+ return html_error("Token FreeSound", str(e))
617
 
618
+ try:
619
+ df_raw = fetch_sound_metadata(fs_client, url)
620
+ except Exception as e:
621
+ return html_error("Erreur API FreeSound", f"Détail : <code>{e}</code>")
622
+
623
+ dur = float(df_raw["duration"].iloc[0])
624
+ if dur < MIN_EFFECT:
625
+ return html_error("Son trop court", f"Durée {dur:.2f}s. Accepté: 0.5–3s ou 10–60s")
626
+ if (MAX_EFFECT < dur < MIN_MUSIC) or dur > MAX_MUSIC:
627
+ return html_error("Son hors plage", f"Durée {dur:.2f}s. Accepté: 0.5–3s ou 10–60s")
628
+
629
+ df_processed, dataset_type = preprocess_sound_metadata(df_raw)
630
+ if df_processed is None:
631
+ return html_error("Preprocessing metadata", "Impossible de prétraiter (durée hors plage).")
632
+
633
+ # Choix modèles / features selon type
634
+ if dataset_type == "effectSound":
635
+ badge = "🔊 FreeSound (metadata) — EffectSound"
636
+ model_nd = effect_model_num_downloads_C
637
+ model_ar = effect_model_avg_rating_C
638
+ model_features = effect_model_features_C
639
+ le = effect_avg_rating_le_C
640
+ else:
641
+ badge = "🎵 FreeSound (metadata) — Music"
642
+ model_nd = music_model_num_downloads_C
643
+ model_ar = music_model_avg_rating_C
644
+ model_features = music_model_features_C
645
+ le = music_avg_rating_le_C
646
+
647
+ # IMPORTANT: tu faisais drop avg_rating + num_downloads_class avant le modèle
648
+ cols_to_remove = ["avg_rating", "num_downloads_class"]
649
+ df_for_model = df_processed.drop(columns=[c for c in cols_to_remove if c in df_processed.columns], errors="ignore")
650
+
651
+ # Forcer exactement les colonnes du modèle
652
+ df_for_model = df_for_model.reindex(columns=model_features, fill_value=0.0).astype(float)
653
+
654
+ pred_num_downloads_val = predict_with_model_df(model_nd, df_for_model, model_features, le=None)
655
+ num_map = {0: "Low", 1: "Medium", 2: "High"}
656
+ pred_num_downloads = num_map.get(pred_num_downloads_val, str(pred_num_downloads_val))
657
+
658
+ pred_avg_rating = predict_with_model_df(model_ar, df_for_model, model_features, le=le)
659
+ avg_class = avg_label_to_class(pred_avg_rating)
660
+ dl_class_for_interp = {"Low":0,"Medium":1,"High":2}.get(pred_num_downloads, 1)
661
+
662
+ debug_html = ""
663
+ if show_debug:
664
+ raw_txt = "\n".join([f"{c}: {df_raw.loc[0,c]}" for c in df_raw.columns])
665
+ proc_txt = "\n".join([f"{c}: {df_processed.loc[0,c]}" for c in df_processed.columns[:120]]) # limite affichage
666
+ glove_note = "OK" if glove_model is not None else "ABSENT (vecteurs à 0)"
667
+ debug_html = f"""
668
+ <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
669
+ <div class="hint"><b>Debug</b> — GloVe: <b>{glove_note}</b></div>
670
+ <details><summary>Voir métadonnées brutes</summary><pre>{raw_txt}</pre></details>
671
+ <details><summary>Voir features après preprocessing (aperçu)</summary><pre>{proc_txt}</pre></details>
672
+ </div>
673
+ """
674
 
675
+ extra = f"""
676
+ <div class="hint">ID FreeSound : <b>{sound_id}</b></div>
677
+ <div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
678
+ {interpret_results(avg_class, dl_class_for_interp)}
679
+ </div>
680
+ {debug_html}
681
+ """
682
+ return html_result(badge, dur, str(pred_avg_rating), str(pred_num_downloads), extra_html=extra)
683
+
684
+
685
+ # ============================================================
686
+ # GRADIO APP (3 onglets)
687
+ # ============================================================
688
+ with gr.Blocks(title="Popularité FreeSound — 3 pipelines", css=CSS, theme=gr.themes.Soft()) as demo:
689
+ gr.HTML(f"""
690
+ <div id="header-title">Popularité FreeSound — 3 pipelines</div>
691
+ <p id="header-sub">
692
+ <b>A)</b> Upload audio → <b>OpenSMILE</b><br>
693
+ <b>B)</b> URL FreeSound → <b>Features acoustiques via API fields</b><br>
694
+ <b>C)</b> URL FreeSound → <b>Metadata + preprocessing</b><br><br>
695
+ <b>Durées acceptées :</b> 🔊 {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 {MIN_MUSIC}–{MAX_MUSIC}s
696
+ </p>
697
+ """)
698
+
699
+ with gr.Tabs():
700
+ with gr.Tab("A) Upload → OpenSMILE"):
701
+ with gr.Row():
702
+ with gr.Column():
703
+ audio_in = gr.Audio(type="filepath", label="Fichier audio")
704
+ btn = gr.Button("🚀 Prédire (OpenSMILE)", variant="primary")
705
+ with gr.Column():
706
+ out = gr.HTML()
707
+ btn.click(predict_opensmile_upload, inputs=audio_in, outputs=out)
708
+
709
+ with gr.Tab("B) URL → Features acoustiques (API)"):
710
+ with gr.Row():
711
+ with gr.Column():
712
+ url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
713
+ btn = gr.Button("🚀 Prédire (Features API)", variant="primary")
714
+ with gr.Column():
715
+ out = gr.HTML()
716
+ btn.click(predict_freesound_acoustic_features, inputs=url_in, outputs=out)
717
+
718
+ with gr.Tab("C) URL → Metadata (prétraitement)"):
719
+ with gr.Row():
720
+ with gr.Column():
721
+ url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
722
+ show_debug = gr.Checkbox(label="Afficher debug (brut + aperçu features)", value=False)
723
+ btn = gr.Button("🚀 Prédire (Metadata)", variant="primary")
724
+ with gr.Column():
725
+ out = gr.HTML()
726
+ btn.click(predict_freesound_metadata, inputs=[url_in, show_debug], outputs=out)
727
 
728
  demo.launch()