IKRAMELHADI commited on
Commit
97483f5
·
1 Parent(s): 49de9df

testtest5

Browse files
app.py CHANGED
@@ -1,313 +1,410 @@
1
- # freesound_preprocess_ui.py
2
- # -*- coding: utf-8 -*-
3
-
4
  import os
5
  import re
6
  import time
7
- import urllib.parse
8
- from typing import Any, Dict, Tuple, Optional, List
9
-
10
  import numpy as np
11
  import pandas as pd
12
- import requests
13
  import gradio as gr
14
 
15
- from sklearn.feature_extraction.text import TfidfVectorizer
16
- from sklearn.preprocessing import StandardScaler
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
 
19
- # ----------------------------
20
- # Robust network helpers
21
- # ----------------------------
 
 
 
22
 
23
- DEFAULT_TIMEOUT = 20
 
 
 
 
 
 
 
 
24
 
25
- def _session() -> requests.Session:
26
- s = requests.Session()
27
- s.headers.update({
28
- "User-Agent": "Mozilla/5.0 (freesound-metadata-preprocess/1.0)",
29
- "Accept": "application/json,text/plain,*/*",
30
- "Connection": "keep-alive",
31
- })
32
- return s
33
 
34
- def fetch_json_with_retry(
35
- url: str,
36
- headers: Dict[str, str],
37
- max_retries: int = 6,
38
- base_sleep: float = 0.8,
39
- timeout: int = DEFAULT_TIMEOUT,
40
- ) -> Dict[str, Any]:
41
  """
42
- GET JSON robuste: gère 429 (rate limit), 5xx et déconnexions.
 
43
  """
44
- sess = _session()
45
  last_err = None
46
-
47
- for attempt in range(max_retries):
48
  try:
49
- resp = sess.get(url, headers=headers, timeout=timeout)
50
-
51
- # rate limit
52
- if resp.status_code == 429:
53
- time.sleep(base_sleep * (2 ** attempt))
54
- continue
55
-
56
- # serveur instable
57
- if resp.status_code >= 500:
58
- time.sleep(base_sleep * (2 ** attempt))
59
- continue
60
-
61
- resp.raise_for_status()
62
- return resp.json()
63
-
64
  except Exception as e:
65
  last_err = e
66
- time.sleep(base_sleep * (2 ** attempt))
67
-
68
- raise RuntimeError(f"Échec requête après {max_retries} essais. Dernière erreur: {last_err}")
69
-
70
-
71
- # ----------------------------
72
- # URL -> sound_id -> API endpoint
73
- # ----------------------------
74
-
75
- def sound_id_from_freesound_page(url: str) -> int:
76
- """
77
- Extrait l'ID depuis une URL FreeSound de page son:
78
- https://freesound.org/people/.../sounds/<id>/
79
- """
80
- u = url.strip()
81
- u = urllib.parse.unquote(u)
82
-
83
- m = re.search(r"freesound\.org\/.*\/sounds\/(\d+)\/?", u)
84
- if not m:
85
- # si l'utilisateur colle juste l'ID (optionnel)
86
- if re.fullmatch(r"\d+", u):
87
- return int(u)
88
- raise ValueError("URL non reconnue. Colle l’URL FreeSound du son (page), ex: .../sounds/844708/")
89
- return int(m.group(1))
90
-
91
- def api_url_from_sound_id(sound_id: int) -> str:
92
- return f"https://freesound.org/apiv2/sounds/{sound_id}/"
93
-
94
-
95
- # ----------------------------
96
- # Preprocessing helpers
97
- # ----------------------------
98
-
99
- def clean_tags(tags: Any) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  """
101
- Nettoie tags :
102
- - support list ou str
103
- - décode %3B etc
104
- - split sur ; , espace
105
- - lower
106
- - supprime doublons
107
  """
108
- if tags is None:
109
- return ""
110
-
111
- if isinstance(tags, list):
112
- raw = " ".join([str(t) for t in tags])
113
- else:
114
- raw = str(tags)
115
-
116
- raw = urllib.parse.unquote(raw)
117
- raw = raw.replace(",", " ").replace(";", " ").replace("|", " ")
118
- raw = re.sub(r"\s+", " ", raw).strip().lower()
119
-
120
- toks = [t for t in raw.split(" ") if t]
121
- toks = [t for t in toks if len(t) >= 2]
122
-
123
- seen = set()
124
- out = []
125
- for t in toks:
126
- if t not in seen:
127
- seen.add(t)
128
- out.append(t)
129
- return " ".join(out)
130
-
131
- def clean_text(x: Any) -> str:
132
- if x is None:
133
- return ""
134
- s = str(x)
135
- s = urllib.parse.unquote(s)
136
- s = s.lower()
137
- s = re.sub(r"\s+", " ", s).strip()
138
- return s
139
-
140
- def safe_num(x: Any) -> float:
141
  try:
142
- if x is None:
143
- return 0.0
144
- return float(x)
145
- except Exception:
146
- return 0.0
147
-
148
- def safe_len_list(x: Any) -> int:
149
- if isinstance(x, list):
150
- return len(x)
151
- return 0
152
-
153
-
154
- # ----------------------------
155
- # Extract raw features (before)
156
- # ----------------------------
157
-
158
- RAW_COLUMNS = [
159
- "id", "name", "username", "license", "created",
160
- "description", "tags",
161
- "duration", "samplerate", "bitrate", "bitdepth", "channels",
162
- "filesize", "type",
163
- "num_downloads", "num_ratings", "avg_rating",
164
- ]
165
-
166
- def extract_raw_df(sound_json: Dict[str, Any]) -> pd.DataFrame:
167
- row = {k: sound_json.get(k) for k in RAW_COLUMNS}
168
-
169
- # certains champs peuvent être absents selon droits/endpoint
170
- if "tags" not in row:
171
- row["tags"] = sound_json.get("tags")
172
-
173
- return pd.DataFrame([row])
174
-
175
-
176
- # ----------------------------
177
- # Build "after preprocessing" features
178
- # ----------------------------
179
-
180
- def build_after_features(raw_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]:
181
- """
182
- Retourne:
183
- - after_readable_df : colonnes interprétables (nettoyées + dérivées)
184
- - after_vector_df : features vectorisées (TFIDF + numeric scaled) pour "voir" l’embedding
185
- """
186
- df = raw_df.copy()
187
-
188
- # Nettoyages
189
- df["tags_clean"] = df["tags"].apply(clean_tags)
190
- df["name_clean"] = df["name"].apply(clean_text)
191
- df["desc_clean"] = df["description"].apply(clean_text)
192
-
193
- # Features dérivées (lisibles)
194
- df["num_tags"] = df["tags"].apply(safe_len_list)
195
- df["name_len"] = df["name_clean"].apply(lambda s: len(s))
196
- df["desc_len"] = df["desc_clean"].apply(lambda s: len(s))
197
- df["text_all"] = (df["name_clean"].fillna("") + " " + df["desc_clean"].fillna("") + " " + df["tags_clean"].fillna("")).str.strip()
198
-
199
- # Numeric basic
200
- numeric_cols = ["duration", "samplerate", "bitrate", "bitdepth", "channels", "filesize", "num_downloads", "num_ratings", "avg_rating",
201
- "num_tags", "name_len", "desc_len"]
202
- for c in numeric_cols:
203
- df[c] = df[c].apply(safe_num)
204
-
205
- # 1) after_readable_df (ce que tu veux lire facilement)
206
- after_readable_cols = [
207
- "id", "type", "license", "created",
208
- "name_clean", "tags_clean",
209
- "duration", "samplerate", "channels", "filesize",
210
- "num_downloads", "num_ratings", "avg_rating",
211
- "num_tags", "name_len", "desc_len",
212
- ]
213
- after_readable_df = df[after_readable_cols].copy()
214
-
215
- # 2) vectorisation texte (TF-IDF) + standardisation numeric
216
- # Sur un seul son, TF-IDF marche quand même (tu verras les termes présents).
217
- tfidf = TfidfVectorizer(max_features=60, ngram_range=(1, 2))
218
- X_text = tfidf.fit_transform(df["text_all"].fillna(""))
219
-
220
- # Numeric scaling
221
- scaler = StandardScaler()
222
- X_num = scaler.fit_transform(df[numeric_cols].to_numpy())
223
-
224
- # Assemble en DataFrame pour affichage
225
- text_feature_names = [f"tfidf:{t}" for t in tfidf.get_feature_names_out()]
226
- X_text_dense = X_text.toarray()
227
-
228
- num_feature_names = [f"num:{c}" for c in numeric_cols]
229
-
230
- all_features = np.concatenate([X_num, X_text_dense], axis=1)
231
- all_names = num_feature_names + text_feature_names
232
-
233
- after_vector_df = pd.DataFrame(all_features, columns=all_names)
234
-
235
- return after_readable_df, after_vector_df
236
-
237
-
238
- # ----------------------------
239
- # Main analysis function
240
- # ----------------------------
241
-
242
- def analyze(url: str, api_key: str) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
243
- if not url or not url.strip():
244
- raise ValueError("Colle l’URL du son FreeSound.")
245
-
246
- api_key = (api_key or "").strip() or os.environ.get("FREESOUND_API_KEY", "").strip()
247
- if not api_key:
248
- raise ValueError("Il faut une clé FreeSound API. Mets-la dans le champ 'API key' ou dans FREESOUND_API_KEY.")
249
-
250
- sound_id = sound_id_from_freesound_page(url)
251
- api_url = api_url_from_sound_id(sound_id)
252
-
253
- headers = {"Authorization": f"Token {api_key}"}
254
-
255
- sound_json = fetch_json_with_retry(api_url, headers=headers)
256
 
257
- before_df = extract_raw_df(sound_json)
 
 
 
 
 
 
 
 
 
 
258
 
259
- after_readable_df, after_vector_df = build_after_features(before_df)
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- # Bonus: afficher seulement les top features TF-IDF non-nulles
262
- # (sur un seul sample, c'est plus clair)
263
- nonzero = after_vector_df.loc[0]
264
- top = nonzero[nonzero != 0].sort_values(key=lambda s: np.abs(s), ascending=False).head(30)
265
- top_df = top.reset_index()
266
- top_df.columns = ["feature", "value"]
 
 
 
 
 
267
 
268
- return before_df, after_readable_df, top_df
 
 
 
 
 
 
 
 
 
 
269
 
 
 
 
 
 
 
270
 
271
- # ----------------------------
272
- # Gradio UI
273
- # ----------------------------
 
 
 
 
 
274
 
275
- with gr.Blocks(title="FreeSound - Prétraitement Metadata") as demo:
276
- gr.Markdown("## 🎧 FreeSound – Prétraitement Metadata\n"
277
- "Objectif : **visualiser les features AVANT et APRÈS preprocessing**.\n\n"
278
- "- Entrée = **URL du son FreeSound** (page)\n"
279
- "- Sorties = **tableau avant**, **tableau après**, **top features (vectorisées)**")
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
- with gr.Row():
282
- url_in = gr.Textbox(
283
- label="URL du son FreeSound",
284
- placeholder="https://freesound.org/people/.../sounds/844708/",
285
- value="",
 
 
 
 
 
286
  )
287
 
288
- api_in = gr.Textbox(
289
- label="API key (Token) FreeSound (optionnel si FREESOUND_API_KEY est set)",
290
- placeholder="Colle ta clé ici (Token ...)",
291
- type="password",
292
- value="",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  )
294
 
295
- btn = gr.Button("Analyser")
 
 
 
 
 
 
296
 
297
- gr.Markdown("### Avant (raw metadata)")
298
- before_out = gr.Dataframe(interactive=False, wrap=True)
299
 
300
- gr.Markdown("### Après (nettoyé + features dérivées lisibles)")
301
- after_out = gr.Dataframe(interactive=False, wrap=True)
 
 
302
 
303
- gr.Markdown("### Top features après vectorisation (num + TF-IDF) valeurs non nulles")
304
- top_out = gr.Dataframe(interactive=False, wrap=True)
305
 
306
  btn.click(
307
- fn=analyze,
308
- inputs=[url_in, api_in],
309
- outputs=[before_out, after_out, top_out],
310
  )
311
 
312
- if __name__ == "__main__":
313
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
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)
180
+ return len(audio) / 1000.0
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:
188
+ with sf.SoundFile(filepath) as f:
189
+ if f.samplerate == SR_TARGET and f.channels == 1:
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)
197
+ tmp.close()
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_:
233
+ booster = est.get_booster() if hasattr(est, "get_booster") else est
234
+ dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
235
+ p = booster.predict(dm)
236
+ preds.append(np.asarray(p).reshape(-1))
237
+ return np.column_stack(preds)
238
+
239
+ booster = model.get_booster() if hasattr(model, "get_booster") else model
240
+ dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
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()
 
xgb_avg_rating_effectsound_label_encoder.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea0d4f212926af0fb0d1d4ddc2a77f10e62ba4c0b87131297514ae697d979d29
3
+ size 508
xgb_avg_rating_effectsound_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:af8cfbf2e681e80ea641e30b40ac280999e9a72e9bd95b28f33755771ccd51e5
3
+ size 10219909
xgb_avg_rating_music_features.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fa0509103f06306aa1d21f074fc89ee08d90d9cf4b0c2b3a9a5b3c4436d4c5af
3
+ size 631
xgb_avg_rating_music_label_encoder.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea0d4f212926af0fb0d1d4ddc2a77f10e62ba4c0b87131297514ae697d979d29
3
+ size 508
xgb_avg_rating_music_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b4e996630b373e8594079d5ae3bf2a52707f8f163aedd6c6330416b9a056b8e9
3
+ size 7046656
xgb_model_EffectSound.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f41317a1a2ac6916e2fc40a8a43097021520ea0de78632149a30ee946b1c697a
3
+ size 16161360
xgb_model_Music.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:89dc204e1e774da5b44df74d25d654bce417e4d7304b3bf2efde901dccaf2919
3
+ size 16904032
xgb_num_downloads_effectsound_features.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fa0509103f06306aa1d21f074fc89ee08d90d9cf4b0c2b3a9a5b3c4436d4c5af
3
+ size 631
xgb_num_downloads_effectsound_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2a33ae31fc84dd8dc080d75f61e4016690fa2730cdd9b7dbb9720d7eb778adca
3
+ size 8595460
xgb_num_downloads_music_features.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fa0509103f06306aa1d21f074fc89ee08d90d9cf4b0c2b3a9a5b3c4436d4c5af
3
+ size 631
xgb_num_downloads_music_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5b8abbccd5ee7f195386936b8447c7d9c5b336f6f8b5740563d6803389a2c45a
3
+ size 8754226