IKRAMELHADI
modif interpretation results
4ed7c05
import os
import tempfile
import numpy as np
import pandas as pd
import gradio as gr
import joblib
import soundfile as sf
from pydub import AudioSegment
import opensmile
import freesound
import xgboost as xgb
# =========================
# RÈGLES DURÉE
# =========================
MIN_EFFECT, MAX_EFFECT = 0.5, 3.0
MIN_MUSIC, MAX_MUSIC = 10.0, 60.0
SR_TARGET = 16000
# =========================
# UI (CSS)
# =========================
CSS = """
.card {
border: 1px solid #e5e7eb;
background: #ffffff;
padding: 16px;
border-radius: 16px;
}
.card-error{
border-color: #fca5a5;
background: #fff1f2;
}
.card-title{
font-weight: 950;
margin-bottom: 8px;
}
.badges{
display:flex;
gap:10px;
flex-wrap:wrap;
margin-bottom:12px;
}
.badge{
padding:6px 10px;
border-radius:999px;
font-weight:900;
font-size: 13px;
border: 1px solid #e5e7eb;
}
.badge-type{ background:#eef2ff; color:#3730a3;}
.badge-time{ background:#ecfeff; color:#155e75;}
.grid{
display:grid;
grid-template-columns: 1fr;
gap:10px;
}
.box{
border:1px solid #e5e7eb;
border-radius:14px;
padding:12px;
background:#fafafa;
}
.box-title{ font-weight:900; margin-bottom:4px; }
.box-value{ font-size:18px; font-weight:800; }
.hint{
margin-top:10px;
color:#6b7280;
font-size:12px;
}
#header-title { font-size: 28px; font-weight: 950; margin-bottom: 6px; }
#header-sub { color:#6b7280; margin-top:0px; line-height:1.45; }
"""
def html_error(title, body_html):
return f"""
<div class="card card-error">
<div class="card-title">❌ {title}</div>
<div>{body_html}</div>
</div>
""".strip()
def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""):
return f"""
<div class="card">
<div class="badges">
<span class="badge badge-type">{badge_text}</span>
<span class="badge badge-time">⏱️ {duration:.2f} s</span>
</div>
<div class="grid">
<div class="box">
<div class="box-title">📈 Popularité de la note moyenne</div>
<div class="box-value">{rating_text}</div>
</div>
<div class="box">
<div class="box-title">⬇️ Popularité des téléchargements</div>
<div class="box-value">{downloads_text}</div>
</div>
</div>
{extra_html}
<div class="hint">
Résultats affichés en <b>niveaux</b> (faible / moyen / élevé), pas en valeurs exactes.
</div>
</div>
""".strip()
# =========================
# INTERPRETATION (COMMUNE)
# =========================
def interpret_results(avg_class: int, dl_class: int) -> str:
"""
avg_class: 0=Missed info, 1=Low, 2=Medium, 3=High
dl_class: 0=Low, 1=Medium, 2=High
"""
if avg_class == 0:
return (
"ℹ️ <b>Interprétation</b> :<br>"
"Aucune évaluation possible (rating manquant).<br>"
)
rating_txt = {1: "faible", 2: "moyenne", 3: "élevée"}.get(avg_class, "inconnue")
downloads_txt = {0: "faible", 1: "modérée", 2: "élevée"}.get(dl_class, "inconnue")
if avg_class == 3 and dl_class == 2:
potentiel = "très fort"
detail = "contenu de haute qualité et très populaire."
elif avg_class == 3 and dl_class == 1:
potentiel = "fort"
detail = "contenu bien apprécié, en croissance."
elif avg_class == 3 and dl_class == 0:
potentiel = "prometteur"
detail = "bonne qualité mais faible visibilité (peut gagner en popularité)."
elif avg_class == 2 and dl_class == 2:
potentiel = "modéré à fort"
detail = "populaire mais qualité perçue moyenne."
elif avg_class == 2 and dl_class == 1:
potentiel = "modéré"
detail = "profil standard, popularité stable."
elif avg_class == 2 and dl_class == 0:
potentiel = "limité"
detail = "engagement faible, diffusion limitée."
elif avg_class == 1 and dl_class == 2:
potentiel = "contradictoire"
detail = "très téléchargé mais peu apprécié (usage pratique possible)."
elif avg_class == 1 and dl_class == 1:
potentiel = "faible"
detail = "peu attractif pour les utilisateurs."
else:
potentiel = "très faible"
detail = "faible intérêt global."
return (
"<b>Interprétation</b> :<br>"
f"Potentiel estimé : <b>{potentiel}</b> — {detail}"
)
def avg_label_to_class(avg_label: str) -> int:
"""
Convertit un label texte (LabelEncoder) en classe 0..3 :
0=Missed info, 1=Low, 2=Medium, 3=High
Robuste aux variantes.
"""
if avg_label is None:
return 0
s = str(avg_label).strip().lower()
if "miss" in s or "missing" in s or "none" in s or "no" in s:
return 0
if "high" in s or "élev" in s or "eleve" in s:
return 3
if "medium" in s or "moy" in s:
return 2
if "low" in s or "faibl" in s:
return 1
return 0
# ============================================================
# PARTIE A — Upload audio → openSMILE → modèles (toi)
# ============================================================
MODEL_EFFECT = joblib.load("xgb_model_EffectSound.pkl")
MODEL_MUSIC = joblib.load("xgb_model_Music.pkl")
RATING_DISPLAY_AUDIO = {
0: "❌ Informations manquantes",
1: "⭐ Faible",
2: "⭐⭐ Moyen",
3: "⭐⭐⭐ Élevé",
}
DOWNLOADS_DISPLAY_AUDIO = {
0: "⭐ Faible",
1: "⭐⭐ Moyen",
2: "⭐⭐⭐ Élevé",
}
SMILE = opensmile.Smile(
feature_set=opensmile.FeatureSet.eGeMAPSv02,
feature_level=opensmile.FeatureLevel.Functionals,
)
def get_duration_seconds(filepath):
ext = os.path.splitext(filepath)[1].lower()
if ext == ".mp3":
audio = AudioSegment.from_file(filepath)
return len(audio) / 1000.0
with sf.SoundFile(filepath) as f:
return len(f) / f.samplerate
def to_wav_16k_mono(filepath):
ext = os.path.splitext(filepath)[1].lower()
if ext == ".wav":
try:
with sf.SoundFile(filepath) as f:
if f.samplerate == SR_TARGET and f.channels == 1:
return filepath
except Exception:
pass
audio = AudioSegment.from_file(filepath)
audio = audio.set_channels(1).set_frame_rate(SR_TARGET)
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
tmp.close()
audio.export(tmp.name, format="wav")
return tmp.name
def extract_opensmile_features(filepath):
wav_path = to_wav_16k_mono(filepath)
feats = SMILE.process_file(wav_path)
feats = feats.select_dtypes(include=[np.number]).reset_index(drop=True)
return feats
def predict_upload_with_dmatrix(model, X_df: pd.DataFrame):
"""
Résout 'data did not contain feature names' en passant via Booster + DMatrix(feature_names=...).
Retour: array shape (1, n_outputs)
"""
if hasattr(model, "estimators_"):
preds = []
for est in model.estimators_:
booster = est.get_booster() if hasattr(est, "get_booster") else est
dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
p = booster.predict(dm)
preds.append(np.asarray(p).reshape(-1))
return np.column_stack(preds)
booster = model.get_booster() if hasattr(model, "get_booster") else model
dm = xgb.DMatrix(X_df.values, feature_names=list(X_df.columns))
p = booster.predict(dm)
return np.asarray(p).reshape(1, -1)
def predict_from_uploaded_audio(audio_file):
if audio_file is None:
return html_error("Aucun fichier", "Veuillez importer un fichier audio (wav, mp3, flac…).")
# Durée
try:
duration = get_duration_seconds(audio_file)
except Exception as e:
return html_error("Audio illisible", f"Impossible de lire l'audio.<br>Détail : <code>{e}</code>")
# Vérif durées
if duration < MIN_EFFECT:
return html_error(
"Audio trop court",
f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
f"Plages acceptées :<br>"
f"• Effet sonore : <b>{MIN_EFFECT}{MAX_EFFECT} s</b><br>"
f"• Musique : <b>{MIN_MUSIC}{MAX_MUSIC} s</b>"
)
if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC:
return html_error(
"Audio hors plage",
f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
f"Plages acceptées :<br>"
f"• Effet sonore : <b>{MIN_EFFECT}{MAX_EFFECT} s</b><br>"
f"• Musique : <b>{MIN_MUSIC}{MAX_MUSIC} s</b>"
)
# Type + modèle
if duration <= MAX_EFFECT:
badge = "🔊 Effet sonore (upload)"
model = MODEL_EFFECT
else:
badge = "🎵 Musique (upload)"
model = MODEL_MUSIC
# openSMILE
try:
X = extract_opensmile_features(audio_file)
except Exception as e:
return html_error("Extraction openSMILE échouée", f"Détail : <code>{e}</code>")
# Align features
try:
expected = model.estimators_[0].feature_names_in_ if hasattr(model, "estimators_") else model.feature_names_in_
X = X.reindex(columns=list(expected), fill_value=0)
except Exception as e:
return html_error("Alignement des features échoué", f"Détail : <code>{e}</code>")
# Predict
try:
y = predict_upload_with_dmatrix(model, X)
except Exception as e:
return html_error("Prédiction échouée", f"Détail : <code>{e}</code>")
y = np.array(y)
avg_class = int(y[0, 0])
dl_class = int(y[0, 1])
rating_text = RATING_DISPLAY_AUDIO.get(avg_class, "Inconnu")
downloads_text = DOWNLOADS_DISPLAY_AUDIO.get(dl_class, "Inconnu")
conclusion = interpret_results(avg_class, dl_class)
extra = f"""
<div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
{conclusion}
</div>
"""
return html_result(badge, duration, rating_text, downloads_text, extra_html=extra)
# ============================================================
# PARTIE B — URL FreeSound → API → modèles (collègue)
# ============================================================
API_TOKEN = "zE9NjEOgUMzH9K7mjiGBaPJiNwJLjSM53LevarRK" # <-- tu remplaces ici
fs_client = freesound.FreesoundClient()
fs_client.set_token(API_TOKEN, "token")
# Music
xgb_music_num = joblib.load("xgb_num_downloads_music_model.pkl")
xgb_music_feat_num = joblib.load("xgb_num_downloads_music_features.pkl")
xgb_music_avg = joblib.load("xgb_avg_rating_music_model.pkl")
xgb_music_feat_avg = joblib.load("xgb_avg_rating_music_features.pkl")
le_music_avg = joblib.load("xgb_avg_rating_music_label_encoder.pkl")
# Effect Sound
xgb_effect_num = joblib.load("xgb_num_downloads_effectsound_model.pkl")
xgb_effect_feat_num = joblib.load("xgb_num_downloads_effectsound_features.pkl")
xgb_effect_avg = joblib.load("xgb_avg_rating_effectsound_model.pkl")
xgb_effect_feat_avg = joblib.load("xgb_avg_rating_effectsound_features.pkl")
le_effect_avg = joblib.load("xgb_avg_rating_effectsound_label_encoder.pkl")
NUM_DOWNLOADS_MAP = {0: "Faible", 1: "Moyen", 2: "Élevé"}
def safe_float(v):
try:
return float(v)
except Exception:
return 0.0
def predict_with_model_fs(model, features_dict, feat_list, label_encoder=None):
row = []
for col in feat_list:
val = features_dict.get(col, 0)
if val is None or isinstance(val, (list, dict)):
val = 0
row.append(safe_float(val))
X = pd.DataFrame([row], columns=feat_list)
dmatrix = xgb.DMatrix(X.values, feature_names=feat_list)
pred_int = int(model.get_booster().predict(dmatrix)[0])
if label_encoder is not None:
return label_encoder.inverse_transform([pred_int])[0]
return pred_int
def predict_from_freesound_url(url: str):
if not url or not url.strip():
return html_error("URL vide", "Collez une URL FreeSound du type <code>https://freesound.org/s/123456/</code>")
# ID
try:
sound_id = int(url.rstrip("/").split("/")[-1])
except Exception:
return html_error("URL invalide", "Impossible d'extraire l'ID depuis l'URL.")
all_features = list(set(
xgb_music_feat_num + xgb_music_feat_avg + xgb_effect_feat_num + xgb_effect_feat_avg
))
fields = "duration," + ",".join(all_features)
try:
results = fs_client.search(query="", filter=f"id:{sound_id}", fields=fields)
except Exception as e:
return html_error("Erreur API FreeSound", f"Détail : <code>{e}</code>")
if len(results.results) == 0:
return html_error("Son introuvable", "Aucun résultat pour cet ID.")
sound = results.results[0]
duration = safe_float(sound.get("duration", 0))
# Effect Sound
if MIN_EFFECT <= duration <= MAX_EFFECT:
badge = "🔊 Effet sonore (FreeSound URL)"
dl_class = int(predict_with_model_fs(xgb_effect_num, sound, xgb_effect_feat_num))
avg_text = str(predict_with_model_fs(xgb_effect_avg, sound, xgb_effect_feat_avg, le_effect_avg))
dl_text = NUM_DOWNLOADS_MAP.get(dl_class, str(dl_class))
avg_class = avg_label_to_class(avg_text)
conclusion = interpret_results(avg_class, dl_class)
extra = f"""
<div class="hint">ID FreeSound : <b>{sound_id}</b></div>
<div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
{conclusion}
</div>
"""
return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
# Music
if MIN_MUSIC <= duration <= MAX_MUSIC:
badge = "🎵 Musique (FreeSound URL)"
dl_class = int(predict_with_model_fs(xgb_music_num, sound, xgb_music_feat_num))
avg_text = str(predict_with_model_fs(xgb_music_avg, sound, xgb_music_feat_avg, le_music_avg))
dl_text = NUM_DOWNLOADS_MAP.get(dl_class, str(dl_class))
avg_class = avg_label_to_class(avg_text)
conclusion = interpret_results(avg_class, dl_class)
extra = f"""
<div class="hint">ID FreeSound : <b>{sound_id}</b></div>
<div style="margin-top:12px; padding-top:10px; border-top:1px dashed #d1d5db">
{conclusion}
</div>
"""
return html_result(badge, duration, avg_text, dl_text, extra_html=extra)
return html_error(
"Durée non supportée",
f"Durée détectée : <b>{duration:.2f} s</b><br><br>"
f"Plages acceptées :<br>"
f"• Effet sonore : <b>{MIN_EFFECT}{MAX_EFFECT} s</b><br>"
f"• Musique : <b>{MIN_MUSIC}{MAX_MUSIC} s</b>"
)
# =========================
# APP UI (2 onglets)
# =========================
theme = gr.themes.Soft()
with gr.Blocks(title="Démo — Popularité Audio", css=CSS) as demo:
gr.HTML(
f"""
<div id="header-title">Démo — Prédiction de popularité audio</div>
<p id="header-sub">
Deux modes : <b>Upload audio</b> (openSMILE) ou <b>URL FreeSound</b> (features API).<br><br>
<b>Durées acceptées :</b> 🔊 Effet sonore {MIN_EFFECT}{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}{MAX_MUSIC}s
</p>
"""
)
with gr.Tabs():
with gr.Tab("1) Upload audio (openSMILE)"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Importer un fichier")
audio_in = gr.Audio(type="filepath", label="Fichier audio")
btn_audio = gr.Button("🚀 Prédire (upload)", variant="primary")
with gr.Column(scale=1):
gr.Markdown("### Résultat")
out_audio = gr.HTML()
btn_audio.click(predict_from_uploaded_audio, inputs=audio_in, outputs=out_audio)
with gr.Tab("2) URL FreeSound (features API)"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Coller une URL FreeSound")
url_in = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/")
btn_url = gr.Button("🚀 Prédire (URL FreeSound)", variant="primary")
with gr.Column(scale=1):
gr.Markdown("### Résultat")
out_url = gr.HTML()
btn_url.click(predict_from_freesound_url, inputs=url_in, outputs=out_url)
demo.launch(theme=theme)