trixy194t commited on
Commit
46cd10d
ยท
verified ยท
1 Parent(s): 4dd2852

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +161 -95
app.py CHANGED
@@ -32,14 +32,16 @@ N_FFT = 2048
32
  HOP_LENGTH = 512
33
  N_MELS = 128
34
  FMAX = 8000
35
- DURATION = 10
36
 
37
  # --- LOAD YOUR FINE-TUNED MODEL ---
38
  FINETUNED_MODEL_PATH = "snoring_detection_model fine tuned.h5"
39
  finetuned_snore_model = None
40
 
41
  def build_finetuned_model_architecture():
42
- """Reconstruct the EXACT fine-tuned model architecture from training"""
 
 
43
  inputs = layers.Input(shape=(128, 431, 1))
44
 
45
  # Block 1
@@ -123,38 +125,57 @@ except Exception as e:
123
  finetuned_snore_model = None
124
 
125
  # ========================================
126
- # KAGGLE METHOD - SNORING DETECTION
127
  # ========================================
128
 
129
  def create_mel_spectrogram(y, sr):
130
- """Create mel-spectrogram EXACTLY as in Kaggle cleaning pipeline"""
 
 
 
131
  mel_spec = librosa.feature.melspectrogram(
132
  y=y, sr=sr, n_fft=N_FFT, hop_length=HOP_LENGTH,
133
  n_mels=N_MELS, fmax=FMAX, power=1.0
134
  )
135
  mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
 
136
  mel_spec_db = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-10)
137
  return mel_spec_db
138
 
139
  def predict_snoring_segment(y_segment, sr, model):
140
- """Predict snoring for a segment - EXACT from Kaggle cleaning pipeline"""
141
- target_length = SAMPLE_RATE * DURATION
 
 
 
 
142
 
143
  if len(y_segment) < target_length:
 
144
  padding = target_length - len(y_segment)
145
  y_segment = np.pad(y_segment, (0, padding), mode='constant')
146
  elif len(y_segment) > target_length:
 
147
  y_segment = y_segment[:target_length]
148
 
 
149
  y_segment = y_segment / (np.max(np.abs(y_segment)) + 1e-10)
 
 
150
  mel_spec = create_mel_spectrogram(y_segment, sr)
 
 
151
  mel_spec_input = mel_spec[np.newaxis, ..., np.newaxis]
 
 
152
  probability = model.predict(mel_spec_input, verbose=0)[0][0]
153
 
154
  return float(probability)
155
 
156
  def analyze_audio_segments(y, sr, model, segment_duration=10):
157
- """Analyze audio in segments - EXACT from Kaggle cleaning pipeline"""
 
 
158
  segment_length = int(segment_duration * sr)
159
  total_duration = len(y) / sr
160
  num_segments = int(np.ceil(total_duration / segment_duration))
@@ -167,15 +188,19 @@ def analyze_audio_segments(y, sr, model, segment_duration=10):
167
  start_sample = i * segment_length
168
  end_sample = min(start_sample + segment_length, len(y))
169
 
 
170
  segment = y[start_sample:end_sample]
171
 
 
172
  if len(segment) < segment_length:
173
  padding = segment_length - len(segment)
174
  segment = np.pad(segment, (0, padding), mode='constant')
175
 
 
176
  if sr != SAMPLE_RATE:
177
  segment = librosa.resample(segment, orig_sr=sr, target_sr=SAMPLE_RATE)
178
 
 
179
  probability = predict_snoring_segment(segment, SAMPLE_RATE, model)
180
  is_snoring = probability > 0.5
181
 
@@ -190,82 +215,121 @@ def analyze_audio_segments(y, sr, model, segment_duration=10):
190
 
191
  segment_results.append(segment_result)
192
 
 
193
  if is_snoring:
194
  print(f" โœ… Segment {i+1}/{num_segments}: SNORE (prob: {probability:.4f})")
 
 
195
 
196
  return segment_results
197
 
198
  # ========================================
199
- # SMART SOFT VALIDATION (LENIENT)
200
  # ========================================
201
 
202
- def smart_audio_validation(y, sr):
203
- """
204
- SMART SOFT VALIDATION - Only reject obvious non-sleep audio
205
-
206
- REJECTS (HARD STOP):
207
- - Completely silent audio (blank recordings)
208
- - Music with strong beat patterns
209
- - Clear speech/conversation (high speech frequencies)
210
- - Very loud audio (concerts, parties)
211
-
212
- ALLOWS (PROCESS ANYWAY):
213
- - Short audio (even < 20s)
214
- - Low breathing confidence (snoring doesn't always have regular breathing)
215
- - Moderate energy audio
216
- - Any audio with potential sleep sounds
217
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  duration = len(y) / sr
 
 
 
 
219
  rms_energy = np.sqrt(np.mean(y**2))
 
 
 
 
 
220
 
221
- # HARD REJECT 1: Completely silent (blank recording)
222
- if rms_energy < 0.0005:
223
- return False, "REJECT: Audio is completely blank/silent (no sound detected)"
 
 
 
 
 
 
 
 
 
224
 
225
- # HARD REJECT 2: Extremely loud (not a bedroom recording)
226
- if rms_energy > 0.7:
227
- return False, "REJECT: Audio too loud (likely music/TV/party, not sleep recording)"
228
 
229
- # Spectral analysis
230
  stft = np.abs(librosa.stft(y))
231
  freq_bins = librosa.fft_frequencies(sr=sr)
232
 
233
- # Frequency distribution
234
  low_freq_energy = np.sum(stft[freq_bins < 2000, :])
235
  high_freq_energy = np.sum(stft[freq_bins > 4000, :])
236
- very_high_freq = np.sum(stft[freq_bins > 6000, :])
237
  total_energy = np.sum(stft) + 1e-10
238
 
239
  low_freq_ratio = low_freq_energy / total_energy
240
  high_freq_ratio = high_freq_energy / total_energy
241
- very_high_ratio = very_high_freq / total_energy
242
 
243
- # HARD REJECT 3: Too much very high frequency = music/speech
244
- if very_high_ratio > 0.25:
245
- return False, "REJECT: Too much very high-frequency content (likely music or TV)"
246
 
247
- # HARD REJECT 4: Almost no low frequency AND lots of high frequency = speech
248
- if low_freq_ratio < 0.2 and high_freq_ratio > 0.5:
249
- return False, "REJECT: Frequency pattern matches speech/conversation, not sleep audio"
250
 
251
- # SOFT WARNINGS (but still process)
252
- warnings = []
253
-
254
- if duration < 10:
255
- warnings.append("โš ๏ธ Very short audio (< 10s) - limited analysis")
256
-
257
- if rms_energy < 0.003:
258
- warnings.append("โš ๏ธ Very quiet audio - may be mostly silence")
259
-
260
- if high_freq_ratio > 0.35:
261
- warnings.append("โš ๏ธ Moderate high-frequency content detected")
262
-
263
- # ACCEPT: Let the model decide
264
- if warnings:
265
- warning_msg = " | ".join(warnings)
266
- return True, f"SOFT WARNING: {warning_msg}"
267
  else:
268
- return True, "PASS: Audio appears suitable for sleep analysis"
269
 
270
  # --- API ENDPOINTS ---
271
 
@@ -277,13 +341,15 @@ async def root():
277
  "model_loaded": finetuned_snore_model is not None,
278
  "model_type": "Fine-tuned CNN with Kaggle preprocessing",
279
  "model_accuracy": "95.33%",
280
- "validation": "Smart soft validation (lenient)"
 
281
  }
282
 
283
  @app.post("/analyze")
284
  async def analyze_audio(file: UploadFile = File(...)):
285
- """Analyze audio using smart soft validation + Kaggle method"""
286
 
 
287
  if finetuned_snore_model is None:
288
  raise HTTPException(
289
  status_code=503,
@@ -292,43 +358,40 @@ async def analyze_audio(file: UploadFile = File(...)):
292
 
293
  temp_path = f"temp_{os.getpid()}_{file.filename}"
294
 
 
295
  with open(temp_path, "wb") as buffer:
296
  shutil.copyfileobj(file.file, buffer)
297
 
298
  try:
299
- # 1. LOAD AUDIO
300
  print(f"\n๐Ÿ”Š Loading audio: {file.filename}")
301
  y, sr = librosa.load(temp_path, sr=None, dtype=np.float32)
302
- print(f" Duration: {len(y)/sr:.1f}s, Sample rate: {sr} Hz")
 
303
 
304
- # 2. SMART SOFT VALIDATION
305
- print("\n๐Ÿ” Smart audio validation...")
306
- is_valid, validation_message = smart_audio_validation(y, sr)
307
 
308
- if not is_valid:
309
- # HARD REJECT - Don't process
310
- print(f" โŒ {validation_message}")
311
- raise HTTPException(
312
- status_code=400,
313
- detail=validation_message
314
- )
315
 
316
- # SOFT WARNING or PASS - Continue processing
317
- print(f" โœ… {validation_message}")
318
 
319
- # 3. NOISE REDUCTION
320
- print("๐Ÿงน Applying noise reduction...")
321
  y_clean = nr.reduce_noise(y=y, sr=sr, stationary=True)
322
 
323
- # 4. KAGGLE METHOD - ANALYZE
324
- print(f"๐ŸŽฏ Analyzing with fine-tuned model (Kaggle method)...")
325
  segment_results = analyze_audio_segments(y_clean, sr, finetuned_snore_model, segment_duration=10)
326
 
327
- # 5. COUNT RESULTS
328
  snore_count = sum(1 for seg in segment_results if seg['is_snoring'])
329
  total_segments = len(segment_results)
330
 
331
- print(f"\n๐Ÿ“Š Analysis Results:")
332
  print(f" Total segments: {total_segments}")
333
  print(f" Snoring segments: {snore_count}")
334
  print(f" Snoring percentage: {(snore_count/total_segments)*100:.1f}%")
@@ -337,7 +400,7 @@ async def analyze_audio(file: UploadFile = File(...)):
337
  annotations = []
338
  apnea_count = 0
339
 
340
- # Snoring annotations
341
  for seg in segment_results:
342
  if seg['is_snoring']:
343
  annotations.append({
@@ -349,45 +412,48 @@ async def analyze_audio(file: UploadFile = File(...)):
349
  "confidence": round(seg['confidence'], 4)
350
  })
351
 
352
- # Apnea detection (gaps between snoring)
353
  for i in range(len(segment_results) - 1):
354
  current = segment_results[i]
355
  next_seg = segment_results[i + 1]
356
 
 
357
  if current['is_snoring'] and not next_seg['is_snoring']:
 
 
358
  for j in range(i + 1, len(segment_results)):
359
  if segment_results[j]['is_snoring']:
360
- gap_duration = segment_results[j]['start_time'] - current['end_time']
 
361
 
 
362
  if 10.0 <= gap_duration <= 120.0:
363
  apnea_count += 1
364
  risk = "LOW" if gap_duration < 15.0 else ("MEDIUM" if gap_duration < 20.0 else "HIGH")
365
  annotations.append({
366
  "label": "APNEA",
367
- "start_sec": round(current['end_time'], 2),
368
- "end_sec": round(segment_results[j]['start_time'], 2),
369
  "duration": round(gap_duration, 2),
370
  "risk_level": risk
371
  })
372
- print(f" โš ๏ธ Apnea: {gap_duration:.1f}s gap")
373
  break
374
 
375
- # 7. STATISTICS
376
  total_duration = segment_results[-1]['end_time']
377
  duration_hours = total_duration / 3600
378
  ahi = apnea_count / duration_hours if duration_hours > 0 else 0
379
 
380
  snoring_percentage = (snore_count / total_segments) * 100
381
 
382
- # Risk based on snoring percentage
383
- if snoring_percentage > 60:
384
  risk_summary = "HIGH"
385
- elif snoring_percentage > 40:
386
  risk_summary = "MEDIUM"
387
- elif snoring_percentage > 20:
388
  risk_summary = "LOW"
389
- else:
390
- risk_summary = "NORMAL"
391
 
392
  # Cleanup
393
  del y, y_clean
@@ -395,7 +461,8 @@ async def analyze_audio(file: UploadFile = File(...)):
395
 
396
  return {
397
  "valid_recording": True,
398
- "validation_message": validation_message,
 
399
  "snore_count": snore_count,
400
  "total_segments": total_segments,
401
  "snoring_percentage": round(snoring_percentage, 2),
@@ -403,11 +470,10 @@ async def analyze_audio(file: UploadFile = File(...)):
403
  "overall_risk": risk_summary,
404
  "ahi_score": round(ahi, 1),
405
  "model_used": "Fine-tuned CNN (Kaggle method)",
 
406
  "events": annotations
407
  }
408
 
409
- except HTTPException:
410
- raise
411
  except Exception as e:
412
  print(f"๐Ÿ”ฅ ERROR: {str(e)}")
413
  import traceback
 
32
  HOP_LENGTH = 512
33
  N_MELS = 128
34
  FMAX = 8000
35
+ DURATION = 10 # 10 seconds per segment
36
 
37
  # --- LOAD YOUR FINE-TUNED MODEL ---
38
  FINETUNED_MODEL_PATH = "snoring_detection_model fine tuned.h5"
39
  finetuned_snore_model = None
40
 
41
  def build_finetuned_model_architecture():
42
+ """
43
+ Reconstruct the EXACT fine-tuned model architecture from training.
44
+ """
45
  inputs = layers.Input(shape=(128, 431, 1))
46
 
47
  # Block 1
 
125
  finetuned_snore_model = None
126
 
127
  # ========================================
128
+ # SNORING DETECTION - EXACT FROM KAGGLE CLEANING PIPELINE
129
  # ========================================
130
 
131
  def create_mel_spectrogram(y, sr):
132
+ """
133
+ Create mel-spectrogram EXACTLY as in Kaggle cleaning pipeline
134
+ This is the EXACT function from your working code
135
+ """
136
  mel_spec = librosa.feature.melspectrogram(
137
  y=y, sr=sr, n_fft=N_FFT, hop_length=HOP_LENGTH,
138
  n_mels=N_MELS, fmax=FMAX, power=1.0
139
  )
140
  mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
141
+ # Normalize to 0-1 range
142
  mel_spec_db = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-10)
143
  return mel_spec_db
144
 
145
  def predict_snoring_segment(y_segment, sr, model):
146
+ """
147
+ Predict snoring for a segment - EXACT from Kaggle cleaning pipeline
148
+ This is the EXACT function from your working code
149
+ """
150
+ # Ensure segment is exactly 10 seconds
151
+ target_length = SAMPLE_RATE * DURATION # 22050 * 10 = 220500 samples
152
 
153
  if len(y_segment) < target_length:
154
+ # Pad with zeros
155
  padding = target_length - len(y_segment)
156
  y_segment = np.pad(y_segment, (0, padding), mode='constant')
157
  elif len(y_segment) > target_length:
158
+ # Truncate
159
  y_segment = y_segment[:target_length]
160
 
161
+ # Normalize
162
  y_segment = y_segment / (np.max(np.abs(y_segment)) + 1e-10)
163
+
164
+ # Create mel-spectrogram
165
  mel_spec = create_mel_spectrogram(y_segment, sr)
166
+
167
+ # Add batch and channel dimensions
168
  mel_spec_input = mel_spec[np.newaxis, ..., np.newaxis]
169
+
170
+ # Predict
171
  probability = model.predict(mel_spec_input, verbose=0)[0][0]
172
 
173
  return float(probability)
174
 
175
  def analyze_audio_segments(y, sr, model, segment_duration=10):
176
+ """
177
+ Analyze audio in segments - EXACT from Kaggle cleaning pipeline
178
+ """
179
  segment_length = int(segment_duration * sr)
180
  total_duration = len(y) / sr
181
  num_segments = int(np.ceil(total_duration / segment_duration))
 
188
  start_sample = i * segment_length
189
  end_sample = min(start_sample + segment_length, len(y))
190
 
191
+ # Extract segment
192
  segment = y[start_sample:end_sample]
193
 
194
+ # Pad if too short
195
  if len(segment) < segment_length:
196
  padding = segment_length - len(segment)
197
  segment = np.pad(segment, (0, padding), mode='constant')
198
 
199
+ # Resample to 22050 Hz (training sample rate)
200
  if sr != SAMPLE_RATE:
201
  segment = librosa.resample(segment, orig_sr=sr, target_sr=SAMPLE_RATE)
202
 
203
+ # Predict using EXACT Kaggle method
204
  probability = predict_snoring_segment(segment, SAMPLE_RATE, model)
205
  is_snoring = probability > 0.5
206
 
 
215
 
216
  segment_results.append(segment_result)
217
 
218
+ # Progress and logging
219
  if is_snoring:
220
  print(f" โœ… Segment {i+1}/{num_segments}: SNORE (prob: {probability:.4f})")
221
+ else:
222
+ print(f" โฌœ Segment {i+1}/{num_segments}: No snore (prob: {probability:.4f})")
223
 
224
  return segment_results
225
 
226
  # ========================================
227
+ # BREATHING & SLEEP RECORDING VALIDATION
228
  # ========================================
229
 
230
+ def has_speech_in_segment(y_segment, sr):
231
+ """Check if segment contains human speech"""
232
+ if len(y_segment) < sr * 0.2:
233
+ return False
234
+
235
+ zcr = np.mean(librosa.feature.zero_crossing_rate(y_segment)[0])
236
+ if zcr > 0.30:
237
+ return True
238
+
239
+ centroid = np.mean(librosa.feature.spectral_centroid(y=y_segment, sr=sr)[0])
240
+ if centroid > 3000:
241
+ return True
242
+
243
+ stft = np.abs(librosa.stft(y_segment))
244
+ freq_bins = librosa.fft_frequencies(sr=sr)
245
+ speech_range = (freq_bins >= 2000) & (freq_bins <= 8000)
246
+ speech_energy = np.sum(stft[speech_range, :])
247
+ total_energy = np.sum(stft) + 1e-10
248
+ speech_ratio = speech_energy / total_energy
249
+
250
+ if speech_ratio > 0.4:
251
+ return True
252
+
253
+ return False
254
+
255
+ def is_human_breathing_sound(y_segment, sr):
256
+ """Detect if segment contains breathing sounds"""
257
+ if len(y_segment) < sr * 0.05:
258
+ return False
259
+
260
+ rms_energy = np.sqrt(np.mean(y_segment**2))
261
+
262
+ if rms_energy < 0.002 or rms_energy > 0.8:
263
+ return False
264
+
265
+ stft = np.abs(librosa.stft(y_segment))
266
+ freq_bins = librosa.fft_frequencies(sr=sr)
267
+
268
+ breathing_range = (freq_bins >= 200) & (freq_bins <= 2000)
269
+ breathing_energy = np.sum(stft[breathing_range, :])
270
+ total_energy = np.sum(stft) + 1e-10
271
+ breathing_ratio = breathing_energy / total_energy
272
+
273
+ if breathing_ratio < 0.2:
274
+ return False
275
+
276
+ zcr = librosa.feature.zero_crossing_rate(y_segment)[0]
277
+ mean_zcr = np.mean(zcr)
278
+
279
+ if mean_zcr > 0.25:
280
+ return False
281
+
282
+ return True
283
+
284
+ def validate_sleep_recording(y, sr):
285
+ """Validate that this is a sleep recording"""
286
  duration = len(y) / sr
287
+
288
+ if duration < 20:
289
+ return False, 0.0, "Audio too short (< 20 seconds)"
290
+
291
  rms_energy = np.sqrt(np.mean(y**2))
292
+ if rms_energy < 0.001:
293
+ return False, 0.0, "Audio is blank/silent"
294
+
295
+ if rms_energy > 0.6:
296
+ return False, 0.0, "Audio too loud (not a sleep recording)"
297
 
298
+ num_samples = min(10, int(duration / 5))
299
+ breathing_segments = 0
300
+
301
+ for i in range(num_samples):
302
+ start = int((duration / num_samples) * i * sr)
303
+ end = int(start + 3 * sr)
304
+ if end > len(y):
305
+ end = len(y)
306
+ segment = y[start:end]
307
+
308
+ if is_human_breathing_sound(segment, sr):
309
+ breathing_segments += 1
310
 
311
+ breathing_confidence = breathing_segments / num_samples
 
 
312
 
 
313
  stft = np.abs(librosa.stft(y))
314
  freq_bins = librosa.fft_frequencies(sr=sr)
315
 
 
316
  low_freq_energy = np.sum(stft[freq_bins < 2000, :])
317
  high_freq_energy = np.sum(stft[freq_bins > 4000, :])
 
318
  total_energy = np.sum(stft) + 1e-10
319
 
320
  low_freq_ratio = low_freq_energy / total_energy
321
  high_freq_ratio = high_freq_energy / total_energy
 
322
 
323
+ if low_freq_ratio < 0.4:
324
+ return False, breathing_confidence, "Frequency distribution doesn't match sleep audio"
 
325
 
326
+ if high_freq_ratio > 0.3:
327
+ return False, breathing_confidence, "Too much high-frequency content (speech/TV detected)"
 
328
 
329
+ if breathing_confidence >= 0.3:
330
+ return True, breathing_confidence, "Valid sleep recording detected"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  else:
332
+ return False, breathing_confidence, "Insufficient breathing patterns detected"
333
 
334
  # --- API ENDPOINTS ---
335
 
 
341
  "model_loaded": finetuned_snore_model is not None,
342
  "model_type": "Fine-tuned CNN with Kaggle preprocessing",
343
  "model_accuracy": "95.33%",
344
+ "model_auc": "99.43%",
345
+ "preprocessing": "Exact match to Kaggle cleaning pipeline"
346
  }
347
 
348
  @app.post("/analyze")
349
  async def analyze_audio(file: UploadFile = File(...)):
350
+ """Analyze audio using Kaggle preprocessing method"""
351
 
352
+ # Check if model is loaded
353
  if finetuned_snore_model is None:
354
  raise HTTPException(
355
  status_code=503,
 
358
 
359
  temp_path = f"temp_{os.getpid()}_{file.filename}"
360
 
361
+ # Save uploaded file
362
  with open(temp_path, "wb") as buffer:
363
  shutil.copyfileobj(file.file, buffer)
364
 
365
  try:
366
+ # 1. LOAD: Use original sample rate, will resample per segment
367
  print(f"\n๐Ÿ”Š Loading audio: {file.filename}")
368
  y, sr = librosa.load(temp_path, sr=None, dtype=np.float32)
369
+ print(f" Original sample rate: {sr} Hz")
370
+ print(f" Duration: {len(y)/sr:.1f}s")
371
 
372
+ # 2. VALIDATE: Check if valid sleep recording
373
+ print("\n๐Ÿ” Validating sleep recording...")
374
+ is_valid_recording, breathing_confidence, validation_reason = validate_sleep_recording(y, sr)
375
 
376
+ print(f" Validation: {'โœ… PASS' if is_valid_recording else 'โš ๏ธ WARNING'}")
377
+ print(f" Breathing confidence: {breathing_confidence*100:.1f}%")
378
+ print(f" Reason: {validation_reason}")
 
 
 
 
379
 
380
+ validation_warning = None if is_valid_recording else validation_reason
 
381
 
382
+ # 3. CLEAN: Apply noise reduction
383
+ print("\n๐Ÿงน Applying noise reduction...")
384
  y_clean = nr.reduce_noise(y=y, sr=sr, stationary=True)
385
 
386
+ # 4. ANALYZE: Use EXACT Kaggle method
387
+ print(f"\n๐ŸŽฏ Analyzing with fine-tuned model (Kaggle method)...")
388
  segment_results = analyze_audio_segments(y_clean, sr, finetuned_snore_model, segment_duration=10)
389
 
390
+ # 5. COUNT SNORING
391
  snore_count = sum(1 for seg in segment_results if seg['is_snoring'])
392
  total_segments = len(segment_results)
393
 
394
+ print(f"\n๐Ÿ“Š Results:")
395
  print(f" Total segments: {total_segments}")
396
  print(f" Snoring segments: {snore_count}")
397
  print(f" Snoring percentage: {(snore_count/total_segments)*100:.1f}%")
 
400
  annotations = []
401
  apnea_count = 0
402
 
403
+ # Add snoring annotations
404
  for seg in segment_results:
405
  if seg['is_snoring']:
406
  annotations.append({
 
412
  "confidence": round(seg['confidence'], 4)
413
  })
414
 
415
+ # Detect apnea (gaps between snoring)
416
  for i in range(len(segment_results) - 1):
417
  current = segment_results[i]
418
  next_seg = segment_results[i + 1]
419
 
420
+ # If current is snoring and next is not, check gap
421
  if current['is_snoring'] and not next_seg['is_snoring']:
422
+ gap_start = current['end_time']
423
+ # Find next snoring segment
424
  for j in range(i + 1, len(segment_results)):
425
  if segment_results[j]['is_snoring']:
426
+ gap_end = segment_results[j]['start_time']
427
+ gap_duration = gap_end - gap_start
428
 
429
+ # Apnea = 10-120 second gaps
430
  if 10.0 <= gap_duration <= 120.0:
431
  apnea_count += 1
432
  risk = "LOW" if gap_duration < 15.0 else ("MEDIUM" if gap_duration < 20.0 else "HIGH")
433
  annotations.append({
434
  "label": "APNEA",
435
+ "start_sec": round(gap_start, 2),
436
+ "end_sec": round(gap_end, 2),
437
  "duration": round(gap_duration, 2),
438
  "risk_level": risk
439
  })
440
+ print(f" โš ๏ธ Apnea detected: {gap_duration:.1f}s gap")
441
  break
442
 
443
+ # 7. Calculate statistics
444
  total_duration = segment_results[-1]['end_time']
445
  duration_hours = total_duration / 3600
446
  ahi = apnea_count / duration_hours if duration_hours > 0 else 0
447
 
448
  snoring_percentage = (snore_count / total_segments) * 100
449
 
450
+ risk_summary = "NORMAL"
451
+ if snoring_percentage > 50:
452
  risk_summary = "HIGH"
453
+ elif snoring_percentage > 30:
454
  risk_summary = "MEDIUM"
455
+ elif snoring_percentage > 15:
456
  risk_summary = "LOW"
 
 
457
 
458
  # Cleanup
459
  del y, y_clean
 
461
 
462
  return {
463
  "valid_recording": True,
464
+ "validation_warning": validation_warning,
465
+ "breathing_confidence": round(breathing_confidence, 4),
466
  "snore_count": snore_count,
467
  "total_segments": total_segments,
468
  "snoring_percentage": round(snoring_percentage, 2),
 
470
  "overall_risk": risk_summary,
471
  "ahi_score": round(ahi, 1),
472
  "model_used": "Fine-tuned CNN (Kaggle method)",
473
+ "model_accuracy": "95.33%",
474
  "events": annotations
475
  }
476
 
 
 
477
  except Exception as e:
478
  print(f"๐Ÿ”ฅ ERROR: {str(e)}")
479
  import traceback