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"""
❌ {title}
{body_html}
""".strip() def html_result(badge_text, duration, rating_text, downloads_text, extra_html=""): return f"""
{badge_text} ⏱️ {duration:.2f} s
📈 Popularité de la note moyenne
{rating_text}
⬇️ Popularité des téléchargements
{downloads_text}
{extra_html}
Résultats affichés en niveaux (faible / moyen / élevé), pas en valeurs exactes.
""".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 ( "ℹ️ Interprétation :
" "Aucune évaluation possible (rating manquant).
" ) 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 ( "Interprétation :
" f"Potentiel estimé : {potentiel} — {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.
Détail : {e}") # Vérif durées if duration < MIN_EFFECT: return html_error( "Audio trop court", f"Durée détectée : {duration:.2f} s

" f"Plages acceptées :
" f"• Effet sonore : {MIN_EFFECT}–{MAX_EFFECT} s
" f"• Musique : {MIN_MUSIC}–{MAX_MUSIC} s" ) if (MAX_EFFECT < duration < MIN_MUSIC) or duration > MAX_MUSIC: return html_error( "Audio hors plage", f"Durée détectée : {duration:.2f} s

" f"Plages acceptées :
" f"• Effet sonore : {MIN_EFFECT}–{MAX_EFFECT} s
" f"• Musique : {MIN_MUSIC}–{MAX_MUSIC} s" ) # 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 : {e}") # 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 : {e}") # Predict try: y = predict_upload_with_dmatrix(model, X) except Exception as e: return html_error("Prédiction échouée", f"Détail : {e}") 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"""
{conclusion}
""" 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 https://freesound.org/s/123456/") # 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 : {e}") 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"""
ID FreeSound : {sound_id}
{conclusion}
""" 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"""
ID FreeSound : {sound_id}
{conclusion}
""" 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 : {duration:.2f} s

" f"Plages acceptées :
" f"• Effet sonore : {MIN_EFFECT}–{MAX_EFFECT} s
" f"• Musique : {MIN_MUSIC}–{MAX_MUSIC} s" ) # ========================= # APP UI (2 onglets) # ========================= theme = gr.themes.Soft() with gr.Blocks(title="Démo — Popularité Audio", css=CSS) as demo: gr.HTML( f"""
Démo — Prédiction de popularité audio

Deux modes : Upload audio (openSMILE) ou URL FreeSound (features API).

Durées acceptées : 🔊 Effet sonore {MIN_EFFECT}–{MAX_EFFECT}s · 🎵 Musique {MIN_MUSIC}–{MAX_MUSIC}s

""" ) 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)