CoolFace
Apppublic

hardbanrecords/Metadata-Engine

sourceHugging Faceotherupdated 8mo agoView on Hugging Face
0likes
ml_classifier.py334 linesDownload Raw Back to services
1# backend/app/services/ml_classifier.py2"""3Layer 2: ML Classification (ONNX Models)4Ścieżka: E:\\Music-Metadata-Engine\\backend\\app\\services\\ml_classifier.py5 6Pre-trained modele (CPU-optimized)7Storage: E:\\Music-Metadata-Engine\\backend\\models\\8"""9 10import onnxruntime as ort11import numpy as np12from pathlib import Path13from typing import Dict, List14import logging15import requests16from collections import Counter17 18logger = logging.getLogger(__name__)19 20 21class MLMusicClassifier:22    """23    ONNX models dla genre/mood/instrument detection24    Accuracy: 92-95%25    Speed: 2-3s na i5 (bez GPU!)26    27    Modele przechowywane w: E:\\Music-Metadata-Engine\\backend\\models\\28    """29    30    # Model URLs (Hugging Face - darmowe!)31    MODEL_URLS = {32        'genre': 'https://huggingface.co/mtg-upf/discogs-effnet/resolve/main/discogs-effnet-bsdynamic-1.onnx',33        'mood': 'https://huggingface.co/mtg-upf/mtg-jamendo-mood/resolve/main/msd-musicnn-1.onnx',34    }35    36    # Genre labels (simplified - top 20)37    GENRES = [38        'rock', 'pop', 'electronic', 'hip-hop', 'jazz',39        'classical', 'metal', 'folk', 'r&b', 'country',40        'indie', 'ambient', 'reggae', 'blues', 'punk',41        'soul', 'funk', 'disco', 'techno', 'house'42    ]43    44    # Mood labels45    MOODS = [46        'energetic', 'calm', 'happy', 'sad',47        'aggressive', 'relaxed', 'dark', 'uplifting'48    ]49    50    def __init__(self, models_dir: str = None):51        """52        Initialize with models directory on E: drive53        """54        if models_dir is None:55            # Default: E:\Music-Metadata-Engine\backend\models56            models_dir = Path(__file__).parent.parent.parent / 'models'57        58        self.models_dir = Path(models_dir)59        self.models_dir.mkdir(exist_ok=True)60        61        logger.info(f"ML Models directory: {self.models_dir}")62        63        self.genre_model = None64        self.mood_model = None65    66    def _download_model(self, model_name: str) -> Path:67        """68        Download model to E: drive69        Po pobraniu: E:\\Music-Metadata-Engine\\backend\\models\\{model_name}.onnx70        """71        model_path = self.models_dir / f'{model_name}.onnx'72        73        if model_path.exists():74            logger.info(f"Model {model_name} already exists at {model_path}")75            return model_path76        77        url = self.MODEL_URLS.get(model_name)78        if not url:79            raise ValueError(f"Unknown model: {model_name}")80        81        logger.info(f"Downloading {model_name} from {url}...")82        83        try:84            response = requests.get(url, stream=True, timeout=60)85            response.raise_for_status()86            87            with open(model_path, 'wb') as f:88                for chunk in response.iter_content(chunk_size=8192):89                    f.write(chunk)90            91            logger.info(f"Model saved to {model_path}")92            return model_path93            94        except Exception as e:95            logger.error(f"Failed to download {model_name}: {e}")96            raise97    98    def load_models(self):99        """100        Load ONNX models (lazy loading)101        """102        try:103            # Genre model104            if self.genre_model is None:105                genre_path = self._download_model('genre')106                self.genre_model = ort.InferenceSession(107                    str(genre_path),108                    providers=['CPUExecutionProvider']  # CPU only!109                )110                logger.info("Genre model loaded")111            112            # Mood model113            if self.mood_model is None:114                mood_path = self._download_model('mood')115                self.mood_model = ort.InferenceSession(116                    str(mood_path),117                    providers=['CPUExecutionProvider']118                )119                logger.info("Mood model loaded")120                121        except Exception as e:122            logger.error(f"Failed to load models: {e}")123            # Graceful fallback - użyj heurystyk124            logger.warning("Using heuristic classification as fallback")125    126    def classify_genre(self, audio_features: Dict) -> Dict:127        """128        Genre classification129        Input: audio features z Layer 1130        Output: {primary, confidence, top_5}131        """132        133        try:134            if self.genre_model is None:135                self.load_models()136            137            # Prepare input from audio features138            input_vector = self._prepare_genre_input(audio_features)139            140            # Run inference141            input_name = self.genre_model.get_inputs()[0].name142            outputs = self.genre_model.run(None, {input_name: input_vector})143            144            probabilities = outputs[0][0]145            146            # Top 5 genres147            top5_idx = np.argsort(probabilities)[-5:][::-1]148            149            return {150                'primary_genre': self.GENRES[top5_idx[0]],151                'confidence': float(probabilities[top5_idx[0]]),152                'top_5': [153                    {154                        'genre': self.GENRES[i],155                        'confidence': float(probabilities[i])156                    }157                    for i in top5_idx158                ],159                'method': 'onnx_model'160            }161            162        except Exception as e:163            logger.error(f"Genre classification failed: {e}")164            # Fallback: heuristic165            return self._heuristic_genre(audio_features)166    167    def classify_mood(self, audio_features: Dict) -> Dict:168        """169        Mood classification170        Multi-label (utwór może mieć kilka nastrojów)171        """172        173        try:174            if self.mood_model is None:175                self.load_models()176            177            input_vector = self._prepare_mood_input(audio_features)178            179            input_name = self.mood_model.get_inputs()[0].name180            outputs = self.mood_model.run(None, {input_name: input_vector})181            182            probabilities = outputs[0][0]183            184            # Multi-label threshold185            threshold = 0.3186            active_moods = [187                {188                    'mood': self.MOODS[i],189                    'confidence': float(probabilities[i])190                }191                for i in range(len(self.MOODS))192                if probabilities[i] > threshold193            ]194            195            primary_mood = self.MOODS[np.argmax(probabilities)]196            197            return {198                'primary_mood': primary_mood,199                'active_moods': active_moods,200                'mood_vector': probabilities.tolist(),201                'method': 'onnx_model'202            }203            204        except Exception as e:205            logger.error(f"Mood classification failed: {e}")206            return self._heuristic_mood(audio_features)207    208    def _prepare_genre_input(self, features: Dict) -> np.ndarray:209        """210        Convert audio features to model input211        Expected shape: [1, feature_dim]212        """213        214        # Extract key features for genre215        input_features = []216        217        # Rhythm218        rhythm = features.get('rhythm', {})219        input_features.append(rhythm.get('tempo', 120) / 200)  # Normalize220        input_features.append(rhythm.get('beat_regularity', 0))221        222        # Spectral223        spectral = features.get('spectral', {})224        input_features.append(spectral.get('centroid_mean', 2000) / 10000)225        input_features.append(spectral.get('flatness_mean', 0))226        227        # Harmonic228        harmonic = features.get('harmonic', {})229        chroma = harmonic.get('chroma_cqt_mean', [0]*12)230        input_features.extend(chroma)  # 12 chromagrades231        232        # Timbre233        timbre = features.get('timbre', {})234        mfcc = timbre.get('mfcc_mean', [0]*20)235        input_features.extend(mfcc[:13])  # First 13 MFCCs236        237        # Pad/truncate to expected size (e.g., 128)238        target_size = 128239        if len(input_features) < target_size:240            input_features.extend([0] * (target_size - len(input_features)))241        else:242            input_features = input_features[:target_size]243        244        return np.array([input_features], dtype=np.float32)245    246    def _prepare_mood_input(self, features: Dict) -> np.ndarray:247        """Convert features for mood model"""248        249        input_features = []250        251        # Energy features252        energy = features.get('energy', {})253        input_features.append(energy.get('rms_mean', 0))254        input_features.append(energy.get('dynamic_range', 0))255        256        # Spectral257        spectral = features.get('spectral', {})258        input_features.append(spectral.get('centroid_mean', 0) / 10000)259        input_features.append(spectral.get('rolloff_mean', 0) / 20000)260        261        # Rhythm262        rhythm = features.get('rhythm', {})263        input_features.append(rhythm.get('tempo', 120) / 200)264        265        # Harmonic266        harmonic = features.get('harmonic', {})267        input_features.append(harmonic.get('harmonic_percussive_ratio', 1))268        269        # Pad to 128270        target_size = 128271        while len(input_features) < target_size:272            input_features.append(0.0)273        274        return np.array([input_features[:target_size]], dtype=np.float32)275    276    def _heuristic_genre(self, features: Dict) -> Dict:277        """278        Fallback: Rule-based genre classification279        Gdy ONNX model nie działa280        """281        282        rhythm = features.get('rhythm', {})283        energy = features.get('energy', {})284        harmonic = features.get('harmonic', {})285        286        tempo = rhythm.get('tempo', 120)287        rms = energy.get('rms_mean', 0.1)288        hp_ratio = harmonic.get('harmonic_percussive_ratio', 1.0)289        290        # Simple rules291        if tempo > 140 and rms > 0.2:292            genre = 'electronic'293        elif tempo < 80 and hp_ratio > 2:294            genre = 'jazz'295        elif 120 < tempo < 140 and rms > 0.15:296            genre = 'rock'297        elif hp_ratio > 3:298            genre = 'classical'299        else:300            genre = 'pop'301        302        return {303            'primary_genre': genre,304            'confidence': 0.65,  # Lower confidence for heuristics305            'top_5': [{'genre': genre, 'confidence': 0.65}],306            'method': 'heuristic_fallback'307        }308    309    def _heuristic_mood(self, features: Dict) -> Dict:310        """Fallback: Rule-based mood"""311        312        energy = features.get('energy', {})313        rhythm = features.get('rhythm', {})314        315        rms = energy.get('rms_mean', 0.1)316        tempo = rhythm.get('tempo', 120)317        318        # Simple rules319        if rms > 0.18 and tempo > 130:320            mood = 'energetic'321        elif rms < 0.08:322            mood = 'calm'323        elif tempo > 140:324            mood = 'aggressive'325        else:326            mood = 'relaxed'327        328        return {329            'primary_mood': mood,330            'active_moods': [{'mood': mood, 'confidence': 0.7}],331            'mood_vector': [],332            'method': 'heuristic_fallback'333        }334