IKRAMELHADI commited on
Commit ·
e137539
1
Parent(s): 4ed7c05
testtest
Browse files- app.py +85 -297
- xgb_avg_rating_effectsound_features.pkl → effectSound_model_num_downloads.joblib +2 -2
- xgb_num_downloads_effectsound_features.pkl → effectSound_xgb_avg_rating.joblib +2 -2
- xgb_avg_rating_effectsound_label_encoder.pkl → effectSound_xgb_avg_rating_label_encoder.joblib +0 -0
- xgb_avg_rating_effectsound_model.pkl → effect_model_features_list.joblib +2 -2
- model_features_list.joblib +3 -0
- music_model_features_list.joblib +3 -0
- xgb_avg_rating_music_features.pkl → music_model_num_downloads.joblib +2 -2
- xgb_avg_rating_music_model.pkl → music_xgb_avg_rating.joblib +2 -2
- xgb_avg_rating_music_label_encoder.pkl → music_xgb_avg_rating_label_encoder.joblib +0 -0
- xgb_model_EffectSound.pkl +0 -3
- xgb_model_Music.pkl +0 -3
- xgb_num_downloads_effectsound_model.pkl +0 -3
- xgb_num_downloads_music_features.pkl +0 -3
- xgb_num_downloads_music_model.pkl +0 -3
app.py
CHANGED
|
@@ -1,24 +1,22 @@
|
|
| 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 |
|
| 16 |
# =========================
|
| 17 |
-
#
|
| 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 |
# =========================
|
|
@@ -35,45 +33,21 @@ CSS = """
|
|
| 35 |
border-color: #fca5a5;
|
| 36 |
background: #fff1f2;
|
| 37 |
}
|
| 38 |
-
.card-title{
|
| 39 |
-
|
| 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 |
-
|
| 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; }
|
|
@@ -118,68 +92,50 @@ def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""
|
|
| 118 |
|
| 119 |
|
| 120 |
# =========================
|
| 121 |
-
#
|
| 122 |
# =========================
|
| 123 |
def interpret_results(avg_class: int, dl_class: int) -> str:
|
| 124 |
-
"""
|
| 125 |
-
avg_class: 0=Missed info, 1=Low, 2=Medium, 3=High
|
| 126 |
-
dl_class: 0=Low, 1=Medium, 2=High
|
| 127 |
-
"""
|
| 128 |
if avg_class == 0:
|
| 129 |
return (
|
| 130 |
"ℹ️ <b>Interprétation</b> :<br>"
|
| 131 |
-
"Aucune
|
| 132 |
-
|
| 133 |
)
|
| 134 |
|
| 135 |
rating_txt = {1: "faible", 2: "moyenne", 3: "élevée"}.get(avg_class, "inconnue")
|
| 136 |
downloads_txt = {0: "faible", 1: "modérée", 2: "élevée"}.get(dl_class, "inconnue")
|
| 137 |
|
| 138 |
if avg_class == 3 and dl_class == 2:
|
| 139 |
-
potentiel = "très fort"
|
| 140 |
-
detail = "contenu de haute qualité et très populaire."
|
| 141 |
elif avg_class == 3 and dl_class == 1:
|
| 142 |
-
potentiel = "fort"
|
| 143 |
-
detail = "contenu bien apprécié, en croissance."
|
| 144 |
elif avg_class == 3 and dl_class == 0:
|
| 145 |
-
potentiel = "prometteur"
|
| 146 |
-
detail = "bonne qualité mais faible visibilité (peut gagner en popularité)."
|
| 147 |
elif avg_class == 2 and dl_class == 2:
|
| 148 |
-
potentiel = "modéré à fort"
|
| 149 |
-
detail = "populaire mais qualité perçue moyenne."
|
| 150 |
elif avg_class == 2 and dl_class == 1:
|
| 151 |
-
potentiel = "modéré"
|
| 152 |
-
detail = "profil standard, popularité stable."
|
| 153 |
elif avg_class == 2 and dl_class == 0:
|
| 154 |
-
potentiel = "limité"
|
| 155 |
-
detail = "engagement faible, diffusion limitée."
|
| 156 |
elif avg_class == 1 and dl_class == 2:
|
| 157 |
-
potentiel = "contradictoire"
|
| 158 |
-
detail = "très téléchargé mais peu apprécié (usage pratique possible)."
|
| 159 |
elif avg_class == 1 and dl_class == 1:
|
| 160 |
-
potentiel = "faible"
|
| 161 |
-
detail = "peu attractif pour les utilisateurs."
|
| 162 |
else:
|
| 163 |
-
potentiel = "très faible"
|
| 164 |
-
detail = "faible intérêt global."
|
| 165 |
|
| 166 |
return (
|
| 167 |
-
"<b>Interprétation</b> :<br>"
|
| 168 |
-
f"
|
|
|
|
|
|
|
| 169 |
)
|
| 170 |
|
| 171 |
|
| 172 |
def avg_label_to_class(avg_label: str) -> int:
|
| 173 |
-
"""
|
| 174 |
-
Convertit un label texte (LabelEncoder) en classe 0..3 :
|
| 175 |
-
0=Missed info, 1=Low, 2=Medium, 3=High
|
| 176 |
-
Robuste aux variantes.
|
| 177 |
-
"""
|
| 178 |
if avg_label is None:
|
| 179 |
return 0
|
| 180 |
-
|
| 181 |
s = str(avg_label).strip().lower()
|
| 182 |
-
|
| 183 |
if "miss" in s or "missing" in s or "none" in s or "no" in s:
|
| 184 |
return 0
|
| 185 |
if "high" in s or "élev" in s or "eleve" in s:
|
|
@@ -191,168 +147,16 @@ def avg_label_to_class(avg_label: str) -> int:
|
|
| 191 |
return 0
|
| 192 |
|
| 193 |
|
| 194 |
-
# =========================
|
| 195 |
-
#
|
| 196 |
-
# =========================
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
MODEL_MUSIC = joblib.load("xgb_model_Music.pkl")
|
| 200 |
-
|
| 201 |
-
RATING_DISPLAY_AUDIO = {
|
| 202 |
-
0: "❌ Informations manquantes",
|
| 203 |
-
1: "⭐ Faible",
|
| 204 |
-
2: "⭐⭐ Moyen",
|
| 205 |
-
3: "⭐⭐⭐ Élevé",
|
| 206 |
-
}
|
| 207 |
-
DOWNLOADS_DISPLAY_AUDIO = {
|
| 208 |
-
0: "⭐ Faible",
|
| 209 |
-
1: "⭐⭐ Moyen",
|
| 210 |
-
2: "⭐⭐⭐ Élevé",
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
SMILE = opensmile.Smile(
|
| 214 |
-
feature_set=opensmile.FeatureSet.eGeMAPSv02,
|
| 215 |
-
feature_level=opensmile.FeatureLevel.Functionals,
|
| 216 |
-
)
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
def get_duration_seconds(filepath):
|
| 220 |
-
ext = os.path.splitext(filepath)[1].lower()
|
| 221 |
-
if ext == ".mp3":
|
| 222 |
-
audio = AudioSegment.from_file(filepath)
|
| 223 |
-
return len(audio) / 1000.0
|
| 224 |
-
with sf.SoundFile(filepath) as f:
|
| 225 |
-
return len(f) / f.samplerate
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
def to_wav_16k_mono(filepath):
|
| 229 |
-
ext = os.path.splitext(filepath)[1].lower()
|
| 230 |
-
if ext == ".wav":
|
| 231 |
-
try:
|
| 232 |
-
with sf.SoundFile(filepath) as f:
|
| 233 |
-
if f.samplerate == SR_TARGET and f.channels == 1:
|
| 234 |
-
return filepath
|
| 235 |
-
except Exception:
|
| 236 |
-
pass
|
| 237 |
-
|
| 238 |
-
audio = AudioSegment.from_file(filepath)
|
| 239 |
-
audio = audio.set_channels(1).set_frame_rate(SR_TARGET)
|
| 240 |
-
|
| 241 |
-
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
|
| 242 |
-
tmp.close()
|
| 243 |
-
audio.export(tmp.name, format="wav")
|
| 244 |
-
return tmp.name
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
def extract_opensmile_features(filepath):
|
| 248 |
-
wav_path = to_wav_16k_mono(filepath)
|
| 249 |
-
feats = SMILE.process_file(wav_path)
|
| 250 |
-
feats = feats.select_dtypes(include=[np.number]).reset_index(drop=True)
|
| 251 |
-
return feats
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
def predict_upload_with_dmatrix(model, X_df: pd.DataFrame):
|
| 255 |
-
"""
|
| 256 |
-
Résout 'data did not contain feature names' en passant via Booster + DMatrix(feature_names=...).
|
| 257 |
-
Retour: array shape (1, n_outputs)
|
| 258 |
-
"""
|
| 259 |
-
if hasattr(model, "estimators_"):
|
| 260 |
-
preds = []
|
| 261 |
-
for est in model.estimators_:
|
| 262 |
-
booster = est.get_booster() if hasattr(est, "get_booster") else est
|
| 263 |
-
dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
|
| 264 |
-
p = booster.predict(dm)
|
| 265 |
-
preds.append(np.asarray(p).reshape(-1))
|
| 266 |
-
return np.column_stack(preds)
|
| 267 |
-
|
| 268 |
-
booster = model.get_booster() if hasattr(model, "get_booster") else model
|
| 269 |
-
dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
|
| 270 |
-
p = booster.predict(dm)
|
| 271 |
-
return np.asarray(p).reshape(1, -1)
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
def predict_from_uploaded_audio(audio_file):
|
| 275 |
-
if audio_file is None:
|
| 276 |
-
return html_error("Aucun fichier", "Veuillez importer un fichier audio (wav, mp3, flac…).")
|
| 277 |
-
|
| 278 |
-
# Durée
|
| 279 |
-
try:
|
| 280 |
-
duration = get_duration_seconds(audio_file)
|
| 281 |
-
except Exception as e:
|
| 282 |
-
return html_error("Audio illisible", f"Impossible de lire l'audio.<br>Détail : <code>{e}</code>")
|
| 283 |
-
|
| 284 |
-
# Vérif durées
|
| 285 |
-
if duration < MIN_EFFECT:
|
| 286 |
-
return html_error(
|
| 287 |
-
"Audio trop court",
|
| 288 |
-
f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
|
| 289 |
-
f"Plages acceptées :<br>"
|
| 290 |
-
f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
|
| 291 |
-
f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
|
| 292 |
-
)
|
| 293 |
-
|
| 294 |
-
if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
|
| 295 |
-
return html_error(
|
| 296 |
-
"Audio hors plage",
|
| 297 |
-
f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
|
| 298 |
-
f"Plages acceptées :<br>"
|
| 299 |
-
f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
|
| 300 |
-
f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
|
| 301 |
-
)
|
| 302 |
-
|
| 303 |
-
# Type + modèle
|
| 304 |
-
if duration <= MAX_EFFECT:
|
| 305 |
-
badge = "🔊 Effet sonore (upload)"
|
| 306 |
-
model = MODEL_EFFECT
|
| 307 |
-
else:
|
| 308 |
-
badge = "🎵 Musique (upload)"
|
| 309 |
-
model = MODEL_MUSIC
|
| 310 |
-
|
| 311 |
-
# openSMILE
|
| 312 |
-
try:
|
| 313 |
-
X = extract_opensmile_features(audio_file)
|
| 314 |
-
except Exception as e:
|
| 315 |
-
return html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>")
|
| 316 |
-
|
| 317 |
-
# Align features
|
| 318 |
-
try:
|
| 319 |
-
expected = model.estimators_[0].feature_names_in_ if hasattr(model, "estimators_") else model.feature_names_in_
|
| 320 |
-
X = X.reindex(columns=list(expected), fill_value=0)
|
| 321 |
-
except Exception as e:
|
| 322 |
-
return html_error("Alignement des features échoué", f"Détail : <code>{e}</code>")
|
| 323 |
-
|
| 324 |
-
# Predict
|
| 325 |
-
try:
|
| 326 |
-
y = predict_upload_with_dmatrix(model, X)
|
| 327 |
-
except Exception as e:
|
| 328 |
-
return html_error("Prédiction échouée", f"Détail : <code>{e}</code>")
|
| 329 |
-
|
| 330 |
-
y = np.array(y)
|
| 331 |
-
avg_class = int(y[0, 0])
|
| 332 |
-
dl_class = int(y[0, 1])
|
| 333 |
-
|
| 334 |
-
rating_text = RATING_DISPLAY_AUDIO.get(avg_class, "Inconnu")
|
| 335 |
-
downloads_text = DOWNLOADS_DISPLAY_AUDIO.get(dl_class, "Inconnu")
|
| 336 |
-
|
| 337 |
-
conclusion = interpret_results(avg_class, dl_class)
|
| 338 |
-
extra = f"""
|
| 339 |
-
<div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
|
| 340 |
-
{conclusion}
|
| 341 |
-
</div>
|
| 342 |
-
"""
|
| 343 |
-
|
| 344 |
-
return html_result(badge, duration, rating_text, downloads_text, extra_html=extra)
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
# ============================================================
|
| 348 |
-
# PARTIE B — URL FreeSound → API → modèles (collègue)
|
| 349 |
-
# ============================================================
|
| 350 |
-
|
| 351 |
-
API_TOKEN = "zE9NjEOgUMzH9K7mjiGBaPJiNwJLjSM53LevarRK" # <-- tu remplaces ici
|
| 352 |
|
| 353 |
-
fs_client = freesound.FreesoundClient()
|
| 354 |
-
fs_client.set_token(API_TOKEN, "token")
|
| 355 |
|
|
|
|
|
|
|
|
|
|
| 356 |
# Music
|
| 357 |
xgb_music_num = joblib.load("xgb_num_downloads_music_model.pkl")
|
| 358 |
xgb_music_feat_num = joblib.load("xgb_num_downloads_music_features.pkl")
|
|
@@ -377,41 +181,40 @@ def safe_float(v):
|
|
| 377 |
return 0.0
|
| 378 |
|
| 379 |
|
| 380 |
-
def
|
| 381 |
row = []
|
| 382 |
for col in feat_list:
|
| 383 |
-
val =
|
| 384 |
if val is None or isinstance(val, (list, dict)):
|
| 385 |
val = 0
|
| 386 |
row.append(safe_float(val))
|
| 387 |
|
| 388 |
X = pd.DataFrame([row], columns=feat_list)
|
| 389 |
dmatrix = xgb.DMatrix(X.values, feature_names=feat_list)
|
| 390 |
-
|
| 391 |
pred_int = int(model.get_booster().predict(dmatrix)[0])
|
| 392 |
|
| 393 |
-
if
|
| 394 |
-
return
|
| 395 |
return pred_int
|
| 396 |
|
| 397 |
|
| 398 |
-
def
|
| 399 |
if not url or not url.strip():
|
| 400 |
return html_error("URL vide", "Collez une URL FreeSound du type <code>https://freesound.org/s/123456/</code>")
|
| 401 |
|
| 402 |
-
# ID
|
| 403 |
try:
|
| 404 |
sound_id = int(url.rstrip("/").split("/")[-1])
|
| 405 |
except Exception:
|
| 406 |
return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
|
| 407 |
|
|
|
|
| 408 |
all_features = list(set(
|
| 409 |
xgb_music_feat_num + xgb_music_feat_avg + xgb_effect_feat_num + xgb_effect_feat_avg
|
| 410 |
))
|
| 411 |
fields = "duration," + ",".join(all_features)
|
| 412 |
|
| 413 |
try:
|
| 414 |
-
results =
|
| 415 |
except Exception as e:
|
| 416 |
return html_error("Erreur API FreeSound", f"Détail : <code>{e}</code>")
|
| 417 |
|
|
@@ -421,11 +224,25 @@ def predict_from_freesound_url(url: str):
|
|
| 421 |
sound = results.results[0]
|
| 422 |
duration = safe_float(sound.get("duration", 0))
|
| 423 |
|
| 424 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
if MIN_EFFECT <= duration <= MAX_EFFECT:
|
| 426 |
-
badge = "🔊 Effet sonore (
|
| 427 |
-
dl_class = int(
|
| 428 |
-
avg_text = str(
|
| 429 |
dl_text = NUM_DOWNLOADS_MAP.get(dl_class, str(dl_class))
|
| 430 |
|
| 431 |
avg_class = avg_label_to_class(avg_text)
|
|
@@ -433,76 +250,47 @@ def predict_from_freesound_url(url: str):
|
|
| 433 |
|
| 434 |
extra = f"""
|
| 435 |
<div class="hint">ID FreeSound : <b>{sound_id}</b></div>
|
| 436 |
-
<div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
|
| 437 |
-
{conclusion}
|
| 438 |
-
</div>
|
| 439 |
"""
|
| 440 |
return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
|
| 441 |
|
| 442 |
# Music
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
dl_text = NUM_DOWNLOADS_MAP.get(dl_class, str(dl_class))
|
| 448 |
|
| 449 |
-
|
| 450 |
-
|
| 451 |
|
| 452 |
-
|
| 453 |
<div class="hint">ID FreeSound : <b>{sound_id}</b></div>
|
| 454 |
-
<div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
|
| 455 |
-
{conclusion}
|
| 456 |
-
</div>
|
| 457 |
"""
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
return html_error(
|
| 461 |
-
"Durée non supportée",
|
| 462 |
-
f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
|
| 463 |
-
f"Plages acceptées :<br>"
|
| 464 |
-
f"• Effet sonore : <b>{MIN_EFFECT}–{MAX_EFFECT} s</b><br>"
|
| 465 |
-
f"• Musique : <b>{MIN_MUSIC}–{MAX_MUSIC} s</b>"
|
| 466 |
-
)
|
| 467 |
|
| 468 |
|
| 469 |
# =========================
|
| 470 |
-
#
|
| 471 |
# =========================
|
| 472 |
theme = gr.themes.Soft()
|
| 473 |
|
| 474 |
-
with gr.Blocks(title="
|
| 475 |
gr.HTML(
|
| 476 |
f"""
|
| 477 |
-
<div id="header-title">
|
| 478 |
<p id="header-sub">
|
| 479 |
-
|
|
|
|
| 480 |
<b>Durées acceptées :</b> 🔊 Effet sonore {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}–{MAX_MUSIC}s
|
| 481 |
</p>
|
| 482 |
"""
|
| 483 |
)
|
| 484 |
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
audio_in = gr.Audio(type="filepath", label="Fichier audio")
|
| 491 |
-
btn_audio = gr.Button("🚀 Prédire (upload)", variant="primary")
|
| 492 |
-
with gr.Column(scale=1):
|
| 493 |
-
gr.Markdown("### Résultat")
|
| 494 |
-
out_audio = gr.HTML()
|
| 495 |
-
btn_audio.click(predict_from_uploaded_audio, inputs=audio_in, outputs=out_audio)
|
| 496 |
-
|
| 497 |
-
with gr.Tab("2) URL FreeSound (features API)"):
|
| 498 |
-
with gr.Row():
|
| 499 |
-
with gr.Column(scale=1):
|
| 500 |
-
gr.Markdown("### Coller une URL FreeSound")
|
| 501 |
-
url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
|
| 502 |
-
btn_url = gr.Button("🚀 Prédire (URL FreeSound)", variant="primary")
|
| 503 |
-
with gr.Column(scale=1):
|
| 504 |
-
gr.Markdown("### Résultat")
|
| 505 |
-
out_url = gr.HTML()
|
| 506 |
-
btn_url.click(predict_from_freesound_url, inputs=url_in, outputs=out_url)
|
| 507 |
|
| 508 |
demo.launch(theme=theme)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
import joblib
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import xgboost as xgb
|
| 6 |
+
import freesound
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# =========================
|
| 10 |
+
# Token FreeSound
|
| 11 |
+
# =========================
|
| 12 |
+
API_TOKEN = "A ECRIRE" # <- tu remplaces ici
|
| 13 |
|
| 14 |
|
| 15 |
# =========================
|
| 16 |
+
# Durées
|
| 17 |
# =========================
|
| 18 |
MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
|
| 19 |
MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
# =========================
|
|
|
|
| 33 |
border-color: #fca5a5;
|
| 34 |
background: #fff1f2;
|
| 35 |
}
|
| 36 |
+
.card-title{ font-weight: 950; margin-bottom: 8px; }
|
| 37 |
+
.badges{ display:flex; gap:10px; flex-wrap:wrap; margin-bottom:12px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
.badge{
|
| 39 |
+
padding:6px 10px; border-radius:999px; font-weight:900; font-size:13px;
|
|
|
|
|
|
|
|
|
|
| 40 |
border: 1px solid #e5e7eb;
|
| 41 |
}
|
| 42 |
.badge-type{ background:#eef2ff; color:#3730a3;}
|
| 43 |
.badge-time{ background:#ecfeff; color:#155e75;}
|
| 44 |
|
| 45 |
+
.grid{ display:grid; grid-template-columns:1fr; gap:10px; }
|
| 46 |
+
.box{ border:1px solid #e5e7eb; border-radius:14px; padding:12px; background:#fafafa; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
.box-title{ font-weight:900; margin-bottom:4px; }
|
| 48 |
.box-value{ font-size:18px; font-weight:800; }
|
| 49 |
|
| 50 |
+
.hint{ margin-top:10px; color:#6b7280; font-size:12px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
#header-title { font-size: 28px; font-weight: 950; margin-bottom: 6px; }
|
| 53 |
#header-sub { color:#6b7280; margin-top:0px; line-height:1.45; }
|
|
|
|
| 92 |
|
| 93 |
|
| 94 |
# =========================
|
| 95 |
+
# Interprétation
|
| 96 |
# =========================
|
| 97 |
def interpret_results(avg_class: int, dl_class: int) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
if avg_class == 0:
|
| 99 |
return (
|
| 100 |
"ℹ️ <b>Interprétation</b> :<br>"
|
| 101 |
+
"Aucune/peu d'évaluations utilisateurs (rating manquant).<br>"
|
| 102 |
+
"La popularité est donc probablement liée à l'usage (téléchargements) plutôt qu'à la qualité perçue."
|
| 103 |
)
|
| 104 |
|
| 105 |
rating_txt = {1: "faible", 2: "moyenne", 3: "élevée"}.get(avg_class, "inconnue")
|
| 106 |
downloads_txt = {0: "faible", 1: "modérée", 2: "élevée"}.get(dl_class, "inconnue")
|
| 107 |
|
| 108 |
if avg_class == 3 and dl_class == 2:
|
| 109 |
+
potentiel, detail = "très fort", "contenu de haute qualité et très populaire."
|
|
|
|
| 110 |
elif avg_class == 3 and dl_class == 1:
|
| 111 |
+
potentiel, detail = "fort", "contenu bien apprécié, en croissance."
|
|
|
|
| 112 |
elif avg_class == 3 and dl_class == 0:
|
| 113 |
+
potentiel, detail = "prometteur", "bonne qualité mais faible visibilité."
|
|
|
|
| 114 |
elif avg_class == 2 and dl_class == 2:
|
| 115 |
+
potentiel, detail = "modéré à fort", "populaire mais qualité perçue moyenne."
|
|
|
|
| 116 |
elif avg_class == 2 and dl_class == 1:
|
| 117 |
+
potentiel, detail = "modéré", "profil standard, popularité stable."
|
|
|
|
| 118 |
elif avg_class == 2 and dl_class == 0:
|
| 119 |
+
potentiel, detail = "limité", "engagement faible, diffusion limitée."
|
|
|
|
| 120 |
elif avg_class == 1 and dl_class == 2:
|
| 121 |
+
potentiel, detail = "contradictoire", "très téléchargé mais peu apprécié."
|
|
|
|
| 122 |
elif avg_class == 1 and dl_class == 1:
|
| 123 |
+
potentiel, detail = "faible", "peu attractif pour les utilisateurs."
|
|
|
|
| 124 |
else:
|
| 125 |
+
potentiel, detail = "très faible", "faible intérêt global."
|
|
|
|
| 126 |
|
| 127 |
return (
|
| 128 |
+
"🧠 <b>Interprétation</b> :<br>"
|
| 129 |
+
f"- Qualité perçue : <b>{rating_txt}</b><br>"
|
| 130 |
+
f"- Popularité : <b>{downloads_txt}</b><br><br>"
|
| 131 |
+
f"👉 Potentiel estimé : <b>{potentiel}</b> — {detail}"
|
| 132 |
)
|
| 133 |
|
| 134 |
|
| 135 |
def avg_label_to_class(avg_label: str) -> int:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
if avg_label is None:
|
| 137 |
return 0
|
|
|
|
| 138 |
s = str(avg_label).strip().lower()
|
|
|
|
| 139 |
if "miss" in s or "missing" in s or "none" in s or "no" in s:
|
| 140 |
return 0
|
| 141 |
if "high" in s or "élev" in s or "eleve" in s:
|
|
|
|
| 147 |
return 0
|
| 148 |
|
| 149 |
|
| 150 |
+
# =========================
|
| 151 |
+
# Init FreeSound Client
|
| 152 |
+
# =========================
|
| 153 |
+
client = freesound.FreesoundClient()
|
| 154 |
+
client.set_token(API_TOKEN, "token")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
|
|
|
|
|
|
| 156 |
|
| 157 |
+
# =========================
|
| 158 |
+
# Charger modèles (metadata)
|
| 159 |
+
# =========================
|
| 160 |
# Music
|
| 161 |
xgb_music_num = joblib.load("xgb_num_downloads_music_model.pkl")
|
| 162 |
xgb_music_feat_num = joblib.load("xgb_num_downloads_music_features.pkl")
|
|
|
|
| 181 |
return 0.0
|
| 182 |
|
| 183 |
|
| 184 |
+
def predict_with_model(model, features, feat_list, le=None):
|
| 185 |
row = []
|
| 186 |
for col in feat_list:
|
| 187 |
+
val = features.get(col, 0)
|
| 188 |
if val is None or isinstance(val, (list, dict)):
|
| 189 |
val = 0
|
| 190 |
row.append(safe_float(val))
|
| 191 |
|
| 192 |
X = pd.DataFrame([row], columns=feat_list)
|
| 193 |
dmatrix = xgb.DMatrix(X.values, feature_names=feat_list)
|
|
|
|
| 194 |
pred_int = int(model.get_booster().predict(dmatrix)[0])
|
| 195 |
|
| 196 |
+
if le:
|
| 197 |
+
return le.inverse_transform([pred_int])[0]
|
| 198 |
return pred_int
|
| 199 |
|
| 200 |
|
| 201 |
+
def extract_and_predict(url: str):
|
| 202 |
if not url or not url.strip():
|
| 203 |
return html_error("URL vide", "Collez une URL FreeSound du type <code>https://freesound.org/s/123456/</code>")
|
| 204 |
|
|
|
|
| 205 |
try:
|
| 206 |
sound_id = int(url.rstrip("/").split("/")[-1])
|
| 207 |
except Exception:
|
| 208 |
return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
|
| 209 |
|
| 210 |
+
# Champs nécessaires
|
| 211 |
all_features = list(set(
|
| 212 |
xgb_music_feat_num + xgb_music_feat_avg + xgb_effect_feat_num + xgb_effect_feat_avg
|
| 213 |
))
|
| 214 |
fields = "duration," + ",".join(all_features)
|
| 215 |
|
| 216 |
try:
|
| 217 |
+
results = client.search(query="", filter=f"id:{sound_id}", fields=fields)
|
| 218 |
except Exception as e:
|
| 219 |
return html_error("Erreur API FreeSound", f"Détail : <code>{e}</code>")
|
| 220 |
|
|
|
|
| 224 |
sound = results.results[0]
|
| 225 |
duration = safe_float(sound.get("duration", 0))
|
| 226 |
|
| 227 |
+
# Vérif durées
|
| 228 |
+
if duration < MIN_EFFECT:
|
| 229 |
+
return html_error(
|
| 230 |
+
"Audio trop court",
|
| 231 |
+
f"Durée : <b>{duration:.2f}s</b><br><br>"
|
| 232 |
+
f"Plages : Effet sonore <b>{MIN_EFFECT}-{MAX_EFFECT}s</b> | Musique <b>{MIN_MUSIC}-{MAX_MUSIC}s</b>"
|
| 233 |
+
)
|
| 234 |
+
if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
|
| 235 |
+
return html_error(
|
| 236 |
+
"Audio hors plage",
|
| 237 |
+
f"Durée : <b>{duration:.2f}s</b><br><br>"
|
| 238 |
+
f"Plages : Effet sonore <b>{MIN_EFFECT}-{MAX_EFFECT}s</b> | Musique <b>{MIN_MUSIC}-{MAX_MUSIC}s</b>"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Effect
|
| 242 |
if MIN_EFFECT <= duration <= MAX_EFFECT:
|
| 243 |
+
badge = "🔊 Effet sonore (metadata FreeSound)"
|
| 244 |
+
dl_class = int(predict_with_model(xgb_effect_num, sound, xgb_effect_feat_num))
|
| 245 |
+
avg_text = str(predict_with_model(xgb_effect_avg, sound, xgb_effect_feat_avg, le_effect_avg))
|
| 246 |
dl_text = NUM_DOWNLOADS_MAP.get(dl_class, str(dl_class))
|
| 247 |
|
| 248 |
avg_class = avg_label_to_class(avg_text)
|
|
|
|
| 250 |
|
| 251 |
extra = f"""
|
| 252 |
<div class="hint">ID FreeSound : <b>{sound_id}</b></div>
|
| 253 |
+
<div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">{conclusion}</div>
|
|
|
|
|
|
|
| 254 |
"""
|
| 255 |
return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
|
| 256 |
|
| 257 |
# Music
|
| 258 |
+
badge = "🎵 Musique (metadata FreeSound)"
|
| 259 |
+
dl_class = int(predict_with_model(xgb_music_num, sound, xgb_music_feat_num))
|
| 260 |
+
avg_text = str(predict_with_model(xgb_music_avg, sound, xgb_music_feat_avg, le_music_avg))
|
| 261 |
+
dl_text = NUM_DOWNLOADS_MAP.get(dl_class, str(dl_class))
|
|
|
|
| 262 |
|
| 263 |
+
avg_class = avg_label_to_class(avg_text)
|
| 264 |
+
conclusion = interpret_results(avg_class, dl_class)
|
| 265 |
|
| 266 |
+
extra = f"""
|
| 267 |
<div class="hint">ID FreeSound : <b>{sound_id}</b></div>
|
| 268 |
+
<div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">{conclusion}</div>
|
|
|
|
|
|
|
| 269 |
"""
|
| 270 |
+
return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
|
| 273 |
# =========================
|
| 274 |
+
# UI Gradio (metadata only)
|
| 275 |
# =========================
|
| 276 |
theme = gr.themes.Soft()
|
| 277 |
|
| 278 |
+
with gr.Blocks(title="Test — Metadata FreeSound", css=CSS) as demo:
|
| 279 |
gr.HTML(
|
| 280 |
f"""
|
| 281 |
+
<div id="header-title">🔎 Test — Prédiction via Metadata FreeSound</div>
|
| 282 |
<p id="header-sub">
|
| 283 |
+
Collez une URL FreeSound. L'app récupère les <b>metadata</b> via l'API et prédit la popularité (avg_rating, num_downloads).
|
| 284 |
+
<br><br>
|
| 285 |
<b>Durées acceptées :</b> 🔊 Effet sonore {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}–{MAX_MUSIC}s
|
| 286 |
</p>
|
| 287 |
"""
|
| 288 |
)
|
| 289 |
|
| 290 |
+
url = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
|
| 291 |
+
btn = gr.Button("🚀 Tester la prédiction", variant="primary")
|
| 292 |
+
out = gr.HTML()
|
| 293 |
+
|
| 294 |
+
btn.click(extract_and_predict, inputs=url, outputs=out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
demo.launch(theme=theme)
|
xgb_avg_rating_effectsound_features.pkl → effectSound_model_num_downloads.joblib
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d1d4c378ad69f9339733e16537a49ca700425404ce7b19f8c4d28f72e32ab087
|
| 3 |
+
size 7327682
|
xgb_num_downloads_effectsound_features.pkl → effectSound_xgb_avg_rating.joblib
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:378c975a1ae60a53d18d02f6e3f50567060c7c7a3660216ab487fb3eb7559356
|
| 3 |
+
size 6617890
|
xgb_avg_rating_effectsound_label_encoder.pkl → effectSound_xgb_avg_rating_label_encoder.joblib
RENAMED
|
File without changes
|
xgb_avg_rating_effectsound_model.pkl → effect_model_features_list.joblib
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7e69161319b7759e8ca2c0d0d4352c45e3970c2b2a14ee24db00f51752179b42
|
| 3 |
+
size 201078598
|
model_features_list.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f3de53a97822783e64c599135996ba7e92b3b53c3a05d8ebd34ad9f4c5664942
|
| 3 |
+
size 125604014
|
music_model_features_list.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f3de53a97822783e64c599135996ba7e92b3b53c3a05d8ebd34ad9f4c5664942
|
| 3 |
+
size 125604014
|
xgb_avg_rating_music_features.pkl → music_model_num_downloads.joblib
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f157e23ef2de700563a45a120e12b3d0827eda1fd82cbd490b5b2d4ad1508a89
|
| 3 |
+
size 8004183
|
xgb_avg_rating_music_model.pkl → music_xgb_avg_rating.joblib
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3415743a7fb86e1c9bd2d7e59f146ac4aee2e56b4343ea8e92ad486d934dd06b
|
| 3 |
+
size 6578402
|
xgb_avg_rating_music_label_encoder.pkl → music_xgb_avg_rating_label_encoder.joblib
RENAMED
|
File without changes
|
xgb_model_EffectSound.pkl
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:f41317a1a2ac6916e2fc40a8a43097021520ea0de78632149a30ee946b1c697a
|
| 3 |
-
size 16161360
|
|
|
|
|
|
|
|
|
|
|
|
xgb_model_Music.pkl
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:89dc204e1e774da5b44df74d25d654bce417e4d7304b3bf2efde901dccaf2919
|
| 3 |
-
size 16904032
|
|
|
|
|
|
|
|
|
|
|
|
xgb_num_downloads_effectsound_model.pkl
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:2a33ae31fc84dd8dc080d75f61e4016690fa2730cdd9b7dbb9720d7eb778adca
|
| 3 |
-
size 8595460
|
|
|
|
|
|
|
|
|
|
|
|
xgb_num_downloads_music_features.pkl
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:fa0509103f06306aa1d21f074fc89ee08d90d9cf4b0c2b3a9a5b3c4436d4c5af
|
| 3 |
-
size 631
|
|
|
|
|
|
|
|
|
|
|
|
xgb_num_downloads_music_model.pkl
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:5b8abbccd5ee7f195386936b8447c7d9c5b336f6f8b5740563d6803389a2c45a
|
| 3 |
-
size 8754226
|
|
|
|
|
|
|
|
|
|
|
|