File size: 11,113 Bytes
4c94e8e
 
 
 
 
c01d934
11b7395
 
4c94e8e
 
c01d934
 
71d9dd5
 
 
 
c01d934
71d9dd5
 
 
 
c01d934
08855d8
71d9dd5
7318fc9
11b7395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c01d934
71d9dd5
 
 
 
 
 
08855d8
e41c430
b038422
e41c430
c01d934
71d9dd5
 
08855d8
 
 
c01d934
08855d8
4c94e8e
11b7395
71d9dd5
11b7395
 
 
71d9dd5
11b7395
71d9dd5
11b7395
 
71d9dd5
11b7395
 
 
 
71d9dd5
11b7395
 
71d9dd5
11b7395
 
71d9dd5
11b7395
 
 
7318fc9
11b7395
 
 
7318fc9
11b7395
 
 
7318fc9
11b7395
 
 
 
7318fc9
11b7395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71d9dd5
11b7395
7318fc9
11b7395
7318fc9
11b7395
 
 
7318fc9
11b7395
 
7318fc9
11b7395
 
7318fc9
11b7395
 
 
7318fc9
11b7395
71d9dd5
4c94e8e
 
 
 
 
 
 
 
 
7318fc9
4c94e8e
 
 
 
 
7318fc9
4c94e8e
 
 
 
 
 
 
 
 
 
11b7395
 
 
 
 
 
 
 
 
 
4c94e8e
11b7395
 
 
 
 
c01d934
11b7395
 
 
 
 
 
 
4c94e8e
11b7395
 
 
 
 
 
 
 
 
 
4c94e8e
71d9dd5
11b7395
71d9dd5
 
 
 
4c94e8e
 
71d9dd5
 
 
 
 
 
 
c01d934
11b7395
71d9dd5
e41c430
11b7395
71d9dd5
 
 
 
 
c01d934
4c94e8e
71d9dd5
 
 
 
4c94e8e
 
71d9dd5
 
 
 
 
11b7395
 
 
 
 
71d9dd5
7318fc9
e41c430
11b7395
 
e41c430
11b7395
 
e41c430
b038422
11b7395
b038422
4c94e8e
 
 
b038422
 
 
 
11b7395
71d9dd5
b038422
 
 
71d9dd5
b038422
 
 
11b7395
71d9dd5
b038422
 
 
 
 
11b7395
b038422
4c94e8e
 
c01d934
 
4c94e8e
 
 
c01d934
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
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)))