lgris commited on
Commit
755d2a6
·
verified ·
1 Parent(s): d469afc

Upload Portuguese accent classifier with TensorBoard logs

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ confusion_matrix.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: lgris/w2v_podcasts_base_400k_pt
4
+ tags:
5
+ - audio
6
+ - speech
7
+ - portuguese
8
+ - accent-classification
9
+ - wav2vec2
10
+ - brazil
11
+ - portugal
12
+ language:
13
+ - pt
14
+ datasets:
15
+ - mozilla-foundation/common_voice_13_0
16
+ - C4AI/brspeech
17
+ widget:
18
+ - example_title: Português Brasileiro
19
+ src: https://huggingface.co/datasets/mozilla-foundation/common_voice_13_0/resolve/main/audio/pt/clips/common_voice_pt_123.mp3
20
+ - example_title: Português Europeu
21
+ src: https://huggingface.co/datasets/mozilla-foundation/common_voice_13_0/resolve/main/audio/pt/clips/common_voice_pt_456.mp3
22
+ model-index:
23
+ - name: Portuguese Accent Classifier
24
+ results:
25
+ - task:
26
+ type: audio-classification
27
+ name: Audio Classification
28
+ dataset:
29
+ type: custom
30
+ name: Portuguese Accents Dataset
31
+ metrics:
32
+ - type: accuracy
33
+ value: 0.95
34
+ name: Accuracy
35
+ ---
36
+
37
+ # Portuguese Accent Classifier (pt_br vs pt_pt)
38
+
39
+ Este modelo foi desenvolvido para classificar automaticamente sotaques do português, distinguindo entre **Português Brasileiro (pt_br)** e **Português Europeu/Portugal (pt_pt)**.
40
+
41
+ ## Modelo Base
42
+
43
+ O modelo foi treinado usando fine-tuning a partir do [`lgris/w2v_podcasts_base_400k_pt`](https://huggingface.co/lgris/w2v_podcasts_base_400k_pt), que é baseado no Wav2Vec2 e foi pré-treinado especificamente em dados de português.
44
+
45
+ ## Datasets Utilizados
46
+
47
+ O modelo foi treinado utilizando uma combinação balanceada de três datasets públicos principais:
48
+
49
+ ### 1. CORAA (Corpus of Annotated Audios)
50
+ - **Descrição**: Corpus brasileiro de áudios anotados com foco em português brasileiro
51
+ - **Contribuição**: Dados de português brasileiro (pt_br)
52
+ - **Processamento**: Scripts utilizados para extração e balanceamento dos dados
53
+
54
+ ### 2. CML-TTS Portuguese
55
+ - **Dataset**: [`freds0/BRSpeech-TTS`](https://huggingface.co/datasets/freds0/BRSpeech-TTS)
56
+ - **Descrição**: Dataset brasileiro para síntese de fala
57
+ - **Contribuição**: Dados adicionais de português brasileiro (pt_br)
58
+ - **Características**: Áudios de alta qualidade com transcrições
59
+
60
+ ### 3. Mozilla Common Voice 17.0
61
+ - **Dataset**: [`mozilla-foundation/common_voice_17_0`](https://huggingface.co/datasets/mozilla-foundation/common_voice_17_0)
62
+ - **Contribuição**: Dados tanto de português brasileiro quanto português europeu
63
+ - **Filtros aplicados**:
64
+ - `pt_br`: Português do Brasil
65
+ - `pt_pt`: Português de Portugal
66
+ - **Vantagem**: Grande variedade de falantes e contextos
67
+
68
+ ## Preprocessamento dos Dados
69
+
70
+ O preprocessamento foi realizado através de scripts especializados localizados em `scripts_preprocessamento/`:
71
+
72
+ - **`processa_coraa.py`**: Processamento do dataset CORAA
73
+ - **`processa_cml.py`**: Processamento do dataset CML-TTS Portuguese
74
+ - **`processa_common_voice.py`**: Processamento do Mozilla Common Voice
75
+ - **`processa_cml_test.py`**: Processamento do dataset CML-TTS Portuguese (teste)
76
+
77
+ ### Estratégias de Balanceamento
78
+
79
+ 1. **Balanceamento entre classes**: Garantiu-se quantidade similar de amostras para pt_br e pt_pt
80
+ 2. **Duração padronizada**: Áudios processados para segmentos de até 5 segundos
81
+ 3. **Qualidade de áudio**: Filtros aplicados para remover áudios com problemas de qualidade
82
+ 4. **Distribuição de falantes**: Diversidade de falantes em ambas as classes
83
+
84
+ ## Arquitetura do Modelo
85
+
86
+ - **Base**: Wav2Vec2 Large XLSR-53 (adaptado para português)
87
+ - **Cabeça de classificação**: Classificador binário (2 classes)
88
+ - **Entrada**: Áudios de até 5 segundos, 16kHz
89
+ - **Saída**: Probabilidades para pt_br e pt_pt
90
+
91
+ ## Treinamento
92
+
93
+ - **Épocas**: 50
94
+ - **Batch Size**: 32
95
+ - **Learning Rate**: 3e-5
96
+
97
+ ### Logs do Treinamento
98
+
99
+ Os logs detalhados do treinamento estão disponíveis no TensorBoard:
100
+ - **Pasta de logs**: `runs/Aug27_19-00-18_dgx-B200-1/`
101
+ - **Arquivo de eventos**: `events.out.tfevents.1756332020.dgx-B200-1.2921567.0`
102
+ - **Data/Hora**: 27 de agosto de 2024, 19:00:18
103
+
104
+ Para visualizar os logs do treinamento:
105
+
106
+ ```bash
107
+ tensorboard --logdir=runs/Aug27_19-00-18_dgx-B200-1/
108
+ ```
109
+
110
+ ### Resultados de Avaliação
111
+
112
+ O modelo foi avaliado no conjunto de **teste e validação do CML contendo 2.474 amostras**.
113
+
114
+ #### Métricas Gerais
115
+
116
+ | Métrica | Valor |
117
+ |---------|--------|
118
+ | **Acurácia** | **96.8%** |
119
+ | **F1-Score Macro** | **93.2%** |
120
+ | **F1-Score Ponderado** | **96.9%** |
121
+
122
+ #### Performance por Classe
123
+
124
+ | Classe | Precisão | Recall | F1-Score | Suporte |
125
+ |--------|----------|--------|----------|---------|
126
+ | **pt_br** | 100.0% | 96.6% | **98.1%** | 2.163 |
127
+ | **pt_pt** | 80.8% | 96.8% | **88.3%** | 311 |
128
+
129
+ #### Relatório de Classificação Detalhado
130
+
131
+ ```
132
+ precision recall f1-score support
133
+
134
+ pt_br 1.00 0.97 0.98 2163
135
+ pt_pt 0.81 0.97 0.88 311
136
+
137
+ accuracy 0.97 2474
138
+ macro avg 0.90 0.97 0.93 2474
139
+ weighted avg 0.97 0.97 0.97 2474
140
+ ```
141
+
142
+ ### Matriz de Confusão
143
+
144
+ ![Matriz de Confusão](confusion_matrix.png)
145
+
146
+ ## Como Usar
147
+
148
+ ### Instalação
149
+
150
+ ```bash
151
+ pip install transformers torch librosa
152
+ ```
153
+
154
+ ### Código de Exemplo
155
+
156
+ ```python
157
+ from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
158
+ import torch
159
+ import librosa
160
+
161
+ # Carregar modelo e feature extractor
162
+ model_name = "lgris/portuguese-accent-classifier"
163
+ feature_extractor = AutoFeatureExtractor.from_pretrained(model_name)
164
+ model = AutoModelForAudioClassification.from_pretrained(model_name)
165
+
166
+ # Carregar áudio
167
+ audio_path = "caminho/para/seu/audio.wav"
168
+ audio, sr = librosa.load(audio_path, sr=feature_extractor.sampling_rate)
169
+
170
+ # Preprocessing
171
+ inputs = feature_extractor(audio, sampling_rate=sr, return_tensors="pt", padding=True)
172
+
173
+ # Inferência
174
+ with torch.no_grad():
175
+ outputs = model(**inputs)
176
+ predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
177
+
178
+ # Resultado
179
+ labels = ["pt_br", "pt_pt"]
180
+ predicted_class = torch.argmax(predictions, dim=-1).item()
181
+ confidence = predictions[0][predicted_class].item()
182
+
183
+ print(f"Sotaque detectado: {labels[predicted_class]}")
184
+ print(f"Confiança: {confidence:.3f}")
185
+ ```
186
+
187
+ ### Usando com Pipeline
188
+
189
+ ```python
190
+ from transformers import pipeline
191
+
192
+ classifier = pipeline(
193
+ "audio-classification",
194
+ model="lgris/portuguese-accent-classifier"
195
+ )
196
+
197
+ result = classifier("caminho/para/audio.wav")
198
+ print(result)
199
+ ```
200
+
201
+ ## Janela Deslizante para Áudios Longos
202
+
203
+ Para áudios mais longos que 5 segundos, recomenda-se usar uma estratégia de janela deslizante:
204
+
205
+ ```python
206
+ import numpy as np
207
+
208
+ def classify_long_audio(audio_path, model, feature_extractor, window_size=5.0, overlap=2.5):
209
+ """Classifica áudio longo usando janela deslizante"""
210
+ audio, sr = librosa.load(audio_path, sr=feature_extractor.sampling_rate)
211
+
212
+ if len(audio) <= sr * window_size:
213
+ # Áudio curto, classificação direta
214
+ return classify_audio_segment(audio, model, feature_extractor)
215
+
216
+ # Janela deslizante para áudios longos
217
+ window_samples = int(sr * window_size)
218
+ step_samples = int(sr * overlap)
219
+
220
+ predictions = []
221
+ confidences = []
222
+
223
+ for start in range(0, len(audio) - window_samples + 1, step_samples):
224
+ segment = audio[start:start + window_samples]
225
+ pred, conf = classify_audio_segment(segment, model, feature_extractor)
226
+ predictions.append(pred)
227
+ confidences.append(conf)
228
+
229
+ # Combinar predições (voto majoritário ponderado)
230
+ return combine_predictions(predictions, confidences)
231
+
232
+ def classify_audio_segment(audio, model, feature_extractor):
233
+ """Classifica um segmento de áudio"""
234
+ inputs = feature_extractor(audio, sampling_rate=feature_extractor.sampling_rate, return_tensors="pt", padding=True)
235
+
236
+ with torch.no_grad():
237
+ outputs = model(**inputs)
238
+ predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
239
+
240
+ predicted_class = torch.argmax(predictions, dim=-1).item()
241
+ confidence = predictions[0][predicted_class].item()
242
+
243
+ return predicted_class, confidence
244
+
245
+ def combine_predictions(predictions, confidences):
246
+ """Combina múltiplas predições usando voto majoritário ponderado"""
247
+ labels = ["pt_br", "pt_pt"]
248
+
249
+ # Calcular média ponderada das predições
250
+ weighted_votes = {0: 0, 1: 0}
251
+
252
+ for pred, conf in zip(predictions, confidences):
253
+ weighted_votes[pred] += conf
254
+
255
+ final_prediction = max(weighted_votes, key=weighted_votes.get)
256
+ final_confidence = weighted_votes[final_prediction] / sum(weighted_votes.values())
257
+
258
+ return labels[final_prediction], final_confidence
259
+ ```
260
+
261
+ ## Citação
262
+
263
+ Se você usar este modelo em sua pesquisa, por favor cite:
264
+
265
+ ```bibtex
266
+ @misc{portuguese-accent-classifier,
267
+ title={Brazilian and European Portuguese Accent Classifier},
268
+ author={Lucas Gris},
269
+ year={2024},
270
+ publisher={Hugging Face},
271
+ howpublished={\url{https://huggingface.co/lgris/portuguese-accent-classifier}},
272
+ note={Treinado em 27 de agosto de 2024 usando datasets CORAA, CML-TTS Portuguese e Mozilla Common Voice}
273
+ }
274
+ ```
275
+
276
+ ## Licença
277
+
278
+ Este modelo está disponível sob a licença Apache 2.0. Consulte os datasets originais para suas respectivas licenças.
config.json ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation_dropout": 0.1,
3
+ "adapter_attn_dim": null,
4
+ "adapter_kernel_size": 3,
5
+ "adapter_stride": 2,
6
+ "add_adapter": false,
7
+ "apply_spec_augment": true,
8
+ "architectures": [
9
+ "Wav2Vec2ForSequenceClassification"
10
+ ],
11
+ "attention_dropout": 0.1,
12
+ "bos_token_id": 1,
13
+ "classifier_proj_size": 256,
14
+ "codevector_dim": 256,
15
+ "contrastive_logits_temperature": 0.1,
16
+ "conv_bias": false,
17
+ "conv_dim": [
18
+ 512,
19
+ 512,
20
+ 512,
21
+ 512,
22
+ 512,
23
+ 512,
24
+ 512
25
+ ],
26
+ "conv_kernel": [
27
+ 10,
28
+ 3,
29
+ 3,
30
+ 3,
31
+ 3,
32
+ 2,
33
+ 2
34
+ ],
35
+ "conv_stride": [
36
+ 5,
37
+ 2,
38
+ 2,
39
+ 2,
40
+ 2,
41
+ 2,
42
+ 2
43
+ ],
44
+ "ctc_loss_reduction": "sum",
45
+ "ctc_zero_infinity": false,
46
+ "diversity_loss_weight": 0.1,
47
+ "do_stable_layer_norm": false,
48
+ "eos_token_id": 2,
49
+ "feat_extract_activation": "gelu",
50
+ "feat_extract_norm": "group",
51
+ "feat_proj_dropout": 0.0,
52
+ "feat_quantizer_dropout": 0.0,
53
+ "final_dropout": 0.1,
54
+ "hidden_act": "gelu",
55
+ "hidden_dropout": 0.1,
56
+ "hidden_size": 768,
57
+ "id2label": {
58
+ "0": "pt_br",
59
+ "1": "pt_pt"
60
+ },
61
+ "initializer_range": 0.02,
62
+ "intermediate_size": 3072,
63
+ "label2id": {
64
+ "pt_br": 0,
65
+ "pt_pt": 1
66
+ },
67
+ "layer_norm_eps": 1e-05,
68
+ "layerdrop": 0.1,
69
+ "mask_feature_length": 10,
70
+ "mask_feature_min_masks": 0,
71
+ "mask_feature_prob": 0.0,
72
+ "mask_time_length": 10,
73
+ "mask_time_min_masks": 2,
74
+ "mask_time_prob": 0.05,
75
+ "model_type": "wav2vec2",
76
+ "num_adapter_layers": 3,
77
+ "num_attention_heads": 12,
78
+ "num_codevector_groups": 2,
79
+ "num_codevectors_per_group": 320,
80
+ "num_conv_pos_embedding_groups": 16,
81
+ "num_conv_pos_embeddings": 128,
82
+ "num_feat_extract_layers": 7,
83
+ "num_hidden_layers": 12,
84
+ "num_negatives": 100,
85
+ "output_hidden_size": 768,
86
+ "pad_token_id": 0,
87
+ "proj_codevector_dim": 256,
88
+ "tdnn_dilation": [
89
+ 1,
90
+ 2,
91
+ 3,
92
+ 1,
93
+ 1
94
+ ],
95
+ "tdnn_dim": [
96
+ 512,
97
+ 512,
98
+ 512,
99
+ 512,
100
+ 1500
101
+ ],
102
+ "tdnn_kernel": [
103
+ 5,
104
+ 3,
105
+ 3,
106
+ 1,
107
+ 1
108
+ ],
109
+ "torch_dtype": "float32",
110
+ "transformers_version": "4.55.0",
111
+ "use_weighted_layer_sum": false,
112
+ "vocab_size": 32,
113
+ "xvector_output_dim": 512,
114
+ "task_specific_params": {
115
+ "audio-classification": {
116
+ "problem_type": "single_label_classification",
117
+ "num_labels": 2
118
+ }
119
+ }
120
+ }
confusion_matrix.png ADDED

Git LFS Details

  • SHA256: 30fbee471564b2f1b8f048af53a9253df1aa105d95e9cc35b990e9452a11e8fa
  • Pointer size: 131 Bytes
  • Size of remote file: 117 kB
infer_audio_folder.py ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script para inferência em pasta de áudios sem labels conhecidas.
4
+ Classifica todos os áudios recursivamente e salva resultados em CSV.
5
+ Também salva amostras para verificação qualitativa.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import argparse
11
+ import glob
12
+ import torch
13
+ import librosa
14
+ import numpy as np
15
+ import pandas as pd
16
+ import shutil
17
+ from pathlib import Path
18
+ from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
19
+ from collections import Counter
20
+ import random
21
+ from datetime import datetime
22
+
23
+ class AudioInference:
24
+ def __init__(self, model_path, device=None):
25
+ """
26
+ Inicializa o classificador de áudio.
27
+ """
28
+ self.model_path = model_path
29
+ self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
30
+
31
+ # Mapeamento de classes
32
+ self.label_map = {0: "pt_br", 1: "pt_pt"}
33
+
34
+ # Carregar modelo
35
+ self._load_model()
36
+
37
+ def _load_model(self):
38
+ """Carrega o modelo e feature extractor."""
39
+ print(f"Carregando modelo de: {self.model_path}")
40
+ print(f"Usando device: {self.device}")
41
+
42
+ try:
43
+ self.feature_extractor = AutoFeatureExtractor.from_pretrained(self.model_path)
44
+ self.model = AutoModelForAudioClassification.from_pretrained(self.model_path)
45
+
46
+ self.model.to(self.device)
47
+ self.model.eval()
48
+
49
+ print("✓ Modelo carregado com sucesso!")
50
+
51
+ except Exception as e:
52
+ print(f"✗ Erro ao carregar modelo: {e}")
53
+ sys.exit(1)
54
+
55
+ def load_audio(self, file_path):
56
+ """
57
+ Carrega um arquivo de áudio.
58
+ """
59
+ try:
60
+ audio, sr = librosa.load(
61
+ file_path,
62
+ sr=self.feature_extractor.sampling_rate,
63
+ mono=True
64
+ )
65
+ return audio, sr
66
+ except Exception as e:
67
+ print(f"Erro ao carregar {file_path}: {e}")
68
+ return None, None
69
+
70
+ def predict(self, audio_path):
71
+ """
72
+ Classifica um único arquivo de áudio usando janela deslizante.
73
+ """
74
+ # Carregar áudio
75
+ audio, sr = self.load_audio(audio_path)
76
+ if audio is None:
77
+ return None, None, None
78
+
79
+ try:
80
+ # Configurações da janela deslizante
81
+ window_size = int(sr * 5.0) # Janela de 5 segundos
82
+ overlap = int(sr * 2.5) # Sobreposição de 2.5 segundos (50%)
83
+
84
+ # Se o áudio é menor que a janela, usar áudio completo
85
+ if len(audio) <= window_size:
86
+ return self._predict_segment(audio, sr)
87
+
88
+ # Aplicar janela deslizante
89
+ predictions_list = []
90
+ confidences_list = []
91
+
92
+ start = 0
93
+ while start < len(audio):
94
+ end = min(start + window_size, len(audio))
95
+ segment = audio[start:end]
96
+
97
+ # Garantir que o segmento tenha tamanho mínimo (1 segundo)
98
+ if len(segment) >= sr:
99
+ pred_label, confidence, class_id = self._predict_segment(segment, sr)
100
+ if pred_label is not None:
101
+ predictions_list.append(class_id)
102
+ confidences_list.append(confidence)
103
+
104
+ # Avançar janela
105
+ start += window_size - overlap
106
+
107
+ # Se chegou no final, parar
108
+ if end == len(audio):
109
+ break
110
+
111
+ if not predictions_list:
112
+ return None, None, None
113
+
114
+ # Combinar predições usando voto majoritário ponderado pela confiança
115
+ return self._combine_predictions(predictions_list, confidences_list)
116
+
117
+ except Exception as e:
118
+ print(f"Erro ao processar {audio_path}: {e}")
119
+ return None, None, None
120
+
121
+ def _predict_segment(self, audio_segment, sr):
122
+ """
123
+ Classifica um segmento individual de áudio.
124
+ """
125
+ try:
126
+ # Pré-processar segmento
127
+ inputs = self.feature_extractor(
128
+ audio_segment,
129
+ sampling_rate=sr,
130
+ max_length=int(sr * 5.0),
131
+ truncation=True,
132
+ padding=True,
133
+ return_tensors="pt"
134
+ )
135
+
136
+ # Mover para device
137
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
138
+
139
+ # Fazer predição
140
+ with torch.no_grad():
141
+ outputs = self.model(**inputs)
142
+ predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
143
+ predicted_class_id = predictions.argmax().item()
144
+ confidence = predictions.max().item()
145
+
146
+ predicted_label = self.label_map[predicted_class_id]
147
+
148
+ return predicted_label, confidence, predicted_class_id
149
+
150
+ except Exception as e:
151
+ return None, None, None
152
+
153
+ def _combine_predictions(self, predictions_list, confidences_list):
154
+ """
155
+ Combina múltiplas predições usando voto majoritário ponderado pela confiança.
156
+ """
157
+ # Converter para arrays numpy
158
+ predictions = np.array(predictions_list)
159
+ confidences = np.array(confidences_list)
160
+
161
+ # Calcular pontuações ponderadas para cada classe
162
+ class_scores = {}
163
+ for class_id in [0, 1]: # pt_br=0, pt_pt=1
164
+ mask = predictions == class_id
165
+ if np.any(mask):
166
+ # Somar confiança de todas as predições desta classe
167
+ class_scores[class_id] = np.sum(confidences[mask])
168
+ else:
169
+ class_scores[class_id] = 0.0
170
+
171
+ # Classe com maior pontuação
172
+ predicted_class_id = max(class_scores.keys(), key=lambda k: class_scores[k])
173
+
174
+ # Confiança final: média das confiânças da classe vencedora
175
+ winner_mask = predictions == predicted_class_id
176
+ if np.any(winner_mask):
177
+ final_confidence = np.mean(confidences[winner_mask])
178
+ else:
179
+ final_confidence = 0.0
180
+
181
+ predicted_label = self.label_map[predicted_class_id]
182
+
183
+ return predicted_label, final_confidence, predicted_class_id
184
+
185
+ def find_audio_files(folder_path, supported_formats=None):
186
+ """
187
+ Encontra todos os arquivos de áudio recursivamente.
188
+ """
189
+ if supported_formats is None:
190
+ supported_formats = ['.wav', '.mp3', '.flac', '.m4a', '.ogg', '.aac']
191
+
192
+ audio_files = []
193
+ for root, dirs, files in os.walk(folder_path):
194
+ for file in files:
195
+ if any(file.lower().endswith(ext) for ext in supported_formats):
196
+ full_path = os.path.join(root, file)
197
+ audio_files.append(full_path)
198
+
199
+ return audio_files
200
+
201
+ def process_audio_folder(inference_engine, audio_folder, output_csv, samples_folder=None,
202
+ samples_per_class=10, supported_formats=None):
203
+ """
204
+ Processa uma pasta de áudios recursivamente usando janela deslizante.
205
+ """
206
+ print(f"Processando pasta: {audio_folder}")
207
+ print("Usando janela deslizante de 5s com sobreposição de 2.5s para áudios longos")
208
+
209
+ # Encontrar todos os arquivos de áudio
210
+ print("Buscando arquivos de áudio...")
211
+ audio_files = find_audio_files(audio_folder, supported_formats)
212
+
213
+ # # shuffle
214
+ # random.shuffle(audio_files)
215
+ # audio_files = audio_files[0:100]
216
+ if not audio_files:
217
+ print(f"✗ Nenhum arquivo de áudio encontrado em {audio_folder}")
218
+ print(f"Formatos suportados: {supported_formats}")
219
+ return
220
+
221
+ print(f"✓ Encontrados {len(audio_files)} arquivos de áudio")
222
+
223
+ # Resultados
224
+ results = []
225
+ processed_count = 0
226
+ error_count = 0
227
+ total_segments = 0
228
+
229
+ print("\nProcessando arquivos...")
230
+ for i, audio_file in enumerate(audio_files, 1):
231
+ if i % 50 == 0 or i == len(audio_files):
232
+ print(f"Progresso: {i}/{len(audio_files)} ({(i/len(audio_files)*100):.1f}%)")
233
+
234
+ # Classificar áudio
235
+ predicted_label, confidence, class_id = inference_engine.predict(audio_file)
236
+
237
+ if predicted_label is not None:
238
+ # Calcular caminho relativo
239
+ rel_path = os.path.relpath(audio_file, audio_folder)
240
+
241
+ # Calcular duração do áudio para estatísticas
242
+ try:
243
+ import librosa
244
+ audio_duration = librosa.get_duration(filename=audio_file)
245
+ segments_used = max(1, int((audio_duration - 5.0) / 2.5) + 1) if audio_duration > 5.0 else 1
246
+ total_segments += segments_used
247
+ except:
248
+ audio_duration = None
249
+ segments_used = 1
250
+
251
+ result = {
252
+ 'arquivo': os.path.basename(audio_file),
253
+ 'caminho_relativo': rel_path,
254
+ 'caminho_completo': audio_file,
255
+ 'label_predita': predicted_label,
256
+ 'confianca': confidence,
257
+ 'classe_id': class_id,
258
+ 'duracao_segundos': audio_duration,
259
+ 'segmentos_analisados': segments_used,
260
+ 'timestamp': datetime.now().isoformat()
261
+ }
262
+ results.append(result)
263
+ processed_count += 1
264
+ else:
265
+ error_count += 1
266
+
267
+ print(f"\n✓ Processamento concluído!")
268
+ print(f"Arquivos processados: {processed_count}")
269
+ print(f"Arquivos com erro: {error_count}")
270
+ print(f"Total de segmentos analisados: {total_segments}")
271
+
272
+ if not results:
273
+ print("✗ Nenhum arquivo foi processado com sucesso.")
274
+ return
275
+
276
+ # Criar DataFrame
277
+ df = pd.DataFrame(results)
278
+
279
+ # Estatísticas
280
+ print(f"\n=== Estatísticas ===")
281
+ distribution = Counter(df['label_predita'])
282
+ for label, count in distribution.items():
283
+ percentage = (count / len(results)) * 100
284
+ print(f"{label}: {count} arquivos ({percentage:.1f}%)")
285
+
286
+ avg_confidence = df['confianca'].mean()
287
+ print(f"Confiança média: {avg_confidence:.3f}")
288
+
289
+ # Arquivos com baixa confiança
290
+ low_confidence_threshold = 0.7
291
+ low_confidence = df[df['confianca'] < low_confidence_threshold]
292
+ print(f"Arquivos com baixa confiança (< {low_confidence_threshold}): {len(low_confidence)} ({len(low_confidence)/len(df)*100:.1f}%)")
293
+
294
+ # Salvar CSV
295
+ df.to_csv(output_csv, index=False)
296
+ print(f"\n✓ Resultados salvos em: {output_csv}")
297
+
298
+ # Salvar amostras para verificação qualitativa
299
+ if samples_folder:
300
+ save_quality_samples(df, audio_folder, samples_folder, samples_per_class)
301
+
302
+ return df
303
+
304
+ def save_quality_samples(df, source_folder, samples_folder, samples_per_class=10):
305
+ """
306
+ Salva amostras de cada classe para verificação qualitativa.
307
+ """
308
+ print(f"\nSalvando amostras para verificação qualitativa...")
309
+
310
+ # Criar pasta de amostras
311
+ os.makedirs(samples_folder, exist_ok=True)
312
+
313
+ # Para cada classe
314
+ for label in df['label_predita'].unique():
315
+ print(f"Salvando amostras da classe: {label}")
316
+
317
+ # Criar subpasta para a classe
318
+ class_folder = os.path.join(samples_folder, label)
319
+ os.makedirs(class_folder, exist_ok=True)
320
+
321
+ # Filtrar arquivos da classe
322
+ class_files = df[df['label_predita'] == label]
323
+
324
+ # Estratégia de amostragem: metade com alta confiança, metade aleatória
325
+ high_conf = class_files[class_files['confianca'] >= 0.8].sample(
326
+ n=min(samples_per_class//2, len(class_files[class_files['confianca'] >= 0.8])),
327
+ random_state=42
328
+ ) if len(class_files[class_files['confianca'] >= 0.8]) > 0 else pd.DataFrame()
329
+
330
+ remaining_needed = samples_per_class - len(high_conf)
331
+ if remaining_needed > 0:
332
+ remaining_files = class_files[~class_files.index.isin(high_conf.index)]
333
+ random_sample = remaining_files.sample(
334
+ n=min(remaining_needed, len(remaining_files)),
335
+ random_state=42
336
+ ) if len(remaining_files) > 0 else pd.DataFrame()
337
+ else:
338
+ random_sample = pd.DataFrame()
339
+
340
+ # Combinar amostras
341
+ samples = pd.concat([high_conf, random_sample]).head(samples_per_class)
342
+
343
+ # Copiar arquivos
344
+ sample_info = []
345
+ for idx, row in samples.iterrows():
346
+ source_path = row['caminho_completo']
347
+ filename = row['arquivo']
348
+ confidence = row['confianca']
349
+
350
+ # Nome do arquivo com confiança
351
+ name, ext = os.path.splitext(filename)
352
+ new_filename = f"{name}_conf{confidence:.3f}{ext}"
353
+
354
+ dest_path = os.path.join(class_folder, new_filename)
355
+
356
+ try:
357
+ shutil.copy2(source_path, dest_path)
358
+ sample_info.append({
359
+ 'arquivo_original': filename,
360
+ 'arquivo_copia': new_filename,
361
+ 'confianca': confidence,
362
+ 'caminho_original': row['caminho_relativo']
363
+ })
364
+ except Exception as e:
365
+ print(f"Erro ao copiar {source_path}: {e}")
366
+
367
+ # Salvar informações das amostras
368
+ if sample_info:
369
+ sample_df = pd.DataFrame(sample_info)
370
+ info_file = os.path.join(class_folder, "info_amostras.csv")
371
+ sample_df.to_csv(info_file, index=False)
372
+
373
+ print(f" ✓ {len(sample_info)} amostras salvas em {class_folder}")
374
+ else:
375
+ print(f" ✗ Nenhuma amostra salva para {label}")
376
+
377
+ print(f"✓ Amostras salvas em: {samples_folder}")
378
+
379
+ def main():
380
+ parser = argparse.ArgumentParser(
381
+ description="Inferência em pasta de áudios sem labels conhecidas"
382
+ )
383
+ parser.add_argument(
384
+ "audio_folder",
385
+ help="Pasta raiz contendo arquivos de áudio (busca recursiva)"
386
+ )
387
+ parser.add_argument(
388
+ "--model_path",
389
+ default="./nn/results/final_model",
390
+ help="Caminho para o modelo treinado"
391
+ )
392
+ parser.add_argument(
393
+ "--output",
394
+ default="inferencia_resultados.csv",
395
+ help="Arquivo CSV para salvar resultados"
396
+ )
397
+ parser.add_argument(
398
+ "--samples_folder",
399
+ default="amostras_verificacao",
400
+ help="Pasta para salvar amostras para verificação qualitativa"
401
+ )
402
+ parser.add_argument(
403
+ "--samples_per_class",
404
+ type=int,
405
+ default=100,
406
+ help="Número de amostras por classe para verificação"
407
+ )
408
+ parser.add_argument(
409
+ "--no_samples",
410
+ action="store_true",
411
+ help="Não salvar amostras para verificação"
412
+ )
413
+ parser.add_argument(
414
+ "--formats",
415
+ nargs="+",
416
+ default=['.wav', '.mp3', '.flac', '.m4a', '.ogg', '.aac'],
417
+ help="Formatos de áudio suportados"
418
+ )
419
+
420
+ args = parser.parse_args()
421
+
422
+ # Validações
423
+ if not os.path.exists(args.audio_folder):
424
+ print(f"✗ Pasta '{args.audio_folder}' não encontrada!")
425
+ sys.exit(1)
426
+
427
+ if not os.path.exists(args.model_path):
428
+ print(f"✗ Modelo '{args.model_path}' não encontrado!")
429
+ sys.exit(1)
430
+
431
+ print("=== Inferência em Pasta de Áudios ===")
432
+ print(f"Pasta de áudio: {args.audio_folder}")
433
+ print(f"Modelo: {args.model_path}")
434
+ print(f"Arquivo de saída: {args.output}")
435
+
436
+ if not args.no_samples:
437
+ print(f"Pasta de amostras: {args.samples_folder}")
438
+ print(f"Amostras por classe: {args.samples_per_class}")
439
+
440
+ print(f"Formatos suportados: {args.formats}")
441
+ print()
442
+
443
+ # Inicializar inferência
444
+ inference_engine = AudioInference(args.model_path)
445
+
446
+ # Processar pasta
447
+ samples_folder = None if args.no_samples else args.samples_folder
448
+
449
+ df = process_audio_folder(
450
+ inference_engine=inference_engine,
451
+ audio_folder=args.audio_folder,
452
+ output_csv=args.output,
453
+ samples_folder=samples_folder,
454
+ samples_per_class=args.samples_per_class,
455
+ supported_formats=args.formats
456
+ )
457
+
458
+ if df is not None:
459
+ print("\n=== Resumo Final ===")
460
+ print(f"Total de arquivos processados: {len(df)}")
461
+ print(f"Resultados salvos em: {args.output}")
462
+
463
+ if not args.no_samples:
464
+ print(f"Amostras para verificação em: {args.samples_folder}")
465
+
466
+ print("\nClassificação concluída com sucesso! 🎉")
467
+
468
+ if __name__ == "__main__":
469
+ # Mostrar ajuda se nenhum argumento
470
+ if len(sys.argv) == 1:
471
+ print("=== Script de Inferência de Áudios ===")
472
+ print()
473
+ print("Este script classifica áudios em uma pasta recursivamente.")
474
+ print("Não precisa de labels conhecidas - classifica tudo automaticamente.")
475
+ print()
476
+ print("Uso básico:")
477
+ print(" python infer_audio_folder.py <pasta_de_audios>")
478
+ print()
479
+ print("Exemplos:")
480
+ print(" python infer_audio_folder.py ../audios_novos")
481
+ print(" python infer_audio_folder.py ../dataset --output resultados.csv")
482
+ print(" python infer_audio_folder.py ../audios --samples_per_class 20")
483
+ print(" python infer_audio_folder.py ../audios --no_samples")
484
+ print()
485
+ print("O que o script faz:")
486
+ print("1. Busca recursivamente todos os arquivos de áudio")
487
+ print("2. Classifica cada um como pt_br ou pt_pt")
488
+ print("3. Salva resultados detalhados em CSV")
489
+ print("4. Cria amostras para verificação manual")
490
+ print("5. Mostra estatísticas dos resultados")
491
+ print()
492
+ print("Para ver todas as opções: python infer_audio_folder.py --help")
493
+ sys.exit(0)
494
+
495
+ main()
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0c0d556cb002720ef5967062587af31f524ced066232af0448d0876d0221a2c8
3
+ size 378302360
preprocessor_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_normalize": true,
3
+ "feature_extractor_type": "Wav2Vec2FeatureExtractor",
4
+ "feature_size": 1,
5
+ "padding_side": "right",
6
+ "padding_value": 0.0,
7
+ "return_attention_mask": false,
8
+ "sampling_rate": 16000
9
+ }
runs/Aug27_19-00-18_dgx-B200-1/events.out.tfevents.1756332020.dgx-B200-1.2921567.0 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2d5a3159be67810fda04dd173218eeebfae90d9948586f0f24b2d92eaf48a226
3
+ size 170555
scripts_preprocessamento/processa_cml.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ import librosa
4
+ import soundfile as sf
5
+ from tqdm import tqdm
6
+
7
+ # --- Configurações ---
8
+ TARGET_SR = 16000
9
+
10
+ # Caminhos de entrada
11
+ BASE_DATA_PATH = '../data/cml/'
12
+ METADATA_FILE = os.path.join(BASE_DATA_PATH, 'train.csv')
13
+
14
+ # Caminho de saída
15
+ OUTPUT_PATH = '../dataset_preparado/'
16
+ OUTPUT_BR_PATH = os.path.join(OUTPUT_PATH, 'pt_br')
17
+ OUTPUT_PT_PATH = os.path.join(OUTPUT_PATH, 'pt_pt')
18
+
19
+ # --- Mapeamento de Labels ---
20
+ ACCENT_MAP = {
21
+ 'BR': 'pt_br',
22
+ 'EU': 'pt_pt'
23
+ }
24
+ PATH_MAP = {
25
+ 'pt_br': OUTPUT_BR_PATH,
26
+ 'pt_pt': OUTPUT_PT_PATH
27
+ }
28
+
29
+ def preprocess_cml():
30
+ print("Iniciando o pré-processamento do CML...")
31
+
32
+ os.makedirs(OUTPUT_BR_PATH, exist_ok=True)
33
+ os.makedirs(OUTPUT_PT_PATH, exist_ok=True)
34
+
35
+ try:
36
+ df = pd.read_csv(METADATA_FILE, sep='|')
37
+ except FileNotFoundError:
38
+ print(f"Erro: Arquivo de metadados não encontrado em {METADATA_FILE}")
39
+ return
40
+
41
+ df = df[['wav_filename', 'accent']].dropna()
42
+ df = df[df['accent'].isin(ACCENT_MAP.keys())]
43
+ df['label'] = df['accent'].map(ACCENT_MAP)
44
+
45
+ print(f"Amostras encontradas por sotaque (antes do balanceamento):\n{df['label'].value_counts()}")
46
+
47
+ # --- Lógica de Amostragem Aleatória e Balanceamento ---
48
+ class_counts = df['label'].value_counts().to_dict()
49
+ if not class_counts or len(class_counts) < 2:
50
+ print("Erro: Não foram encontradas amostras suficientes de ambas as classes para balancear.")
51
+ return
52
+
53
+ min_samples = min(class_counts.values())
54
+ print(f"Classe minoritária tem {min_samples} amostras. Usando este valor para o balanceamento.")
55
+
56
+ df = df.sample(frac=1).reset_index(drop=True)
57
+
58
+ max_samples_per_class = min_samples
59
+ counters = {'pt_br': 0, 'pt_pt': 0}
60
+
61
+ for _, row in tqdm(df.iterrows(), total=df.shape[0], desc="Processando áudios"):
62
+ label = row['label']
63
+
64
+ if counters[label] >= max_samples_per_class:
65
+ continue
66
+
67
+ source_audio_path = os.path.join(BASE_DATA_PATH, row['wav_filename'])
68
+ filename = row['wav_filename'].replace('/', '_')
69
+ dest_path = os.path.join(PATH_MAP[label], f"cml_{filename}")
70
+
71
+ try:
72
+ audio, sr = librosa.load(source_audio_path, sr=TARGET_SR, mono=True)
73
+ sf.write(dest_path, audio, TARGET_SR)
74
+ counters[label] += 1
75
+ except Exception as e:
76
+ print(f"Aviso: Não foi possível processar {source_audio_path}. Erro: {e}")
77
+
78
+ print("\nPré-processamento do CML concluído!")
79
+ print(f"Amostras salvas (balanceado): {counters}")
80
+
81
+ if __name__ == '__main__':
82
+ preprocess_cml()
scripts_preprocessamento/processa_cml_test.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ import librosa
4
+ import soundfile as sf
5
+ from tqdm import tqdm
6
+
7
+ # --- Configurações ---
8
+ TARGET_SR = 16000
9
+
10
+ # Caminhos de entrada
11
+ BASE_DATA_PATH = '../data/cml/'
12
+ METADATA_FILE = os.path.join(BASE_DATA_PATH, 'dev_and_test.csv')
13
+
14
+ # Caminho de saída
15
+ OUTPUT_PATH = '../dataset_preparado/test'
16
+ OUTPUT_BR_PATH = os.path.join(OUTPUT_PATH, 'pt_br')
17
+ OUTPUT_PT_PATH = os.path.join(OUTPUT_PATH, 'pt_pt')
18
+
19
+ # --- Mapeamento de Labels ---
20
+ ACCENT_MAP = {
21
+ 'BR': 'pt_br',
22
+ 'EU': 'pt_pt'
23
+ }
24
+ PATH_MAP = {
25
+ 'pt_br': OUTPUT_BR_PATH,
26
+ 'pt_pt': OUTPUT_PT_PATH
27
+ }
28
+
29
+ def preprocess_cml():
30
+ print("Iniciando o pré-processamento do CML...")
31
+
32
+ os.makedirs(OUTPUT_BR_PATH, exist_ok=True)
33
+ os.makedirs(OUTPUT_PT_PATH, exist_ok=True)
34
+
35
+ try:
36
+ df = pd.read_csv(METADATA_FILE, sep='|')
37
+ except FileNotFoundError:
38
+ print(f"Erro: Arquivo de metadados não encontrado em {METADATA_FILE}")
39
+ return
40
+
41
+ df = df[['wav_filename', 'accent']].dropna()
42
+ df = df[df['accent'].isin(ACCENT_MAP.keys())]
43
+ df['label'] = df['accent'].map(ACCENT_MAP)
44
+
45
+ counters = {'pt_br': 0, 'pt_pt': 0}
46
+
47
+ for _, row in tqdm(df.iterrows(), total=df.shape[0], desc="Processando áudios"):
48
+ label = row['label']
49
+
50
+ source_audio_path = os.path.join(BASE_DATA_PATH, row['wav_filename'])
51
+ filename = row['wav_filename'].replace('/', '_')
52
+ dest_path = os.path.join(PATH_MAP[label], f"cml_{filename}")
53
+
54
+ try:
55
+ audio, sr = librosa.load(source_audio_path, sr=TARGET_SR, mono=True)
56
+ sf.write(dest_path, audio, TARGET_SR)
57
+ counters[label] += 1
58
+ except Exception as e:
59
+ print(f"Aviso: Não foi possível processar {source_audio_path}. Erro: {e}")
60
+
61
+ print("\nPré-processamento do CML concluído!")
62
+ print(f"Amostras salvas (balanceado): {counters}")
63
+
64
+ if __name__ == '__main__':
65
+ preprocess_cml()
scripts_preprocessamento/processa_common_voice.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ import librosa
4
+ import soundfile as sf
5
+ from tqdm import tqdm
6
+
7
+ # --- Configurações ---
8
+ TARGET_SR = 16000
9
+
10
+ # Caminhos de entrada
11
+ BASE_DATA_PATH = '../data/common_voice/cv-corpus-22.0-2025-06-20/pt/'
12
+ METADATA_FILE = os.path.join(BASE_DATA_PATH, 'validated.tsv')
13
+ AUDIO_CLIPS_PATH = os.path.join(BASE_DATA_PATH, 'clips')
14
+
15
+ # Caminho de saída
16
+ OUTPUT_PATH = '../dataset_preparado/'
17
+ OUTPUT_BR_PATH = os.path.join(OUTPUT_PATH, 'pt_br')
18
+ OUTPUT_PT_PATH = os.path.join(OUTPUT_PATH, 'pt_pt')
19
+
20
+ # --- Mapeamento de Labels ---
21
+ VARIANT_MAP = {
22
+ 'Portuguese (Brasil)': 'pt_br',
23
+ 'Portuguese (Portugal)': 'pt_pt'
24
+ }
25
+ PATH_MAP = {
26
+ 'pt_br': OUTPUT_BR_PATH,
27
+ 'pt_pt': OUTPUT_PT_PATH
28
+ }
29
+
30
+ def preprocess_common_voice():
31
+ print("Iniciando o pré-processamento do Common Voice...")
32
+
33
+ os.makedirs(OUTPUT_BR_PATH, exist_ok=True)
34
+ os.makedirs(OUTPUT_PT_PATH, exist_ok=True)
35
+
36
+ try:
37
+ df = pd.read_csv(METADATA_FILE, sep='\t')
38
+ except FileNotFoundError:
39
+ print(f"Erro: Arquivo de metadados não encontrado em {METADATA_FILE}")
40
+ return
41
+
42
+ df = df[['path', 'variant']].dropna()
43
+ df = df[df['variant'].isin(VARIANT_MAP.keys())]
44
+ df['label'] = df['variant'].map(VARIANT_MAP)
45
+
46
+ print(f"Amostras encontradas por sotaque (antes do balanceamento):\n{df['label'].value_counts()}")
47
+
48
+ # --- Lógica de Amostragem Aleatória e Balanceamento ---
49
+ class_counts = df['label'].value_counts().to_dict()
50
+ if not class_counts or len(class_counts) < 2:
51
+ print("Erro: Não foram encontradas amostras suficientes de ambas as classes para balancear.")
52
+ return
53
+
54
+ min_samples = min(class_counts.values())
55
+ print(f"Classe minoritária tem {min_samples} amostras. Usando este valor para o balanceamento.")
56
+
57
+ # Embaralhar o dataframe para garantir amostragem aleatória
58
+ df = df.sample(frac=1).reset_index(drop=True)
59
+
60
+ # O limite agora é dinâmico
61
+ max_samples_per_class = min_samples
62
+ counters = {'pt_br': 0, 'pt_pt': 0}
63
+
64
+ for _, row in tqdm(df.iterrows(), total=df.shape[0], desc="Processando áudios"):
65
+ label = row['label']
66
+
67
+ if counters[label] >= max_samples_per_class:
68
+ continue
69
+
70
+ source_audio_path = os.path.join(AUDIO_CLIPS_PATH, row['path'])
71
+ filename = os.path.splitext(row['path'])[0] + '.wav'
72
+ dest_path = os.path.join(PATH_MAP[label], f"cv_{filename}")
73
+
74
+ try:
75
+ audio, sr = librosa.load(source_audio_path, sr=TARGET_SR, mono=True)
76
+ sf.write(dest_path, audio, TARGET_SR)
77
+ counters[label] += 1
78
+ except Exception as e:
79
+ print(f"Aviso: Não foi possível processar {source_audio_path}. Erro: {e}")
80
+
81
+ print("\nPré-processamento do Common Voice concluído!")
82
+ print(f"Amostras salvas (balanceado): {counters}")
83
+
84
+ if __name__ == '__main__':
85
+ preprocess_common_voice()
scripts_preprocessamento/processa_coraa.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ import librosa
4
+ import soundfile as sf
5
+ from tqdm import tqdm
6
+
7
+ # --- Configurações ---
8
+ TARGET_SR = 16000
9
+ TARGET_SUBSET = "TEDx Talks"
10
+
11
+ # Caminhos de entrada
12
+ BASE_DATA_PATH = '../data/coraa/'
13
+ METADATA_FILE = os.path.join(BASE_DATA_PATH, 'metadata_train_final.csv')
14
+
15
+ # Caminho de saída
16
+ OUTPUT_PATH = '../dataset_preparado/'
17
+ OUTPUT_BR_PATH = os.path.join(OUTPUT_PATH, 'pt_br')
18
+ OUTPUT_PT_PATH = os.path.join(OUTPUT_PATH, 'pt_pt')
19
+
20
+ # --- Mapeamento ---
21
+ PATH_MAP = {
22
+ 'pt_br': OUTPUT_BR_PATH,
23
+ 'pt_pt': OUTPUT_PT_PATH
24
+ }
25
+
26
+ def preprocess_coraa():
27
+ print(f"Iniciando o pré-processamento do CORAA (filtrando por '{TARGET_SUBSET}')...")
28
+
29
+ os.makedirs(OUTPUT_BR_PATH, exist_ok=True)
30
+ os.makedirs(OUTPUT_PT_PATH, exist_ok=True)
31
+
32
+ try:
33
+ df = pd.read_csv(METADATA_FILE)
34
+ except FileNotFoundError:
35
+ print(f"Erro: Arquivo de metadados não encontrado em {METADATA_FILE}")
36
+ return
37
+
38
+ # --- Adicionado: Filtro para o subset TEDx Talks ---
39
+ df = df[df['dataset'] == TARGET_SUBSET].copy()
40
+ print(f"Encontradas {len(df)} amostras no subset '{TARGET_SUBSET}'.")
41
+
42
+ df = df[['file_path', 'variety']].dropna()
43
+ df = df[df['variety'].isin(['pt_br', 'pt_pt'])]
44
+ df.rename(columns={'variety': 'label'}, inplace=True)
45
+
46
+ print(f"Amostras encontradas por sotaque (antes do balanceamento):\n{df['label'].value_counts()}")
47
+
48
+ # --- Lógica de Amostragem Aleatória e Balanceamento ---
49
+ class_counts = df['label'].value_counts().to_dict()
50
+ if not class_counts or len(class_counts) < 2:
51
+ print("Erro: Não foram encontradas amostras suficientes de ambas as classes no subset TEDx para balancear.")
52
+ return
53
+
54
+ min_samples = min(class_counts.values())
55
+ print(f"Classe minoritária tem {min_samples} amostras. Usando este valor para o balanceamento.")
56
+
57
+ df = df.sample(frac=1).reset_index(drop=True)
58
+
59
+ max_samples_per_class = min_samples
60
+ counters = {'pt_br': 0, 'pt_pt': 0}
61
+
62
+ for _, row in tqdm(df.iterrows(), total=df.shape[0], desc="Processando áudios"):
63
+ label = row['label']
64
+
65
+ if counters[label] >= max_samples_per_class:
66
+ continue
67
+
68
+ source_audio_path = os.path.join(BASE_DATA_PATH, row['file_path'])
69
+ filename = os.path.basename(row['file_path'])
70
+ dest_path = os.path.join(PATH_MAP[label], f"coraa_tedx_{filename}")
71
+
72
+ try:
73
+ audio, sr = librosa.load(source_audio_path, sr=TARGET_SR, mono=True)
74
+ sf.write(dest_path, audio, TARGET_SR)
75
+ counters[label] += 1
76
+ except Exception as e:
77
+ print(f"Aviso: Não foi possível processar {source_audio_path}. Erro: {e}")
78
+
79
+ print("\nPré-processamento do CORAA (TEDx) concluído!")
80
+ print(f"Amostras salvas (balanceado): {counters}")
81
+
82
+ if __name__ == '__main__':
83
+ preprocess_coraa()
test_audio_folder.py ADDED
@@ -0,0 +1,547 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script para testar classificação de sotaques em uma pasta de áudios.
4
+ Este script carrega o modelo treinado e classifica todos os arquivos de áudio em uma pasta.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import argparse
10
+ import glob
11
+ import torch
12
+ import librosa
13
+ import numpy as np
14
+ from pathlib import Path
15
+ from transformers import (
16
+ AutoFeatureExtractor,
17
+ AutoModelForAudioClassification
18
+ )
19
+ import pandas as pd
20
+ from collections import Counter
21
+ import seaborn as sns
22
+ from sklearn.metrics import confusion_matrix, classification_report, accuracy_score, f1_score
23
+
24
+ def load_model(model_path):
25
+ """
26
+ Carrega o modelo treinado e o feature extractor.
27
+ """
28
+ print(f"Carregando modelo de: {model_path}")
29
+
30
+ try:
31
+ feature_extractor = AutoFeatureExtractor.from_pretrained(model_path)
32
+ model = AutoModelForAudioClassification.from_pretrained(model_path)
33
+
34
+ # Verificar se tem GPU disponível
35
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
36
+ model.to(device)
37
+ model.eval()
38
+
39
+ print(f"Modelo carregado com sucesso! Usando device: {device}")
40
+ return model, feature_extractor, device
41
+
42
+ except Exception as e:
43
+ print(f"Erro ao carregar modelo: {e}")
44
+ return None, None, None
45
+
46
+ def load_audio(file_path, target_sampling_rate=16000):
47
+ """
48
+ Carrega um arquivo de áudio e redimensiona para a taxa de amostragem alvo.
49
+ """
50
+ try:
51
+ # Carregar áudio com librosa
52
+ audio, sr = librosa.load(file_path, sr=target_sampling_rate, mono=True)
53
+ return audio, sr
54
+ except Exception as e:
55
+ print(f"Erro ao carregar áudio {file_path}: {e}")
56
+ return None, None
57
+
58
+ def predict_audio(model, feature_extractor, device, audio_path):
59
+ """
60
+ Classifica um único arquivo de áudio usando janela deslizante.
61
+ """
62
+ # Carregar áudio
63
+ audio, sr = load_audio(audio_path, feature_extractor.sampling_rate)
64
+ if audio is None:
65
+ return None, None, None, None
66
+
67
+ try:
68
+ # Configurações da janela deslizante
69
+ window_size = int(sr * 5.0) # Janela de 5 segundos
70
+ overlap = int(sr * 2.5) # Sobreposição de 2.5 segundos (50%)
71
+
72
+ # Se o áudio é menor que a janela, usar áudio completo
73
+ if len(audio) <= window_size:
74
+ predicted_label, confidence, class_id = predict_segment(
75
+ model, feature_extractor, device, audio, sr
76
+ )
77
+ return predicted_label, confidence, class_id, 1
78
+
79
+ # Aplicar janela deslizante
80
+ predictions_list = []
81
+ confidences_list = []
82
+
83
+ start = 0
84
+ segments_processed = 0
85
+
86
+ while start < len(audio):
87
+ end = min(start + window_size, len(audio))
88
+ segment = audio[start:end]
89
+
90
+ # Garantir que o segmento tenha tamanho mínimo (1 segundo)
91
+ if len(segment) >= sr:
92
+ pred_label, confidence, class_id = predict_segment(
93
+ model, feature_extractor, device, segment, sr
94
+ )
95
+ if pred_label is not None:
96
+ predictions_list.append(class_id)
97
+ confidences_list.append(confidence)
98
+ segments_processed += 1
99
+
100
+ # Avançar janela
101
+ start += window_size - overlap
102
+
103
+ # Se chegou no final, parar
104
+ if end == len(audio):
105
+ break
106
+
107
+ if not predictions_list:
108
+ return None, None, None, 0
109
+
110
+ # Combinar predições usando voto majoritário ponderado pela confiança
111
+ predicted_label, final_confidence, predicted_class_id = combine_predictions(
112
+ predictions_list, confidences_list
113
+ )
114
+
115
+ return predicted_label, final_confidence, predicted_class_id, segments_processed
116
+
117
+ except Exception as e:
118
+ print(f"Erro ao processar {audio_path}: {e}")
119
+ return None, None, None, 0
120
+
121
+ def predict_segment(model, feature_extractor, device, audio_segment, sr):
122
+ """
123
+ Classifica um segmento individual de áudio.
124
+ """
125
+ try:
126
+ # Pré-processar segmento
127
+ inputs = feature_extractor(
128
+ audio_segment,
129
+ sampling_rate=sr,
130
+ max_length=int(sr * 5.0),
131
+ truncation=True,
132
+ padding=True,
133
+ return_tensors="pt"
134
+ )
135
+
136
+ # Mover para device
137
+ inputs = {k: v.to(device) for k, v in inputs.items()}
138
+
139
+ # Fazer predição
140
+ with torch.no_grad():
141
+ outputs = model(**inputs)
142
+ predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
143
+ predicted_class_id = predictions.argmax().item()
144
+ confidence = predictions.max().item()
145
+
146
+ # Mapear para label
147
+ label_map = {0: "pt_br", 1: "pt_pt"}
148
+ predicted_label = label_map.get(predicted_class_id, "unknown")
149
+
150
+ return predicted_label, confidence, predicted_class_id
151
+
152
+ except Exception as e:
153
+ return None, None, None
154
+
155
+ def combine_predictions(predictions_list, confidences_list):
156
+ """
157
+ Combina múltiplas predições usando voto majoritário ponderado pela confiança.
158
+ """
159
+ # Converter para arrays numpy
160
+ predictions = np.array(predictions_list)
161
+ confidences = np.array(confidences_list)
162
+
163
+ # Calcular pontuações ponderadas para cada classe
164
+ class_scores = {}
165
+ for class_id in [0, 1]: # pt_br=0, pt_pt=1
166
+ mask = predictions == class_id
167
+ if np.any(mask):
168
+ # Somar confiança de todas as predições desta classe
169
+ class_scores[class_id] = np.sum(confidences[mask])
170
+ else:
171
+ class_scores[class_id] = 0.0
172
+
173
+ # Classe com maior pontuação
174
+ predicted_class_id = max(class_scores.keys(), key=lambda k: class_scores[k])
175
+
176
+ # Confiança final: média das confiânças da classe vencedora
177
+ winner_mask = predictions == predicted_class_id
178
+ if np.any(winner_mask):
179
+ final_confidence = np.mean(confidences[winner_mask])
180
+ else:
181
+ final_confidence = 0.0
182
+
183
+ # Mapear para label
184
+ label_map = {0: "pt_br", 1: "pt_pt"}
185
+ predicted_label = label_map.get(predicted_class_id, "unknown")
186
+
187
+ return predicted_label, final_confidence, predicted_class_id
188
+
189
+ def test_folder(model_path, audio_folder, output_file=None, supported_formats=None):
190
+ """
191
+ Testa todos os áudios em uma pasta usando janela deslizante.
192
+ """
193
+ if supported_formats is None:
194
+ supported_formats = ['.wav', '.mp3', '.flac', '.m4a', '.ogg']
195
+
196
+ print(f"Testando áudios na pasta: {audio_folder}")
197
+ print("Usando janela deslizante de 5s com sobreposição de 2.5s para áudios longos")
198
+
199
+ # Carregar modelo
200
+ model, feature_extractor, device = load_model(model_path)
201
+ if model is None:
202
+ return
203
+
204
+ # Encontrar todos os arquivos de áudio
205
+ audio_files = []
206
+ for ext in supported_formats:
207
+ pattern = os.path.join(audio_folder, f"**/*{ext}")
208
+ audio_files.extend(glob.glob(pattern, recursive=True))
209
+
210
+ if not audio_files:
211
+ print(f"Nenhum arquivo de áudio encontrado na pasta {audio_folder}")
212
+ print(f"Formatos suportados: {supported_formats}")
213
+ return
214
+
215
+ print(f"Encontrados {len(audio_files)} arquivos de áudio")
216
+
217
+ # Resultados
218
+ results = []
219
+ total_segments = 0
220
+
221
+ # Processar cada arquivo
222
+ for i, audio_file in enumerate(audio_files, 1):
223
+ perc = (i / len(audio_files)) * 100
224
+ print(f"Processando {i}/{len(audio_files)} ({perc:.2f}%): {os.path.basename(audio_file)}")
225
+
226
+ # Classificar áudio com janela deslizante
227
+ predicted_label, confidence, class_id, segments_used = predict_audio(
228
+ model, feature_extractor, device, audio_file
229
+ )
230
+
231
+ if predicted_label is not None:
232
+ # Calcular duração do áudio
233
+ try:
234
+ audio_duration = librosa.get_duration(filename=audio_file)
235
+ except:
236
+ audio_duration = None
237
+
238
+ result = {
239
+ 'arquivo': os.path.basename(audio_file),
240
+ 'caminho_completo': audio_file,
241
+ 'predição': predicted_label,
242
+ 'confiança': confidence,
243
+ 'classe_id': class_id,
244
+ 'duração_segundos': audio_duration,
245
+ 'segmentos_analisados': segments_used
246
+ }
247
+ results.append(result)
248
+ total_segments += segments_used
249
+
250
+ # Mostrar resultado
251
+ if segments_used > 1:
252
+ print(f" -> {predicted_label} (confiança: {confidence:.3f}) [{segments_used} segmentos]")
253
+ else:
254
+ print(f" -> {predicted_label} (confiança: {confidence:.3f})")
255
+ else:
256
+ print(f" -> Erro ao processar arquivo")
257
+
258
+ # Criar DataFrame com resultados
259
+ if results:
260
+ df = pd.DataFrame(results)
261
+
262
+ # Estatísticas
263
+ print(f"\n=== Resumo dos Resultados ===")
264
+ print(f"Total de arquivos processados: {len(results)}")
265
+ print(f"Arquivos com erro: {len(audio_files) - len(results)}")
266
+ print(f"Total de segmentos analisados: {total_segments}")
267
+
268
+ # Estatísticas de segmentos
269
+ if 'segmentos_analisados' in df.columns:
270
+ avg_segments = df['segmentos_analisados'].mean()
271
+ max_segments = df['segmentos_analisados'].max()
272
+ multi_segment_files = len(df[df['segmentos_analisados'] > 1])
273
+ print(f"Segmentos por arquivo (média): {avg_segments:.1f}")
274
+ print(f"Máximo de segmentos: {max_segments}")
275
+ print(f"Arquivos com múltiplos segmentos: {multi_segment_files}")
276
+
277
+ # Estatísticas de duração
278
+ if 'duração_segundos' in df.columns and df['duração_segundos'].notna().any():
279
+ avg_duration = df['duração_segundos'].mean()
280
+ max_duration = df['duração_segundos'].max()
281
+ print(f"Duração média dos áudios: {avg_duration:.1f}s")
282
+ print(f"Duração máxima: {max_duration:.1f}s")
283
+
284
+ # Distribuição por classe
285
+ print(f"\nDistribuição das predições:")
286
+ distribution = Counter(df['predição'])
287
+ for label, count in distribution.items():
288
+ percentage = (count / len(results)) * 100
289
+ print(f" {label}: {count} arquivos ({percentage:.1f}%)")
290
+
291
+ # Confiança média
292
+ avg_confidence = df['confiança'].mean()
293
+ print(f"\nConfiança média: {avg_confidence:.3f}")
294
+
295
+ # Arquivos com baixa confiança
296
+ low_confidence = df[df['confiança'] < 0.7]
297
+ if not low_confidence.empty:
298
+ print(f"\nArquivos com baixa confiança (< 0.7): {len(low_confidence)}")
299
+ for _, row in low_confidence.iterrows():
300
+ segments_info = f" [{row.get('segmentos_analisados', 1)} seg]" if row.get('segmentos_analisados', 1) > 1 else ""
301
+ print(f" {row['arquivo']}: {row['predição']} ({row['confiança']:.3f}){segments_info}")
302
+
303
+ # Salvar resultados se especificado
304
+ if output_file:
305
+ df.to_csv(output_file, index=False)
306
+ print(f"\nResultados salvos em: {output_file}")
307
+
308
+ # Salvar também um arquivo de métricas se houver labels conhecidos para matrix de confusão
309
+ if results is not None and len([r for r in results if 'pt_br' in r.get('caminho_completo', '') or 'pt_pt' in r.get('caminho_completo', '')]) > 0:
310
+ metrics_file = output_file.replace('.csv', '_metrics.txt')
311
+ with open(metrics_file, 'w', encoding='utf-8') as f:
312
+ f.write("=== MÉTRICAS DE CLASSIFICAÇÃO ===\n\n")
313
+ f.write(f"Total de arquivos processados: {len(results)}\n")
314
+ f.write(f"Confiança média: {avg_confidence:.3f}\n")
315
+ f.write(f"Total de segmentos analisados: {total_segments}\n\n")
316
+ f.write("Distribuição das predições:\n")
317
+ for label, count in distribution.items():
318
+ percentage = (count / len(results)) * 100
319
+ f.write(f" {label}: {count} arquivos ({percentage:.1f}%)\n")
320
+ print(f"Métricas básicas salvas em: {metrics_file}")
321
+
322
+ return df
323
+ else:
324
+ print("Nenhum arquivo foi processado com sucesso.")
325
+ return None
326
+
327
+ def main():
328
+ parser = argparse.ArgumentParser(
329
+ description="Testa classificação de sotaques em uma pasta de áudios"
330
+ )
331
+ parser.add_argument(
332
+ "audio_folder",
333
+ help="Pasta contendo os arquivos de áudio para teste"
334
+ )
335
+ parser.add_argument(
336
+ "--model_path",
337
+ default="./nn/results/final_model",
338
+ help="Caminho para o modelo treinado (default: ./nn/results/final_model)"
339
+ )
340
+ parser.add_argument(
341
+ "--output",
342
+ help="Arquivo CSV para salvar os resultados (opcional)"
343
+ )
344
+ parser.add_argument(
345
+ "--formats",
346
+ nargs="+",
347
+ default=['.wav', '.mp3', '.flac', '.m4a', '.ogg'],
348
+ help="Formatos de áudio suportados (default: .wav .mp3 .flac .m4a .ogg)"
349
+ )
350
+
351
+ args = parser.parse_args()
352
+
353
+ # Verificar se a pasta existe
354
+ if not os.path.exists(args.audio_folder):
355
+ print(f"Erro: Pasta '{args.audio_folder}' não encontrada!")
356
+ sys.exit(1)
357
+
358
+ # Verificar se o modelo existe
359
+ if not os.path.exists(args.model_path):
360
+ print(f"Erro: Modelo '{args.model_path}' não encontrado!")
361
+ sys.exit(1)
362
+
363
+ # Executar teste
364
+ results = test_folder(
365
+ model_path=args.model_path,
366
+ audio_folder=args.audio_folder,
367
+ output_file=args.output,
368
+ supported_formats=args.formats
369
+ )
370
+
371
+ # Plotar matriz de confusão se há resultados
372
+ if results is not None and not results.empty:
373
+ import matplotlib.pyplot as plt
374
+
375
+ # Para matriz de confusão, precisamos de labels verdadeiros
376
+ # Vamos inferir do caminho completo do arquivo (não apenas do nome)
377
+ true_labels = []
378
+ pred_labels = results['predição'].tolist()
379
+
380
+ for idx, row in results.iterrows():
381
+ arquivo = row['arquivo']
382
+ caminho_completo = row['caminho_completo']
383
+
384
+ # Tentar inferir label do caminho completo (mais confiável)
385
+ if '/pt_br/' in caminho_completo or caminho_completo.endswith('/pt_br') or '\\pt_br\\' in caminho_completo or caminho_completo.endswith('\\pt_br'):
386
+ true_labels.append('pt_br')
387
+ elif '/pt_pt/' in caminho_completo or caminho_completo.endswith('/pt_pt') or '\\pt_pt\\' in caminho_completo or caminho_completo.endswith('\\pt_pt'):
388
+ true_labels.append('pt_pt')
389
+ # Fallback: tentar inferir do nome do arquivo
390
+ elif 'pt_br' in arquivo.lower() or 'brasil' in arquivo.lower():
391
+ true_labels.append('pt_br')
392
+ elif 'pt_pt' in arquivo.lower() or 'portugal' in arquivo.lower():
393
+ true_labels.append('pt_pt')
394
+ else:
395
+ # Se não conseguir inferir, marcar como desconhecido para não enviesar a matriz
396
+ true_labels.append('unknown')
397
+
398
+ # Criar matriz de confusão apenas para arquivos com labels conhecidos
399
+ known_mask = [label != 'unknown' for label in true_labels]
400
+ known_true = [true_labels[i] for i in range(len(true_labels)) if known_mask[i]]
401
+ known_pred = [pred_labels[i] for i in range(len(pred_labels)) if known_mask[i]]
402
+
403
+ if len(known_true) > 0:
404
+ labels = ['pt_br', 'pt_pt']
405
+ cm = confusion_matrix(known_true, known_pred, labels=labels)
406
+
407
+ # Mostrar estatísticas
408
+ unknown_count = len(true_labels) - len(known_true)
409
+ accuracy = accuracy_score(known_true, known_pred)
410
+ f1_macro = f1_score(known_true, known_pred, labels=labels, average='macro')
411
+ f1_weighted = f1_score(known_true, known_pred, labels=labels, average='weighted')
412
+ f1_pt_br = f1_score(known_true, known_pred, labels=labels, pos_label='pt_br', average='binary') if 'pt_br' in labels else 0
413
+ f1_pt_pt = f1_score(known_true, known_pred, labels=labels, pos_label='pt_pt', average='binary') if 'pt_pt' in labels else 0
414
+
415
+ print(f"\nEstatísticas da Matriz de Confusão:")
416
+ print(f"Arquivos com labels conhecidos: {len(known_true)}")
417
+ print(f"Arquivos com labels desconhecidos: {unknown_count}")
418
+ print(f"Acurácia: {accuracy:.3f} ({accuracy*100:.1f}%)")
419
+ print(f"F1-Score Macro: {f1_macro:.3f} ({f1_macro*100:.1f}%)")
420
+ print(f"F1-Score Ponderado: {f1_weighted:.3f} ({f1_weighted*100:.1f}%)")
421
+ print(f"F1-Score PT-BR: {f1_pt_br:.3f} ({f1_pt_br*100:.1f}%)")
422
+ print(f"F1-Score PT-PT: {f1_pt_pt:.3f} ({f1_pt_pt*100:.1f}%)")
423
+
424
+ # Relatório de classificação detalhado
425
+ print(f"\nRelatório de Classificação:")
426
+ print(classification_report(known_true, known_pred, labels=labels, zero_division=0))
427
+
428
+ if unknown_count > 0:
429
+ print(f"\nArquivos ignorados (sem label inferível):")
430
+ for i, (true_label, arquivo) in enumerate(zip(true_labels, results['arquivo'])):
431
+ if true_label == 'unknown':
432
+ print(f" {arquivo}")
433
+
434
+ # Mostrar erros de classificação
435
+ errors = []
436
+ for i, (true_label, pred_label, arquivo) in enumerate(zip(known_true, known_pred,
437
+ [results.iloc[j]['arquivo'] for j in range(len(results)) if known_mask[j]])):
438
+ if true_label != pred_label:
439
+ confidence = [results.iloc[j]['confiança'] for j in range(len(results)) if known_mask[j]][i]
440
+ errors.append({
441
+ 'arquivo': arquivo,
442
+ 'verdadeiro': true_label,
443
+ 'predito': pred_label,
444
+ 'confianca': confidence
445
+ })
446
+
447
+ if errors:
448
+ print(f"\nErros de Classificação ({len(errors)} arquivos):")
449
+ for error in errors:
450
+ print(f" {error['arquivo']}: {error['verdadeiro']} → {error['predito']} (conf: {error['confianca']:.3f})")
451
+ else:
452
+ print(f"\n✓ Nenhum erro de classificação encontrado!")
453
+
454
+ # Plotar
455
+ plt.figure(figsize=(8, 6))
456
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
457
+ xticklabels=labels, yticklabels=labels)
458
+ plt.title(f'Matriz de Confusão - Classificação de Sotaques\n({len(known_true)} arquivos, Acc: {accuracy:.1%}, F1-Macro: {f1_macro:.1%})')
459
+ plt.xlabel('Predição')
460
+ plt.ylabel('Verdadeiro')
461
+ plt.tight_layout()
462
+
463
+ # Salvar figura
464
+ confusion_matrix_path = args.output.replace('.csv', '_confusion_matrix.png') if args.output else 'confusion_matrix.png'
465
+ plt.savefig(confusion_matrix_path, dpi=300, bbox_inches='tight')
466
+ print(f"\nMatriz de confusão salva em: {confusion_matrix_path}")
467
+
468
+ # Salvar métricas detalhadas em arquivo
469
+ if args.output:
470
+ detailed_metrics_file = args.output.replace('.csv', '_detailed_metrics.txt')
471
+ with open(detailed_metrics_file, 'w', encoding='utf-8') as f:
472
+ f.write("=== MÉTRICAS DETALHADAS DE CLASSIFICAÇÃO ===\n\n")
473
+ f.write(f"Total de arquivos processados: {len(results)}\n")
474
+ f.write(f"Arquivos com labels conhecidos: {len(known_true)}\n")
475
+ f.write(f"Arquivos com labels desconhecidos: {unknown_count}\n")
476
+ f.write(f"Total de segmentos analisados: {total_segments}\n\n")
477
+
478
+ f.write("=== MÉTRICAS DE PERFORMANCE ===\n")
479
+ f.write(f"Acurácia: {accuracy:.3f} ({accuracy*100:.1f}%)\n")
480
+ f.write(f"F1-Score Macro: {f1_macro:.3f} ({f1_macro*100:.1f}%)\n")
481
+ f.write(f"F1-Score Ponderado: {f1_weighted:.3f} ({f1_weighted*100:.1f}%)\n")
482
+ f.write(f"F1-Score PT-BR: {f1_pt_br:.3f} ({f1_pt_br*100:.1f}%)\n")
483
+ f.write(f"F1-Score PT-PT: {f1_pt_pt:.3f} ({f1_pt_pt*100:.1f}%)\n\n")
484
+
485
+ f.write("=== RELATÓRIO DE CLASSIFICAÇÃO ===\n")
486
+ f.write(classification_report(known_true, known_pred, labels=labels, zero_division=0))
487
+ f.write("\n\n=== MATRIZ DE CONFUSÃO ===\n")
488
+ f.write(f" Predito\n")
489
+ f.write(f" pt_br pt_pt\n")
490
+ f.write(f"Real\n")
491
+ f.write(f"pt_br {cm[0][0]:4d} {cm[0][1]:4d}\n")
492
+ f.write(f"pt_pt {cm[1][0]:4d} {cm[1][1]:4d}\n\n")
493
+
494
+ if errors:
495
+ f.write(f"=== ERROS DE CLASSIFICAÇÃO ({len(errors)} arquivos) ===\n")
496
+ for error in errors:
497
+ f.write(f"{error['arquivo']}: {error['verdadeiro']} → {error['predito']} (conf: {error['confianca']:.3f})\n")
498
+ else:
499
+ f.write("=== ERROS DE CLASSIFICAÇÃO ===\n")
500
+ f.write("Nenhum erro de classificação encontrado!\n")
501
+
502
+ print(f"Métricas detalhadas salvas em: {detailed_metrics_file}")
503
+
504
+ plt.show()
505
+ else:
506
+ print(f"\n⚠️ Não foi possível criar matriz de confusão:")
507
+ print(f"Nenhum arquivo tinha label inferível do caminho ou nome.")
508
+ print(f"Para usar a matriz de confusão, organize os arquivos em pastas 'pt_br' e 'pt_pt'")
509
+ print(f"ou garanta que os nomes dos arquivos contenham essas strings.")
510
+
511
+ if __name__ == "__main__":
512
+ # Exemplos de uso no código para referência
513
+ if len(sys.argv) == 1:
514
+ print("=== Script de Teste de Classificação de Sotaques ===")
515
+ print()
516
+ print("Este script testa um modelo treinado em uma pasta de áudios usando janela deslizante.")
517
+ print()
518
+ print("Uso:")
519
+ print(" python test_audio_folder.py <pasta_de_audios>")
520
+ print()
521
+ print("Exemplos:")
522
+ print(" python test_audio_folder.py ./audios_teste")
523
+ print(" python test_audio_folder.py ./audios_teste --output resultados.csv")
524
+ print(" python test_audio_folder.py ./audios_teste --model_path ./results/checkpoint-20000")
525
+ print()
526
+ print("Parâmetros:")
527
+ print(" pasta_de_audios : Pasta com arquivos de áudio para classificar")
528
+ print(" --model_path : Caminho do modelo treinado (default: ./results/final_model)")
529
+ print(" --output : Arquivo CSV para salvar resultados (opcional)")
530
+ print(" --formats : Formatos suportados (default: .wav .mp3 .flac .m4a .ogg)")
531
+ print()
532
+ print("Funcionalidades da Janela Deslizante:")
533
+ print("- Janelas de 5 segundos com sobreposição de 2.5s")
534
+ print("- Áudios curtos: classificação direta")
535
+ print("- Áudios longos: múltiplos segmentos combinados")
536
+ print("- Resultado final: voto majoritário ponderado por confiança")
537
+ print()
538
+ print("O script irá:")
539
+ print("1. Carregar o modelo treinado")
540
+ print("2. Encontrar todos os arquivos de áudio na pasta")
541
+ print("3. Classificar cada áudio usando janela deslizante")
542
+ print("4. Mostrar estatísticas detalhadas dos resultados")
543
+ print("5. Salvar resultados em CSV (se especificado)")
544
+ print("6. Gerar matriz de confusão (se possível inferir labels)")
545
+ sys.exit(0)
546
+
547
+ main()
train.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import glob
4
+ from datasets import load_dataset, Audio, Dataset, ClassLabel
5
+ from transformers import (
6
+ AutoFeatureExtractor,
7
+ AutoModelForAudioClassification,
8
+ TrainingArguments,
9
+ Trainer
10
+ )
11
+ import numpy as np
12
+ import evaluate
13
+ import random
14
+
15
+ # --- Configurações ---
16
+ # Modelo base do Hugging Face. XLSR-53 é uma ótima escolha multilíngue.
17
+ MODEL_NAME = "lgris/w2v_podcasts_base_400k_pt"
18
+ # Pasta onde os dados pré-processados foram salvos
19
+ DATASET_PATH = "./dataset_preparado/"
20
+ # Pasta para salvar o modelo treinado e os logs
21
+ OUTPUT_DIR = "./portuguese-accent-classifier"
22
+ # Hiperparâmetros de treinamento
23
+ NUM_TRAIN_EPOCHS = 50
24
+ BATCH_SIZE = 32 # Reduza se tiver problemas de memória na GPU
25
+
26
+ def train_model():
27
+ """
28
+ Carrega o dataset, pré-processa os dados e faz o fine-tuning do modelo
29
+ para classificação de sotaques.
30
+ """
31
+ print("Carregando dataset a partir das pastas...")
32
+
33
+ # Carregar todos os arquivos manualmente para contornar limitação do audiofolder
34
+ pt_br_files = glob.glob(os.path.join(DATASET_PATH, "pt_br", "*.wav"))
35
+ pt_pt_files = glob.glob(os.path.join(DATASET_PATH, "pt_pt", "*.wav"))
36
+
37
+ print(f"Arquivos pt_br encontrados: {len(pt_br_files)}")
38
+ print(f"Arquivos pt_pt encontrados: {len(pt_pt_files)}")
39
+
40
+ # Criar listas de arquivos e labels
41
+ all_files = []
42
+ all_labels = []
43
+
44
+ # Adicionar arquivos pt_br com label 0
45
+ for file_path in pt_br_files:
46
+ all_files.append(file_path)
47
+ all_labels.append(0)
48
+
49
+ # Adicionar arquivos pt_pt com label 1
50
+ for file_path in pt_pt_files:
51
+ all_files.append(file_path)
52
+ all_labels.append(1)
53
+
54
+ print(f"Total de arquivos carregados: {len(all_files)}")
55
+
56
+ # Criar dataset customizado
57
+ data_dict = {
58
+ "audio": all_files,
59
+ "label": all_labels
60
+ }
61
+
62
+ dataset = Dataset.from_dict(data_dict)
63
+ dataset = dataset.cast_column("audio", Audio())
64
+
65
+ # Configurar labels como ClassLabel para permitir estratificação
66
+ labels = ["pt_br", "pt_pt"]
67
+ dataset = dataset.cast_column("label", ClassLabel(names=labels))
68
+
69
+ print(f"Dataset criado com {len(dataset)} exemplos")
70
+
71
+ # Dividir em treino e teste
72
+ dataset = dataset.train_test_split(test_size=0.1, shuffle=True, stratify_by_column="label")
73
+
74
+ print("Dataset carregado e dividido:")
75
+ print(dataset)
76
+
77
+ # Criar mapeamento de labels
78
+ label2id, id2label = {}, {}
79
+ for i, label in enumerate(labels):
80
+ label2id[label] = str(i)
81
+ id2label[str(i)] = label
82
+
83
+ print(f"Mapeamento de labels: {id2label}")
84
+
85
+ # Carregar o Feature Extractor
86
+ feature_extractor = AutoFeatureExtractor.from_pretrained(MODEL_NAME)
87
+ target_sampling_rate = feature_extractor.sampling_rate
88
+ target_length = int(target_sampling_rate * 5.0) # 5 segundos em samples
89
+
90
+ def preprocess_function(examples):
91
+ # O dataset já foi resampleado, mas garantimos aqui
92
+ # A função Audio() do 'datasets' carrega o áudio
93
+ audio_arrays = [x["array"] for x in examples["audio"]]
94
+
95
+ # Processar cada áudio individualmente para truncamento/padding aleatório
96
+ processed_audios = []
97
+ for audio_array in audio_arrays:
98
+ audio_length = len(audio_array)
99
+
100
+ if audio_length > target_length:
101
+ # Áudio maior que 5s: truncar aleatoriamente
102
+ start_idx = random.randint(0, audio_length - target_length)
103
+ processed_audio = audio_array[start_idx:start_idx + target_length]
104
+ else:
105
+ # Áudio menor que 5s: adicionar silêncio aleatoriamente
106
+ padding_needed = target_length - audio_length
107
+
108
+ # Distribuir o padding aleatoriamente entre início e fim
109
+ left_padding = random.randint(0, padding_needed)
110
+ right_padding = padding_needed - left_padding
111
+
112
+ # Criar arrays de silêncio (zeros)
113
+ left_silence = np.zeros(left_padding, dtype=audio_array.dtype)
114
+ right_silence = np.zeros(right_padding, dtype=audio_array.dtype)
115
+
116
+ # Concatenar: silêncio_esquerdo + áudio + silêncio_direito
117
+ processed_audio = np.concatenate([left_silence, audio_array, right_silence])
118
+
119
+ processed_audios.append(processed_audio)
120
+
121
+ # Usar o feature_extractor com os áudios já processados
122
+ inputs = feature_extractor(
123
+ processed_audios,
124
+ sampling_rate=target_sampling_rate,
125
+ padding=False, # Não precisamos de padding adicional
126
+ truncation=False # Não precisamos de truncamento adicional
127
+ )
128
+ return inputs
129
+
130
+ # Aplicar o pré-processamento ao dataset
131
+ print("Aplicando pré-processamento...")
132
+ encoded_dataset = dataset.map(preprocess_function, remove_columns="audio", batched=True)
133
+
134
+ # Carregar o Modelo
135
+ num_labels = len(labels)
136
+ model = AutoModelForAudioClassification.from_pretrained(
137
+ MODEL_NAME,
138
+ num_labels=num_labels,
139
+ label2id={k: int(v) for k, v in label2id.items()}, # Convertendo keys de str para int
140
+ id2label=id2label,
141
+ ignore_mismatched_sizes=True # Permite substituir a 'cabeça' de classificação
142
+ )
143
+
144
+ # Métrica de avaliação
145
+ accuracy = evaluate.load("accuracy")
146
+ def compute_metrics(eval_pred):
147
+ predictions = np.argmax(eval_pred.predictions, axis=1)
148
+ return accuracy.compute(predictions=predictions, references=eval_pred.label_ids)
149
+
150
+ # Argumentos de Treinamento
151
+ training_args = TrainingArguments(
152
+ output_dir=OUTPUT_DIR,
153
+ eval_strategy="epoch", # Adicionado para compatibilidade com load_best_model_at_end
154
+ save_strategy="epoch",
155
+ learning_rate=3e-5,
156
+ per_device_train_batch_size=BATCH_SIZE,
157
+ per_device_eval_batch_size=BATCH_SIZE,
158
+ num_train_epochs=NUM_TRAIN_EPOCHS,
159
+ weight_decay=0.01,
160
+ logging_steps=10,
161
+ load_best_model_at_end=True,
162
+ metric_for_best_model="accuracy",
163
+ report_to="tensorboard",
164
+ push_to_hub=False,
165
+ )
166
+
167
+ # Inicializar o Trainer
168
+ trainer = Trainer(
169
+ model=model,
170
+ args=training_args,
171
+ train_dataset=encoded_dataset["train"],
172
+ eval_dataset=encoded_dataset["test"],
173
+ tokenizer=feature_extractor,
174
+ compute_metrics=compute_metrics,
175
+ )
176
+
177
+ # Iniciar o Treinamento
178
+ print("\n--- Iniciando o Fine-tuning ---")
179
+ trainer.train()
180
+ print("--- Treinamento Concluído ---\n")
181
+
182
+ # Salvar o modelo final
183
+ final_model_path = os.path.join(OUTPUT_DIR, "final_model")
184
+ trainer.save_model(final_model_path)
185
+ print(f"Modelo final salvo em: {final_model_path}")
186
+
187
+
188
+ if __name__ == '__main__':
189
+ # Garante que o treinamento use GPU se disponível
190
+ if not torch.cuda.is_available():
191
+ print("Aviso: Nenhuma GPU encontrada. O treinamento será muito lento.")
192
+ train_model()
training_args.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ad21406f7ae2dc27d3daf024fa3f33654987214d214e74d138e8c8a60b3182b
3
+ size 5777