# app.py 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 # (Optionnel) GloVe via gensim (si dispo / autorisé) try: import gensim.downloader as api _GENSIM_OK = True except Exception: _GENSIM_OK = False # ========================= # RÈGLES DURÉE # ========================= MIN_EFFECT, MAX_EFFECT = 0.5, 3.0 MIN_MUSIC, MAX_MUSIC = 10.0, 60.0 SR_TARGET = 16000 # ========================= # HELPERS LOAD # ========================= def load_artifact(*candidate_paths: str): """ Charge un artifact joblib/pkl depuis la racine ou chemins candidats. Essaie tous les chemins donnés, puis lève une erreur claire. """ for p in candidate_paths: if p and os.path.exists(p): return joblib.load(p) tried = "\n".join([f"- {p}" for p in candidate_paths if p]) raise FileNotFoundError( "Artifact introuvable. J'ai essayé :\n" + (tried if tried else "(aucun chemin)") ) # ========================= # 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() def normalize_avg_rating_label_fr(label) -> str: """ Convertit n'importe quel label avg_rating (EN/FR/variantes) en FR stable. Sorties possibles : "Informations manquantes", "Faible", "Moyen", "Élevé" """ if label is None: return "Informations manquantes" s = str(label).strip().lower() # manquant if "miss" in s or "missing" in s or "none" in s or "no" in s or "nan" in s: return "Informations manquantes" if "info" in s and "manq" in s: return "Informations manquantes" # élevé if "high" in s or "élev" in s or "eleve" in s: return "Élevé" # moyen if "medium" in s or "moy" in s: return "Moyen" # faible if "low" in s or "faibl" in s: return "Faible" # fallback (si le modèle renvoie un truc inattendu) return "Informations manquantes" def avg_fr_to_class(avg_fr: str) -> int: """ Convertit l'étiquette FR en classe 0..3 pour interpret_results() """ s = str(avg_fr).strip().lower() if "manqu" in s: return 0 if "faibl" in s: return 1 if "moy" in s: return 2 if "élev" in s or "eleve" in s: return 3 return 0 # ========================= # 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)." ) 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 # ========================= # FreeSound client (commun) # ========================= API_TOKEN = os.getenv("FREESOUND_TOKEN", "").strip() fs_client = freesound.FreesoundClient() if API_TOKEN: fs_client.set_token(API_TOKEN, "token") # ============================================================ # ONGLET 1 — Upload audio → openSMILE → modèle local # ============================================================ MODEL_EFFECT = load_artifact("xgb_model_EffectSound.pkl") MODEL_MUSIC = load_artifact("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) # ============================================================ # ONGLET 2 — URL FreeSound → features API → modèles locaux # ============================================================ xgb_music_num = load_artifact("xgb_num_downloads_music_model.pkl") xgb_music_feat_num = load_artifact("xgb_num_downloads_music_features.pkl") xgb_music_avg = load_artifact("xgb_avg_rating_music_model.pkl") xgb_music_feat_avg = load_artifact("xgb_avg_rating_music_features.pkl") le_music_avg = load_artifact("xgb_avg_rating_music_label_encoder.pkl") xgb_effect_num = load_artifact("xgb_num_downloads_effectsound_model.pkl") xgb_effect_feat_num = load_artifact("xgb_num_downloads_effectsound_features.pkl") xgb_effect_avg = load_artifact("xgb_avg_rating_effectsound_model.pkl") xgb_effect_feat_avg = load_artifact("xgb_avg_rating_effectsound_features.pkl") le_effect_avg = load_artifact("xgb_avg_rating_effectsound_label_encoder.pkl") NUM_DOWNLOADS_MAP_FR = {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 API_TOKEN: return html_error( "Token FreeSound manquant", "Ajoute la variable d’environnement FREESOUND_TOKEN pour activer cet onglet." ) 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( list(xgb_music_feat_num) + list(xgb_music_feat_avg) + list(xgb_effect_feat_num) + list(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 (URL → features API)" dl_class = int(predict_with_model_fs(xgb_effect_num, sound, xgb_effect_feat_num)) avg_text_raw = str(predict_with_model_fs(xgb_effect_avg, sound, xgb_effect_feat_avg, le_effect_avg)) avg_text = normalize_avg_rating_label_fr(avg_text_raw) avg_class = avg_fr_to_class(avg_text) dl_text = NUM_DOWNLOADS_MAP_FR.get(dl_class, str(dl_class)) 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 (URL → features API)" dl_class = int(predict_with_model_fs(xgb_music_num, sound, xgb_music_feat_num)) avg_text_raw = str(predict_with_model_fs(xgb_music_avg, sound, xgb_music_feat_avg, le_music_avg)) avg_text = normalize_avg_rating_label_fr(avg_text_raw) avg_class = avg_fr_to_class(avg_text) dl_text = NUM_DOWNLOADS_MAP_FR.get(dl_class, str(dl_class)) 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" ) # ============================================================ # ONGLET 3 — URL FreeSound → METADATA → preprocessing complet → modèles # (reprend la logique du script metadata, mais sans HF hub obligatoire) # ============================================================ class AvgRatingTransformer: def __init__(self, est, class_mapping=None): self.est = est if class_mapping is None: self.class_mapping = {0: "MissedInfo", 1: "Low", 2: "Medium", 3: "High"} else: self.class_mapping = class_mapping def transform(self, X): X = np.asarray(X) mask_non_zero = X != 0 Xt = np.zeros_like(X, dtype=int) if mask_non_zero.any(): Xt[mask_non_zero] = self.est.transform(X[mask_non_zero].reshape(-1, 1)).flatten() + 1 return np.array([self.class_mapping.get(v, "MissedInfo") for v in Xt]) # ---- Artifacts preprocessing (music/effect) ---- # Supporte soit "à la racine", soit encore dans music/ et effectSound/ scaler_samplerate_music = load_artifact("scaler_music_samplerate.joblib", "music/scaler_music_samplerate.joblib") scaler_age_days_music = load_artifact("scaler_music_age_days_log.joblib", "music/scaler_music_age_days_log.joblib") username_freq_music = load_artifact("username_freq_dict_music.joblib", "music/username_freq_dict_music.joblib") est_num_downloads_music = load_artifact("est_num_downloads_music.joblib", "music/est_num_downloads_music.joblib") avg_rating_transformer_music = load_artifact("avg_rating_transformer_music.joblib", "music/avg_rating_transformer_music.joblib") music_subcategory_cols = load_artifact("music_subcategory_cols.joblib", "music/music_subcategory_cols.joblib") music_onehot_cols = load_artifact("music_onehot_cols.joblib", "music/music_onehot_cols.joblib") music_onehot_tags = load_artifact("music_onehot_tags.joblib", "music/music_onehot_tags.joblib") scaler_samplerate_effect = load_artifact("scaler_effectSamplerate.joblib", "effectSound/scaler_effectSamplerate.joblib") scaler_age_days_effect = load_artifact("scaler_effectSound_age_days_log.joblib", "effectSound/scaler_effectSound_age_days_log.joblib") username_freq_effect = load_artifact("username_freq_dict_effectSound.joblib", "effectSound/username_freq_dict_effectSound.joblib") est_num_downloads_effect = load_artifact("est_num_downloads_effectSound.joblib", "effectSound/est_num_downloads_effectSound.joblib") avg_rating_transformer_effect = load_artifact("avg_rating_transformer_effectSound.joblib", "effectSound/avg_rating_transformer_effectSound.joblib") effect_subcategory_cols = load_artifact("effectSound_subcategory_cols.joblib", "effectSound/effectSound_subcategory_cols.joblib") effect_onehot_cols = load_artifact("effectSound_onehot_cols.joblib", "effectSound/effectSound_onehot_cols.joblib") effect_onehot_tags = load_artifact("effect_onehot_tags.joblib", "effectSound/effect_onehot_tags.joblib") # ---- Modèles metadata (num_downloads + avg_rating + features list) ---- # (à mettre idéalement à la racine) music_model_num_downloads = load_artifact("music_model_num_downloads.joblib") music_model_avg_rating = load_artifact("music_xgb_avg_rating.joblib") music_avg_rating_le_meta = load_artifact("music_xgb_avg_rating_label_encoder.joblib") music_model_features = load_artifact("music_model_features_list.joblib") effect_model_num_downloads = load_artifact("effectSound_model_num_downloads.joblib") effect_model_avg_rating = load_artifact("effectSound_xgb_avg_rating.joblib") effect_avg_rating_le_meta = load_artifact("effectSound_xgb_avg_rating_label_encoder.joblib") effect_model_features = load_artifact("effect_model_features_list.joblib") # Nettoyage doublons (comme ta collègue) music_model_features = list(dict.fromkeys(list(music_model_features))) effect_model_features = list(dict.fromkeys(list(effect_model_features))) # GloVe (optionnel) if _GENSIM_OK: try: glove_model = api.load("glove-wiki-gigaword-100") except Exception: glove_model = None else: glove_model = None def preprocess_name(df, vec_dim=8): # Version simple: hashing via sklearn n'est pas importé ici pour rester léger. # Pour rester fidèle au code collègue, on refait le hashing "à la main" avec pandas+numpy. # (Si tu veux EXACTEMENT HashingVectorizer, dis-moi et je te le remets.) df = df.copy() name = df["name_clean"].fillna("").astype(str) df["name_len"] = name.str.len() # hashing rudimentaire en vec_dim dimensions vec = np.zeros((len(df), vec_dim), dtype=float) for i, s in enumerate(name.tolist()): h = abs(hash(s)) for k in range(vec_dim): vec[i, k] = ((h >> (k * 3)) & 0x7) # petit pattern stable for k in range(vec_dim): df[f"name_vec_{k}"] = vec[:, k] return df def description_to_vec(text, model, dim=100): if model is None: return np.zeros(dim) if not text: return np.zeros(dim) words = str(text).lower().split() vecs = [model[w] for w in words if w in model] if len(vecs) == 0: return np.zeros(dim) return np.mean(vecs, axis=0) def fetch_sound_metadata(sound_url: str) -> pd.DataFrame: """ Récupère les metadata FreeSound (sans télécharger l'audio). """ if not API_TOKEN: raise RuntimeError("Token FreeSound manquant (FREESOUND_TOKEN).") sound_id = int(sound_url.rstrip("/").split("/")[-1]) sound = fs_client.get_sound(sound_id) data = { "id": sound_id, "file_path": None, "name": getattr(sound, "name", ""), "num_ratings": getattr(sound, "num_ratings", 0), "tags": ",".join(getattr(sound, "tags", []) or []), "username": getattr(sound, "username", ""), "description": getattr(sound, "description", "") or "", "created": getattr(sound, "created", ""), "license": getattr(sound, "license", ""), "num_downloads": getattr(sound, "num_downloads", 0), "channels": getattr(sound, "channels", 0), "filesize": getattr(sound, "filesize", 0), "num_comments": getattr(sound, "num_comments", 0), "category_is_user_provided": getattr(sound, "category_is_user_provided", 0), "duration": getattr(sound, "duration", 0), "avg_rating": getattr(sound, "avg_rating", 0), "category": getattr(sound, "category", "Unknown"), "subcategory": getattr(sound, "subcategory", "Other"), "type": getattr(sound, "type", ""), "samplerate": getattr(sound, "samplerate", 0), } return pd.DataFrame([data]) def preprocess_sound(df: pd.DataFrame): """ Preprocessing complet basé sur la durée pour choisir Music vs EffectSound. """ df = df.copy() dur = float(df["duration"].iloc[0]) if MIN_EFFECT <= dur <= MAX_EFFECT: scaler_samplerate = scaler_samplerate_effect scaler_age = scaler_age_days_effect username_freq = username_freq_effect est_num_downloads = est_num_downloads_effect avg_rating_transformer = avg_rating_transformer_effect subcat_cols = effect_subcategory_cols onehot_cols = effect_onehot_cols onehot_tags = effect_onehot_tags elif MIN_MUSIC <= dur <= MAX_MUSIC: scaler_samplerate = scaler_samplerate_music scaler_age = scaler_age_days_music username_freq = username_freq_music est_num_downloads = est_num_downloads_music avg_rating_transformer = avg_rating_transformer_music subcat_cols = music_subcategory_cols onehot_cols = music_onehot_cols onehot_tags = music_onehot_tags else: return f"❌ Son trop court ou trop long ({dur} sec)" # Category bool df["category_is_user_provided"] = df["category_is_user_provided"].astype(int) # Username frequency df["username_freq"] = df["username"].map(username_freq).fillna(0) # Numeric features log1p for col in ["num_ratings", "num_comments", "filesize", "duration"]: df[col] = np.log1p(df[col]) # samplerate scaled df["samplerate"] = scaler_samplerate.transform(df[["samplerate"]]) # Age_days df["created"] = pd.to_datetime(df["created"], errors="coerce").dt.tz_localize(None) df["age_days"] = (pd.Timestamp.now() - df["created"]).dt.days df["age_days_log"] = np.log1p(df["age_days"]) df["age_days_log_scaled"] = scaler_age.transform(df[["age_days_log"]]) df = df.drop(columns=["created", "age_days", "age_days_log"]) # num_downloads_class (binned) df["num_downloads_class"] = est_num_downloads.transform(df[["num_downloads"]]) # avg_rating discretized via transformer df["avg_rating"] = avg_rating_transformer.transform(df["avg_rating"].to_numpy()) # Subcategory onehot for col in subcat_cols: df[col] = 0 subcat_val = df["subcategory"].iloc[0] for col in subcat_cols: cat_name = col.replace("subcategory_", "") if subcat_val == cat_name: df[col] = 1 df.drop(columns=["subcategory"], inplace=True) # One-hot cols (license/category/type) for col in onehot_cols: if col not in df.columns: df[col] = 0 license_val = df.loc[0, "license"] category_val = df.loc[0, "category"] type_val = df.loc[0, "type"] for col_name in [f"license_{license_val}", f"category_{category_val}", f"type_{type_val}"]: if col_name in df.columns: df[col_name] = 1 # Tags for col in ["name", "tags", "description"]: if col not in df.columns: df[col] = "" df["tags_list"] = df["tags"].fillna("").astype(str).str.lower().str.split(",") if not df["tags_list"].iloc[0] or df["tags_list"].iloc[0] == [""]: df["tags_list"] = [["Other"]] for col in onehot_tags: if col not in df.columns: df[col] = 0 tags_list = df["tags"].iloc[0].lower().split(",") if df["tags"].iloc[0] else [] for col in onehot_tags: tag_name = col.replace("tag_", "").lower() if tag_name in tags_list: df[col] = 1 df.drop(columns=["tags"], inplace=True) # Name hashing df["name_clean"] = df["name"].astype(str).str.lower().str.rsplit(".", n=1).str[0] df = preprocess_name(df, vec_dim=8) df.drop(columns=["name", "name_clean"], inplace=True) # Description → glove mean (si glove non dispo: zeros) desc_vec = description_to_vec(df["description"].iloc[0], glove_model) for i in range(100): df[f"description_glove_{i}"] = float(desc_vec[i]) df.drop(columns=["description"], inplace=True) # Drop non-features df.drop( columns=[ "license", "category", "type", "subcategory", "id", "num_downloads", "file_path", "username", "tags_list" ], inplace=True, errors="ignore" ) return df def predict_with_model_meta(model, df_input: pd.DataFrame, le=None): booster_feats = model.get_booster().feature_names X_aligned = df_input.reindex(columns=booster_feats, fill_value=0.0).astype(float) dmatrix = xgb.DMatrix(X_aligned.values, feature_names=list(booster_feats)) preds = model.get_booster().predict(dmatrix) pred_val = preds[0] pred_int = int(round(float(pred_val))) if le is not None: try: return le.inverse_transform([pred_int])[0] except Exception: return f"Classe inconnue ({pred_int})" return pred_int def predict_from_metadata_url(url: str): if not API_TOKEN: return html_error( "Token FreeSound manquant", "Ajoute la variable d’environnement FREESOUND_TOKEN pour activer cet onglet." ) if not url or not url.strip(): return html_error( "URL vide", "Collez une URL FreeSound du type https://freesound.org/s/123456/" ) # 1) metadata brute try: df_raw = fetch_sound_metadata(url) except Exception as e: return html_error("Erreur API FreeSound", f"Détail : {e}") # 2) durée try: dur = float(df_raw["duration"].iloc[0]) except Exception: dur = 0.0 if dur < MIN_EFFECT: return html_error( "Audio trop court", f"Durée détectée : {dur:.2f} s

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

" f"Plages acceptées :
" f"• Effet sonore : {MIN_EFFECT}–{MAX_EFFECT} s
" f"• Musique : {MIN_MUSIC}–{MAX_MUSIC} s" ) # 3) preprocessing complet (mais on NE L’AFFICHE PAS) df_processed = preprocess_sound(df_raw) if isinstance(df_processed, str): return html_error("Preprocessing impossible", df_processed) # 4) features pour modèles cols_to_remove = ["avg_rating", "num_downloads_class"] df_for_model = df_processed.drop( columns=[c for c in cols_to_remove if c in df_processed.columns], errors="ignore" ) # 5) choisir modèles metadata if MIN_EFFECT <= dur <= MAX_EFFECT: badge = "🔊 Effet sonore (URL → metadata)" model_nd = effect_model_num_downloads model_ar = effect_model_avg_rating model_features = effect_model_features current_le = effect_avg_rating_le_meta else: badge = "🎵 Musique (URL → metadata)" model_nd = music_model_num_downloads model_ar = music_model_avg_rating model_features = music_model_features current_le = music_avg_rating_le_meta # 6) forcer colonnes exactes df_for_model = df_for_model.reindex(columns=model_features, fill_value=0.0).astype(float) # 7) prédire pred_num_downloads_val = predict_with_model_meta(model_nd, df_for_model, le=None) pred_num_downloads_val = int(pred_num_downloads_val) if str(pred_num_downloads_val).isdigit() else pred_num_downloads_val # Mapping num downloads NUM_DOWNLOADS_MAP = {0: "Faible", 1: "Moyen", 2: "Élevé"} pred_downloads_text = NUM_DOWNLOADS_MAP.get(pred_num_downloads_val, str(pred_num_downloads_val)) # avg rating (label) pred_avg_rating_label_raw = predict_with_model_meta(model_ar, df_for_model, le=current_le) pred_avg_rating_label = normalize_avg_rating_label_fr(pred_avg_rating_label_raw) avg_class = avg_fr_to_class(pred_avg_rating_label) dl_class = int(pred_num_downloads_val) if isinstance(pred_num_downloads_val, (int, np.integer)) else 0 rating_display = str(pred_avg_rating_label) downloads_display = pred_downloads_text conclusion = interpret_results(avg_class, dl_class) extra = f"""
{conclusion}
""" return html_result(badge, dur, rating_display, downloads_display, extra_html=extra) # ========================= # APP UI (3 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

Trois modes : Upload audio (openSMILE), URL FreeSound (features API), URL FreeSound (metadata + preprocessing complet).

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

""" ) if not API_TOKEN: gr.Markdown( "⚠️ **FREESOUND_TOKEN non défini** : les onglets URL (2 et 3) ne fonctionneront pas tant que tu ne l’ajoutes pas." ) with gr.Tabs(): # -------- TAB 1 -------- 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) # -------- TAB 2 -------- 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 → features API)", 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) # -------- TAB 3 -------- with gr.Tab("3) URL FreeSound (metadata)"): with gr.Row(): with gr.Column(scale=1): gr.Markdown("### Coller une URL FreeSound") url_meta = gr.Textbox(label="URL FreeSound", placeholder="https://freesound.org/s/123456/") btn_meta = gr.Button("🚀 Prédire (URL → metadata)", variant="primary") with gr.Column(scale=1): gr.Markdown("### Résultat") out_meta = gr.HTML() btn_meta.click(predict_from_metadata_url, inputs=url_meta, outputs=out_meta) demo.launch(theme=theme)