duongve/dataset_testing
02k
1!pip install fastapi uvicorn python-multipart librosa numpy ai-edge-litert pycloudflared nest-asyncio2import numpy as np3import uvicorn4import librosa5import io6import threading7import asyncio8import shutil9import os10from fastapi import FastAPI, File, UploadFile11from fastapi.responses import HTMLResponse, JSONResponse12from fastapi.middleware.cors import CORSMiddleware13from numpy.lib.stride_tricks import as_strided14from typing import Tuple, Optional15from ai_edge_litert.interpreter import Interpreter16from pycloudflared import try_cloudflare17from pydub import AudioSegment18 19# ==========================================20# 1. CORE LOGIC (GIỮ NGUYÊN)21# ==========================================22 23def mel_scale_scalar(freq: float) -> float:24 return 1127.0 * np.log(1.0 + freq / 700.0)25 26def mel_scale(freq: np.ndarray) -> np.ndarray:27 return 1127.0 * np.log(1.0 + freq / 700.0)28 29def inverse_mel_scale(mel: np.ndarray) -> np.ndarray:30 return 700.0 * (np.exp(mel / 1127.0) - 1.0)31 32def get_mel_banks(num_bins, window_length_padded, sample_freq, low_freq, high_freq, vtln_low, vtln_high, vtln_warp_factor):33 assert num_bins > 334 assert window_length_padded % 2 == 035 num_fft_bins = window_length_padded // 236 nyquist = 0.5 * sample_freq37 if high_freq <= 0.0: high_freq += nyquist38 fft_bin_width = sample_freq / window_length_padded39 mel_low_freq = mel_scale_scalar(low_freq)40 mel_high_freq = mel_scale_scalar(high_freq)41 mel_freq_delta = (mel_high_freq - mel_low_freq) / (num_bins + 1)42 if vtln_high < 0.0: vtln_high += nyquist43 bin = np.arange(num_bins)[:, np.newaxis]44 left_mel = mel_low_freq + bin * mel_freq_delta45 center_mel = mel_low_freq + (bin + 1.0) * mel_freq_delta46 right_mel = mel_low_freq + (bin + 2.0) * mel_freq_delta47 center_freqs = inverse_mel_scale(center_mel).squeeze(-1)48 mel = mel_scale(fft_bin_width * np.arange(num_fft_bins))[np.newaxis, :]49 up_slope = (mel - left_mel) / (center_mel - left_mel)50 down_slope = (right_mel - mel) / (right_mel - center_mel)51 bins = np.maximum(0.0, np.minimum(up_slope, down_slope))52 return bins, center_freqs53 54def stft(input, n_fft, hop_length=None, win_length=None, window=None, center=True, pad_mode="reflect", normalized=False, onesided=True, return_complex=True):55 if hop_length is None: hop_length = n_fft // 456 if win_length is None: win_length = n_fft57 if window is None: window = np.ones(win_length)58 if len(window) < n_fft:59 pad_width = (n_fft - len(window)) // 260 window = np.pad(window, (pad_width, n_fft - len(window) - pad_width))61 62 input = np.asarray(input)63 if input.ndim == 1:64 input = input[np.newaxis, :]65 squeeze_batch = True66 else:67 squeeze_batch = False68 69 if center:70 pad_width = int(n_fft // 2)71 input = np.pad(input, ((0, 0), (pad_width, pad_width)), mode=pad_mode)72 73 n_frames = 1 + (input.shape[-1] - n_fft) // hop_length74 frame_length = n_fft75 frame_step = hop_length76 frame_stride = input.strides[-1]77 shape = (input.shape[0], n_frames, frame_length)78 strides = (input.strides[0], frame_step * frame_stride, frame_stride)79 frames = as_strided(input, shape=shape, strides=strides, writeable=False)80 frames = frames * window81 stft_matrix = np.fft.fft(frames, n=n_fft, axis=-1)82 83 if normalized: stft_matrix = stft_matrix / np.sqrt(n_fft)84 if onesided: stft_matrix = stft_matrix[..., :(n_fft // 2) + 1]85 86 result = stft_matrix if return_complex else np.stack((stft_matrix.real, stft_matrix.imag), axis=-1)87 if squeeze_batch: result = result[0]88 return result89 90class MelSTFT:91 def __init__(self, n_mels=128, sr=32000, win_length=800, hopsize=320, n_fft=1024, fmin=0.0, fmax=None):92 self.n_mels = n_mels93 self.sr = sr94 self.win_length = win_length95 self.hopsize = hopsize96 self.n_fft = n_fft97 self.fmin = fmin98 self.fmax = fmax if fmax else sr // 2 - 100099 self.window = np.hanning(win_length)100 self.mel_basis, _ = get_mel_banks(self.n_mels, self.n_fft, self.sr, self.fmin, self.fmax, 100.0, -500., 1.0)101 self.mel_basis = np.pad(self.mel_basis, ((0, 0), (0, 1)), mode='constant', constant_values=0)102 self.preemphasis_coefficient = np.array([-.97, 1]).reshape(1, 1, 2)103 104 def preemphasis(self, x):105 x = x.reshape(1, 1, -1)106 output_size = x.shape[2] - self.preemphasis_coefficient.shape[2] + 1107 result = np.zeros((1, 1, output_size))108 for i in range(output_size):109 result[0, 0, i] = np.sum(x[0, 0, i:i+2] * self.preemphasis_coefficient[0, 0])110 return result[0]111 112 def __call__(self, x):113 x = self.preemphasis(x)114 spec_x = stft(input=x, n_fft=self.n_fft, hop_length=self.hopsize, win_length=self.win_length, window=self.window, return_complex=False)115 spec_x = np.sum(spec_x ** 2, axis=-1)116 melspec = np.dot(self.mel_basis, spec_x.transpose(0,2,1)).transpose(1,0,2)117 melspec = np.log(melspec + 1e-5)118 melspec = (melspec + 4.5) / 5.119 return melspec120 121def softmax(x):122 exp_x = np.exp(x - np.max(x))123 return exp_x / np.sum(exp_x, axis=-1, keepdims=True)124 125# ==========================================126# 2. SETUP BACKEND127# ==========================================128 129app = FastAPI()130app.add_middleware(131 CORSMiddleware,132 allow_origins=["*"],133 allow_methods=["*"],134 allow_headers=["*"],135)136 137MODEL_PATH = '/content/emotion_model_2025_08_18212.tflite'138interpreter = None139input_details = None140output_details = None141model_lock = threading.Lock()142 143mel_processor = MelSTFT(n_mels=128, sr=32000, win_length=800, hopsize=320)144CLASSES = ['Angry', 'Disgust', 'Fear', 'Happy', 'Neutral', 'Sad', 'Surprise']145 146@app.on_event("startup")147def load_model():148 global interpreter, input_details, output_details149 try:150 interpreter = Interpreter(model_path=MODEL_PATH)151 interpreter.allocate_tensors()152 input_details = interpreter.get_input_details()153 output_details = interpreter.get_output_details()154 print("✅ Model loaded successfully!")155 except Exception as e:156 print(f"❌ Error loading model: {e}")157 158# ==========================================159# 3. FRONTEND INTERFACE (CÓ THÊM REPLAY)160# ==========================================161 162html_content = """163<!DOCTYPE html>164<html>165<head>166 <title>AI Emotion Detection</title>167 <meta name="viewport" content="width=device-width, initial-scale=1">168 <style>169 body { font-family: 'Segoe UI', sans-serif; text-align: center; padding: 20px; background: #f0f2f5; color: #333; }170 .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); }171 h1 { color: #2c3e50; margin-bottom: 5px; }172 p { color: #7f8c8d; }173 174 button { padding: 15px 30px; font-size: 18px; cursor: pointer; border-radius: 50px; border: none; margin: 20px auto; transition: 0.3s; display: block; width: 80%; font-weight: bold;}175 #recordBtn { background-color: #ff4757; color: white; box-shadow: 0 4px 10px rgba(255, 71, 87, 0.3); }176 #recordBtn:hover { background-color: #ff6b81; transform: translateY(-2px); }177 #recordBtn.recording { background-color: #2ed573; animation: pulse 1.5s infinite; }178 179 #playbackContainer { display: none; margin: 20px 0; padding: 15px; background: #f1f2f6; border-radius: 10px; }180 audio { width: 100%; outline: none; }181 182 #status { margin: 10px 0; font-style: italic; color: #666; height: 20px;}183 184 #results { margin-top: 30px; text-align: left; }185 .bar-container { margin-bottom: 12px; display: flex; align-items: center; }186 .label { font-weight: bold; width: 70px; font-size: 14px; }187 .bar-bg { flex-grow: 1; background: #dfe4ea; height: 12px; border-radius: 6px; margin: 0 10px; overflow: hidden;}188 .bar-fill { height: 100%; background: linear-gradient(90deg, #3498db, #2980b9); border-radius: 6px; width: 0%; transition: width 0.6s ease-out; }189 .percent { width: 40px; font-size: 14px; color: #555; text-align: right;}190 191 @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(46, 213, 115, 0.7); } 70% { box-shadow: 0 0 0 15px rgba(46, 213, 115, 0); } 100% { box-shadow: 0 0 0 0 rgba(46, 213, 115, 0); } }192 </style>193</head>194<body>195 <div class="container">196 <h1>🎙️ Cảm xúc giọng nói</h1>197 <p>Hệ thống phân tích cảm xúc qua giọng nói (AI)</p>198 199 <button id="recordBtn" onclick="toggleRecording()">Bắt đầu Ghi âm</button>200 <div id="status">Sẵn sàng</div>201 202 <div id="playbackContainer">203 <p style="margin: 0 0 10px 0; font-size: 14px;">🎧 Nghe lại giọng của bạn:</p>204 <audio id="audioPlayer" controls></audio>205 </div>206 207 <div id="results"></div>208 </div>209 210 <script>211 let mediaRecorder;212 let audioChunks = [];213 let isRecording = false;214 215 async function toggleRecording() {216 const btn = document.getElementById('recordBtn');217 const status = document.getElementById('status');218 const playbackContainer = document.getElementById('playbackContainer');219 const resultsContainer = document.getElementById('results');220 221 if (!isRecording) {222 // BẮT ĐẦU GHI223 try {224 const stream = await navigator.mediaDevices.getUserMedia({ audio: true });225 mediaRecorder = new MediaRecorder(stream);226 audioChunks = [];227 228 // Ẩn kết quả cũ khi ghi âm mới229 playbackContainer.style.display = 'none';230 resultsContainer.innerHTML = '';231 232 mediaRecorder.ondataavailable = event => {233 audioChunks.push(event.data);234 };235 236 mediaRecorder.onstop = async () => {237 // Tạo blob audio238 const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });239 240 // 1. TẠO URL ĐỂ NGHE LẠI (CLIENT-SIDE)241 const audioUrl = URL.createObjectURL(audioBlob);242 const audioPlayer = document.getElementById('audioPlayer');243 audioPlayer.src = audioUrl;244 playbackContainer.style.display = 'block'; // Hiện trình phát245 246 // 2. GỬI LÊN SERVER247 uploadAudio(audioBlob);248 };249 250 mediaRecorder.start();251 isRecording = true;252 btn.textContent = "⏹ Dừng & Phân tích";253 btn.classList.add("recording");254 status.textContent = "Đang thu âm...";255 } catch (err) {256 alert("Không thể truy cập microphone: " + err);257 }258 } else {259 // DỪNG GHI260 mediaRecorder.stop();261 isRecording = false;262 btn.textContent = "🎙️ Bắt đầu Ghi âm mới";263 btn.classList.remove("recording");264 status.textContent = "Đang gửi dữ liệu...";265 }266 }267 268 async function uploadAudio(blob) {269 const formData = new FormData();270 formData.append("file", blob, "recording.webm");271 272 try {273 const response = await fetch("/predict", {274 method: "POST",275 body: formData276 });277 278 if (!response.ok) {279 throw new Error(`Server error: ${response.status}`);280 }281 282 const data = await response.json();283 displayResults(data);284 document.getElementById('status').textContent = "Hoàn tất!";285 } catch (error) {286 console.error("Error:", error);287 document.getElementById('status').textContent = "Lỗi: " + error.message;288 }289 }290 291 function displayResults(data) {292 const container = document.getElementById('results');293 container.innerHTML = "<h3>📊 Kết quả phân tích:</h3>";294 295 data.results.forEach(item => {296 const percentage = (item.score * 100).toFixed(1);297 // Đổi màu thanh bar nếu > 50%298 let barColor = percentage > 50 ? '#2ed573' : 'linear-gradient(90deg, #3498db, #2980b9)';299 300 const html = `301 <div class="bar-container">302 <span class="label">${item.label}</span>303 <div class="bar-bg">304 <div class="bar-fill" style="width: ${percentage}%; background: ${barColor}"></div>305 </div>306 <span class="percent">${percentage}%</span>307 </div>308 `;309 container.innerHTML += html;310 });311 }312 </script>313</body>314</html>315"""316 317@app.get("/", response_class=HTMLResponse)318async def home():319 return html_content320 321# ==========================================322# 4. API PREDICT (ĐÃ FIX PYDUB CHO WEBM)323# ==========================================324 325@app.post("/predict")326async def predict(file: UploadFile = File(...)):327 # Tên file tạm328 webm_filename = "temp_input.webm"329 wav_filename = "temp_converted.wav"330 331 try:332 # 1. Lưu file WebM gốc333 with open(webm_filename, "wb") as buffer:334 shutil.copyfileobj(file.file, buffer)335 336 # 2. Convert WebM -> WAV (Fix lỗi librosa)337 audio = AudioSegment.from_file(webm_filename)338 audio = audio.set_frame_rate(32000).set_channels(1)339 audio.export(wav_filename, format="wav")340 341 # 3. Librosa đọc342 waveform, _ = librosa.load(wav_filename, sr=32000, mono=True)343 344 except Exception as e:345 import traceback346 traceback.print_exc()347 return JSONResponse(status_code=500, content={"error": f"Lỗi xử lý file: {str(e)}"})348 349 finally:350 # Dọn dẹp351 if os.path.exists(webm_filename): os.remove(webm_filename)352 if os.path.exists(wav_filename): os.remove(wav_filename)353 354 # 4. Preprocessing & Inference355 waveform = np.stack([waveform])356 spec = mel_processor(waveform)357 358 target_len = 400359 if spec.shape[-1] > target_len:360 spec = spec[:, :, :target_len]361 elif spec.shape[-1] < target_len:362 spec = np.pad(spec, ((0, 0), (0, 0), (0, target_len - spec.shape[-1])), mode='constant')363 364 spec = np.expand_dims(spec, axis=0).astype(np.float32)365 366 if interpreter is None:367 return JSONResponse(status_code=500, content={"error": "Model not loaded"})368 369 with model_lock:370 interpreter.set_tensor(input_details[0]['index'], spec)371 interpreter.invoke()372 output_data = interpreter.get_tensor(output_details[0]['index'])373 374 preds = softmax(output_data[0])375 376 results = []377 sorted_indexes = np.argsort(preds)[::-1]378 for k in range(len(CLASSES)):379 results.append({380 "label": CLASSES[sorted_indexes[k]],381 "score": float(preds[sorted_indexes[k]])382 })383 384 return {"results": results}385 386# ==========================================387# 5. RUN SERVER (FIX COLAB)388# ==========================================389 390if __name__ == "__main__":391 import nest_asyncio392 import uvicorn393 from pycloudflared import try_cloudflare394 395 nest_asyncio.apply()396 397 print("🚀 Đang khởi động Cloudflare Tunnel...")398 tunnel_url = try_cloudflare(port=8000)399 print(f"🔗 PUBLIC URL CỦA BẠN: {tunnel_url.tunnel}")400 print("👉 Click link trên để truy cập Web App")401 402 config = uvicorn.Config(app, host="0.0.0.0", port=8000)403 server = uvicorn.Server(config)404 await server.serve()