GitHunt
WI

wissalouarda6/SYSTEME-DE-DETECTION-D-EMOTIONS-VOCALES

un système capable de détecter 8 émotions à partir de la voix en temps réel

SYSTÈME DE DÉTECTION D'ÉMOTIONS VOCALES

TABLE DES MATIÈRES

  1. Vue d'ensemble
  2. Installation
  3. Architecture du Projet
  4. Dataset RAVDESS
  5. Explication Détaillée du Code
  6. Utilisation
  7. Résultats
  8. Limitations

VUE D'ENSEMBLE

Objectif

Développer un système capable de détecter 8 émotions à partir de la voix en temps réel :

  • neutral (neutre)
  • calm (calme)
  • happy (joyeux)
  • sad (triste)
  • angry (en colère)
  • fearful (peureux)
  • disgust (dégoût)
  • surprised (surpris)

Technologies Utilisées

  • Python 3.8+ : Langage de programmation
  • PyTorch : Framework de Deep Learning
  • Librosa : Extraction de features audio
  • Scikit-learn : Prétraitement et validation
  • PyAudio : Capture audio en temps réel
  • NumPy/Pandas : Manipulation de données
  • Matplotlib : Visualisation

INSTALLATION

Étape 1 : Installer les dépendances

pip install torch torchaudio librosa scikit-learn numpy pandas matplotlib seaborn
pip install pyaudio soundfile joblib tqdm

Étape 2 : Télécharger le dataset RAVDESS

  1. Aller sur Zenodo RAVDESS
  2. Télécharger "Audio_Speech_Actors_01-24.zip"
  3. Extraire dans un dossier (ex: C:\...\Audio_Speech_Actors_01-24)

Étape 3 : Configuration

Modifier le chemin dans config.py :

DATASET_PATH = "C:\\votre\\chemin\\Audio_Speech_Actors_01-24"

ARCHITECTURE DU PROJET

projet/
│
├── app.py           # Configuration globale
│
├── models/             # Modèles sauvegardés
│   ├── best_emotion_model.pth
│   ├── emotion_scaler.pkl
│   └── label_encoder.pkl
│
└── logs/               # Historiques d'entraînement
    ├── training_history.pkl
    └── training_history.png

DATASET RAVDESS

Qu'est-ce que RAVDESS ?

RAVDESS = Ryerson Audio-Visual Database of Emotional Speech and Song

  • Base de données créée par l'Université Ryerson (Canada)
  • Contient des enregistrements d'acteurs professionnels
  • Standard dans la recherche en reconnaissance d'émotions

Structure du Dataset

  • 24 acteurs (12 hommes, 12 femmes)
  • 1440 fichiers audio au total
  • Chaque acteur prononce 2 phrases avec différentes émotions
  • Format : WAV, 48kHz, 16-bit

Nomenclature des Fichiers

Format : 03-01-06-01-02-01-12.wav

03 = modalité (01=vidéo, 02=vidéo+audio, 03=audio seul)
01 = canal vocal (01=speech, 02=song)
06 = émotion (01=neutral, 02=calm, 03=happy, 04=sad, 05=angry, 06=fearful, 07=disgust, 08=surprised)
01 = intensité émotionnelle (01=normal, 02=strong)
02 = phrase (01="Kids are talking by the door", 02="Dogs are sitting by the door")
01 = répétition (01=1ère prise, 02=2ème prise)
12 = acteur (01-24)

Pourquoi RAVDESS ?

✅ Dataset standard et reconnu
✅ Qualité audio professionnelle
✅ Émotions bien différenciées
✅ Équilibré (même nombre par émotion)
✅ Gratuit et accessible


🔧 EXPLICATION DÉTAILLÉE DU CODE

1. CONFIG.PY - Configuration Globale

class Config:
    # Mapping des émotions RAVDESS
    EMOTION_MAP = {
        1: 'neutral',    # Code 01 dans le nom de fichier
        2: 'calm',       # Code 02
        3: 'happy',      # Code 03
        # ... etc
    }

Rôle : Centraliser tous les paramètres du projet
Pourquoi ? : Facilite les modifications sans toucher au code principal

Paramètres Importants :

SAMPLE_RATE = 16000        # 16kHz (suffisant pour la voix)
DURATION = 3               # 3 secondes par enregistrement
BATCH_SIZE = 32            # Nombre d'échantillons par batch
EPOCHS = 50                # Nombre de passages sur les données
LEARNING_RATE = 0.001      # Vitesse d'apprentissage
NUM_FEATURES = 200         # Nombre de caractéristiques extraites

2. DATASET.PY - Extraction de Features Audio

2.1 Classe FeatureExtractor

Qu'est-ce qu'une Feature ?
Une caractéristique numérique qui représente un aspect du son.

Features Extraites (10 types, ~200 valeurs) :

1. MFCCs (Mel-Frequency Cepstral Coefficients) - 80 valeurs

mfccs = librosa.feature.mfcc(y=audio, sr=sr, n_mfcc=40)
features.extend(np.mean(mfccs.T, axis=0))  # 40 moyennes
features.extend(np.std(mfccs.T, axis=0))   # 40 écarts-types
  • Qu'est-ce que c'est ? Représentation compacte du spectre audio
  • Pourquoi ? Capture les caractéristiques vocales importantes
  • Comment ? Transforme le son en coefficients (comme une "empreinte")
  • 40 coefficients : standard pour la voix
  • On calcule moyenne + écart-type pour avoir 80 valeurs

2. Chroma Features - 24 valeurs

chroma = librosa.feature.chroma_stft(y=audio, sr=sr)
features.extend(np.mean(chroma.T, axis=0))  # 12 moyennes
features.extend(np.std(chroma.T, axis=0))   # 12 écarts-types
  • Qu'est-ce que c'est ? Les 12 notes de musique (Do, Do#, Ré...)
  • Pourquoi ? Capture la tonalité de la voix
  • Utile pour ? Différencier émotions par le ton

3. Mel-Spectrogram - 40 valeurs

mel_spec = librosa.feature.melspectrogram(y=audio, sr=sr)
features.extend(np.mean(mel_spec.T, axis=0)[:20])  # 20 premières moyennes
features.extend(np.std(mel_spec.T, axis=0)[:20])   # 20 premiers écarts-types
  • Qu'est-ce que c'est ? Représentation fréquence-temps du son
  • Pourquoi ? Montre l'évolution des fréquences dans le temps
  • Échelle Mel : proche de la perception humaine

4. Spectral Features - 6 valeurs

spectral_centroid = librosa.feature.spectral_centroid(y=audio, sr=sr)
spectral_bandwidth = librosa.feature.spectral_bandwidth(y=audio, sr=sr)
spectral_rolloff = librosa.feature.spectral_rolloff(y=audio, sr=sr)
  • Centroid : "Centre de gravité" du spectre (voix aiguë/grave)
  • Bandwidth : Largeur du spectre (richesse du son)
  • Rolloff : Fréquence en dessous de laquelle est 85% de l'énergie

5. Zero Crossing Rate - 2 valeurs

zcr = librosa.feature.zero_crossing_rate(audio)
  • Qu'est-ce que c'est ? Nombre de fois que le signal change de signe
  • Utile pour ? Différencier voix/bruit, déterminer le type de son

6. RMS Energy - 2 valeurs

rms = librosa.feature.rms(y=audio)
  • Qu'est-ce que c'est ? Énergie moyenne du signal
  • Utile pour ? Mesurer le volume et l'intensité

7. Pitch (Hauteur) - 3 valeurs

pitches = librosa.yin(audio, fmin=50, fmax=500)
features.extend([np.mean(pitches), np.std(pitches), np.median(pitches)])
  • Qu'est-ce que c'est ? Fréquence fondamentale de la voix
  • Pourquoi ? La colère = pitch élevé, tristesse = pitch bas
  • 50-500 Hz : gamme de la voix humaine

8. Tempo - 1 valeur

tempo, _ = librosa.beat.beat_track(y=audio, sr=sr)
  • Qu'est-ce que c'est ? Rythme en BPM (battements par minute)
  • Utile pour ? Voix rapide (stress) vs lente (calme)

9. Harmonic/Percussive - 2 valeurs

harmonic, percussive = librosa.effects.hpss(audio)
features.extend([np.mean(harmonic), np.mean(percussive)])
  • Harmonic : Partie mélodique (voyelles)
  • Percussive : Partie percussive (consonnes)

10. Spectral Contrast - 7 valeurs

contrast = librosa.feature.spectral_contrast(y=audio, sr=sr)
features.extend(np.mean(contrast.T, axis=0))
  • Qu'est-ce que c'est ? Différence entre pics et vallées dans le spectre
  • Utile pour ? Texture du son, différencier émotions

TOTAL : ~200 features


2.2 Classe RAVDESSDataset

Rôle : Charger et préparer les données RAVDESS

def _load_dataset(self):
    """Charge tous les fichiers RAVDESS"""
    for wav_file in self.audio_dir.rglob("*.wav"):
        parts = wav_file.stem.split('-')  # Sépare le nom par '-'
        if len(parts) >= 7:
            emotion_code = int(parts[2])   # 3ème position = émotion
            intensity_code = int(parts[3]) # 4ème position = intensité
            actor_code = int(parts[6])     # 7ème position = acteur

            emotion = Config.EMOTION_MAP.get(emotion_code)
            # ... stockage

Processus :

  1. Parcourt tous les fichiers .wav
  2. Extrait les codes du nom de fichier
  3. Mappe le code émotion → nom émotion
  4. Stocke le chemin et les métadonnées

LabelEncoder :

self.label_encoder = LabelEncoder()
self.emotions_encoded = self.label_encoder.fit_transform(self.emotions)
  • Convertit les labels texte en nombres : 'happy' → 2, 'sad' → 3...
  • Nécessaire pour le modèle ML

StandardScaler :

self.scaler = StandardScaler()
self.scaler.fit(all_features)
all_features = self.scaler.transform(all_features)
  • Pourquoi ? Normalise les features (moyenne=0, écart-type=1)
  • Problème : MFCCs entre -50 et 50, RMS entre 0 et 1 → échelles différentes
  • Solution : Le scaler met tout sur la même échelle
  • Important : On sauvegarde le scaler pour l'utiliser en inférence

3. MODELS.PY - Architectures Neuronales

3.1 EmotionCNN (Architecture Principale)

Qu'est-ce qu'un CNN ?
Convolutional Neural Network = réseau qui apprend des patterns locaux

class EmotionCNN(nn.Module):
    def __init__(self, input_size=200, num_classes=8):
        super(EmotionCNN, self).__init__()

        # PARTIE 1 : COUCHES CONVOLUTIONNELLES
        self.conv_layers = nn.Sequential(
            # Bloc 1
            nn.Conv1d(1, 64, kernel_size=5, padding=2),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.MaxPool1d(2),
            nn.Dropout(0.2),
            # ... 2 autres blocs similaires
        )

        # PARTIE 2 : COUCHES FULLY CONNECTED
        self.fc_layers = nn.Sequential(
            nn.Linear(256, 512),
            # ... classification
        )

Décomposition Étape par Étape :

1. Input : (batch, 1, 200)

  • batch = nombre d'échantillons (ex: 32)
  • 1 = un seul canal (comme une image en noir et blanc)
  • 200 = nos 200 features

2. Premier Bloc Convolutionnel

nn.Conv1d(1, 64, kernel_size=5, padding=2)
  • Conv1d : convolution 1D (sur la séquence de features)
  • 1 → 64 : transforme 1 canal en 64 "filtres" différents
  • kernel_size=5 : chaque filtre regarde 5 features à la fois
  • padding=2 : ajoute des zéros aux bords pour garder la taille
  • Output : (batch, 64, 200)
nn.BatchNorm1d(64)
  • BatchNorm : normalise les activations
  • Pourquoi ? Stabilise l'entraînement, accélère la convergence
nn.ReLU()
  • ReLU : fonction d'activation, met les valeurs négatives à 0
  • Formule : f(x) = max(0, x)
  • Pourquoi ? Introduit de la non-linéarité
nn.MaxPool1d(2)
  • MaxPooling : garde le maximum tous les 2 éléments
  • Effect : réduit la taille de 200 → 100
  • Pourquoi ? Réduit les calculs, rend le modèle plus robuste
  • Output : (batch, 64, 100)
nn.Dropout(0.2)
  • Dropout : désactive aléatoirement 20% des neurones
  • Pourquoi ? Évite le surapprentissage (overfitting)

3. Deuxième Bloc : 64 → 128 canaux, taille 100 → 50

4. Troisième Bloc : 128 → 256 canaux

nn.AdaptiveAvgPool1d(1)
  • AdaptiveAvgPool : moyenne tout pour avoir taille 1
  • Output : (batch, 256, 1) → après squeeze → (batch, 256)

5. Couches Fully Connected

nn.Linear(256, 512)  # 256 entrées → 512 neurones
nn.BatchNorm1d(512)
nn.ReLU()
nn.Dropout(0.4)

nn.Linear(512, 256)  # 512 → 256
nn.BatchNorm1d(256)
nn.ReLU()
nn.Dropout(0.3)

nn.Linear(256, 8)    # 256 → 8 classes (nos émotions)

6. Forward Pass (Passage Avant)

def forward(self, x):
    x = x.unsqueeze(1)          # (batch, 200) → (batch, 1, 200)
    x = self.conv_layers(x)     # → (batch, 256, 1)
    x = x.squeeze(-1)           # → (batch, 256)
    x = self.fc_layers(x)       # → (batch, 8)
    return x

Pourquoi cette Architecture ?

  • Les Conv apprennent des patterns locaux dans les features
  • Le MaxPool réduit la dimension progressivement
  • Les FC combinent tout pour la classification finale
  • Le Dropout évite l'overfitting

3.2 EmotionLSTM (Architecture Alternative)

Qu'est-ce qu'un LSTM ?
Long Short-Term Memory = réseau qui traite des séquences

self.lstm = nn.LSTM(
    input_size=input_size,    # 200 features
    hidden_size=256,          # 256 neurones cachés
    num_layers=2,             # 2 couches LSTM empilées
    batch_first=True,
    bidirectional=True,       # Lit dans les 2 sens
    dropout=0.3
)

Bidirectionnel :

  • Lit la séquence de gauche à droite ET de droite à gauche
  • Capture le contexte avant ET après chaque feature
  • Output : (batch, seq, 512) car 256*2 directions

Mécanisme d'Attention :

self.attention = nn.Sequential(
    nn.Linear(512, 128),
    nn.Tanh(),
    nn.Linear(128, 1)
)
  • Apprend à donner plus d'importance à certaines parties
  • Calcule un "score" pour chaque position de la séquence
  • Fait une moyenne pondérée

Pourquoi LSTM ?

  • Bon pour les dépendances temporelles
  • Peut oublier/retenir des informations sélectivement
  • Bidirectionnel = contexte complet

4. TRAIN.PY - Entraînement du Modèle

4.1 Classe Trainer

Initialisation :

self.criterion = nn.CrossEntropyLoss()
  • CrossEntropyLoss : fonction de perte pour classification multi-classes
  • Mesure l'écart entre prédiction et vérité
self.optimizer = optim.AdamW(
    model.parameters(),
    lr=0.001,           # Learning rate
    weight_decay=1e-4   # Régularisation L2
)
  • AdamW : optimiseur (version améliorée d'Adam)
  • Ajuste les poids du modèle pour minimiser l'erreur
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(
    self.optimizer,
    mode='min',      # Surveille la loss
    factor=0.5,      # Réduit lr de moitié
    patience=5       # Après 5 epochs sans amélioration
)
  • Learning Rate Scheduler : réduit le lr si pas d'amélioration
  • Permet de "raffiner" l'apprentissage

Epoch d'Entraînement :

def train_epoch(self, train_loader):
    self.model.train()  # Mode entraînement

    for features, labels in train_loader:
        # 1. Charger sur GPU si disponible
        features = features.to(self.device)
        labels = labels.squeeze().to(self.device)

        # 2. Réinitialiser gradients
        self.optimizer.zero_grad()

        # 3. Forward pass
        outputs = self.model(features)

        # 4. Calculer la loss
        loss = self.criterion(outputs, labels)

        # 5. Backward pass (calcul gradients)
        loss.backward()

        # 6. Gradient clipping (éviter explosions)
        torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)

        # 7. Mise à jour des poids
        self.optimizer.step()

Validation :

def validate(self, val_loader):
    self.model.eval()  # Mode évaluation

    with torch.no_grad():  # Pas de calcul de gradients
        for features, labels in val_loader:
            outputs = self.model(features)
            loss = self.criterion(outputs, labels)
            # Calcul accuracy

Sauvegarde du Meilleur Modèle :

if val_acc > self.best_val_acc:
    self.best_val_acc = val_acc
    torch.save({
        'epoch': epoch,
        'model_state_dict': self.model.state_dict(),
        'optimizer_state_dict': self.optimizer.state_dict(),
        'val_acc': val_acc,
    }, Config.MODEL_PATH)

5. INFERENCE.PY - Détection Temps Réel

5.1 Classe RealTimeDetector

Chargement du Modèle :

self.model = EmotionCNN()
checkpoint = torch.load(Config.MODEL_PATH, map_location=self.device)
self.model.load_state_dict(checkpoint['model_state_dict'])
self.model.eval()

Chargement Scaler et Encoder :

self.scaler = joblib.load(Config.SCALER_PATH)
self.label_encoder = joblib.load(Config.ENCODER_PATH)
  • Important : Utiliser le MÊME scaler que l'entraînement
  • Sinon les features ne sont pas sur la même échelle

Enregistrement Audio :

def record_audio(self):
    p = pyaudio.PyAudio()
    stream = p.open(
        format=pyaudio.paInt16,  # 16-bit
        channels=1,               # Mono
        rate=16000,               # 16kHz
        input=True,
        frames_per_buffer=2048
    )

Prédiction :

def predict(self, audio):
    # 1. Padding/trimming à 3 secondes
    if len(audio) > Config.SAMPLES_PER_TRACK:
        audio = audio[:Config.SAMPLES_PER_TRACK]
    else:
        padding = Config.SAMPLES_PER_TRACK - len(audio)
        audio = np.pad(audio, (0, padding))

    # 2. Extraire features (comme dans training)
    features = FeatureExtractor.extract(audio)

    # 3. Normaliser (avec le scaler de training)
    features_scaled = self.scaler.transform(features.reshape(1, -1))

    # 4. Convertir en tensor
    features_tensor = torch.FloatTensor(features_scaled).to(self.device)

    # 5. Prédiction
    with torch.no_grad():
        outputs = self.model(features_tensor)
        probs = torch.softmax(outputs, dim=1)  # Convertir en probabilités
        confidence, prediction = torch.max(probs, 1)

    # 6. Décoder l'émotion
    emotion = self.label_encoder.inverse_transform([prediction.item()])[0]

    return {
        'emotion': emotion,
        'confidence': confidence.item() * 100,
        'probabilities': {...}
    }

Lissage Temporel :

self.history = deque(maxlen=20)  # Garde les 20 dernières détections
self.history.append(result['emotion'])
  • Évite les changements brusques
  • Moyenne sur plusieurs prédictions pour stabiliser

🚀 UTILISATION

1. Premier Lancement : Entraîner le Modèle

python main.py
# Choisir option [1]

Processus :

  1. Charge RAVDESS (1440 fichiers)
  2. Extrait features de chaque fichier (~2-3 min)
  3. Split train/test 80/20
  4. Entraîne 50 epochs (~10-15 min sur CPU, 2-3 min sur GPU)
  5. Sauvegarde le meilleur modèle

Fichiers Créés :

  • models/best_emotion_model.pth : modèle entraîné
  • models/emotion_scaler.pkl : scaler pour normalisation
  • models/label_encoder.pkl : encodeur labels
  • logs/training_history.pkl : historique entraînement
  • logs/training_history.png : courbes loss/accuracy

2. Détection Temps Réel

python main.py
# Choisir option [2]

Processus :

  1. Charge le modèle entraîné
  2. Ouvre le micro
  3. Enregistre 3 secondes
  4. Extrait features
  5. Prédit l'émotion
  6. Affiche + répète

Arrêt : Ctrl+C

3. Test sur Fichier Audio

python main.py
# Choisir option [3]
  • Sélectionne un fichier .wav ou .mp3
  • Analyse et affiche l'émotion détectée

📊 RÉSULTATS

Performance Attendue

  • Accuracy Train : 90-95%
  • Accuracy Validation : 80-87%
  • Temps Inférence : ~100-200ms par prédiction

Matrice de Confusion Typique

         pred_happy  pred_sad  pred_angry  ...
happy        85%       5%        3%
sad          4%        82%       6%
angry        2%        3%        88%
...

Facteurs de Performance

Bonne Performance :

  • Émotions fortes (angry, happy, sad)
  • Audio clair et net
  • Locuteur proche du dataset

Performance Réduite :

  • Émotions subtiles (calm, neutral)
  • Bruit de fond important
  • Accents très différents
  • Voix d'enfants (dataset = adultes)

⚠️ LIMITATIONS

1. Dataset

  • Seulement 24 acteurs → généralisation limitée
  • Texte limité (2 phrases)
  • Émotions "jouées" (pas naturelles)
  • Pas de dialectes/accents variés

2. Technique

  • Nécessite audio de 3 secondes minimum
  • Sensible au bruit de fond
  • Une seule langue (anglais)
  • Pas de détection multi-émotions

3. Utilisation

  • Nécessite microphone de qualité
  • Temps de traitement ~200ms (pas instantané)
  • Consomme CPU/GPU en continu

🔧 PARAMÈTRES À AJUSTER

Pour Améliorer la Précision

# Dans config.py
EPOCHS = 100              # Plus d'epochs
BATCH_SIZE = 16           # Batch plus petit
LEARNING_RATE = 0.0005    # LR plus faible

# Dans models.py
# Ajouter plus de couches, plus de neurones

Pour Accélérer l'Inférence

# Réduire la complexité du modèle
# Utiliser moins de features
# Réduire DURATION à 2 secondes

Pour Réduire l'Overfitting

# Augmenter Dropout
nn.Dropout(0.5)  # Au lieu de 0.3

# Augmenter weight_decay
weight_decay=1e-3

📚 RÉFÉRENCES

Dataset

  • RAVDESS on Zenodo
  • Livingstone SR, Russo FA (2018) The Ryerson Audio-Visual Database of Emotional Speech and Song (RAVDESS)

Bibliothèques


💡 AMÉLIORATIONS POSSIBLES

  1. Augmentation de données : ajouter du bruit, changer le pitch, etc.
  2. Ensemblage : combiner CNN + LSTM
  3. Transfer Learning : utiliser Wav2Vec pré-entraîné
  4. Multi-langue : entraîner sur datasets multilingues
  5. Détection continue : analyse par fenêtres glissantes
  6. Calibration : adapter au locuteur spécifique

Auteur : ouhassine wissal ouarda
Date : Janvier 2026