| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 045d2ed commit c14048b
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,18 +1,16 @@ | |||
| 1 | 1 | # Speech Emotion Diarization (SED) | |
| 2 | 2 | ||
| 3 | - Speech Emotion Diarization ([arXiv link](https://arxiv.org/pdf/2306.12991.pdf)) aims to predict the correct emotions and their temporal boundaries within an utterance. For now, the model was trained with audios that contain only 1 non-neutral emotion event. The output is a dictionary of emotion components (neutral/happy/angry/sad) and their boundaries such as: | ||
| 3 | + [Speech Emotion Diarization](https://arxiv.org/pdf/2306.12991.pdf) is a technique that focuses on predicting emotions and their corresponding time boundaries within a speech recording. The model, described in the research paper titled "Speech Emotion Diarization" ([available here](https://arxiv.org/pdf/2306.12991.pdf)), has been trained using audio samples that include neutral and a non-neutral emotional event. The model's output takes the form of a dictionary comprising emotion components (*neutral*, *happy*, *angry*, and *sad*) along with their respective start and end boundaries, as exemplified below: | ||
| 4 | 4 | ||
| 5 | - ``` | ||
| 5 | + ```python | ||
| 6 | 6 | { | |
| 7 | - 'example.wav': | ||
| 8 | - [ | ||
| 9 | - {'start': 0.0, 'end': 1.94, 'emotion': 'n'}, # n -> neutral | ||
| 10 | - {'start': 1.94, 'end': 4.48, 'emotion': 'h'} # h -> happy | ||
| 11 | - ] | ||
| 7 | + 'example.wav': [ | ||
| 8 | + {'start': 0.0, 'end': 1.94, 'emotion': 'n'}, # 'n' denotes neutral | ||
| 9 | + {'start': 1.94, 'end': 4.48, 'emotion': 'h'} # 'h' denotes happy | ||
| 10 | + ] | ||
| 12 | 11 | } | |
| 13 | 12 | ``` | |
| 14 | 13 | ||
| 15 | - | ||
| 16 | 14 | ## Dependencies | |
| 17 | 15 | ||
| 18 | 16 | First, please install the extra dependencies, do `pip install -r extra_requirements.txt` | |
@@ -64,6 +62,8 @@ The EDER (Emotion Diarization Error Rate) reported here was averaged on 5 differ | |||
| 64 | 62 | |:-------------:|:---------------------------:| | |
| 65 | 63 | | WavLM-large | 30.2 ± 1.60 | | |
| 66 | 64 | ||
| 65 | + It takes about 40 mins/epoch with 1xRTX8000(40G), reduce the batch size if OOM. | ||
| 66 | + | ||
| 67 | 67 | ## Inference | |
| 68 | 68 | ||
| 69 | 69 | The pretrained models and a easy-inference interface can be found on [HuggingFace](https://huggingface.co/speechbrain/emotion-diarization-wavlm-large). | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3028,3 +3028,215 @@ def transcribe_batch(self, wavs, wav_lens): | |||
| 3028 | 3028 | def forward(self, wavs, wav_lens): | |
| 3029 | 3029 | """Runs full transcription - note: no gradients through decoding""" | |
| 3030 | 3030 | return self.transcribe_batch(wavs, wav_lens) | |
| 3031 | + | ||
| 3032 | + | ||
| 3033 | + class Speech_Emotion_Diarization(Pretrained): | ||
| 3034 | + """A ready-to-use SED interface (audio -> emotions and their durations) | ||
| 3035 | + | ||
| 3036 | + Arguments | ||
| 3037 | + --------- | ||
| 3038 | + hparams | ||
| 3039 | + Hyperparameters (from HyperPyYAML) | ||
| 3040 | + | ||
| 3041 | + Example | ||
| 3042 | + ------- | ||
| 3043 | + >>> from speechbrain.pretrained import Speech_Emotion_Diarization | ||
| 3044 | + >>> tmpdir = getfixture("tmpdir") | ||
| 3045 | + >>> sed_model = Speech_Emotion_Diarization.from_hparams(source="speechbrain/emotion-diarization-wavlm-large", savedir=tmpdir,) # doctest: +SKIP | ||
| 3046 | + >>> sed_model.diarize_file("speechbrain/emotion-diarization-wavlm-large/example.wav") # doctest: +SKIP | ||
| 3047 | + """ | ||
| 3048 | + | ||
| 3049 | + MODULES_NEEDED = ["input_norm", "wav2vec", "output_mlp"] | ||
| 3050 | + | ||
| 3051 | + def __init__(self, *args, **kwargs): | ||
| 3052 | + super().__init__(*args, **kwargs) | ||
| 3053 | + | ||
| 3054 | + def diarize_file(self, path): | ||
| 3055 | + """Get emotion diarization of a spoken utterance. | ||
| 3056 | + | ||
| 3057 | + Arguments | ||
| 3058 | + --------- | ||
| 3059 | + path : str | ||
| 3060 | + Path to audio file which to diarize. | ||
| 3061 | + | ||
| 3062 | + Returns | ||
| 3063 | + ------- | ||
| 3064 | + dict | ||
| 3065 | + The emotions and their boundaries. | ||
| 3066 | + """ | ||
| 3067 | + waveform = self.load_audio(path) | ||
| 3068 | + # Fake a batch: | ||
| 3069 | + batch = waveform.unsqueeze(0) | ||
| 3070 | + rel_length = torch.tensor([1.0]) | ||
| 3071 | + frame_class = self.diarize_batch( | ||
| 3072 | + batch, rel_length, [path] | ||
| 3073 | + ) | ||
| 3074 | + return frame_class | ||
| 3075 | + | ||
| 3076 | + def encode_batch(self, wavs, wav_lens): | ||
| 3077 | + """Encodes audios into fine-grained emotional embeddings | ||
| 3078 | + | ||
| 3079 | + Arguments | ||
| 3080 | + --------- | ||
| 3081 | + wavs : torch.tensor | ||
| 3082 | + Batch of waveforms [batch, time, channels]. | ||
| 3083 | + wav_lens : torch.tensor | ||
| 3084 | + Lengths of the waveforms relative to the longest one in the | ||
| 3085 | + batch, tensor of shape [batch]. The longest one should have | ||
| 3086 | + relative length 1.0 and others len(waveform) / max_length. | ||
| 3087 | + Used for ignoring padding. | ||
| 3088 | + | ||
| 3089 | + Returns | ||
| 3090 | + ------- | ||
| 3091 | + torch.tensor | ||
| 3092 | + The encoded batch | ||
| 3093 | + """ | ||
| 3094 | + if len(wavs.shape) == 1: | ||
| 3095 | + wavs = wavs.unsqueeze(0) | ||
| 3096 | + | ||
| 3097 | + # Assign full length if wav_lens is not assigned | ||
| 3098 | + if wav_lens is None: | ||
| 3099 | + wav_lens = torch.ones(wavs.shape[0], device=self.device) | ||
| 3100 | + | ||
| 3101 | + wavs, wav_lens = wavs.to(self.device), wav_lens.to(self.device) | ||
| 3102 | + | ||
| 3103 | + wavs = self.mods.input_norm(wavs, wav_lens) | ||
| 3104 | + outputs = self.mods.wav2vec2(wavs) | ||
| 3105 | + return outputs | ||
| 3106 | + | ||
| 3107 | + def diarize_batch(self, wavs, wav_lens, batch_id): | ||
| 3108 | + """Get emotion diarization of a batch of waveforms. | ||
| 3109 | + | ||
| 3110 | + The waveforms should already be in the model's desired format. | ||
| 3111 | + You can call: | ||
| 3112 | + ``normalized = EncoderDecoderASR.normalizer(signal, sample_rate)`` | ||
| 3113 | + to get a correctly converted signal in most cases. | ||
| 3114 | + | ||
| 3115 | + Arguments | ||
| 3116 | + --------- | ||
| 3117 | + wavs : torch.tensor | ||
| 3118 | + Batch of waveforms [batch, time, channels]. | ||
| 3119 | + wav_lens : torch.tensor | ||
| 3120 | + Lengths of the waveforms relative to the longest one in the | ||
| 3121 | + batch, tensor of shape [batch]. The longest one should have | ||
| 3122 | + relative length 1.0 and others len(waveform) / max_length. | ||
| 3123 | + Used for ignoring padding. | ||
| 3124 | + batch_id : torch.tensor | ||
| 3125 | + id of each batch (file names etc.) | ||
| 3126 | + | ||
| 3127 | + Returns | ||
| 3128 | + ------- | ||
| 3129 | + torch.tensor | ||
| 3130 | + The frame-wise predictions | ||
| 3131 | + """ | ||
| 3132 | + outputs = self.encode_batch(wavs, wav_lens) | ||
| 3133 | + averaged_out = self.hparams.avg_pool(outputs) | ||
| 3134 | + outputs = self.mods.output_mlp(averaged_out) | ||
| 3135 | + outputs = self.hparams.log_softmax(outputs) | ||
| 3136 | + score, index = torch.max(outputs, dim=-1) | ||
| 3137 | + preds = self.hparams.label_encoder.decode_torch(index) | ||
| 3138 | + results = self.preds_to_diarization(preds, batch_id) | ||
| 3139 | + return results | ||
| 3140 | + | ||
| 3141 | + def preds_to_diarization(self, prediction, batch_id): | ||
| 3142 | + """Convert frame-wise predictions into a dictionary of | ||
| 3143 | + diarization results. | ||
| 3144 | + | ||
| 3145 | + Returns | ||
| 3146 | + ------- | ||
| 3147 | + dictionary | ||
| 3148 | + A dictionary with the start/end of each emotion | ||
| 3149 | + """ | ||
| 3150 | + results = {} | ||
| 3151 | + | ||
| 3152 | + for i in range(len(prediction)): | ||
| 3153 | + pred = prediction[i] | ||
| 3154 | + lol = [] | ||
| 3155 | + for j in range(len(pred)): | ||
| 3156 | + start = round(self.hparams.stride * 0.02 * j, 2) | ||
| 3157 | + end = round(start + self.hparams.window_length * 0.02, 2) | ||
| 3158 | + lol.append([batch_id[i], start, end, pred[j]]) | ||
| 3159 | + | ||
| 3160 | + lol = self.merge_ssegs_same_emotion_adjacent(lol) | ||
| 3161 | + results[batch_id[i]] = [ | ||
| 3162 | + {"start": k[1], "end":k[2], "emotion": k[3]} for k in lol | ||
| 3163 | + ] | ||
| 3164 | + return results | ||
| 3165 | + | ||
| 3166 | + def forward(self, wavs, wav_lens): | ||
| 3167 | + """Runs full transcription - note: no gradients through decoding""" | ||
| 3168 | + return self.transcribe_batch(wavs, wav_lens) | ||
| 3169 | + | ||
| 3170 | + def is_overlapped(self, end1, start2): | ||
| 3171 | + """Returns True if segments are overlapping. | ||
| 3172 | + | ||
| 3173 | + Arguments | ||
| 3174 | + --------- | ||
| 3175 | + end1 : float | ||
| 3176 | + End time of the first segment. | ||
| 3177 | + start2 : float | ||
| 3178 | + Start time of the second segment. | ||
| 3179 | + | ||
| 3180 | + Returns | ||
| 3181 | + ------- | ||
| 3182 | + overlapped : bool | ||
| 3183 | + True of segments overlapped else False. | ||
| 3184 | + | ||
| 3185 | + Example | ||
| 3186 | + ------- | ||
| 3187 | + >>> from speechbrain.processing import diarization as diar | ||
| 3188 | + >>> diar.is_overlapped(5.5, 3.4) | ||
| 3189 | + True | ||
| 3190 | + >>> diar.is_overlapped(5.5, 6.4) | ||
| 3191 | + False | ||
| 3192 | + """ | ||
| 3193 | + | ||
| 3194 | + if start2 > end1: | ||
| 3195 | + return False | ||
| 3196 | + else: | ||
| 3197 | + return True | ||
| 3198 | + | ||
| 3199 | + def merge_ssegs_same_emotion_adjacent(self, lol): | ||
| 3200 | + """Merge adjacent sub-segs if they are the same emotion. | ||
| 3201 | + Arguments | ||
| 3202 | + --------- | ||
| 3203 | + lol : list of list | ||
| 3204 | + Each list contains [utt_id, sseg_start, sseg_end, emo_label]. | ||
| 3205 | + Returns | ||
| 3206 | + ------- | ||
| 3207 | + new_lol : list of list | ||
| 3208 | + new_lol contains adjacent segments merged from the same emotion ID. | ||
| 3209 | + Example | ||
| 3210 | + ------- | ||
| 3211 | + >>> from speechbrain.utils.EDER import merge_ssegs_same_emotion_adjacent | ||
| 3212 | + >>> lol=[['u1', 0.0, 7.0, 'a'], | ||
| 3213 | + ... ['u1', 7.0, 9.0, 'a'], | ||
| 3214 | + ... ['u1', 9.0, 11.0, 'n'], | ||
| 3215 | + ... ['u1', 11.0, 13.0, 'n'], | ||
| 3216 | + ... ['u1', 13.0, 15.0, 'n'], | ||
| 3217 | + ... ['u1', 15.0, 16.0, 'a']] | ||
| 3218 | + >>> merge_ssegs_same_emotion_adjacent(lol) | ||
| 3219 | + [['u1', 0.0, 9.0, 'a'], ['u1', 9.0, 15.0, 'n'], ['u1', 15.0, 16.0, 'a']] | ||
| 3220 | + """ | ||
| 3221 | + new_lol = [] | ||
| 3222 | + | ||
| 3223 | + # Start from the first sub-seg | ||
| 3224 | + sseg = lol[0] | ||
| 3225 | + flag = False | ||
| 3226 | + for i in range(1, len(lol)): | ||
| 3227 | + next_sseg = lol[i] | ||
| 3228 | + # IF sub-segments overlap AND has same emotion THEN merge | ||
| 3229 | + if self.is_overlapped(sseg[2], next_sseg[1]) and sseg[3] == next_sseg[3]: | ||
| 3230 | + sseg[2] = next_sseg[2] # just update the end time | ||
| 3231 | + # This is important. For the last sseg, if it is the same emotion then merge | ||
| 3232 | + # Make sure we don't append the last segment once more. Hence, set FLAG=True | ||
| 3233 | + if i == len(lol) - 1: | ||
| 3234 | + flag = True | ||
| 3235 | + new_lol.append(sseg) | ||
| 3236 | + else: | ||
| 3237 | + new_lol.append(sseg) | ||
| 3238 | + sseg = next_sseg | ||
| 3239 | + # Add last segment only when it was skipped earlier. | ||
| 3240 | + if flag is False: | ||
| 3241 | + new_lol.append(lol[-1]) | ||
| 3242 | + return new_lol | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,2 +1,2 @@ | |||
| 1 | 1 | Task,Dataset,Script_file,Hparam_file,Data_prep_file,Readme_file,Result_url,HF_repo,test_debug_flags,test_debug_checks | |
| 2 | - Emotion_Diarization,ZaionEmotionDataset,recipes/ZaionEmotionDataset/emotion_diarization/train.py,recipes/ZaionEmotionDataset/emotion_diarization/hparams/train.yaml,recipes/ZaionEmotionDataset/emotion_diarization/zed_prepare.py,recipes/ZaionEmotionDataset/README.md,https://www.dropbox.com/sh/woudm1v31a7vyp5/AADAMxpQOXaxf8E_1hX202GJa?dl=0,https://huggingface.co/speechbrain/emotion-diarization-wavlm-large/,--data_folder=tests/samples/ASR/ --train_annotation=tests/samples/annotation/ASR_train.json --valid_annotation=tests/samples/annotation/ASR_dev.json --test_annotation=tests/samples/annotation/ASR_dev.json --number_of_epochs=2 --skip_prep=True --wav2vec2_folder=tests/tmp/wav2vec2_checkpoint, | ||
| 2 | + Emotion_Diarization,ZaionEmotionDataset,recipes/ZaionEmotionDataset/emotion_diarization/train.py,recipes/ZaionEmotionDataset/emotion_diarization/hparams/train.yaml,recipes/ZaionEmotionDataset/emotion_diarization/zed_prepare.py,recipes/ZaionEmotionDataset/README.md,https://www.dropbox.com/sh/woudm1v31a7vyp5/AADAMxpQOXaxf8E_1hX202GJa?dl=0,https://huggingface.co/speechbrain/emotion-diarization-wavlm-large,--data_folder=tests/samples/ASR/ --train_annotation=tests/samples/annotation/ASR_train.json --valid_annotation=tests/samples/annotation/ASR_dev.json --test_annotation=tests/samples/annotation/ASR_dev.json --number_of_epochs=2 --skip_prep=True --wav2vec2_folder=tests/tmp/wav2vec2_checkpoint, | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments