csprojectworkspace commited on
Commit
11b7395
·
verified ·
1 Parent(s): 7318fc9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +145 -129
app.py CHANGED
@@ -1,10 +1,11 @@
1
  from flask import Flask, request, jsonify, make_response
2
  from flask_cors import CORS
3
  import numpy as np
4
- import librosa
5
  import io
6
  import os
7
  import tensorflow as tf
 
 
8
 
9
  app = Flask(__name__)
10
  CORS(app)
@@ -22,16 +23,42 @@ print(f"DOA Input: {model_doa.input_shape}, Output: {model_doa.output_shape}")
22
  SILENCE_CLASSES = [13, 14]
23
  VAD_SEGMENT_SIZE = 1024
24
 
25
- # DOA parameters
26
- STFT_WINDOW = 1024
27
- HOP_LENGTH = 512 # 50% overlap
28
- N_FFT = 1024
29
- SR = 16000
30
- FREQ_MIN = 500
31
- FREQ_MAX = 4000
32
- N_FRAMES = 34
33
- N_FREQ_BINS = 225
34
- N_PHASES = 3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  def predict_vad(audio_segment):
37
  if len(audio_segment) < VAD_SEGMENT_SIZE:
@@ -52,108 +79,83 @@ def predict_vad(audio_segment):
52
 
53
  return label, confidence, predicted_class
54
 
55
- def extract_coarray_phases(multichannel_audio, sr=16000):
56
  """
57
- Extrait les phases du co-array pour le modele DOA.
58
- Input: audio multicanal (n_channels, n_samples)
59
- Output: (n_frames, 675) features
60
  """
61
- n_channels = multichannel_audio.shape[0]
62
 
63
- # Calculer STFT pour chaque canal
64
- stfts = []
65
- for ch in range(n_channels):
66
- S = librosa.stft(multichannel_audio[ch], n_fft=N_FFT, hop_length=HOP_LENGTH, win_length=STFT_WINDOW)
67
- stfts.append(S)
68
- stfts = np.array(stfts) # (n_channels, n_freq, n_frames)
69
 
70
- # Frequences correspondantes
71
- freqs = librosa.fft_frequencies(sr=sr, n_fft=N_FFT)
72
- freq_mask = (freqs >= FREQ_MIN) & (freqs <= FREQ_MAX)
73
- freq_indices = np.where(freq_mask)[0]
74
 
75
- # Limiter a N_FREQ_BINS bins
76
- if len(freq_indices) > N_FREQ_BINS:
77
- freq_indices = freq_indices[:N_FREQ_BINS]
78
- elif len(freq_indices) < N_FREQ_BINS:
79
- # Padding si pas assez de bins
80
- freq_indices = np.pad(freq_indices, (0, N_FREQ_BINS - len(freq_indices)), mode='edge')
81
 
82
- n_time_frames = stfts.shape[2]
83
- features_list = []
84
 
85
- for t in range(n_time_frames):
86
- frame_features = []
 
87
 
88
- for f_idx in freq_indices:
89
- # Vecteur des signaux pour cette frequence et cette trame
90
- x = stfts[:, f_idx, t] # (n_channels,)
91
-
92
- # Matrice de covariance spatiale
93
- R = np.outer(x, np.conj(x))
94
 
95
- # Extraire les phases du co-array (3 coefficients pour array lineaire 4 mics)
96
- # Phases des elements hors-diagonale
97
- if n_channels >= 2:
98
- phase1 = np.angle(R[0, 1]) # Phase entre mic 0 et 1
99
- phase2 = np.angle(R[0, 2]) if n_channels > 2 else 0.0
100
- phase3 = np.angle(R[1, 2]) if n_channels > 2 else 0.0
101
- else:
102
- phase1, phase2, phase3 = 0.0, 0.0, 0.0
103
 
104
- frame_features.extend([phase1, phase2, phase3])
 
 
 
105
 
106
- features_list.append(frame_features)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
- features = np.array(features_list) # (n_time_frames, 675)
109
- return features
110
 
111
- def predict_doa(multichannel_audio, sr=16000):
112
  """
113
- Prediction DOA avec le modele CNN 1D.
114
- Input: audio multicanal (n_channels, n_samples)
115
- Output: angles predits
116
  """
117
- # Extraire les features co-array
118
- features = extract_coarray_phases(multichannel_audio, sr)
119
-
120
- n_time_frames = features.shape[0]
121
-
122
- if n_time_frames < N_FRAMES:
123
- # Padding si pas assez de trames
124
- padding = np.zeros((N_FRAMES - n_time_frames, features.shape[1]))
125
- features = np.vstack([features, padding])
126
- n_time_frames = N_FRAMES
127
 
128
- # Prendre les 34 premieres trames (ou faire une moyenne sur plusieurs sequences)
129
- all_angles = []
130
 
131
- n_sequences = max(1, n_time_frames // N_FRAMES)
 
 
132
 
133
- for seq_idx in range(min(n_sequences, 5)): # Max 5 sequences pour la vitesse
134
- start = seq_idx * N_FRAMES
135
- end = start + N_FRAMES
136
-
137
- if end > n_time_frames:
138
- break
139
-
140
- seq_features = features[start:end, :] # (34, 675)
141
- seq_features = seq_features.reshape(1, N_FRAMES, -1).astype(np.float32)
142
-
143
- # Prediction
144
- prediction = model_doa.predict(seq_features, verbose=0)
145
- angles = prediction[0] # (34,) angles en degres
146
- all_angles.extend(angles.tolist())
147
-
148
- if not all_angles:
149
- return 0.0, 0.0, [0.0]
150
-
151
- # Angle moyen et confidence
152
- mean_angle = float(np.mean(all_angles))
153
- std_angle = float(np.std(all_angles))
154
- confidence = float(max(0, 1 - std_angle / 60)) # Plus stable = plus confiant
155
-
156
- return mean_angle, confidence, all_angles
157
 
158
  @app.after_request
159
  def after_request(response):
@@ -181,25 +183,44 @@ def analyze():
181
  return jsonify({'error': 'Aucun fichier audio'}), 400
182
 
183
  audio_file = request.files['audio']
184
- y, sr = librosa.load(io.BytesIO(audio_file.read()), sr=16000, mono=False)
 
 
 
 
 
 
 
 
 
185
 
186
- # Gerer mono vs multicanal
187
- if y.ndim == 1:
188
- y_mono = y
189
- # Dupliquer pour creer 4 canaux (necessaire pour DOA)
190
- y_multi = np.tile(y, (4, 1))
191
  else:
192
- y_mono = np.mean(y, axis=0)
193
- y_multi = y
194
- # S'assurer d'avoir 4 canaux
195
- if y_multi.shape[0] < 4:
196
- y_multi = np.vstack([y_multi, np.tile(y_multi[0:1], (4 - y_multi.shape[0], 1))])
 
 
197
 
198
- n_channels = int(y_multi.shape[0])
199
- duration = float(len(y_mono) / sr)
 
 
 
 
 
 
 
 
200
 
201
  # VAD prediction
202
- total_segments = len(y_mono) // VAD_SEGMENT_SIZE
203
  max_segments = min(total_segments, 20)
204
  if max_segments == 0:
205
  max_segments = 1
@@ -214,10 +235,10 @@ def analyze():
214
  start_sample = segment_index * VAD_SEGMENT_SIZE
215
  end_sample = start_sample + VAD_SEGMENT_SIZE
216
 
217
- if end_sample > len(y_mono):
218
  break
219
 
220
- segment = y_mono[start_sample:end_sample]
221
  label, confidence, _ = predict_vad(segment)
222
 
223
  if label == "voice":
@@ -236,27 +257,22 @@ def analyze():
236
  vad_label = "voice" if voice_ratio > 0.5 else "silence"
237
  avg_vad_confidence = float(np.mean(all_confidences)) if all_confidences else 0.0
238
 
239
- # DOA prediction (utilise audio multicanal)
240
- doa_angle, doa_confidence, all_angles = predict_doa(y_multi, sr)
 
 
 
241
 
242
  # DOA trajectory
243
  doa_trajectory = []
244
- n_traj_points = min(len(all_angles), len(vad_timeline))
245
- for i in range(n_traj_points):
246
  doa_trajectory.append({
247
- 'time': vad_timeline[i]['start'] if i < len(vad_timeline) else float(i * duration / n_traj_points),
248
- 'angle': float(round(all_angles[i] if i < len(all_angles) else doa_angle, 1))
249
  })
250
 
251
- # Si pas assez de points, ajouter des points supplementaires
252
- if len(doa_trajectory) < len(vad_timeline):
253
- for i in range(len(doa_trajectory), len(vad_timeline)):
254
- doa_trajectory.append({
255
- 'time': vad_timeline[i]['start'],
256
- 'angle': float(round(doa_angle + np.random.randn() * 2, 1))
257
- })
258
-
259
- mean_y = float(np.mean(y_mono ** 2))
260
  snr = float(round(10 * np.log10(mean_y / 1e-10), 1)) if mean_y > 0 else 0.0
261
 
262
  return jsonify({
@@ -265,7 +281,7 @@ def analyze():
265
  'channels': n_channels,
266
  'sampleRate': int(sr),
267
  'duration': float(round(duration, 2)),
268
- 'samples': int(len(y_mono)),
269
  'segments_analyzed': n_analyzed
270
  },
271
  'vad': {
@@ -274,14 +290,14 @@ def analyze():
274
  'timeline': vad_timeline
275
  },
276
  'doa': {
277
- 'angle': float(round(doa_angle, 1)),
278
  'confidence': float(round(doa_confidence, 3)),
279
  'trajectory': doa_trajectory
280
  },
281
  'metrics': {
282
  'snr': snr,
283
  'voiceRatio': float(round(voice_ratio, 2)),
284
- 'meanDoa': float(round(np.mean([d['angle'] for d in doa_trajectory]), 1))
285
  }
286
  })
287
  except Exception as e:
 
1
  from flask import Flask, request, jsonify, make_response
2
  from flask_cors import CORS
3
  import numpy as np
 
4
  import io
5
  import os
6
  import tensorflow as tf
7
+ from scipy.io import wavfile
8
+ from scipy.fft import fft
9
 
10
  app = Flask(__name__)
11
  CORS(app)
 
23
  SILENCE_CLASSES = [13, 14]
24
  VAD_SEGMENT_SIZE = 1024
25
 
26
+ # DOA parameters (matching MATLAB code exactly)
27
+ M = 4 # nombre de microphones
28
+ D_INTER = 0.02 # espacement 2cm
29
+ WIN_LEN = 1024 # taille fenetre STFT
30
+ HOP = 512 # hop size
31
+ N_SNAPSHOTS = 10 # snapshots pour moyennage
32
+ FS = 16000 # frequence echantillonnage
33
+ N_BLOCKS = 34 # nombre de trames
34
+
35
+ # Frequency bins 500-4000 Hz
36
+ f_axis = np.arange(0, WIN_LEN // 2 + 1) * (FS / WIN_LEN)
37
+ v_bins = np.where((f_axis >= 500) & (f_axis <= 4000))[0]
38
+ N_BINS = len(v_bins) # should be ~225
39
+
40
+ # Co-array parameters for ULA with 4 mics
41
+ # For ULA: differences are -3, -2, -1, 0, 1, 2, 3 (s=7)
42
+ S = 2 * M - 1 # = 7 for 4 mics
43
+ ZERO_IDX = M - 1 # = 3 (0-indexed: position of 0 in diffs)
44
+ M_V = M # = 4
45
+ LEN_FEAT = S - 4 # = 3 (indices 4 to 6, i.e., indices 5:end in MATLAB 1-indexed)
46
+
47
+ def get_coarray_indices(m):
48
+ """Generate co-array index mapping for ULA"""
49
+ # For ULA with m microphones, differences range from -(m-1) to (m-1)
50
+ # index_map[d] = list of (i,j) pairs where j-i = d
51
+ index_map = {}
52
+ for d in range(-(m-1), m):
53
+ pairs = []
54
+ for i in range(m):
55
+ for j in range(m):
56
+ if j - i == d:
57
+ pairs.append((i, j))
58
+ index_map[d] = pairs
59
+ return index_map
60
+
61
+ INDEX_MAP = get_coarray_indices(M)
62
 
63
  def predict_vad(audio_segment):
64
  if len(audio_segment) < VAD_SEGMENT_SIZE:
 
79
 
80
  return label, confidence, predicted_class
81
 
82
+ def extract_doa_features(au_data):
83
  """
84
+ Extract DOA features exactly as in MATLAB code.
85
+ au_data: (n_samples, 4) - 4 channel audio
86
+ Returns: (34, 225, 3) features
87
  """
88
+ n_samples = au_data.shape[0]
89
 
90
+ # Minimum length for 34 blocks
91
+ min_len = (N_BLOCKS * N_SNAPSHOTS * HOP) + WIN_LEN
 
 
 
 
92
 
93
+ # Repeat if too short
94
+ if n_samples < min_len:
95
+ rep = int(np.ceil(min_len / n_samples))
96
+ au_data = np.tile(au_data, (rep, 1))
97
 
98
+ # Take only min_len samples
99
+ au_data = au_data[:min_len, :]
 
 
 
 
100
 
101
+ # Initialize features array (34, 225, 3)
102
+ frame_features = np.zeros((N_BLOCKS, N_BINS, LEN_FEAT))
103
 
104
+ for k in range(N_BLOCKS):
105
+ # Average covariance over snapshots
106
+ R_avg = np.zeros((M, M, N_BINS), dtype=complex)
107
 
108
+ for snap in range(N_SNAPSHOTS):
109
+ idx_sample = k * N_SNAPSHOTS * HOP + snap * HOP
110
+ segment = au_data[idx_sample:idx_sample + WIN_LEN, :] # (1024, 4)
 
 
 
111
 
112
+ # FFT for each channel
113
+ X_f = fft(segment, n=WIN_LEN, axis=0) # (1024, 4)
114
+ X_f = X_f[v_bins, :].T # (4, 225) - transposed to match MATLAB
 
 
 
 
 
115
 
116
+ for b in range(N_BINS):
117
+ vec_f = X_f[:, b] # (4,) complex vector
118
+ norm_sq = np.linalg.norm(vec_f) ** 2 + 1e-10
119
+ R_avg[:, :, b] += np.outer(vec_f, np.conj(vec_f)) / norm_sq
120
 
121
+ R_avg /= N_SNAPSHOTS
122
+
123
+ # Extract co-array phases
124
+ for b in range(N_BINS):
125
+ R = R_avg[:, :, b]
126
+
127
+ # Build co-array vector z
128
+ z = np.zeros(S, dtype=complex)
129
+ jj = 0
130
+ for ii in range(-(M_V - 1), M_V):
131
+ pairs = INDEX_MAP.get(ii, [])
132
+ if pairs:
133
+ z[jj] = np.mean([R[i, j] for i, j in pairs])
134
+ jj += 1
135
+
136
+ # Extract phases from indices 4 to end (MATLAB: 5:s)
137
+ phases = np.angle(z[4:]) # indices 4, 5, 6 -> 3 values
138
+ frame_features[k, b, :] = phases
139
 
140
+ return frame_features
 
141
 
142
+ def predict_doa(multichannel_audio):
143
  """
144
+ Predict DOA angles using CNN model.
145
+ multichannel_audio: (n_samples, 4) array
146
+ Returns: list of 34 predicted angles
147
  """
148
+ # Extract features (34, 225, 3)
149
+ features = extract_doa_features(multichannel_audio)
 
 
 
 
 
 
 
 
150
 
151
+ # Reshape to (1, 34, 675) - flatten last two dims
152
+ features_flat = features.reshape(1, N_BLOCKS, -1).astype(np.float32)
153
 
154
+ # Predict
155
+ prediction = model_doa.predict(features_flat, verbose=0)
156
+ angles = prediction[0].tolist() # 34 angles
157
 
158
+ return angles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  @app.after_request
161
  def after_request(response):
 
183
  return jsonify({'error': 'Aucun fichier audio'}), 400
184
 
185
  audio_file = request.files['audio']
186
+ audio_bytes = audio_file.read()
187
+
188
+ # Read WAV file
189
+ sr, data = wavfile.read(io.BytesIO(audio_bytes))
190
+
191
+ # Convert to float
192
+ if data.dtype == np.int16:
193
+ data = data.astype(np.float32) / 32768.0
194
+ elif data.dtype == np.int32:
195
+ data = data.astype(np.float32) / 2147483648.0
196
 
197
+ # Handle mono vs multi-channel
198
+ if data.ndim == 1:
199
+ # Mono - duplicate to 4 channels
200
+ data_multi = np.tile(data.reshape(-1, 1), (1, 4))
201
+ data_mono = data
202
  else:
203
+ data_multi = data
204
+ data_mono = np.mean(data, axis=1)
205
+ # Ensure 4 channels
206
+ if data_multi.shape[1] < 4:
207
+ data_multi = np.hstack([data_multi, np.tile(data_multi[:, 0:1], (1, 4 - data_multi.shape[1]))])
208
+ elif data_multi.shape[1] > 4:
209
+ data_multi = data_multi[:, :4]
210
 
211
+ n_channels = data_multi.shape[1]
212
+ duration = len(data_mono) / sr
213
+
214
+ # Resample to 16kHz if needed
215
+ if sr != FS:
216
+ from scipy.signal import resample
217
+ new_len = int(len(data_mono) * FS / sr)
218
+ data_mono = resample(data_mono, new_len)
219
+ data_multi = np.column_stack([resample(data_multi[:, i], new_len) for i in range(4)])
220
+ sr = FS
221
 
222
  # VAD prediction
223
+ total_segments = len(data_mono) // VAD_SEGMENT_SIZE
224
  max_segments = min(total_segments, 20)
225
  if max_segments == 0:
226
  max_segments = 1
 
235
  start_sample = segment_index * VAD_SEGMENT_SIZE
236
  end_sample = start_sample + VAD_SEGMENT_SIZE
237
 
238
+ if end_sample > len(data_mono):
239
  break
240
 
241
+ segment = data_mono[start_sample:end_sample]
242
  label, confidence, _ = predict_vad(segment)
243
 
244
  if label == "voice":
 
257
  vad_label = "voice" if voice_ratio > 0.5 else "silence"
258
  avg_vad_confidence = float(np.mean(all_confidences)) if all_confidences else 0.0
259
 
260
+ # DOA prediction (uses 4-channel audio)
261
+ doa_angles = predict_doa(data_multi)
262
+ mean_angle = float(np.mean(doa_angles))
263
+ std_angle = float(np.std(doa_angles))
264
+ doa_confidence = float(max(0, 1 - std_angle / 25))
265
 
266
  # DOA trajectory
267
  doa_trajectory = []
268
+ time_per_frame = duration / len(doa_angles)
269
+ for i, angle in enumerate(doa_angles):
270
  doa_trajectory.append({
271
+ 'time': float(round(i * time_per_frame, 3)),
272
+ 'angle': float(round(angle, 1))
273
  })
274
 
275
+ mean_y = float(np.mean(data_mono ** 2))
 
 
 
 
 
 
 
 
276
  snr = float(round(10 * np.log10(mean_y / 1e-10), 1)) if mean_y > 0 else 0.0
277
 
278
  return jsonify({
 
281
  'channels': n_channels,
282
  'sampleRate': int(sr),
283
  'duration': float(round(duration, 2)),
284
+ 'samples': int(len(data_mono)),
285
  'segments_analyzed': n_analyzed
286
  },
287
  'vad': {
 
290
  'timeline': vad_timeline
291
  },
292
  'doa': {
293
+ 'angle': float(round(mean_angle, 1)),
294
  'confidence': float(round(doa_confidence, 3)),
295
  'trajectory': doa_trajectory
296
  },
297
  'metrics': {
298
  'snr': snr,
299
  'voiceRatio': float(round(voice_ratio, 2)),
300
+ 'meanDoa': float(round(mean_angle, 1))
301
  }
302
  })
303
  except Exception as e: