Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
# app.py
|
2 |
-
# Phiên bản
|
3 |
|
4 |
import os
|
5 |
import joblib
|
@@ -8,10 +8,12 @@ import librosa
|
|
8 |
from flask import Flask, request, jsonify, render_template
|
9 |
from werkzeug.utils import secure_filename
|
10 |
import traceback
|
|
|
11 |
|
12 |
-
# --- Thư viện mới để đọc audio
|
13 |
from pydub import AudioSegment
|
14 |
import noisereduce as nr
|
|
|
15 |
|
16 |
# --- Cấu hình TensorFlow và các thư viện AI ---
|
17 |
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
|
@@ -21,8 +23,6 @@ import torch
|
|
21 |
|
22 |
# --- KHỞI TẠO ỨNG DỤNG FLASK ---
|
23 |
app = Flask(__name__)
|
24 |
-
|
25 |
-
# Cấu hình thư mục tạm để lưu file audio
|
26 |
UPLOAD_FOLDER = 'uploads/'
|
27 |
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
28 |
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
@@ -30,7 +30,6 @@ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
|
30 |
|
31 |
# --- TẢI TẤT CẢ CÁC MÔ HÌNH KHI SERVER KHỞI ĐỘNG ---
|
32 |
print(">>> Đang tải các mô hình AI, quá trình này có thể mất một lúc...")
|
33 |
-
|
34 |
try:
|
35 |
MODEL_PATH = 'models/'
|
36 |
scaler = joblib.load(os.path.join(MODEL_PATH, 'scaler.pkl'))
|
@@ -40,17 +39,13 @@ try:
|
|
40 |
model_cnn = tf.keras.models.load_model(os.path.join(MODEL_PATH, 'cnn.keras'))
|
41 |
wav2vec_processor = Wav2Vec2Processor.from_pretrained("facebook/wav2vec2-base")
|
42 |
wav2vec_model = Wav2Vec2Model.from_pretrained("facebook/wav2vec2-base")
|
43 |
-
|
44 |
print(">>> OK! Tất cả các mô hình đã được tải thành công!")
|
45 |
-
|
46 |
except Exception as e:
|
47 |
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}")
|
48 |
traceback.print_exc()
|
49 |
exit()
|
50 |
|
51 |
# --- CÁC HÀM TRÍCH XUẤT ĐẶC TRƯNG (KHÔNG ĐỔI) ---
|
52 |
-
|
53 |
-
# Các hằng số cấu hình
|
54 |
SAMPLE_RATE = 22050
|
55 |
MAX_LENGTH_SECONDS = 5.0
|
56 |
MAX_SAMPLES = int(SAMPLE_RATE * MAX_LENGTH_SECONDS)
|
@@ -67,16 +62,13 @@ def _extract_traditional_features(y, sr):
|
|
67 |
features = np.append(features, np.std(mel_spec_db, axis=1))
|
68 |
features = np.append(features, np.max(mel_spec_db, axis=1))
|
69 |
features = np.append(features, np.min(mel_spec_db, axis=1))
|
70 |
-
|
71 |
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
|
72 |
features = np.append(features, np.mean(mfccs, axis=1))
|
73 |
features = np.append(features, np.std(mfccs, axis=1))
|
74 |
-
|
75 |
if len(features) > TRADITIONAL_FEATURE_SIZE:
|
76 |
features = features[:TRADITIONAL_FEATURE_SIZE]
|
77 |
elif len(features) < TRADITIONAL_FEATURE_SIZE:
|
78 |
features = np.pad(features, (0, TRADITIONAL_FEATURE_SIZE - len(features)), mode='constant')
|
79 |
-
|
80 |
return features
|
81 |
except Exception as e:
|
82 |
print(f"Lỗi trích xuất đặc trưng truyền thống: {e}")
|
@@ -106,42 +98,62 @@ def _create_spectrogram_image(y, sr):
|
|
106 |
print(f"Lỗi tạo ảnh spectrogram: {e}")
|
107 |
return np.zeros(SPECTROGRAM_SHAPE)
|
108 |
|
109 |
-
# --- HÀM XỬ LÝ AUDIO ĐÃ ĐƯỢC CẬP NHẬT VỚI
|
110 |
def process_audio_file(file_path):
|
111 |
"""
|
112 |
-
Hàm tổng hợp phiên bản
|
113 |
-
|
114 |
"""
|
115 |
try:
|
116 |
# 1. Dùng pydub để mở file audio
|
117 |
audio = AudioSegment.from_file(file_path)
|
118 |
|
119 |
-
# 2. Chuẩn hóa âm lượng
|
120 |
target_dbfs = -20.0
|
121 |
change_in_dbfs = target_dbfs - audio.dBFS
|
122 |
audio = audio.apply_gain(change_in_dbfs)
|
123 |
|
124 |
-
# 3. Đảm bảo audio là mono
|
|
|
|
|
125 |
audio = audio.set_channels(1)
|
126 |
-
audio = audio.set_frame_rate(
|
127 |
|
128 |
-
# 4.
|
129 |
-
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
|
132 |
-
#
|
133 |
-
# Thực hiện giảm nhiễu trên tín hiệu âm thanh
|
134 |
print("DEBUG | Bắt đầu giảm nhiễu...")
|
135 |
y_reduced_noise = nr.reduce_noise(y=y, sr=SAMPLE_RATE, prop_decrease=0.8)
|
136 |
print("DEBUG | Giảm nhiễu hoàn tất.")
|
137 |
|
138 |
-
#
|
139 |
if len(y_reduced_noise) > MAX_SAMPLES:
|
140 |
y_final = y_reduced_noise[:MAX_SAMPLES]
|
141 |
else:
|
142 |
y_final = np.pad(y_reduced_noise, (0, MAX_SAMPLES - len(y_reduced_noise)), mode='constant')
|
143 |
|
144 |
-
#
|
145 |
traditional_features = _extract_traditional_features(y_final, SAMPLE_RATE)
|
146 |
wav2vec_features = _extract_wav2vec_features(y_final, SAMPLE_RATE)
|
147 |
spectrogram = _create_spectrogram_image(y_final, SAMPLE_RATE)
|
@@ -156,12 +168,10 @@ def process_audio_file(file_path):
|
|
156 |
# --- ĐỊNH NGHĨA CÁC ROUTE CỦA ỨNG DỤNG ---
|
157 |
@app.route('/', methods=['GET'])
|
158 |
def home():
|
159 |
-
"""Render trang chủ của ứng dụng."""
|
160 |
return render_template('index.html')
|
161 |
|
162 |
@app.route('/predict', methods=['POST'])
|
163 |
def predict():
|
164 |
-
"""API endpoint để nhận file audio, xử lý và trả về kết quả dự đoán."""
|
165 |
if 'audio_file' not in request.files:
|
166 |
return jsonify({'error': 'Không có file audio nào trong yêu cầu.'}), 400
|
167 |
|
@@ -173,50 +183,37 @@ def predict():
|
|
173 |
filename = secure_filename(file.filename)
|
174 |
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
175 |
file.save(filepath)
|
176 |
-
|
177 |
-
# Xử lý file audio để trích xuất tất cả các đặc trưng cần thiết
|
178 |
trad_feats, w2v_feats, spec_img = process_audio_file(filepath)
|
179 |
-
|
180 |
if trad_feats is None:
|
181 |
return jsonify({'error': 'Không thể xử lý file audio.'}), 500
|
182 |
|
183 |
-
|
184 |
-
print("\n--- BẮT ĐẦU CHẨN ĐOÁN DỮ LIỆU ĐẦU VÀO (SAU KHI GIẢM NHIỄU) ---")
|
185 |
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}")
|
186 |
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}")
|
187 |
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}")
|
188 |
-
|
189 |
-
|
190 |
-
# Chuẩn bị dữ liệu đầu vào cho các mô hình
|
191 |
combined_feats = np.concatenate([trad_feats, w2v_feats]).reshape(1, -1)
|
192 |
scaled_feats = scaler.transform(combined_feats)
|
193 |
spec_img = spec_img / 255.0
|
194 |
spec_img = np.expand_dims(spec_img, axis=0)
|
195 |
-
|
196 |
-
# Lấy dự đoán từ tất cả các mô hình
|
197 |
pred_xgb = model_xgb.predict_proba(scaled_feats)[0][1]
|
198 |
pred_lgb = model_lgb.predict_proba(scaled_feats)[0][1]
|
199 |
pred_cnn = model_cnn.predict(spec_img, verbose=0)[0][0]
|
200 |
|
201 |
-
# ========== BẮT ĐẦU PHẦN GHI LOG DỰ ĐOÁN ==========
|
202 |
print(f"DEBUG | Individual probabilities (for Male): XGB={pred_xgb:.4f}, LGBM={pred_lgb:.4f}, CNN={pred_cnn:.4f}")
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
# Ensemble: Kết hợp kết quả bằng cách lấy trung bình xác suất
|
207 |
final_prediction_prob = (pred_xgb + pred_lgb + pred_cnn) / 3
|
208 |
final_prediction_label_index = 1 if final_prediction_prob > 0.5 else 0
|
209 |
result_label_text = label_encoder.inverse_transform([final_prediction_label_index])[0]
|
210 |
-
|
211 |
-
os.remove(filepath)
|
212 |
|
|
|
213 |
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})")
|
214 |
-
|
215 |
return jsonify({
|
216 |
'prediction': result_label_text.capitalize(),
|
217 |
'probability': f"{final_prediction_prob:.2f}"
|
218 |
})
|
219 |
-
|
220 |
except Exception as e:
|
221 |
print(f"Đã xảy ra lỗi trong quá trình dự đoán: {e}")
|
222 |
traceback.print_exc()
|
@@ -226,4 +223,3 @@ def predict():
|
|
226 |
if __name__ == '__main__':
|
227 |
port = int(os.environ.get("PORT", 7860))
|
228 |
app.run(host='0.0.0.0', port=port)
|
229 |
-
|
|
|
1 |
# app.py
|
2 |
+
# Phiên bản cuối cùng: Thêm Voice Activity Detection (VAD) để lọc bỏ khoảng lặng.
|
3 |
|
4 |
import os
|
5 |
import joblib
|
|
|
8 |
from flask import Flask, request, jsonify, render_template
|
9 |
from werkzeug.utils import secure_filename
|
10 |
import traceback
|
11 |
+
import collections
|
12 |
|
13 |
+
# --- Thư viện mới để đọc audio, giảm nhiễu và VAD ---
|
14 |
from pydub import AudioSegment
|
15 |
import noisereduce as nr
|
16 |
+
import webrtcvad
|
17 |
|
18 |
# --- Cấu hình TensorFlow và các thư viện AI ---
|
19 |
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
|
|
|
23 |
|
24 |
# --- KHỞI TẠO ỨNG DỤNG FLASK ---
|
25 |
app = Flask(__name__)
|
|
|
|
|
26 |
UPLOAD_FOLDER = 'uploads/'
|
27 |
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
28 |
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
|
|
30 |
|
31 |
# --- TẢI TẤT CẢ CÁC MÔ HÌNH KHI SERVER KHỞI ĐỘNG ---
|
32 |
print(">>> Đang tải các mô hình AI, quá trình này có thể mất một lúc...")
|
|
|
33 |
try:
|
34 |
MODEL_PATH = 'models/'
|
35 |
scaler = joblib.load(os.path.join(MODEL_PATH, 'scaler.pkl'))
|
|
|
39 |
model_cnn = tf.keras.models.load_model(os.path.join(MODEL_PATH, 'cnn.keras'))
|
40 |
wav2vec_processor = Wav2Vec2Processor.from_pretrained("facebook/wav2vec2-base")
|
41 |
wav2vec_model = Wav2Vec2Model.from_pretrained("facebook/wav2vec2-base")
|
|
|
42 |
print(">>> OK! Tất cả các mô hình đã được tải thành công!")
|
|
|
43 |
except Exception as e:
|
44 |
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}")
|
45 |
traceback.print_exc()
|
46 |
exit()
|
47 |
|
48 |
# --- CÁC HÀM TRÍCH XUẤT ĐẶC TRƯNG (KHÔNG ĐỔI) ---
|
|
|
|
|
49 |
SAMPLE_RATE = 22050
|
50 |
MAX_LENGTH_SECONDS = 5.0
|
51 |
MAX_SAMPLES = int(SAMPLE_RATE * MAX_LENGTH_SECONDS)
|
|
|
62 |
features = np.append(features, np.std(mel_spec_db, axis=1))
|
63 |
features = np.append(features, np.max(mel_spec_db, axis=1))
|
64 |
features = np.append(features, np.min(mel_spec_db, axis=1))
|
|
|
65 |
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
|
66 |
features = np.append(features, np.mean(mfccs, axis=1))
|
67 |
features = np.append(features, np.std(mfccs, axis=1))
|
|
|
68 |
if len(features) > TRADITIONAL_FEATURE_SIZE:
|
69 |
features = features[:TRADITIONAL_FEATURE_SIZE]
|
70 |
elif len(features) < TRADITIONAL_FEATURE_SIZE:
|
71 |
features = np.pad(features, (0, TRADITIONAL_FEATURE_SIZE - len(features)), mode='constant')
|
|
|
72 |
return features
|
73 |
except Exception as e:
|
74 |
print(f"Lỗi trích xuất đặc trưng truyền thống: {e}")
|
|
|
98 |
print(f"Lỗi tạo ảnh spectrogram: {e}")
|
99 |
return np.zeros(SPECTROGRAM_SHAPE)
|
100 |
|
101 |
+
# --- HÀM XỬ LÝ AUDIO ĐÃ ĐƯỢC CẬP NHẬT VỚI VAD ---
|
102 |
def process_audio_file(file_path):
|
103 |
"""
|
104 |
+
Hàm tổng hợp phiên bản cuối cùng: Thêm Voice Activity Detection (VAD) để
|
105 |
+
lọc bỏ các khoảng lặng trước khi xử lý.
|
106 |
"""
|
107 |
try:
|
108 |
# 1. Dùng pydub để mở file audio
|
109 |
audio = AudioSegment.from_file(file_path)
|
110 |
|
111 |
+
# 2. Chuẩn hóa âm lượng
|
112 |
target_dbfs = -20.0
|
113 |
change_in_dbfs = target_dbfs - audio.dBFS
|
114 |
audio = audio.apply_gain(change_in_dbfs)
|
115 |
|
116 |
+
# 3. Đảm bảo audio là mono và có sample rate đúng cho VAD
|
117 |
+
# VAD hoạt động tốt nhất ở các sample rate 8000, 16000, 32000, 48000
|
118 |
+
vad_sample_rate = 32000
|
119 |
audio = audio.set_channels(1)
|
120 |
+
audio = audio.set_frame_rate(vad_sample_rate)
|
121 |
|
122 |
+
# 4. **BƯỚC MỚI: VOICE ACTIVITY DETECTION**
|
123 |
+
print("DEBUG | Bắt đầu Voice Activity Detection (VAD)...")
|
124 |
+
vad = webrtcvad.Vad(3) # Mức độ mạnh nhất (0-3)
|
125 |
+
frame_duration_ms = 30 # 30ms mỗi frame
|
126 |
+
frame_bytes = int(vad_sample_rate * frame_duration_ms / 1000) * audio.sample_width
|
127 |
+
|
128 |
+
frames = [audio[i:i + frame_duration_ms] for i in range(0, len(audio), frame_duration_ms)]
|
129 |
+
voiced_frames = [f for f in frames if vad.is_speech(f.raw_data, vad_sample_rate)]
|
130 |
+
|
131 |
+
if voiced_frames:
|
132 |
+
# Nối các frame có tiếng nói lại với nhau
|
133 |
+
audio_voiced = sum(voiced_frames, AudioSegment.empty())
|
134 |
+
print(f"DEBUG | VAD hoàn tất. Giữ lại {len(audio_voiced)}ms âm thanh.")
|
135 |
+
else:
|
136 |
+
# Nếu không tìm thấy tiếng nói, dùng lại audio gốc
|
137 |
+
audio_voiced = audio
|
138 |
+
print("DEBUG | VAD không tìm thấy âm thanh, sử dụng audio gốc.")
|
139 |
+
|
140 |
+
# 5. Chuyển đổi audio đã lọc về sample rate mục tiêu
|
141 |
+
audio_final = audio_voiced.set_frame_rate(SAMPLE_RATE)
|
142 |
+
samples = np.array(audio_final.get_array_of_samples()).astype(np.float32)
|
143 |
+
y = samples / (2**(audio_final.sample_width * 8 - 1))
|
144 |
|
145 |
+
# 6. Giảm nhiễu trên tín hiệu đã được lọc
|
|
|
146 |
print("DEBUG | Bắt đầu giảm nhiễu...")
|
147 |
y_reduced_noise = nr.reduce_noise(y=y, sr=SAMPLE_RATE, prop_decrease=0.8)
|
148 |
print("DEBUG | Giảm nhiễu hoàn tất.")
|
149 |
|
150 |
+
# 7. Chuẩn hóa độ dài
|
151 |
if len(y_reduced_noise) > MAX_SAMPLES:
|
152 |
y_final = y_reduced_noise[:MAX_SAMPLES]
|
153 |
else:
|
154 |
y_final = np.pad(y_reduced_noise, (0, MAX_SAMPLES - len(y_reduced_noise)), mode='constant')
|
155 |
|
156 |
+
# 8. Trích xuất đặc trưng
|
157 |
traditional_features = _extract_traditional_features(y_final, SAMPLE_RATE)
|
158 |
wav2vec_features = _extract_wav2vec_features(y_final, SAMPLE_RATE)
|
159 |
spectrogram = _create_spectrogram_image(y_final, SAMPLE_RATE)
|
|
|
168 |
# --- ĐỊNH NGHĨA CÁC ROUTE CỦA ỨNG DỤNG ---
|
169 |
@app.route('/', methods=['GET'])
|
170 |
def home():
|
|
|
171 |
return render_template('index.html')
|
172 |
|
173 |
@app.route('/predict', methods=['POST'])
|
174 |
def predict():
|
|
|
175 |
if 'audio_file' not in request.files:
|
176 |
return jsonify({'error': 'Không có file audio nào trong yêu cầu.'}), 400
|
177 |
|
|
|
183 |
filename = secure_filename(file.filename)
|
184 |
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
185 |
file.save(filepath)
|
|
|
|
|
186 |
trad_feats, w2v_feats, spec_img = process_audio_file(filepath)
|
|
|
187 |
if trad_feats is None:
|
188 |
return jsonify({'error': 'Không thể xử lý file audio.'}), 500
|
189 |
|
190 |
+
print("\n--- BẮT ĐẦU CHẨN ĐOÁN DỮ LIỆU ĐẦU VÀO (SAU KHI VAD & GIẢM NHIỄU) ---")
|
|
|
191 |
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}")
|
192 |
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}")
|
193 |
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}")
|
194 |
+
|
|
|
|
|
195 |
combined_feats = np.concatenate([trad_feats, w2v_feats]).reshape(1, -1)
|
196 |
scaled_feats = scaler.transform(combined_feats)
|
197 |
spec_img = spec_img / 255.0
|
198 |
spec_img = np.expand_dims(spec_img, axis=0)
|
199 |
+
|
|
|
200 |
pred_xgb = model_xgb.predict_proba(scaled_feats)[0][1]
|
201 |
pred_lgb = model_lgb.predict_proba(scaled_feats)[0][1]
|
202 |
pred_cnn = model_cnn.predict(spec_img, verbose=0)[0][0]
|
203 |
|
|
|
204 |
print(f"DEBUG | Individual probabilities (for Male): XGB={pred_xgb:.4f}, LGBM={pred_lgb:.4f}, CNN={pred_cnn:.4f}")
|
205 |
+
|
|
|
|
|
|
|
206 |
final_prediction_prob = (pred_xgb + pred_lgb + pred_cnn) / 3
|
207 |
final_prediction_label_index = 1 if final_prediction_prob > 0.5 else 0
|
208 |
result_label_text = label_encoder.inverse_transform([final_prediction_label_index])[0]
|
|
|
|
|
209 |
|
210 |
+
os.remove(filepath)
|
211 |
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})")
|
212 |
+
|
213 |
return jsonify({
|
214 |
'prediction': result_label_text.capitalize(),
|
215 |
'probability': f"{final_prediction_prob:.2f}"
|
216 |
})
|
|
|
217 |
except Exception as e:
|
218 |
print(f"Đã xảy ra lỗi trong quá trình dự đoán: {e}")
|
219 |
traceback.print_exc()
|
|
|
223 |
if __name__ == '__main__':
|
224 |
port = int(os.environ.get("PORT", 7860))
|
225 |
app.run(host='0.0.0.0', port=port)
|
|