IKRAMELHADI commited on
Commit
f11f89f
·
1 Parent(s): d2ffb31

UI FR Gradio6

Browse files
Files changed (1) hide show
  1. app.py +278 -165
app.py CHANGED
@@ -8,45 +8,145 @@ import joblib
8
  import soundfile as sf
9
  from pydub import AudioSegment
10
  import opensmile
 
 
11
  import xgboost as xgb
12
 
13
 
14
  # =========================
15
- # CONFIG
16
  # =========================
17
- SR_TARGET = 16000
18
-
19
  MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
20
  MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
 
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  MODEL_EFFECT_PATH = "xgb_model_EffectSound.pkl"
23
  MODEL_MUSIC_PATH = "xgb_model_Music.pkl"
24
 
25
- SMILE = opensmile.Smile(
26
- feature_set=opensmile.FeatureSet.eGeMAPSv02,
27
- feature_level=opensmile.FeatureLevel.Functionals,
28
- )
29
-
30
  MODEL_EFFECT = joblib.load(MODEL_EFFECT_PATH)
31
  MODEL_MUSIC = joblib.load(MODEL_MUSIC_PATH)
32
 
33
- # Classes -> affichage (comme tu veux)
34
- RATING_DISPLAY = {
35
  0: "Informations manquantes",
36
  1: "⭐ Faible",
37
  2: "⭐⭐ Moyen",
38
  3: "⭐⭐⭐ Élevé",
39
  }
40
- DOWNLOADS_DISPLAY = {
41
  0: "Faible",
42
  1: "Moyen",
43
  2: "Élevé",
44
  }
45
 
 
 
 
 
 
46
 
47
- # =========================
48
- # AUDIO HELPERS
49
- # =========================
50
  def get_duration_seconds(filepath):
51
  ext = os.path.splitext(filepath)[1].lower()
52
  if ext == ".mp3":
@@ -82,65 +182,20 @@ def extract_opensmile_features(filepath):
82
  return feats
83
 
84
 
85
- # =========================
86
- # SAFE MULTIOUTPUT PREDICT
87
- # =========================
88
- def predict_multioutput(model, X_df):
89
- preds = []
90
- for est in model.estimators_:
91
- try:
92
  p = est.predict(X_df)
93
- except Exception:
94
- booster = est.get_booster()
95
- dm = xgb.DMatrix(X_df, feature_names=list(X_df.columns))
96
- p = booster.predict(dm)
97
- preds.append(np.asarray(p).reshape(-1))
98
- return np.column_stack(preds)
99
 
100
 
101
- # =========================
102
- # UI HELPERS (HTML)
103
- # =========================
104
- def html_error(title, body_html):
105
- return f"""
106
- <div class="card card-error">
107
- <div class="card-title">❌ {title}</div>
108
- <div class="card-body">{body_html}</div>
109
- </div>
110
- """.strip()
111
-
112
-
113
- def html_result(audio_type, duration, rating_text, downloads_text):
114
- badge = "🎵 Musique" if audio_type == "Music" else "🔊 Effet sonore"
115
- return f"""
116
- <div class="card">
117
- <div class="badges">
118
- <span class="badge badge-type">{badge}</span>
119
- <span class="badge badge-time">⏱️ {duration:.2f} s</span>
120
- </div>
121
-
122
- <div class="grid">
123
- <div class="box">
124
- <div class="box-title">📈 Note moyenne (popularité)</div>
125
- <div class="box-value">{rating_text}</div>
126
- </div>
127
- <div class="box">
128
- <div class="box-title">⬇️ Téléchargements (popularité)</div>
129
- <div class="box-value">{downloads_text}</div>
130
- </div>
131
- </div>
132
-
133
- <div class="hint">
134
- Résultats affichés en <b>niveaux</b> (faible / moyen / élevé), pas en valeurs exactes.
135
- </div>
136
- </div>
137
- """.strip()
138
-
139
-
140
- # =========================
141
- # MAIN PREDICTION
142
- # =========================
143
- def predict_popularity(audio_file):
144
  if audio_file is None:
145
  return html_error("Aucun fichier", "Veuillez importer un fichier audio (wav, mp3, flac…).")
146
 
@@ -150,7 +205,7 @@ def predict_popularity(audio_file):
150
  except Exception as e:
151
  return html_error("Audio illisible", f"Impossible de lire l'audio.<br>Détail : <code>{e}</code>")
152
 
153
- # Vérif plages
154
  if duration < MIN_EFFECT:
155
  return html_error(
156
  "Audio trop court",
@@ -171,10 +226,10 @@ def predict_popularity(audio_file):
171
 
172
  # Type + modèle
173
  if duration <= MAX_EFFECT:
174
- audio_type = "SoundEffect"
175
  model = MODEL_EFFECT
176
  else:
177
- audio_type = "Music"
178
  model = MODEL_MUSIC
179
 
180
  # Features openSMILE
@@ -183,121 +238,179 @@ def predict_popularity(audio_file):
183
  except Exception as e:
184
  return html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>")
185
 
186
- # Align colonnes
187
  try:
188
- expected = model.estimators_[0].feature_names_in_
189
- X = X.reindex(columns=expected, fill_value=0)
190
  except Exception as e:
191
  return html_error("Alignement des features échoué", f"Détail : <code>{e}</code>")
192
 
193
  # Predict
194
  try:
195
- y = predict_multioutput(model, X)
196
  except Exception as e:
197
  return html_error("Prédiction échouée", f"Détail : <code>{e}</code>")
198
 
199
- avg_class = int(y[0, 0])
200
- dl_class = int(y[0, 1])
 
 
 
 
 
201
 
202
- rating_text = RATING_DISPLAY.get(avg_class, "Inconnu")
203
- downloads_text = DOWNLOADS_DISPLAY.get(dl_class, "Inconnu")
204
 
205
- return html_result(audio_type, duration, rating_text, downloads_text)
206
 
207
 
208
- # =========================
209
- # UI
210
- # =========================
211
- theme = gr.themes.Soft()
212
 
213
- css = """
214
- /* Layout */
215
- .card {
216
- border: 1px solid #e5e7eb;
217
- background: #ffffff;
218
- padding: 16px;
219
- border-radius: 16px;
220
- }
221
- .card-error{
222
- border-color: #fca5a5;
223
- background: #fff1f2;
224
- }
225
- .card-title{
226
- font-weight: 800;
227
- margin-bottom: 8px;
228
- }
229
- .card-body{
230
- color: #7f1d1d;
231
- line-height: 1.45;
232
- }
233
- .badges{
234
- display:flex;
235
- gap:10px;
236
- flex-wrap:wrap;
237
- margin-bottom:12px;
238
- }
239
- .badge{
240
- padding:6px 10px;
241
- border-radius:999px;
242
- font-weight:700;
243
- font-size: 13px;
244
- border: 1px solid #e5e7eb;
245
- }
246
- .badge-type{ background:#eef2ff; color:#3730a3;}
247
- .badge-time{ background:#ecfeff; color:#155e75;}
248
 
249
- .grid{
250
- display:grid;
251
- grid-template-columns: 1fr;
252
- gap:10px;
253
- }
254
- .box{
255
- border:1px solid #e5e7eb;
256
- border-radius:14px;
257
- padding:12px;
258
- background:#fafafa;
259
- }
260
- .box-title{ font-weight:800; margin-bottom:4px; }
261
- .box-value{ font-size:18px; }
262
 
263
- .hint{
264
- margin-top:10px;
265
- color:#6b7280;
266
- font-size:12px;
267
- }
268
 
269
- #header-title { font-size: 28px; font-weight: 900; margin-bottom: 6px; }
270
- #header-sub { color:#6b7280; margin-top:0px; line-height:1.45; }
271
- """
 
 
 
 
 
 
 
 
 
 
 
272
 
273
- with gr.Blocks(title="Prédiction de popularité audio") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  gr.HTML(
275
  f"""
276
- <div id="header-title">🎧 Prédiction de popularité audio</div>
277
  <p id="header-sub">
278
- Importez un audio. Si la durée est valide, nous extrayons des caractéristiques acoustiques
279
- (openSMILE eGeMAPS) puis nous prédisons des <b>niveaux de popularité</b> pour la note moyenne et les téléchargements.
280
- <br><br>
281
  <b>Durées acceptées :</b> 🔊 Effet sonore {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}–{MAX_MUSIC}s
282
  </p>
283
  """
284
  )
285
 
286
- with gr.Row():
287
- with gr.Column(scale=1):
288
- gr.Markdown("### 1) Importer un fichier")
289
- audio_in = gr.Audio(type="filepath", label="Fichier audio")
290
- btn = gr.Button("🚀 Lancer la prédiction", variant="primary")
291
- gr.Markdown(
292
- """
293
- **Conseil :** utilisez un extrait clair (bonne qualité audio) pour une meilleure extraction de features.
294
- """
295
- )
296
-
297
- with gr.Column(scale=1):
298
- gr.Markdown("### 2) Résultat")
299
- out = gr.HTML(value="")
300
-
301
- btn.click(predict_popularity, inputs=audio_in, outputs=out)
302
-
303
- demo.launch(theme=theme, css=css)
 
 
 
 
 
 
 
8
  import soundfile as sf
9
  from pydub import AudioSegment
10
  import opensmile
11
+
12
+ import freesound
13
  import xgboost as xgb
14
 
15
 
16
  # =========================
17
+ # RÈGLES DURÉE
18
  # =========================
 
 
19
  MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
20
  MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
21
+ SR_TARGET = 16000
22
 
23
+
24
+ # =========================
25
+ # UI (CSS)
26
+ # =========================
27
+ CSS = """
28
+ .card {
29
+ border: 1px solid #e5e7eb;
30
+ background: #ffffff;
31
+ padding: 16px;
32
+ border-radius: 16px;
33
+ }
34
+ .card-error{
35
+ border-color: #fca5a5;
36
+ background: #fff1f2;
37
+ }
38
+ .card-title{
39
+ font-weight: 950;
40
+ margin-bottom: 8px;
41
+ }
42
+ .badges{
43
+ display:flex;
44
+ gap:10px;
45
+ flex-wrap:wrap;
46
+ margin-bottom:12px;
47
+ }
48
+ .badge{
49
+ padding:6px 10px;
50
+ border-radius:999px;
51
+ font-weight:900;
52
+ font-size: 13px;
53
+ border: 1px solid #e5e7eb;
54
+ }
55
+ .badge-type{ background:#eef2ff; color:#3730a3;}
56
+ .badge-time{ background:#ecfeff; color:#155e75;}
57
+
58
+ .grid{
59
+ display:grid;
60
+ grid-template-columns: 1fr;
61
+ gap:10px;
62
+ }
63
+ .box{
64
+ border:1px solid #e5e7eb;
65
+ border-radius:14px;
66
+ padding:12px;
67
+ background:#fafafa;
68
+ }
69
+ .box-title{ font-weight:900; margin-bottom:4px; }
70
+ .box-value{ font-size:18px; font-weight:800; }
71
+
72
+ .hint{
73
+ margin-top:10px;
74
+ color:#6b7280;
75
+ font-size:12px;
76
+ }
77
+
78
+ #header-title { font-size: 28px; font-weight: 950; margin-bottom: 6px; }
79
+ #header-sub { color:#6b7280; margin-top:0px; line-height:1.45; }
80
+ """
81
+
82
+
83
+ def html_error(title, body_html):
84
+ return f"""
85
+ <div class="card card-error">
86
+ <div class="card-title">❌ {title}</div>
87
+ <div>{body_html}</div>
88
+ </div>
89
+ """.strip()
90
+
91
+
92
+ def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""):
93
+ return f"""
94
+ <div class="card">
95
+ <div class="badges">
96
+ <span class="badge badge-type">{badge_text}</span>
97
+ <span class="badge badge-time">⏱️ {duration:.2f} s</span>
98
+ </div>
99
+
100
+ <div class="grid">
101
+ <div class="box">
102
+ <div class="box-title">📈 Popularité de la note moyenne</div>
103
+ <div class="box-value">{rating_text}</div>
104
+ </div>
105
+ <div class="box">
106
+ <div class="box-title">⬇️ Popularité des téléchargements</div>
107
+ <div class="box-value">{downloads_text}</div>
108
+ </div>
109
+ </div>
110
+
111
+ {extra_html}
112
+
113
+ <div class="hint">
114
+ Résultats affichés en <b>niveaux</b> (faible / moyen / élevé), pas en valeurs exactes.
115
+ </div>
116
+ </div>
117
+ """.strip()
118
+
119
+
120
+ # ============================================================
121
+ # PARTIE A — Upload audio → openSMILE → modèles (toi)
122
+ # ============================================================
123
+
124
+ # Tes modèles upload
125
  MODEL_EFFECT_PATH = "xgb_model_EffectSound.pkl"
126
  MODEL_MUSIC_PATH = "xgb_model_Music.pkl"
127
 
 
 
 
 
 
128
  MODEL_EFFECT = joblib.load(MODEL_EFFECT_PATH)
129
  MODEL_MUSIC = joblib.load(MODEL_MUSIC_PATH)
130
 
131
+ # Mapping (sans very high)
132
+ RATING_DISPLAY_AUDIO = {
133
  0: "Informations manquantes",
134
  1: "⭐ Faible",
135
  2: "⭐⭐ Moyen",
136
  3: "⭐⭐⭐ Élevé",
137
  }
138
+ DOWNLOADS_DISPLAY_AUDIO = {
139
  0: "Faible",
140
  1: "Moyen",
141
  2: "Élevé",
142
  }
143
 
144
+ SMILE = opensmile.Smile(
145
+ feature_set=opensmile.FeatureSet.eGeMAPSv02,
146
+ feature_level=opensmile.FeatureLevel.Functionals,
147
+ )
148
+
149
 
 
 
 
150
  def get_duration_seconds(filepath):
151
  ext = os.path.splitext(filepath)[1].lower()
152
  if ext == ".mp3":
 
182
  return feats
183
 
184
 
185
+ def predict_multioutput_safely(model, X_df):
186
+ """
187
+ Supporte MultiOutput wrapper (estimators_) en gardant les feature names.
188
+ """
189
+ if hasattr(model, "estimators_"):
190
+ preds = []
191
+ for est in model.estimators_:
192
  p = est.predict(X_df)
193
+ preds.append(np.asarray(p).reshape(-1))
194
+ return np.column_stack(preds)
195
+ return model.predict(X_df)
 
 
 
196
 
197
 
198
+ def predict_from_uploaded_audio(audio_file):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  if audio_file is None:
200
  return html_error("Aucun fichier", "Veuillez importer un fichier audio (wav, mp3, flac…).")
201
 
 
205
  except Exception as e:
206
  return html_error("Audio illisible", f"Impossible de lire l'audio.<br>Détail : <code>{e}</code>")
207
 
208
+ # Vérif durées
209
  if duration < MIN_EFFECT:
210
  return html_error(
211
  "Audio trop court",
 
226
 
227
  # Type + modèle
228
  if duration <= MAX_EFFECT:
229
+ badge = "🔊 Effet sonore (upload)"
230
  model = MODEL_EFFECT
231
  else:
232
+ badge = "🎵 Musique (upload)"
233
  model = MODEL_MUSIC
234
 
235
  # Features openSMILE
 
238
  except Exception as e:
239
  return html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>")
240
 
241
+ # Align features
242
  try:
243
+ expected = model.estimators_[0].feature_names_in_ if hasattr(model, "estimators_") else model.feature_names_in_
244
+ X = X.reindex(columns=list(expected), fill_value=0)
245
  except Exception as e:
246
  return html_error("Alignement des features échoué", f"Détail : <code>{e}</code>")
247
 
248
  # Predict
249
  try:
250
+ y = predict_multioutput_safely(model, X)
251
  except Exception as e:
252
  return html_error("Prédiction échouée", f"Détail : <code>{e}</code>")
253
 
254
+ y = np.array(y)
255
+ if y.ndim == 2:
256
+ avg_class = int(y[0, 0])
257
+ dl_class = int(y[0, 1])
258
+ else:
259
+ avg_class = int(y[0])
260
+ dl_class = int(y[1])
261
 
262
+ rating_text = RATING_DISPLAY_AUDIO.get(avg_class, "Inconnu")
263
+ downloads_text = DOWNLOADS_DISPLAY_AUDIO.get(dl_class, "Inconnu")
264
 
265
+ return html_result(badge, duration, rating_text, downloads_text)
266
 
267
 
268
+ # ============================================================
269
+ # PARTIE B — URL FreeSound → API → modèles (collègue)
270
+ # ============================================================
 
271
 
272
+ # ⚠️ Ici tu écris ton token (ou tu utilises Secrets si tu veux plus tard)
273
+ API_TOKEN = "A ECRIRE"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ fs_client = freesound.FreesoundClient()
276
+ fs_client.set_token(API_TOKEN, "token")
 
 
 
 
 
 
 
 
 
 
 
277
 
 
 
 
 
 
278
 
279
+ # Modèles collègue
280
+ # Music
281
+ xgb_music_num = joblib.load("xgb_num_downloads_music_model.pkl")
282
+ xgb_music_feat_num = joblib.load("xgb_num_downloads_music_features.pkl")
283
+ xgb_music_avg = joblib.load("xgb_avg_rating_music_model.pkl")
284
+ xgb_music_feat_avg = joblib.load("xgb_avg_rating_music_features.pkl")
285
+ le_music_avg = joblib.load("xgb_avg_rating_music_label_encoder.pkl")
286
+
287
+ # Effect Sound
288
+ xgb_effect_num = joblib.load("xgb_num_downloads_effectsound_model.pkl")
289
+ xgb_effect_feat_num = joblib.load("xgb_num_downloads_effectsound_features.pkl")
290
+ xgb_effect_avg = joblib.load("xgb_avg_rating_effectsound_model.pkl")
291
+ xgb_effect_feat_avg = joblib.load("xgb_avg_rating_effectsound_features.pkl")
292
+ le_effect_avg = joblib.load("xgb_avg_rating_effectsound_label_encoder.pkl")
293
 
294
+ NUM_DOWNLOADS_MAP = {0: "Faible", 1: "Moyen", 2: "Élevé"}
295
+
296
+
297
+ def safe_float(v):
298
+ try:
299
+ return float(v)
300
+ except Exception:
301
+ return 0.0
302
+
303
+
304
+ def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
305
+ row = []
306
+ for col in feat_list:
307
+ val = features_dict.get(col, 0)
308
+ if val is None or isinstance(val, (list, dict)):
309
+ val = 0
310
+ row.append(safe_float(val))
311
+
312
+ X = pd.DataFrame([row], columns=feat_list)
313
+ dmatrix = xgb.DMatrix(X.values, feature_names=feat_list)
314
+
315
+ pred_int = int(model.get_booster().predict(dmatrix)[0])
316
+
317
+ if label_encoder is not None:
318
+ return label_encoder.inverse_transform([pred_int])[0]
319
+ return pred_int
320
+
321
+
322
+ def predict_from_freesound_url(url: str):
323
+ if not url or not url.strip():
324
+ return html_error("URL vide", "Collez une URL FreeSound du type <code>https://freesound.org/s/123456/</code>")
325
+
326
+ # Parse sound_id
327
+ try:
328
+ sound_id = int(url.rstrip("/").split("/")[-1])
329
+ except Exception:
330
+ return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
331
+
332
+ # Champs à récupérer
333
+ all_features = list(set(
334
+ xgb_music_feat_num + xgb_music_feat_avg + xgb_effect_feat_num + xgb_effect_feat_avg
335
+ ))
336
+ fields = "duration," + ",".join(all_features)
337
+
338
+ try:
339
+ results = fs_client.search(query="", filter=f"id:{sound_id}", fields=fields)
340
+ except Exception as e:
341
+ return html_error("Erreur API FreeSound", f"Détail : <code>{e}</code>")
342
+
343
+ if len(results.results) == 0:
344
+ return html_error("Son introuvable", "Aucun résultat pour cet ID.")
345
+
346
+ sound = results.results[0]
347
+ duration = safe_float(sound.get("duration", 0))
348
+
349
+ # Vérif durées + type
350
+ if MIN_EFFECT <= duration <= MAX_EFFECT:
351
+ badge = "🔊 Effet sonore (FreeSound URL)"
352
+ num = predict_with_model_fs(xgb_effect_num, sound, xgb_effect_feat_num)
353
+ avg = predict_with_model_fs(xgb_effect_avg, sound, xgb_effect_feat_avg, le_effect_avg)
354
+ downloads_text = NUM_DOWNLOADS_MAP.get(num, str(num))
355
+ rating_text = str(avg)
356
+ extra = f'<div class="hint">ID FreeSound : <b>{sound_id}</b></div>'
357
+ return html_result(badge, duration, rating_text, downloads_text, extra_html=extra)
358
+
359
+ if MIN_MUSIC <= duration <= MAX_MUSIC:
360
+ badge = "🎵 Musique (FreeSound URL)"
361
+ num = predict_with_model_fs(xgb_music_num, sound, xgb_music_feat_num)
362
+ avg = predict_with_model_fs(xgb_music_avg, sound, xgb_music_feat_avg, le_music_avg)
363
+ downloads_text = NUM_DOWNLOADS_MAP.get(num, str(num))
364
+ rating_text = str(avg)
365
+ extra = f'<div class="hint">ID FreeSound : <b>{sound_id}</b></div>'
366
+ return html_result(badge, duration, rating_text, downloads_text, extra_html=extra)
367
+
368
+ return html_error(
369
+ "Durée non supportée",
370
+ f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
371
+ f"Plages acceptées :<br>"
372
+ f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
373
+ f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
374
+ )
375
+
376
+
377
+ # =========================
378
+ # APP UI (2 onglets)
379
+ # =========================
380
+ theme = gr.themes.Soft()
381
+
382
+ with gr.Blocks(title="Démo — Popularité Audio", css=CSS) as demo:
383
  gr.HTML(
384
  f"""
385
+ <div id="header-title">🎧 Démo — Prédiction de popularité audio</div>
386
  <p id="header-sub">
387
+ Deux modes : <b>Upload audio</b> (openSMILE) ou <b>URL FreeSound</b> (features API).<br><br>
 
 
388
  <b>Durées acceptées :</b> 🔊 Effet sonore {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}–{MAX_MUSIC}s
389
  </p>
390
  """
391
  )
392
 
393
+ with gr.Tabs():
394
+ with gr.Tab("1) Upload audio (openSMILE)"):
395
+ with gr.Row():
396
+ with gr.Column(scale=1):
397
+ gr.Markdown("### Importer un fichier")
398
+ audio_in = gr.Audio(type="filepath", label="Fichier audio")
399
+ btn_audio = gr.Button("🚀 Prédire (upload)", variant="primary")
400
+ with gr.Column(scale=1):
401
+ gr.Markdown("### Résultat")
402
+ out_audio = gr.HTML()
403
+ btn_audio.click(predict_from_uploaded_audio, inputs=audio_in, outputs=out_audio)
404
+
405
+ with gr.Tab("2) URL FreeSound (features API)"):
406
+ with gr.Row():
407
+ with gr.Column(scale=1):
408
+ gr.Markdown("### Coller une URL FreeSound")
409
+ url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
410
+ btn_url = gr.Button("🚀 Prédire (URL FreeSound)", variant="primary")
411
+ with gr.Column(scale=1):
412
+ gr.Markdown("### Résultat")
413
+ out_url = gr.HTML()
414
+ btn_url.click(predict_from_freesound_url, inputs=url_in, outputs=out_url)
415
+
416
+ demo.launch(theme=theme)