ser-standardizer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ser_standardizer/__init__.py +1 -0
- ser_standardizer/api.py +83 -0
- ser_standardizer/features/__init__.py +3 -0
- ser_standardizer/features/extractor.py +97 -0
- ser_standardizer/features/preprocessing.py +92 -0
- ser_standardizer/loaders/__init__.py +21 -0
- ser_standardizer/loaders/crema_d.py +125 -0
- ser_standardizer/loaders/emodb.py +121 -0
- ser_standardizer/loaders/emouerj.py +72 -0
- ser_standardizer/loaders/iemocap.py +140 -0
- ser_standardizer/loaders/msp_improv.py +147 -0
- ser_standardizer/loaders/msp_podcast.py +118 -0
- ser_standardizer/loaders/ravdess.py +111 -0
- ser_standardizer/main.py +40 -0
- ser_standardizer-0.1.0.dist-info/METADATA +166 -0
- ser_standardizer-0.1.0.dist-info/RECORD +20 -0
- ser_standardizer-0.1.0.dist-info/WHEEL +5 -0
- ser_standardizer-0.1.0.dist-info/entry_points.txt +2 -0
- ser_standardizer-0.1.0.dist-info/licenses/LICENSE +21 -0
- ser_standardizer-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .api import load_datasets, load_audio, load_batch, listen, filters, normalize, extract_features
|
ser_standardizer/api.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def load_datasets(datasets):
|
|
6
|
+
if isinstance(datasets, str):
|
|
7
|
+
target_datasets = [datasets]
|
|
8
|
+
else:
|
|
9
|
+
target_datasets = datasets
|
|
10
|
+
|
|
11
|
+
dataframes = []
|
|
12
|
+
DATA_DIR = os.path.join(Path.home(), ".ser_standardizer_data")
|
|
13
|
+
|
|
14
|
+
for name in target_datasets:
|
|
15
|
+
file_path = os.path.join(DATA_DIR, f"process_{name}.csv")
|
|
16
|
+
if not file_path or not os.path.exists(file_path):
|
|
17
|
+
raise FileNotFoundError(
|
|
18
|
+
f"O dataset '{name}' não foi encontrado. "
|
|
19
|
+
f"Certifique-se de ter rodado: ser-std --dataset {name} ..."
|
|
20
|
+
)
|
|
21
|
+
df = pd.read_csv(file_path)
|
|
22
|
+
dataframes.append(df)
|
|
23
|
+
|
|
24
|
+
if not dataframes:
|
|
25
|
+
raise ValueError("Nenhum dataset válido foi fornecido.")
|
|
26
|
+
|
|
27
|
+
return pd.concat(dataframes, ignore_index=True)
|
|
28
|
+
|
|
29
|
+
def filters(df, datasets=None, emotions=None, genders=None, languages=None, speakers=None):
|
|
30
|
+
mask = pd.Series([True] * len(df), index=df.index)
|
|
31
|
+
|
|
32
|
+
def to_list(val):
|
|
33
|
+
if isinstance(val, str):
|
|
34
|
+
return [val]
|
|
35
|
+
return val
|
|
36
|
+
|
|
37
|
+
if datasets:
|
|
38
|
+
target = [x.lower() for x in to_list(datasets)]
|
|
39
|
+
mask = mask & df['dataset'].str.lower().isin(target)
|
|
40
|
+
|
|
41
|
+
if emotions:
|
|
42
|
+
target = [x.lower() for x in to_list(emotions)]
|
|
43
|
+
mask = mask & df['emotion'].str.lower().isin(target)
|
|
44
|
+
|
|
45
|
+
if genders:
|
|
46
|
+
target = [x.lower() for x in to_list(genders)]
|
|
47
|
+
mask = mask & df['gender'].str.lower().isin(target)
|
|
48
|
+
|
|
49
|
+
if languages:
|
|
50
|
+
target = [x.lower() for x in to_list(languages)]
|
|
51
|
+
mask = mask & df['language'].str.lower().isin(target)
|
|
52
|
+
|
|
53
|
+
if speakers:
|
|
54
|
+
target = [x.lower() for x in to_list(speakers)]
|
|
55
|
+
mask = mask & df['speaker_id'].str.lower().isin(target)
|
|
56
|
+
|
|
57
|
+
print(f"Filtro aplicado. Restaram {len(df[mask])} amostras.")
|
|
58
|
+
return df[mask].copy()
|
|
59
|
+
|
|
60
|
+
def normalize(features, group, eps=1e-8):
|
|
61
|
+
"""Aplica z-score por grupo em `features` (ex.: por dataset ou locutor)."""
|
|
62
|
+
g = group.reindex(features.index)
|
|
63
|
+
out = features.groupby(g).transform(
|
|
64
|
+
lambda c: (c - c.mean()) / (c.std(ddof=0) + eps)
|
|
65
|
+
)
|
|
66
|
+
return out.fillna(0.0)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from .features.preprocessing import listen, load_audio, load_batch
|
|
70
|
+
from .features.extractor import extract_features
|
|
71
|
+
FEATURES_AVAILABLE = True
|
|
72
|
+
|
|
73
|
+
except ImportError:
|
|
74
|
+
FEATURES_AVAILABLE = False
|
|
75
|
+
|
|
76
|
+
def _missing_deps_error(*args, **kwargs):
|
|
77
|
+
raise ImportError(
|
|
78
|
+
"Esta função requer o submódulo de extração acústica.\n"
|
|
79
|
+
"Para utilizá-la, reinstale o pacote com as dependências completas:\n\n"
|
|
80
|
+
" pip install '.[features]'"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
listen = load_audio = load_batch = extract_features = _missing_deps_error
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
from tqdm.auto import tqdm
|
|
4
|
+
import opensmile
|
|
5
|
+
from .preprocessing import load_audio, remove_silence
|
|
6
|
+
|
|
7
|
+
def extract_features(df, feature_set='eGeMAPS', feature_level='functionals', use_vad=False, sr=16000):
|
|
8
|
+
"""
|
|
9
|
+
Extrai características acústicas utilizando o openSMILE.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
df: DataFrame contendo a coluna 'file_path'.
|
|
13
|
+
feature_set: 'eGeMAPS' ou 'ComParE' (padrões validados na literatura).
|
|
14
|
+
feature_level: 'functionals' (estatísticas globais por arquivo) ou 'llds' (nível de frame).
|
|
15
|
+
use_vad: Se True, remove silêncios baseados na energia RMS.
|
|
16
|
+
ATENÇÃO: Permitido apenas quando feature_level='llds'.
|
|
17
|
+
sr: Taxa de amostragem esperada.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Um novo pd.DataFrame contendo as features.
|
|
21
|
+
Para 'functionals', mantém o índice original.
|
|
22
|
+
Para 'llds', adiciona o índice original ('file_idx') para cruzamento de metadados.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
if use_vad and feature_level.lower() == 'functionals':
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"Erro de Metodologia: A aplicação de VAD no áudio bruto antes da extração "
|
|
28
|
+
"de funcionais corrompe as métricas temporais padronizadas do openSMILE "
|
|
29
|
+
"(como taxa de fala e proporção de pausas) e introduz artefatos no espectro. \n"
|
|
30
|
+
"-> Para extrair 'functionals' padronizados, defina use_vad=False. \n"
|
|
31
|
+
"-> O uso de use_vad=True é permitido apenas para extração no nível de frame ('llds')."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if feature_set.lower() == 'egemaps':
|
|
35
|
+
fset = opensmile.FeatureSet.eGeMAPSv02
|
|
36
|
+
elif feature_set.lower() == 'compare':
|
|
37
|
+
fset = opensmile.FeatureSet.ComParE_2016
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError("Conjunto não suportado. Escolha 'eGeMAPS' ou 'ComParE'.")
|
|
40
|
+
|
|
41
|
+
# 2. Definir o Nível da Extração
|
|
42
|
+
if feature_level.lower() == 'functionals':
|
|
43
|
+
flevel = opensmile.FeatureLevel.Functionals
|
|
44
|
+
level_name = "Functionals"
|
|
45
|
+
elif feature_level.lower() in ['lld', 'llds']:
|
|
46
|
+
flevel = opensmile.FeatureLevel.LowLevelDescriptors
|
|
47
|
+
level_name = "LowLevelDescriptors"
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError("Nível não suportado. Escolha 'functionals' ou 'llds'.")
|
|
50
|
+
|
|
51
|
+
print(f"Inicializando openSMILE ({feature_set} - {level_name})...")
|
|
52
|
+
smile = opensmile.Smile(
|
|
53
|
+
feature_set=fset,
|
|
54
|
+
feature_level=flevel,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
features_list = []
|
|
58
|
+
|
|
59
|
+
for idx in tqdm(df.index, desc=f"Extraindo características ({level_name})"):
|
|
60
|
+
file_path = df.loc[idx, 'file_path']
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
if use_vad:
|
|
64
|
+
audio = load_audio(df, idx, sr=sr)
|
|
65
|
+
audio_clean = remove_silence(audio, sr=sr)
|
|
66
|
+
|
|
67
|
+
if len(audio_clean) == 0:
|
|
68
|
+
audio_clean = np.zeros(sr)
|
|
69
|
+
|
|
70
|
+
feat_df = smile.process_signal(audio_clean, sr)
|
|
71
|
+
else:
|
|
72
|
+
feat_df = smile.process_file(file_path)
|
|
73
|
+
|
|
74
|
+
if flevel == opensmile.FeatureLevel.Functionals:
|
|
75
|
+
feat_series = feat_df.iloc[0].rename(idx)
|
|
76
|
+
features_list.append(feat_series)
|
|
77
|
+
else:
|
|
78
|
+
feat_df['file_idx'] = idx
|
|
79
|
+
features_list.append(feat_df)
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"Erro na extração do índice {idx}: {e}")
|
|
83
|
+
if flevel == opensmile.FeatureLevel.Functionals:
|
|
84
|
+
features_list.append(pd.Series(dtype=float, name=idx))
|
|
85
|
+
else:
|
|
86
|
+
features_list.append(pd.DataFrame({'file_idx': [idx]}))
|
|
87
|
+
|
|
88
|
+
print("Consolidando matriz de features...")
|
|
89
|
+
|
|
90
|
+
if flevel == opensmile.FeatureLevel.Functionals:
|
|
91
|
+
result_df = pd.DataFrame(features_list)
|
|
92
|
+
else:
|
|
93
|
+
result_df = pd.concat(features_list)
|
|
94
|
+
if 'file_idx' in result_df.columns:
|
|
95
|
+
result_df = result_df.set_index('file_idx', append=True)
|
|
96
|
+
|
|
97
|
+
return result_df
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import soundfile as sf
|
|
2
|
+
import scipy.signal
|
|
3
|
+
import numpy as np
|
|
4
|
+
from IPython.display import Audio, display
|
|
5
|
+
from tqdm.auto import tqdm
|
|
6
|
+
import noisereduce as nr
|
|
7
|
+
|
|
8
|
+
def scipy_load(path, target_sr=None):
|
|
9
|
+
|
|
10
|
+
audio, native_sr = sf.read(path)
|
|
11
|
+
|
|
12
|
+
if audio.ndim > 1:
|
|
13
|
+
audio = np.mean(audio, axis=1)
|
|
14
|
+
|
|
15
|
+
if target_sr is not None and native_sr != target_sr:
|
|
16
|
+
num_samples = round(len(audio) * float(target_sr) / native_sr)
|
|
17
|
+
audio = scipy.signal.resample(audio, num_samples)
|
|
18
|
+
return audio, target_sr
|
|
19
|
+
|
|
20
|
+
return audio, native_sr
|
|
21
|
+
|
|
22
|
+
def remove_silence(audio, sr=16000, frame_length=2048, hop_length=512, threshold=0.25):
|
|
23
|
+
audio_clean = nr.reduce_noise(y=audio, sr=sr)
|
|
24
|
+
|
|
25
|
+
n_frames = 1 + (len(audio_clean) - frame_length) // hop_length
|
|
26
|
+
if n_frames <= 0:
|
|
27
|
+
return audio_clean
|
|
28
|
+
|
|
29
|
+
rms = np.array([
|
|
30
|
+
np.sqrt(np.mean(audio_clean[i*hop_length : i*hop_length + frame_length] ** 2))
|
|
31
|
+
for i in range(n_frames)
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
max_rms = np.max(rms)
|
|
35
|
+
if max_rms == 0:
|
|
36
|
+
return np.array([])
|
|
37
|
+
|
|
38
|
+
rms_norm = rms / max_rms
|
|
39
|
+
speech_mask = rms_norm > threshold
|
|
40
|
+
|
|
41
|
+
sample_mask = np.zeros(len(audio_clean), dtype=bool)
|
|
42
|
+
for i, active in enumerate(speech_mask):
|
|
43
|
+
if active:
|
|
44
|
+
start = i * hop_length
|
|
45
|
+
end = min(start + hop_length, len(audio_clean))
|
|
46
|
+
sample_mask[start:end] = True
|
|
47
|
+
|
|
48
|
+
return audio_clean[sample_mask]
|
|
49
|
+
|
|
50
|
+
def listen(df, index):
|
|
51
|
+
if index not in df.index:
|
|
52
|
+
print(f"Aviso: Não há nenhum áudio disponível com o índice {index}.")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
path = df.loc[index, 'file_path']
|
|
56
|
+
emotion = df.loc[index, 'emotion']
|
|
57
|
+
|
|
58
|
+
print(f"Tocando áudio {index} | Emoção: {emotion}")
|
|
59
|
+
display(Audio(filename=path))
|
|
60
|
+
|
|
61
|
+
def load_audio(df, index, sr=16000):
|
|
62
|
+
if index not in df.index:
|
|
63
|
+
raise IndexError("Índice não encontrado no Dataset.")
|
|
64
|
+
path = df.loc[index, 'file_path']
|
|
65
|
+
audio, _ = scipy_load(path, target_sr=sr)
|
|
66
|
+
return audio
|
|
67
|
+
|
|
68
|
+
def load_batch(df, begin, end, sr=16000, max_1000=True):
|
|
69
|
+
qtd = end - begin
|
|
70
|
+
if qtd > 1000 and max_1000:
|
|
71
|
+
raise ValueError(f"Você tentou carregar {qtd} áudios. O limite é 1000. Defina max_1000=False se tiver certeza.")
|
|
72
|
+
|
|
73
|
+
batch = []
|
|
74
|
+
subset_indices = df.index[begin:end]
|
|
75
|
+
|
|
76
|
+
for idx in tqdm(subset_indices, desc="Carregando áudios"):
|
|
77
|
+
try:
|
|
78
|
+
audio = load_audio(df, idx, sr)
|
|
79
|
+
batch.append(audio)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"Erro ao carregar índice {idx}: {e}")
|
|
82
|
+
batch.append(None)
|
|
83
|
+
|
|
84
|
+
max_len = max(len(x) for x in batch if x is not None)
|
|
85
|
+
padded_batch = np.zeros((len(batch), max_len), dtype=np.float32)
|
|
86
|
+
|
|
87
|
+
for i, audio in enumerate(batch):
|
|
88
|
+
if audio is not None:
|
|
89
|
+
padded_batch[i, :audio.shape[0]] = audio
|
|
90
|
+
|
|
91
|
+
return padded_batch
|
|
92
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .crema_d import process as process_crema
|
|
2
|
+
from .iemocap import process as process_iemocap
|
|
3
|
+
from .msp_improv import process as process_msp
|
|
4
|
+
from .msp_podcast import process as process_msp_podcast
|
|
5
|
+
from .emouerj import process as process_emouerj
|
|
6
|
+
from .ravdess import process as process_ravdess
|
|
7
|
+
from .emodb import process as process_emodb
|
|
8
|
+
|
|
9
|
+
# Dicionário que mapeia o nome do dataset (string) para a função de processamento
|
|
10
|
+
DATASET_LOADERS = {
|
|
11
|
+
'crema_d': process_crema,
|
|
12
|
+
'iemocap': process_iemocap,
|
|
13
|
+
'msp_improv': process_msp,
|
|
14
|
+
'msp_podcast': process_msp_podcast,
|
|
15
|
+
'emouerj': process_emouerj,
|
|
16
|
+
'ravdess': process_ravdess,
|
|
17
|
+
'emodb': process_emodb
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def get_available_datasets():
|
|
21
|
+
return list(DATASET_LOADERS.keys())
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
SENTENCE_MAP = {
|
|
5
|
+
"IEO": "It's eleven o'clock.",
|
|
6
|
+
"TIE": "That is exactly what happened.",
|
|
7
|
+
"IOM": "I'm on my way to the meeting.",
|
|
8
|
+
"IWW": "I wonder what this is about.",
|
|
9
|
+
"TAI": "The airplane is almost full.",
|
|
10
|
+
"MTI": "Maybe tomorrow it will be cold.",
|
|
11
|
+
"IWL": "I would like a new alarm clock.",
|
|
12
|
+
"ITH": "I think I have a cold.",
|
|
13
|
+
"DFA": "Don't forget a jacket.",
|
|
14
|
+
"ITS": "I think I'll wear my new suit.",
|
|
15
|
+
"TSI": "The surface is slick.",
|
|
16
|
+
"WSI": "We'll stop in a couple of minutes."
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
EMOTION_MAP = {
|
|
20
|
+
'A': 'Anger',
|
|
21
|
+
'D': 'Disgust',
|
|
22
|
+
'F': 'Fear',
|
|
23
|
+
'H': 'Happy',
|
|
24
|
+
'S': 'Sad',
|
|
25
|
+
'N': 'Neutral',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def process(base_dir):
|
|
29
|
+
"""
|
|
30
|
+
Processa todos os arquivos de áudio do dataset CREMA-D,
|
|
31
|
+
consolida os metadados e retorna um DataFrame padronizado.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
base_dir (str): O caminho raiz para o dataset CREMA-D
|
|
35
|
+
(ex: ".../data/CREMA-D")
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
pandas.DataFrame: Um DataFrame com metadados padronizados
|
|
39
|
+
ou None se o processamento falhar.
|
|
40
|
+
"""
|
|
41
|
+
print("Iniciando processamento do CREMA-D...")
|
|
42
|
+
|
|
43
|
+
# --- Definir Caminhos ---
|
|
44
|
+
AUDIO_DIR = os.path.join(base_dir, "AudioWAV")
|
|
45
|
+
DEMOGRAPHICS_FILE = os.path.join(base_dir, "VideoDemographics.csv")
|
|
46
|
+
RESPONSES_FILE = os.path.join(base_dir, "finishedResponses.csv")
|
|
47
|
+
|
|
48
|
+
# --- Carregar Metadados ---
|
|
49
|
+
print("Carregando arquivos de metadados do CREMA-D...")
|
|
50
|
+
try:
|
|
51
|
+
demographics_df = pd.read_csv(DEMOGRAPHICS_FILE).set_index('ActorID')
|
|
52
|
+
|
|
53
|
+
raw_responses_df = pd.read_csv(RESPONSES_FILE, usecols=['queryType', 'clipName', 'respEmo'])
|
|
54
|
+
voice_responses_df = raw_responses_df[raw_responses_df['queryType'] == 1].copy()
|
|
55
|
+
|
|
56
|
+
votes_df = pd.crosstab(voice_responses_df['clipName'], voice_responses_df['respEmo'])
|
|
57
|
+
emotion_columns = ['A', 'D', 'F', 'H', 'S', 'N']
|
|
58
|
+
|
|
59
|
+
votes_df['perceived_emotion_label'] = votes_df[emotion_columns].idxmax(axis=1)
|
|
60
|
+
print("Arquivos de metadados do CREMA-D carregados com sucesso.")
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"Erro ao carregar os arquivos CSV do CREMA-D: {e}")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# --- Iterar sobre os arquivos de áudio ---
|
|
67
|
+
print(f"Iniciando processamento dos arquivos em: {AUDIO_DIR}")
|
|
68
|
+
all_file_data = []
|
|
69
|
+
|
|
70
|
+
for filename in os.listdir(AUDIO_DIR):
|
|
71
|
+
if filename.endswith('.wav'):
|
|
72
|
+
full_file_path = os.path.join(AUDIO_DIR, filename)
|
|
73
|
+
file_key = os.path.splitext(filename)[0]
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# --- A. Extrair dados do NOME DO ARQUIVO ---
|
|
77
|
+
parts = file_key.split('_')
|
|
78
|
+
actor_id = int(parts[0])
|
|
79
|
+
sentence_id = parts[1]
|
|
80
|
+
intended_emotion_code = parts[2]
|
|
81
|
+
|
|
82
|
+
# --- B. Buscar dados dos CSVs carregados ---
|
|
83
|
+
speaker_gender = demographics_df.loc[actor_id]['Sex']
|
|
84
|
+
sentence_text = SENTENCE_MAP.get(sentence_id, "Desconhecido")
|
|
85
|
+
perceived_emotion_code = votes_df.loc[file_key]['perceived_emotion_label']
|
|
86
|
+
|
|
87
|
+
# --- C. Padronizar e Consolidar ---
|
|
88
|
+
file_info = {
|
|
89
|
+
'dataset': 'CREMA_D',
|
|
90
|
+
'file_path': full_file_path,
|
|
91
|
+
# 'filename': filename,
|
|
92
|
+
'speaker_id': f"crema_{actor_id}",
|
|
93
|
+
'gender': speaker_gender,
|
|
94
|
+
'language': 'en',
|
|
95
|
+
'sentence_text': sentence_text,
|
|
96
|
+
'emotion': EMOTION_MAP.get(perceived_emotion_code, perceived_emotion_code),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
all_file_data.append(file_info)
|
|
100
|
+
|
|
101
|
+
except KeyError as e:
|
|
102
|
+
print(f"Aviso: Não foi possível encontrar metadados para {filename}. Chave não encontrada: {e}. Pulando...")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"Erro ao processar o arquivo {filename}: {e}. Pulando...")
|
|
105
|
+
|
|
106
|
+
print(f"Processamento do CREMA-D concluído. {len(all_file_data)} arquivos processados.")
|
|
107
|
+
|
|
108
|
+
if not all_file_data:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# --- 4. Criar e Retornar o DataFrame ---
|
|
112
|
+
final_df = pd.DataFrame(all_file_data)
|
|
113
|
+
|
|
114
|
+
# Reordenar colunas para um layout padrão
|
|
115
|
+
standard_columns = [
|
|
116
|
+
'dataset', 'file_path', 'speaker_id', 'gender',
|
|
117
|
+
'emotion', 'sentence_text', 'language'
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
# Adiciona colunas que podem não existir no DF final (caso dê erro em todas)
|
|
121
|
+
for col in standard_columns:
|
|
122
|
+
if col not in final_df:
|
|
123
|
+
final_df[col] = pd.NA
|
|
124
|
+
|
|
125
|
+
return final_df[standard_columns]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
# EmoDB (Berlin Database of Emotional Speech) — alemão.
|
|
5
|
+
# Formato do nome do arquivo: SSTTTEV.wav
|
|
6
|
+
# SS -> número do locutor (2 dígitos)
|
|
7
|
+
# TTT -> código do texto (ex.: a01, b03)
|
|
8
|
+
# E -> letra da emoção (alemão)
|
|
9
|
+
# V -> versão (a, b, c, ...) quando há mais de uma gravação
|
|
10
|
+
|
|
11
|
+
# Letras de emoção (iniciais em alemão) mapeadas para o rótulo padrão do repo.
|
|
12
|
+
# W = Wut (raiva), L = Langeweile (tédio), E = Ekel (nojo),
|
|
13
|
+
# A = Angst (medo), F = Freude (alegria), T = Trauer (tristeza), N = Neutral
|
|
14
|
+
EMOTION_MAP = {
|
|
15
|
+
'W': 'Anger',
|
|
16
|
+
'L': 'Boredom',
|
|
17
|
+
'E': 'Disgust',
|
|
18
|
+
'A': 'Fear',
|
|
19
|
+
'F': 'Happy',
|
|
20
|
+
'T': 'Sad',
|
|
21
|
+
'N': 'Neutral',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Gênero de cada um dos 10 locutores (conforme documentação oficial do EmoDB).
|
|
25
|
+
SPEAKER_GENDER = {
|
|
26
|
+
'03': 'Male',
|
|
27
|
+
'08': 'Female',
|
|
28
|
+
'09': 'Female',
|
|
29
|
+
'10': 'Male',
|
|
30
|
+
'11': 'Male',
|
|
31
|
+
'12': 'Male',
|
|
32
|
+
'13': 'Female',
|
|
33
|
+
'14': 'Female',
|
|
34
|
+
'15': 'Male',
|
|
35
|
+
'16': 'Female',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Frases em alemão associadas a cada código de texto.
|
|
39
|
+
SENTENCE_MAP = {
|
|
40
|
+
'a01': "Der Lappen liegt auf dem Eisschrank.",
|
|
41
|
+
'a02': "Das will sie am Mittwoch abgeben.",
|
|
42
|
+
'a04': "Heute abend könnte ich es ihm sagen.",
|
|
43
|
+
'a05': "Das schwarze Stück Papier befindet sich da oben neben dem Holzstück.",
|
|
44
|
+
'a07': "In sieben Stunden wird es soweit sein.",
|
|
45
|
+
'b01': "Was sind denn das für Tüten, die da unter dem Tisch stehen?",
|
|
46
|
+
'b02': "Sie haben es gerade hochgetragen und jetzt gehen sie wieder runter.",
|
|
47
|
+
'b03': "An den Wochenenden bin ich jetzt immer nach Hause gefahren und habe Agnes besucht.",
|
|
48
|
+
'b09': "Ich will das eben wegbringen und dann mit Karl was trinken gehen.",
|
|
49
|
+
'b10': "Die wird auf dem Platz sein, wo wir sie immer hinlegen.",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def process(base_dir):
|
|
54
|
+
"""
|
|
55
|
+
Processa o dataset EmoDB (Berlin Database of Emotional Speech).
|
|
56
|
+
|
|
57
|
+
A estrutura esperada é uma pasta raiz contendo os arquivos .wav
|
|
58
|
+
(as pastas oficiais EmoDB_1/ e EmoDB_2/ podem ser extraídas dentro dela;
|
|
59
|
+
a busca é recursiva).
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
base_dir (str): Caminho raiz onde os .wav do EmoDB estão salvos.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
pandas.DataFrame: DataFrame padronizado ou None se nada for encontrado.
|
|
66
|
+
"""
|
|
67
|
+
print("Iniciando processamento do EmoDB...")
|
|
68
|
+
|
|
69
|
+
all_file_data = []
|
|
70
|
+
|
|
71
|
+
for root, dirs, files in os.walk(base_dir):
|
|
72
|
+
for filename in files:
|
|
73
|
+
if not filename.lower().endswith('.wav'):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
file_key = os.path.splitext(filename)[0]
|
|
77
|
+
|
|
78
|
+
# Nome padrão do EmoDB tem 7 caracteres: SS TTT E V
|
|
79
|
+
if len(file_key) != 7:
|
|
80
|
+
print(f"Nome fora do padrão, ignorado: {filename}")
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
speaker_id = file_key[0:2]
|
|
84
|
+
text_code = file_key[2:5]
|
|
85
|
+
emotion_code = file_key[5]
|
|
86
|
+
# file_key[6] é a versão, não usada no schema padrão.
|
|
87
|
+
|
|
88
|
+
emotion = EMOTION_MAP.get(emotion_code)
|
|
89
|
+
if emotion is None:
|
|
90
|
+
print(f"Emoção desconhecida ({emotion_code}) em {filename}, ignorado.")
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
file_info = {
|
|
94
|
+
'dataset': 'EmoDB',
|
|
95
|
+
'file_path': os.path.join(root, filename),
|
|
96
|
+
'speaker_id': f"emodb_{speaker_id}",
|
|
97
|
+
'gender': SPEAKER_GENDER.get(speaker_id, pd.NA),
|
|
98
|
+
'language': 'de',
|
|
99
|
+
'emotion': emotion,
|
|
100
|
+
'sentence_text': SENTENCE_MAP.get(text_code, "Unknown"),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
all_file_data.append(file_info)
|
|
104
|
+
|
|
105
|
+
print(f"Processamento do EmoDB concluído. {len(all_file_data)} arquivos processados.")
|
|
106
|
+
|
|
107
|
+
if not all_file_data:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
final_df = pd.DataFrame(all_file_data)
|
|
111
|
+
|
|
112
|
+
standard_columns = [
|
|
113
|
+
'dataset', 'file_path', 'speaker_id', 'gender',
|
|
114
|
+
'emotion', 'sentence_text', 'language'
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
for col in standard_columns:
|
|
118
|
+
if col not in final_df:
|
|
119
|
+
final_df[col] = pd.NA
|
|
120
|
+
|
|
121
|
+
return final_df[standard_columns]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import audb
|
|
3
|
+
|
|
4
|
+
EMOTION_MAP = {
|
|
5
|
+
'anger': 'Anger',
|
|
6
|
+
'happiness': 'Happy',
|
|
7
|
+
'sadness': 'Sad',
|
|
8
|
+
'neutral': 'Neutral'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
def process(base_dir=None):
|
|
12
|
+
"""
|
|
13
|
+
Carrega o dataset EmoUERJ via audb e padroniza para o formato do projeto.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
base_dir (str): Opcional neste caso, pois o audb gerencia o cache.
|
|
17
|
+
Pode ser usado para definir onde o audb salva os arquivos
|
|
18
|
+
se você configurar o cache localmente.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
pandas.DataFrame: DataFrame padronizado.
|
|
22
|
+
"""
|
|
23
|
+
print("Iniciando processamento do EmoUERJ via audb...")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
db = audb.load("emouerj", version="1.0.0", verbose=False)
|
|
27
|
+
|
|
28
|
+
original_df = db.get("emotion", ["gender", "speaker", "take"])
|
|
29
|
+
|
|
30
|
+
original_df['file_path'] = original_df.index
|
|
31
|
+
original_df = original_df.reset_index(drop=True)
|
|
32
|
+
|
|
33
|
+
print(f"EmoUERJ carregado. Processando {len(original_df)} arquivos...")
|
|
34
|
+
|
|
35
|
+
all_file_data = []
|
|
36
|
+
|
|
37
|
+
for row in original_df.itertuples():
|
|
38
|
+
try:
|
|
39
|
+
original_emotion = row.emotion
|
|
40
|
+
speaker = row.speaker
|
|
41
|
+
gender = row.gender.capitalize()
|
|
42
|
+
full_path = row.file_path
|
|
43
|
+
file_info = {
|
|
44
|
+
'dataset': 'EmoUERJ',
|
|
45
|
+
'file_path': full_path,
|
|
46
|
+
'speaker_id': f"emouerj_{speaker}",
|
|
47
|
+
'gender': gender,
|
|
48
|
+
'language': 'pt-br',
|
|
49
|
+
'sentence_text': pd.NA,
|
|
50
|
+
'emotion': EMOTION_MAP.get(original_emotion, original_emotion),
|
|
51
|
+
}
|
|
52
|
+
all_file_data.append(file_info)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"Erro ao processar linha do EmoUERJ: {e}")
|
|
55
|
+
|
|
56
|
+
final_df = pd.DataFrame(all_file_data)
|
|
57
|
+
|
|
58
|
+
standard_columns = [
|
|
59
|
+
'dataset', 'file_path', 'speaker_id', 'gender',
|
|
60
|
+
'emotion', 'sentence_text', 'language'
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
for col in standard_columns:
|
|
64
|
+
if col not in final_df:
|
|
65
|
+
final_df[col] = pd.NA
|
|
66
|
+
|
|
67
|
+
print(f"Processamento do EmoUERJ concluído. {len(final_df)} arquivos.")
|
|
68
|
+
return final_df[standard_columns]
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"Falha crítica ao carregar EmoUERJ via audb: {e}")
|
|
72
|
+
return None
|