File size: 10,402 Bytes
3ead8a3
0f41475
3ead8a3
 
 
 
 
 
 
a870ca2
0f41475
a870ca2
0f41475
a870ca2
9022aca
0f41475
3ead8a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a870ca2
3ead8a3
 
a870ca2
3ead8a3
 
 
 
a870ca2
3ead8a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f41475
3ead8a3
a870ca2
0f41475
 
a870ca2
3ead8a3
9022aca
a870ca2
 
0f41475
e46d13d
 
 
 
0f41475
 
 
a870ca2
0f41475
a870ca2
0f41475
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a870ca2
0f41475
9022aca
 
 
 
0f41475
9022aca
 
3ead8a3
9022aca
3ead8a3
0f41475
9022aca
 
 
3ead8a3
 
a870ca2
3ead8a3
 
a870ca2
3ead8a3
 
a870ca2
3ead8a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f41475
c6a1de6
 
 
0f41475
3ead8a3
 
 
 
0f41475
3ead8a3
 
 
c6a1de6
 
0f41475
3ead8a3
 
 
 
0f41475
3ead8a3
0f41475
3ead8a3
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# app.py
# Phiên bản cuối cùng: Thêm Voice Activity Detection (VAD) để lọc bỏ khoảng lặng.

import os
import joblib
import numpy as np
import librosa
from flask import Flask, request, jsonify, render_template
from werkzeug.utils import secure_filename
import traceback
import collections

# --- Thư viện mới để đọc audio, giảm nhiễu và VAD ---
from pydub import AudioSegment
import noisereduce as nr
import webrtcvad

# --- Cấu hình TensorFlow và các thư viện AI ---
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' 
import tensorflow as tf
from transformers import Wav2Vec2Processor, Wav2Vec2Model
import torch

# --- KHỞI TẠO ỨNG DỤNG FLASK ---
app = Flask(__name__)
UPLOAD_FOLDER = 'uploads/'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)


# --- TẢI TẤT CẢ CÁC MÔ HÌNH KHI SERVER KHỞI ĐỘNG ---
print(">>> Đang tải các mô hình AI, quá trình này có thể mất một lúc...")
try:
    MODEL_PATH = 'models/'
    scaler = joblib.load(os.path.join(MODEL_PATH, 'scaler.pkl'))
    label_encoder = joblib.load(os.path.join(MODEL_PATH, 'label_encoder.pkl'))
    model_xgb = joblib.load(os.path.join(MODEL_PATH, 'xgboost.pkl'))
    model_lgb = joblib.load(os.path.join(MODEL_PATH, 'lightgbm.pkl'))
    model_cnn = tf.keras.models.load_model(os.path.join(MODEL_PATH, 'cnn.keras'))
    wav2vec_processor = Wav2Vec2Processor.from_pretrained("facebook/wav2vec2-base")
    wav2vec_model = Wav2Vec2Model.from_pretrained("facebook/wav2vec2-base")
    print(">>> OK! Tất cả các mô hình đã được tải thành công!")
except Exception as e:
    print(f"!!! LỖI NGHIÊM TRỌNG: Không thể tải một hoặc nhiều mô hình. Lỗi: {e}")
    traceback.print_exc()
    exit()

# --- CÁC HÀM TRÍCH XUẤT ĐẶC TRƯNG (KHÔNG ĐỔI) ---
SAMPLE_RATE = 22050
MAX_LENGTH_SECONDS = 5.0
MAX_SAMPLES = int(SAMPLE_RATE * MAX_LENGTH_SECONDS)
N_MELS = 128
TRADITIONAL_FEATURE_SIZE = 570
WAV2VEC_FEATURE_SIZE = 768
SPECTROGRAM_SHAPE = (224, 224, 3)

def _extract_traditional_features(y, sr):
    try:
        mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=N_MELS)
        mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
        features = np.mean(mel_spec_db, axis=1)
        features = np.append(features, np.std(mel_spec_db, axis=1))
        features = np.append(features, np.max(mel_spec_db, axis=1))
        features = np.append(features, np.min(mel_spec_db, axis=1))
        mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
        features = np.append(features, np.mean(mfccs, axis=1))
        features = np.append(features, np.std(mfccs, axis=1))
        if len(features) > TRADITIONAL_FEATURE_SIZE:
            features = features[:TRADITIONAL_FEATURE_SIZE]
        elif len(features) < TRADITIONAL_FEATURE_SIZE:
            features = np.pad(features, (0, TRADITIONAL_FEATURE_SIZE - len(features)), mode='constant')
        return features
    except Exception as e:
        print(f"Lỗi trích xuất đặc trưng truyền thống: {e}")
        return np.zeros(TRADITIONAL_FEATURE_SIZE)

def _extract_wav2vec_features(y, sr):
    try:
        y_16k = librosa.resample(y, orig_sr=sr, target_sr=16000)
        inputs = wav2vec_processor(y_16k, sampling_rate=16000, return_tensors="pt", padding=True)
        with torch.no_grad():
            outputs = wav2vec_model(**inputs)
        features = outputs.last_hidden_state.mean(dim=1).squeeze().cpu().numpy()
        return features
    except Exception as e:
        print(f"Lỗi trích xuất Wav2Vec2: {e}")
        return np.zeros(WAV2VEC_FEATURE_SIZE)

def _create_spectrogram_image(y, sr):
    try:
        mel_spec = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=SPECTROGRAM_SHAPE[0])
        mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
        mel_spec_norm = ((mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8) * 255).astype(np.uint8)
        img = tf.keras.preprocessing.image.array_to_img(np.stack([mel_spec_norm]*3, axis=-1))
        img = img.resize((SPECTROGRAM_SHAPE[1], SPECTROGRAM_SHAPE[0]))
        return np.array(img)
    except Exception as e:
        print(f"Lỗi tạo ảnh spectrogram: {e}")
        return np.zeros(SPECTROGRAM_SHAPE)

# --- HÀM XỬ LÝ AUDIO ĐÃ ĐƯỢC CẬP NHẬT VỚI VAD ---
def process_audio_file(file_path):
    """
    Hàm tổng hợp phiên bản cuối cùng: Thêm Voice Activity Detection (VAD) để
    lọc bỏ các khoảng lặng trước khi xử lý.
    """
    try:
        # 1. Dùng pydub để mở file audio
        audio = AudioSegment.from_file(file_path)

        # 2. Chuẩn hóa âm lượng
        target_dbfs = -20.0
        change_in_dbfs = target_dbfs - audio.dBFS
        audio = audio.apply_gain(change_in_dbfs)

        # 3. Đảm bảo audio là mono và có sample rate đúng cho VAD
        # VAD hoạt động tốt nhất ở các sample rate 8000, 16000, 32000, 48000
        vad_sample_rate = 32000
        audio = audio.set_channels(1)
        audio = audio.set_frame_rate(vad_sample_rate)

        # 4. **BƯỚC MỚI: VOICE ACTIVITY DETECTION**
        print("DEBUG | Bắt đầu Voice Activity Detection (VAD)...")
        vad = webrtcvad.Vad(3)  # Mức độ mạnh nhất (0-3)
        frame_duration_ms = 30  # 30ms mỗi frame
        frame_bytes = int(vad_sample_rate * frame_duration_ms / 1000) * audio.sample_width
        
        frames = [audio[i:i + frame_duration_ms] for i in range(0, len(audio), frame_duration_ms)]
        voiced_frames = [f for f in frames if vad.is_speech(f.raw_data, vad_sample_rate)]
        
        if voiced_frames:
            # Nối các frame có tiếng nói lại với nhau
            audio_voiced = sum(voiced_frames, AudioSegment.empty())
            print(f"DEBUG | VAD hoàn tất. Giữ lại {len(audio_voiced)}ms âm thanh.")
        else:
            # Nếu không tìm thấy tiếng nói, dùng lại audio gốc
            audio_voiced = audio
            print("DEBUG | VAD không tìm thấy âm thanh, sử dụng audio gốc.")
        
        # 5. Chuyển đổi audio đã lọc về sample rate mục tiêu
        audio_final = audio_voiced.set_frame_rate(SAMPLE_RATE)
        samples = np.array(audio_final.get_array_of_samples()).astype(np.float32)
        y = samples / (2**(audio_final.sample_width * 8 - 1))

        # 6. Giảm nhiễu trên tín hiệu đã được lọc
        print("DEBUG | Bắt đầu giảm nhiễu...")
        y_reduced_noise = nr.reduce_noise(y=y, sr=SAMPLE_RATE, prop_decrease=0.8)
        print("DEBUG | Giảm nhiễu hoàn tất.")

        # 7. Chuẩn hóa độ dài
        if len(y_reduced_noise) > MAX_SAMPLES:
            y_final = y_reduced_noise[:MAX_SAMPLES]
        else:
            y_final = np.pad(y_reduced_noise, (0, MAX_SAMPLES - len(y_reduced_noise)), mode='constant')

        # 8. Trích xuất đặc trưng
        traditional_features = _extract_traditional_features(y_final, SAMPLE_RATE)
        wav2vec_features = _extract_wav2vec_features(y_final, SAMPLE_RATE)
        spectrogram = _create_spectrogram_image(y_final, SAMPLE_RATE)

        return traditional_features, wav2vec_features, spectrogram

    except Exception as e:
        print(f"Lỗi nghiêm trọng khi xử lý file audio {file_path}: {e}")
        traceback.print_exc()
        return None, None, None

# --- ĐỊNH NGHĨA CÁC ROUTE CỦA ỨNG DỤNG ---
@app.route('/', methods=['GET'])
def home():
    return render_template('index.html')

@app.route('/predict', methods=['POST'])
def predict():
    if 'audio_file' not in request.files:
        return jsonify({'error': 'Không có file audio nào trong yêu cầu.'}), 400

    file = request.files['audio_file']
    if file.filename == '':
        return jsonify({'error': 'Tên file không hợp lệ.'}), 400

    try:
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        trad_feats, w2v_feats, spec_img = process_audio_file(filepath)
        if trad_feats is None:
             return jsonify({'error': 'Không thể xử lý file audio.'}), 500

        print("\n--- BẮT ĐẦU CHẨN ĐOÁN DỮ LIỆU ĐẦU VÀO (SAU KHI VAD & GIẢM NHIỄU) ---")
        print(f"DEBUG | trad_feats stats: mean={np.mean(trad_feats):.2f}, std={np.std(trad_feats):.2f}, min={np.min(trad_feats):.2f}, max={np.max(trad_feats):.2f}")
        print(f"DEBUG | w2v_feats stats:  mean={np.mean(w2v_feats):.2f}, std={np.std(w2v_feats):.2f}, min={np.min(w2v_feats):.2f}, max={np.max(w2v_feats):.2f}")
        print(f"DEBUG | spec_img stats:   mean={np.mean(spec_img):.2f}, std={np.std(spec_img):.2f}, min={np.min(spec_img):.2f}, max={np.max(spec_img):.2f}")
        
        combined_feats = np.concatenate([trad_feats, w2v_feats]).reshape(1, -1)
        scaled_feats = scaler.transform(combined_feats)
        spec_img = spec_img / 255.0
        spec_img = np.expand_dims(spec_img, axis=0)
        
        pred_xgb = model_xgb.predict_proba(scaled_feats)[0][1]
        pred_lgb = model_lgb.predict_proba(scaled_feats)[0][1]
        pred_cnn = model_cnn.predict(spec_img, verbose=0)[0][0]
        
        print(f"DEBUG | Individual probabilities (for Male): XGB={pred_xgb:.4f}, LGBM={pred_lgb:.4f}, CNN={pred_cnn:.4f}")
        
        final_prediction_prob = (pred_xgb + pred_lgb + pred_cnn) / 3
        final_prediction_label_index = 1 if final_prediction_prob > 0.5 else 0
        result_label_text = label_encoder.inverse_transform([final_prediction_label_index])[0]
        
        os.remove(filepath)
        print(f"Phân tích hoàn tất. Kết quả: {result_label_text.upper()} (Xác suất: {final_prediction_prob:.2f})")
        
        return jsonify({
            'prediction': result_label_text.capitalize(),
            'probability': f"{final_prediction_prob:.2f}"
        })
    except Exception as e:
        print(f"Đã xảy ra lỗi trong quá trình dự đoán: {e}")
        traceback.print_exc()
        return jsonify({'error': 'Đã xảy ra lỗi không xác định trên máy chủ.'}), 500

# --- ĐIỂM BẮT ĐẦU CHẠY ỨNG DỤNG ---
if __name__ == '__main__':
    port = int(os.environ.get("PORT", 7860))
    app.run(host='0.0.0.0', port=port)