trixy194t commited on
Commit
41c1604
·
verified ·
1 Parent(s): a18dc26

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -42
app.py CHANGED
@@ -634,59 +634,69 @@ def analyze_audio_segments(y, sr, model, segment_duration=10):
634
  return segment_results
635
 
636
 
637
- # ========================================
638
- # HYBRID APNEA DETECTION - NEW!
639
- # ========================================
640
  # ========================================
641
  # HYBRID APNEA DETECTION - RELAXED VALIDATION
642
  # ========================================
643
- def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
644
  """
645
  HYBRID APPROACH - Detects apnea ONLY between snoring periods
646
- RELAXED VALIDATION: More lenient gap silence threshold
647
-
 
 
648
  Risk Levels:
649
  - 10-15 seconds: LOW
650
  - 15-20 seconds: MEDIUM
651
  - 20+ seconds: HIGH
652
-
653
  Returns: (apnea_count, apnea_events_for_annotations)
654
  """
655
  print("\n🎯 APNEA DETECTION (Only between SNORING)")
656
  print(" Stage 1: Identifying confirmed snoring periods...")
657
-
658
  # STAGE 1: Get all confirmed snoring segments from segment_results
 
 
 
 
659
  snoring_periods = []
660
-
661
- for seg in segment_results:
662
  if seg['is_snoring']: # Only HIGH and MEDIUM confidence snores
663
  snoring_periods.append({
664
  'start': seg['start_time'],
665
  'end': seg['end_time'],
666
  'confidence': seg['confidence_level']
667
  })
668
-
669
  print(f" Found {len(snoring_periods)} snoring periods")
670
-
671
  if len(snoring_periods) < 2:
672
  print(" ⚠️ Need at least 2 snoring periods to detect apnea")
673
  return 0, []
674
-
675
  # STAGE 2: Find gaps BETWEEN snoring periods
676
- print(" Stage 2: Finding gaps between snoring periods...")
677
-
 
 
 
678
  apnea_candidates = []
679
-
680
  for i in range(len(snoring_periods) - 1):
681
  current_snore = snoring_periods[i]
682
  next_snore = snoring_periods[i + 1]
683
-
684
  # Calculate gap between end of current snore and start of next snore
685
  gap_start = current_snore['end']
686
  gap_end = next_snore['start']
687
  gap_duration = gap_end - gap_start
688
-
689
- # Only consider gaps in apnea range
 
 
 
 
 
690
  if min_apnea <= gap_duration <= max_apnea:
691
  apnea_candidates.append({
692
  'start': gap_start,
@@ -696,45 +706,48 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
696
  'after_snore': next_snore
697
  })
698
  print(f" Found gap: {gap_start:.1f}s → {gap_end:.1f}s (duration: {gap_duration:.1f}s)")
699
-
 
 
 
700
  print(f" Found {len(apnea_candidates)} potential apnea candidates")
701
-
702
  # STAGE 3: Validate with ML model - RELAXED THRESHOLDS
703
  validated_apneas = []
704
-
705
  if len(apnea_candidates) > 0:
706
  if finetuned_snore_model is not None:
707
  print(" Stage 3: ML validation (RELAXED - confirming reduced activity in gap)...")
708
-
709
  for idx, candidate in enumerate(apnea_candidates):
710
  # Extract gap audio
711
  gap_start_sample = int(candidate['start'] * sr)
712
  gap_end_sample = int(candidate['end'] * sr)
713
  gap_audio = y[gap_start_sample:gap_end_sample]
714
-
715
  try:
716
  # Resample to 22050 for model
717
  if sr != SAMPLE_RATE:
718
  gap_audio = librosa.resample(gap_audio, orig_sr=sr, target_sr=SAMPLE_RATE)
719
-
720
  # Validate that the gap has REDUCED snoring activity
721
  gap_prob = predict_snoring_segment(gap_audio, SAMPLE_RATE, finetuned_snore_model) if len(gap_audio) > sr else 0
722
-
723
  # RELAXED VALIDATION - Three levels of acceptance
724
-
725
  # Level 1: Very silent gap (HIGH confidence apnea)
726
  very_silent = gap_prob < 0.40
727
-
728
  # Level 2: Moderately quiet gap (MEDIUM confidence apnea)
729
  moderately_quiet = gap_prob < 0.65
730
-
731
  # Level 3: Less snoring than before/after (LOW confidence apnea)
732
  # Accept if gap has less activity than surrounding snoring
733
  less_activity = gap_prob < 0.75
734
-
735
  # Accept if ANY level matches
736
  is_valid = very_silent or moderately_quiet or less_activity
737
-
738
  if is_valid:
739
  # Determine confidence based on gap silence
740
  if very_silent:
@@ -743,7 +756,7 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
743
  validation_confidence = "MEDIUM"
744
  else:
745
  validation_confidence = "LOW"
746
-
747
  # Determine risk level based on duration
748
  duration = candidate['duration']
749
  if duration < 15:
@@ -752,7 +765,7 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
752
  risk_level = "MEDIUM"
753
  else: # >= 20 seconds
754
  risk_level = "HIGH"
755
-
756
  validated_apneas.append({
757
  'label': 'APNEA',
758
  'start_sec': round(candidate['start'], 2),
@@ -771,10 +784,10 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
771
  else:
772
  print(f" ❌ REJECTED: {candidate['start']:.1f}s → {candidate['end']:.1f}s "
773
  f"(Gap has too much snoring: {gap_prob:.2f})")
774
-
775
  except Exception as e:
776
  print(f" ⚠️ Validation error for candidate {idx+1}: {e}")
777
-
778
  # On error, accept gaps >= 12 seconds
779
  if candidate['duration'] >= 12:
780
  duration = candidate['duration']
@@ -784,7 +797,7 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
784
  risk_level = "MEDIUM"
785
  else:
786
  risk_level = "HIGH"
787
-
788
  validated_apneas.append({
789
  'label': 'APNEA',
790
  'start_sec': round(candidate['start'], 2),
@@ -805,7 +818,7 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
805
  risk_level = "MEDIUM"
806
  else:
807
  risk_level = "HIGH"
808
-
809
  validated_apneas.append({
810
  'label': 'APNEA',
811
  'start_sec': round(candidate['start'], 2),
@@ -813,9 +826,9 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
813
  'duration': round(candidate['duration'], 2),
814
  'risk_level': risk_level
815
  })
816
-
817
  print(f" ✅ Final: {len(validated_apneas)} validated apnea events\n")
818
-
819
  return len(validated_apneas), validated_apneas
820
 
821
 
@@ -983,13 +996,14 @@ async def analyze_audio(file: UploadFile = File(...)):
983
 
984
  annotations.append(annotation)
985
 
986
- # NEW: HYBRID APNEA DETECTION (replaces old segment-based method)
 
987
  apnea_count, apnea_events = count_precise_apneas(
988
  y_clean,
989
  sr,
990
  segment_results,
991
- min_apnea=10,
992
- max_apnea=90
993
  )
994
 
995
  # Add apnea events to annotations
 
634
  return segment_results
635
 
636
 
 
 
 
637
  # ========================================
638
  # HYBRID APNEA DETECTION - RELAXED VALIDATION
639
  # ========================================
640
+ def count_precise_apneas(y, sr, segment_results, min_apnea=1, max_apnea=60):
641
  """
642
  HYBRID APPROACH - Detects apnea ONLY between snoring periods
643
+ FIXED: Apnea is only valid when there is confirmed snoring IMMEDIATELY
644
+ before AND immediately after the gap (consecutive snoring periods).
645
+ Gap silence range: 1 to 60 seconds.
646
+
647
  Risk Levels:
648
  - 10-15 seconds: LOW
649
  - 15-20 seconds: MEDIUM
650
  - 20+ seconds: HIGH
651
+
652
  Returns: (apnea_count, apnea_events_for_annotations)
653
  """
654
  print("\n🎯 APNEA DETECTION (Only between SNORING)")
655
  print(" Stage 1: Identifying confirmed snoring periods...")
656
+
657
  # STAGE 1: Get all confirmed snoring segments from segment_results
658
+ # Build a full ordered list of all segments with their snoring status
659
+ # so we can check immediate neighbors
660
+ all_segments_ordered = sorted(segment_results, key=lambda s: s['start_time'])
661
+
662
  snoring_periods = []
663
+ for seg in all_segments_ordered:
 
664
  if seg['is_snoring']: # Only HIGH and MEDIUM confidence snores
665
  snoring_periods.append({
666
  'start': seg['start_time'],
667
  'end': seg['end_time'],
668
  'confidence': seg['confidence_level']
669
  })
670
+
671
  print(f" Found {len(snoring_periods)} snoring periods")
672
+
673
  if len(snoring_periods) < 2:
674
  print(" ⚠️ Need at least 2 snoring periods to detect apnea")
675
  return 0, []
676
+
677
  # STAGE 2: Find gaps BETWEEN snoring periods
678
+ # FIXED: Only consider CONSECUTIVE snoring periods (i and i+1 in the
679
+ # snoring_periods list). This guarantees that the segment immediately
680
+ # before the gap is snoring AND the segment immediately after is snoring.
681
+ print(" Stage 2: Finding gaps between consecutive snoring periods...")
682
+
683
  apnea_candidates = []
684
+
685
  for i in range(len(snoring_periods) - 1):
686
  current_snore = snoring_periods[i]
687
  next_snore = snoring_periods[i + 1]
688
+
689
  # Calculate gap between end of current snore and start of next snore
690
  gap_start = current_snore['end']
691
  gap_end = next_snore['start']
692
  gap_duration = gap_end - gap_start
693
+
694
+ # FIXED: The gap must be > 0 (there must actually be a silent gap
695
+ # between two snoring segments that are adjacent in the snoring list)
696
+ if gap_duration <= 0:
697
+ continue
698
+
699
+ # FIXED: Only consider gaps in the 1–60 second range
700
  if min_apnea <= gap_duration <= max_apnea:
701
  apnea_candidates.append({
702
  'start': gap_start,
 
706
  'after_snore': next_snore
707
  })
708
  print(f" Found gap: {gap_start:.1f}s → {gap_end:.1f}s (duration: {gap_duration:.1f}s)")
709
+ else:
710
+ print(f" Skipping gap: {gap_start:.1f}s → {gap_end:.1f}s "
711
+ f"(duration: {gap_duration:.1f}s) - outside 1-60s range")
712
+
713
  print(f" Found {len(apnea_candidates)} potential apnea candidates")
714
+
715
  # STAGE 3: Validate with ML model - RELAXED THRESHOLDS
716
  validated_apneas = []
717
+
718
  if len(apnea_candidates) > 0:
719
  if finetuned_snore_model is not None:
720
  print(" Stage 3: ML validation (RELAXED - confirming reduced activity in gap)...")
721
+
722
  for idx, candidate in enumerate(apnea_candidates):
723
  # Extract gap audio
724
  gap_start_sample = int(candidate['start'] * sr)
725
  gap_end_sample = int(candidate['end'] * sr)
726
  gap_audio = y[gap_start_sample:gap_end_sample]
727
+
728
  try:
729
  # Resample to 22050 for model
730
  if sr != SAMPLE_RATE:
731
  gap_audio = librosa.resample(gap_audio, orig_sr=sr, target_sr=SAMPLE_RATE)
732
+
733
  # Validate that the gap has REDUCED snoring activity
734
  gap_prob = predict_snoring_segment(gap_audio, SAMPLE_RATE, finetuned_snore_model) if len(gap_audio) > sr else 0
735
+
736
  # RELAXED VALIDATION - Three levels of acceptance
737
+
738
  # Level 1: Very silent gap (HIGH confidence apnea)
739
  very_silent = gap_prob < 0.40
740
+
741
  # Level 2: Moderately quiet gap (MEDIUM confidence apnea)
742
  moderately_quiet = gap_prob < 0.65
743
+
744
  # Level 3: Less snoring than before/after (LOW confidence apnea)
745
  # Accept if gap has less activity than surrounding snoring
746
  less_activity = gap_prob < 0.75
747
+
748
  # Accept if ANY level matches
749
  is_valid = very_silent or moderately_quiet or less_activity
750
+
751
  if is_valid:
752
  # Determine confidence based on gap silence
753
  if very_silent:
 
756
  validation_confidence = "MEDIUM"
757
  else:
758
  validation_confidence = "LOW"
759
+
760
  # Determine risk level based on duration
761
  duration = candidate['duration']
762
  if duration < 15:
 
765
  risk_level = "MEDIUM"
766
  else: # >= 20 seconds
767
  risk_level = "HIGH"
768
+
769
  validated_apneas.append({
770
  'label': 'APNEA',
771
  'start_sec': round(candidate['start'], 2),
 
784
  else:
785
  print(f" ❌ REJECTED: {candidate['start']:.1f}s → {candidate['end']:.1f}s "
786
  f"(Gap has too much snoring: {gap_prob:.2f})")
787
+
788
  except Exception as e:
789
  print(f" ⚠️ Validation error for candidate {idx+1}: {e}")
790
+
791
  # On error, accept gaps >= 12 seconds
792
  if candidate['duration'] >= 12:
793
  duration = candidate['duration']
 
797
  risk_level = "MEDIUM"
798
  else:
799
  risk_level = "HIGH"
800
+
801
  validated_apneas.append({
802
  'label': 'APNEA',
803
  'start_sec': round(candidate['start'], 2),
 
818
  risk_level = "MEDIUM"
819
  else:
820
  risk_level = "HIGH"
821
+
822
  validated_apneas.append({
823
  'label': 'APNEA',
824
  'start_sec': round(candidate['start'], 2),
 
826
  'duration': round(candidate['duration'], 2),
827
  'risk_level': risk_level
828
  })
829
+
830
  print(f" ✅ Final: {len(validated_apneas)} validated apnea events\n")
831
+
832
  return len(validated_apneas), validated_apneas
833
 
834
 
 
996
 
997
  annotations.append(annotation)
998
 
999
+ # NEW: HYBRID APNEA DETECTION
1000
+ # FIXED: min_apnea=1, max_apnea=60 (1 to 60 second gaps only)
1001
  apnea_count, apnea_events = count_precise_apneas(
1002
  y_clean,
1003
  sr,
1004
  segment_results,
1005
+ min_apnea=1,
1006
+ max_apnea=60
1007
  )
1008
 
1009
  # Add apnea events to annotations