from flask import Flask, request, jsonify, make_response from flask_cors import CORS import numpy as np import io import os import tensorflow as tf from scipy.io import wavfile from scipy.fft import fft app = Flask(__name__) CORS(app) # Load VAD model print("Chargement du modele VAD (CRNN)...") model_vad = tf.keras.models.load_model("CRNN_model_final.h5", compile=False) print(f"VAD Input: {model_vad.input_shape}, Output: {model_vad.output_shape}") # Load DOA model print("Chargement du modele DOA...") model_doa = tf.keras.models.load_model("model_keras.h5", compile=False) print(f"DOA Input: {model_doa.input_shape}, Output: {model_doa.output_shape}") SILENCE_CLASSES = [13, 14] VAD_SEGMENT_SIZE = 1024 # DOA parameters (matching MATLAB code exactly) M = 4 # nombre de microphones D_INTER = 0.02 # espacement 2cm WIN_LEN = 1024 # taille fenetre STFT HOP = 512 # hop size N_SNAPSHOTS = 10 # snapshots pour moyennage FS = 16000 # frequence echantillonnage N_BLOCKS = 34 # nombre de trames # Frequency bins 500-4000 Hz f_axis = np.arange(0, WIN_LEN // 2 + 1) * (FS / WIN_LEN) v_bins = np.where((f_axis >= 500) & (f_axis <= 4000))[0] N_BINS = len(v_bins) # should be ~225 # Co-array parameters for ULA with 4 mics # For ULA: differences are -3, -2, -1, 0, 1, 2, 3 (s=7) S = 2 * M - 1 # = 7 for 4 mics ZERO_IDX = M - 1 # = 3 (0-indexed: position of 0 in diffs) M_V = M # = 4 LEN_FEAT = S - 4 # = 3 (indices 4 to 6, i.e., indices 5:end in MATLAB 1-indexed) def get_coarray_indices(m): """Generate co-array index mapping for ULA""" # For ULA with m microphones, differences range from -(m-1) to (m-1) # index_map[d] = list of (i,j) pairs where j-i = d index_map = {} for d in range(-(m-1), m): pairs = [] for i in range(m): for j in range(m): if j - i == d: pairs.append((i, j)) index_map[d] = pairs return index_map INDEX_MAP = get_coarray_indices(M) def predict_vad(audio_segment): if len(audio_segment) < VAD_SEGMENT_SIZE: audio_segment = np.pad(audio_segment, (0, VAD_SEGMENT_SIZE - len(audio_segment)), mode='constant') elif len(audio_segment) > VAD_SEGMENT_SIZE: start = (len(audio_segment) - VAD_SEGMENT_SIZE) // 2 audio_segment = audio_segment[start:start + VAD_SEGMENT_SIZE] max_val = np.max(np.abs(audio_segment)) if max_val > 0: audio_segment = audio_segment / max_val features = audio_segment.reshape(1, VAD_SEGMENT_SIZE, 1).astype(np.float32) prediction = model_vad.predict(features, verbose=0) predicted_class = int(np.argmax(prediction[0])) confidence = float(np.max(prediction[0])) label = "silence" if predicted_class in SILENCE_CLASSES else "voice" return label, confidence, predicted_class def extract_doa_features(au_data): """ Extract DOA features exactly as in MATLAB code. au_data: (n_samples, 4) - 4 channel audio Returns: (34, 225, 3) features """ n_samples = au_data.shape[0] # Minimum length for 34 blocks min_len = (N_BLOCKS * N_SNAPSHOTS * HOP) + WIN_LEN # Repeat if too short if n_samples < min_len: rep = int(np.ceil(min_len / n_samples)) au_data = np.tile(au_data, (rep, 1)) # Take only min_len samples au_data = au_data[:min_len, :] # Initialize features array (34, 225, 3) frame_features = np.zeros((N_BLOCKS, N_BINS, LEN_FEAT)) for k in range(N_BLOCKS): # Average covariance over snapshots R_avg = np.zeros((M, M, N_BINS), dtype=complex) for snap in range(N_SNAPSHOTS): idx_sample = k * N_SNAPSHOTS * HOP + snap * HOP segment = au_data[idx_sample:idx_sample + WIN_LEN, :] # (1024, 4) # FFT for each channel X_f = fft(segment, n=WIN_LEN, axis=0) # (1024, 4) X_f = X_f[v_bins, :].T # (4, 225) - transposed to match MATLAB for b in range(N_BINS): vec_f = X_f[:, b] # (4,) complex vector norm_sq = np.linalg.norm(vec_f) ** 2 + 1e-10 R_avg[:, :, b] += np.outer(vec_f, np.conj(vec_f)) / norm_sq R_avg /= N_SNAPSHOTS # Extract co-array phases for b in range(N_BINS): R = R_avg[:, :, b] # Build co-array vector z z = np.zeros(S, dtype=complex) jj = 0 for ii in range(-(M_V - 1), M_V): pairs = INDEX_MAP.get(ii, []) if pairs: z[jj] = np.mean([R[i, j] for i, j in pairs]) jj += 1 # Extract phases from indices 4 to end (MATLAB: 5:s) phases = np.angle(z[4:]) # indices 4, 5, 6 -> 3 values frame_features[k, b, :] = phases return frame_features def predict_doa(multichannel_audio): """ Predict DOA angles using CNN model. multichannel_audio: (n_samples, 4) array Returns: list of 34 predicted angles """ # Extract features (34, 225, 3) features = extract_doa_features(multichannel_audio) # Reshape to (1, 34, 675) - flatten last two dims features_flat = features.reshape(1, N_BLOCKS, -1).astype(np.float32) # Predict prediction = model_doa.predict(features_flat, verbose=0) angles = prediction[0].tolist() # 34 angles return angles @app.after_request def after_request(response): response.headers.add('Access-Control-Allow-Origin', '*') response.headers.add('Access-Control-Allow-Headers', '*') response.headers.add('Access-Control-Allow-Methods', 'GET,POST,OPTIONS') return response @app.route('/', methods=['GET']) def home(): return jsonify({'message': 'AcoustiTrack API', 'status': 'running', 'models': ['VAD-CRNN', 'DOA-CNN1D']}) @app.route('/api/health', methods=['GET', 'OPTIONS']) def health(): if request.method == 'OPTIONS': return make_response('', 204) return jsonify({'status': 'ok', 'model': 'CRNN VAD + CNN1D DOA'}) @app.route('/api/analyze', methods=['POST', 'OPTIONS']) def analyze(): if request.method == 'OPTIONS': return make_response('', 204) try: if 'audio' not in request.files: return jsonify({'error': 'Aucun fichier audio'}), 400 audio_file = request.files['audio'] audio_bytes = audio_file.read() # Read WAV file sr, data = wavfile.read(io.BytesIO(audio_bytes)) # Convert to float if data.dtype == np.int16: data = data.astype(np.float32) / 32768.0 elif data.dtype == np.int32: data = data.astype(np.float32) / 2147483648.0 # Handle mono vs multi-channel if data.ndim == 1: # Mono - duplicate to 4 channels data_multi = np.tile(data.reshape(-1, 1), (1, 4)) data_mono = data else: data_multi = data data_mono = np.mean(data, axis=1) # Ensure 4 channels if data_multi.shape[1] < 4: data_multi = np.hstack([data_multi, np.tile(data_multi[:, 0:1], (1, 4 - data_multi.shape[1]))]) elif data_multi.shape[1] > 4: data_multi = data_multi[:, :4] n_channels = data_multi.shape[1] duration = len(data_mono) / sr # Resample to 16kHz if needed if sr != FS: from scipy.signal import resample new_len = int(len(data_mono) * FS / sr) data_mono = resample(data_mono, new_len) data_multi = np.column_stack([resample(data_multi[:, i], new_len) for i in range(4)]) sr = FS # VAD prediction total_segments = len(data_mono) // VAD_SEGMENT_SIZE max_segments = min(total_segments, 20) if max_segments == 0: max_segments = 1 step = max(1, total_segments // max_segments) vad_timeline = [] voice_count = 0 all_confidences = [] for i in range(max_segments): segment_index = i * step start_sample = segment_index * VAD_SEGMENT_SIZE end_sample = start_sample + VAD_SEGMENT_SIZE if end_sample > len(data_mono): break segment = data_mono[start_sample:end_sample] label, confidence, _ = predict_vad(segment) if label == "voice": voice_count += 1 all_confidences.append(confidence) vad_timeline.append({ 'start': float(round(start_sample / sr, 3)), 'end': float(round(end_sample / sr, 3)), 'label': label, 'confidence': float(round(confidence, 3)) }) n_analyzed = len(vad_timeline) voice_ratio = float(voice_count / n_analyzed) if n_analyzed > 0 else 0.0 vad_label = "voice" if voice_ratio > 0.5 else "silence" avg_vad_confidence = float(np.mean(all_confidences)) if all_confidences else 0.0 # DOA prediction (uses 4-channel audio) doa_angles = predict_doa(data_multi) mean_angle = float(np.mean(doa_angles)) std_angle = float(np.std(doa_angles)) doa_confidence = float(max(0, 1 - std_angle / 25)) # DOA trajectory doa_trajectory = [] time_per_frame = duration / len(doa_angles) for i, angle in enumerate(doa_angles): doa_trajectory.append({ 'time': float(round(i * time_per_frame, 3)), 'angle': float(round(angle, 1)) }) mean_y = float(np.mean(data_mono ** 2)) snr = float(round(10 * np.log10(mean_y / 1e-10), 1)) if mean_y > 0 else 0.0 return jsonify({ 'success': True, 'metadata': { 'channels': n_channels, 'sampleRate': int(sr), 'duration': float(round(duration, 2)), 'samples': int(len(data_mono)), 'segments_analyzed': n_analyzed }, 'vad': { 'prediction': vad_label, 'confidence': float(round(avg_vad_confidence, 3)), 'timeline': vad_timeline }, 'doa': { 'angle': float(round(mean_angle, 1)), 'confidence': float(round(doa_confidence, 3)), 'trajectory': doa_trajectory }, 'metrics': { 'snr': snr, 'voiceRatio': float(round(voice_ratio, 2)), 'meanDoa': float(round(mean_angle, 1)) } }) except Exception as e: import traceback traceback.print_exc() return jsonify({'error': str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))