trixy194t commited on
Commit
d61a3b0
·
verified ·
1 Parent(s): c90243a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -199
app.py CHANGED
@@ -638,13 +638,14 @@ def analyze_audio_segments(y, sr, model, segment_duration=10):
638
  # HYBRID APNEA DETECTION - NEW!
639
  # ========================================
640
  # ========================================
641
- # HYBRID APNEA DETECTION - RELAXED VERSION
642
  # ========================================
643
  def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
644
  """
645
- HYBRID APPROACH with RELAXED validation criteria
646
- Stage 1: Quick energy-based detection (1-second resolution)
647
- Stage 2: Validate candidates with ML model (RELAXED thresholds)
 
648
 
649
  Risk Levels:
650
  - 10-15 seconds: LOW
@@ -653,200 +654,142 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
653
 
654
  Returns: (apnea_count, apnea_events_for_annotations)
655
  """
656
- print("\n🎯 HYBRID APNEA DETECTION (RELAXED)")
657
- print(" Stage 1: Fast activity screening...")
658
-
659
- # STAGE 1: Quick activity detection at 1-second intervals
660
- frame_duration = 1.0
661
- frame_length = int(frame_duration * sr)
662
- num_frames = int(np.ceil(len(y) / frame_length))
663
-
664
- timeline = []
665
- for i in range(num_frames):
666
- start = i * frame_length
667
- end = min(start + frame_length, len(y))
668
- chunk = y[start:end]
669
-
670
- if len(chunk) < frame_length // 2:
671
- continue
672
-
673
- # RELAXED: More lenient energy check
674
- rms = np.sqrt(np.mean(chunk**2)) if len(chunk) > 0 else 0
675
-
676
- # Simple zero-crossing rate
677
- zcr = np.sum(np.abs(np.diff(np.sign(chunk)))) / (2 * len(chunk)) if len(chunk) > 0 else 0
678
-
679
- # RELAXED activity detection: lower threshold
680
- is_active = (
681
- rms > 0.008 and # RELAXED from 0.01 (more sensitive to quiet snoring)
682
- rms < 0.6 and # RELAXED from 0.5 (allow louder sounds)
683
- zcr < 0.18 # RELAXED from 0.15 (more tolerant)
684
- )
685
-
686
- timeline.append({
687
- 'time': i * frame_duration,
688
- 'active': is_active,
689
- 'energy': rms
690
- })
691
 
692
- active_count = sum(1 for t in timeline if t['active'])
693
- print(f" Active: {active_count}/{len(timeline)} seconds")
694
 
695
- # STAGE 2: Find gaps - RELAXED validation
696
- print(" Stage 2: Finding and validating gaps...")
 
 
 
 
697
 
698
  apnea_candidates = []
699
- in_gap = False
700
- gap_start = None
701
- last_active_time = 0
702
 
703
- for i in range(len(timeline)):
704
- current = timeline[i]
 
705
 
706
- if current['active']:
707
- # Active period
708
- if in_gap and gap_start is not None:
709
- # Gap just ended
710
- gap_duration = current['time'] - gap_start
711
-
712
- if min_apnea <= gap_duration <= max_apnea:
713
- apnea_candidates.append({
714
- 'start': gap_start,
715
- 'end': current['time'],
716
- 'duration': gap_duration,
717
- 'before_time': last_active_time,
718
- 'after_time': current['time']
719
- })
720
-
721
- in_gap = False
722
- gap_start = None
723
-
724
- last_active_time = current['time']
725
- else:
726
- # Inactive period
727
- if not in_gap:
728
- gap_start = current['time']
729
- in_gap = True
730
 
731
  print(f" Found {len(apnea_candidates)} potential apnea candidates")
732
 
733
- # STAGE 3: Validate with ML model - RELAXED THRESHOLDS
734
  validated_apneas = []
735
 
736
- if len(apnea_candidates) > 0 and finetuned_snore_model is not None:
737
- print(" Stage 3: ML validation (RELAXED thresholds)...")
738
-
739
- for idx, candidate in enumerate(apnea_candidates):
740
- # Extract audio segments around the gap
741
- buffer = 5.0 # 5 seconds before/after
742
-
743
- # Before segment (should be snoring)
744
- before_start = max(0, candidate['start'] - buffer)
745
- before_end = candidate['start']
746
- before_start_sample = int(before_start * sr)
747
- before_end_sample = int(before_end * sr)
748
- before_audio = y[before_start_sample:before_end_sample]
749
-
750
- # After segment (should be snoring)
751
- after_start = candidate['end']
752
- after_end = min(len(y)/sr, candidate['end'] + buffer)
753
- after_start_sample = int(after_start * sr)
754
- after_end_sample = int(after_end * sr)
755
- after_audio = y[after_start_sample:after_end_sample]
756
-
757
- # Gap segment (should NOT be snoring)
758
- gap_start_sample = int(candidate['start'] * sr)
759
- gap_end_sample = int(candidate['end'] * sr)
760
- gap_audio = y[gap_start_sample:gap_end_sample]
761
 
762
- # Validate with model
763
- try:
764
- # Resample to 22050 for model
765
- if sr != SAMPLE_RATE:
766
- before_audio = librosa.resample(before_audio, orig_sr=sr, target_sr=SAMPLE_RATE)
767
- after_audio = librosa.resample(after_audio, orig_sr=sr, target_sr=SAMPLE_RATE)
768
- gap_audio = librosa.resample(gap_audio, orig_sr=sr, target_sr=SAMPLE_RATE)
769
 
770
- # Get predictions
771
- before_prob = predict_snoring_segment(before_audio, SAMPLE_RATE, finetuned_snore_model) if len(before_audio) > sr else 0
772
- after_prob = predict_snoring_segment(after_audio, SAMPLE_RATE, finetuned_snore_model) if len(after_audio) > sr else 0
773
- gap_prob = predict_snoring_segment(gap_audio, SAMPLE_RATE, finetuned_snore_model) if len(gap_audio) > sr else 0
774
 
775
- # RELAXED validation thresholds
776
- snoring_threshold = 0.35 # RELAXED from 0.50 (accept more borderline cases)
777
- silence_threshold = 0.50 # RELAXED from 0.40 (accept higher gap values)
778
-
779
- # RELAXED validation logic - THREE scenarios
780
-
781
- # Scenario 1: IDEAL - Both sides snoring, gap is silent
782
- ideal_pattern = (
783
- before_prob > snoring_threshold and
784
- after_prob > snoring_threshold and
785
- gap_prob < silence_threshold
786
- )
787
-
788
- # Scenario 2: ACCEPTABLE - At least ONE side snoring, gap is silent
789
- acceptable_pattern = (
790
- (before_prob > snoring_threshold or after_prob > snoring_threshold) and
791
- gap_prob < silence_threshold
792
- )
793
-
794
- # Scenario 3: MINIMAL - Just the gap is clearly silent (energy-based detection was good enough)
795
- minimal_pattern = (
796
- gap_prob < silence_threshold - 0.1 # Very low gap probability (< 0.4)
797
- )
798
-
799
- # Accept if ANY scenario matches
800
- is_valid = ideal_pattern or acceptable_pattern or minimal_pattern
801
-
802
- if is_valid:
803
- # Determine confidence level
804
- if ideal_pattern:
805
- confidence_type = "HIGH"
806
- elif acceptable_pattern:
807
- confidence_type = "MEDIUM"
808
- else:
809
- confidence_type = "LOW"
810
 
811
- # NEW RISK LEVEL ASSIGNMENT
812
- duration = candidate['duration']
813
- if duration < 15:
814
- risk_level = "LOW"
815
- elif duration < 20:
816
- risk_level = "MEDIUM"
817
- else: # >= 20 seconds
818
- risk_level = "HIGH"
819
 
820
- validated_apneas.append({
821
- 'label': 'APNEA',
822
- 'start_sec': round(candidate['start'], 2),
823
- 'end_sec': round(candidate['end'], 2),
824
- 'duration': round(candidate['duration'], 2),
825
- 'risk_level': risk_level, # NEW: Updated risk levels
826
- 'confidence': {
827
- 'before_snoring': round(before_prob, 3),
828
- 'gap_silence': round(1 - gap_prob, 3),
829
- 'after_snoring': round(after_prob, 3),
830
- 'validation_type': confidence_type
831
- }
832
- })
833
- print(f" ✅ VALIDATED ({confidence_type}): {candidate['start']:.1f}s → {candidate['end']:.1f}s "
834
- f"(Before: {before_prob:.2f}, Gap: {gap_prob:.2f}, After: {after_prob:.2f}) - Risk: {risk_level}")
835
- else:
836
- print(f" ❌ REJECTED: {candidate['start']:.1f}s → {candidate['end']:.1f}s "
837
- f"(Before: {before_prob:.2f}, Gap: {gap_prob:.2f}, After: {after_prob:.2f})")
838
 
839
- except Exception as e:
840
- print(f" ⚠️ Validation error for candidate {idx+1}: {e}")
841
-
842
- # RELAXED: On error, accept the candidate anyway (energy-based detection was good)
843
- # NEW RISK LEVEL ASSIGNMENT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  duration = candidate['duration']
845
  if duration < 15:
846
  risk_level = "LOW"
847
  elif duration < 20:
848
  risk_level = "MEDIUM"
849
- else: # >= 20 seconds
850
  risk_level = "HIGH"
851
 
852
  validated_apneas.append({
@@ -854,30 +797,8 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=10, max_apnea=90):
854
  'start_sec': round(candidate['start'], 2),
855
  'end_sec': round(candidate['end'], 2),
856
  'duration': round(candidate['duration'], 2),
857
- 'risk_level': risk_level # NEW: Updated risk levels
858
  })
859
- print(f" ⚠️ ACCEPTED (validation skipped): {candidate['start']:.1f}s → {candidate['end']:.1f}s - Risk: {risk_level}")
860
- continue
861
- else:
862
- # No model validation - ACCEPT all energy-based candidates
863
- print(" Stage 3: Skipping ML validation, accepting all candidates...")
864
- for candidate in apnea_candidates:
865
- # NEW RISK LEVEL ASSIGNMENT
866
- duration = candidate['duration']
867
- if duration < 15:
868
- risk_level = "LOW"
869
- elif duration < 20:
870
- risk_level = "MEDIUM"
871
- else: # >= 20 seconds
872
- risk_level = "HIGH"
873
-
874
- validated_apneas.append({
875
- 'label': 'APNEA',
876
- 'start_sec': round(candidate['start'], 2),
877
- 'end_sec': round(candidate['end'], 2),
878
- 'duration': round(candidate['duration'], 2),
879
- 'risk_level': risk_level # NEW: Updated risk levels
880
- })
881
 
882
  print(f" ✅ Final: {len(validated_apneas)} validated apnea events\n")
883
 
 
638
  # HYBRID APNEA DETECTION - NEW!
639
  # ========================================
640
  # ========================================
641
+ # HYBRID APNEA DETECTION - ONLY BETWEEN SNORING
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
+ Stage 1: Use segment_results to find confirmed snoring periods
647
+ Stage 2: Find gaps between snoring periods
648
+ Stage 3: Validate gaps with ML model
649
 
650
  Risk Levels:
651
  - 10-15 seconds: LOW
 
654
 
655
  Returns: (apnea_count, apnea_events_for_annotations)
656
  """
657
+ print("\n🎯 APNEA DETECTION (Only between SNORING)")
658
+ print(" Stage 1: Identifying confirmed snoring periods...")
659
+
660
+ # STAGE 1: Get all confirmed snoring segments from segment_results
661
+ snoring_periods = []
662
+
663
+ for seg in segment_results:
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
+ print(" Stage 2: Finding gaps between snoring periods...")
679
 
680
  apnea_candidates = []
 
 
 
681
 
682
+ for i in range(len(snoring_periods) - 1):
683
+ current_snore = snoring_periods[i]
684
+ next_snore = snoring_periods[i + 1]
685
 
686
+ # Calculate gap between end of current snore and start of next snore
687
+ gap_start = current_snore['end']
688
+ gap_end = next_snore['start']
689
+ gap_duration = gap_end - gap_start
690
+
691
+ # Only consider gaps in apnea range
692
+ if min_apnea <= gap_duration <= max_apnea:
693
+ apnea_candidates.append({
694
+ 'start': gap_start,
695
+ 'end': gap_end,
696
+ 'duration': gap_duration,
697
+ 'before_snore': current_snore,
698
+ 'after_snore': next_snore
699
+ })
700
+ print(f" Found gap: {gap_start:.1f}s → {gap_end:.1f}s (duration: {gap_duration:.1f}s)")
 
 
 
 
 
 
 
 
 
701
 
702
  print(f" Found {len(apnea_candidates)} potential apnea candidates")
703
 
704
+ # STAGE 3: Validate with ML model
705
  validated_apneas = []
706
 
707
+ if len(apnea_candidates) > 0:
708
+ if finetuned_snore_model is not None:
709
+ print(" Stage 3: ML validation (confirming silence in gap)...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710
 
711
+ for idx, candidate in enumerate(apnea_candidates):
712
+ # We already KNOW there's snoring before and after (from segment_results)
713
+ # We just need to confirm the GAP is actually silent
 
 
 
 
714
 
715
+ # Extract gap audio
716
+ gap_start_sample = int(candidate['start'] * sr)
717
+ gap_end_sample = int(candidate['end'] * sr)
718
+ gap_audio = y[gap_start_sample:gap_end_sample]
719
 
720
+ try:
721
+ # Resample to 22050 for model
722
+ if sr != SAMPLE_RATE:
723
+ gap_audio = librosa.resample(gap_audio, orig_sr=sr, target_sr=SAMPLE_RATE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
 
725
+ # Validate that the gap is NOT snoring (should be silent)
726
+ gap_prob = predict_snoring_segment(gap_audio, SAMPLE_RATE, finetuned_snore_model) if len(gap_audio) > sr else 0
 
 
 
 
 
 
727
 
728
+ # RELAXED threshold: gap should have low snoring probability
729
+ silence_threshold = 0.50 # Gap should be < 0.50 snoring probability
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
 
731
+ is_silent = gap_prob < silence_threshold
732
+
733
+ if is_silent:
734
+ # Determine risk level
735
+ duration = candidate['duration']
736
+ if duration < 15:
737
+ risk_level = "LOW"
738
+ elif duration < 20:
739
+ risk_level = "MEDIUM"
740
+ else: # >= 20 seconds
741
+ risk_level = "HIGH"
742
+
743
+ validated_apneas.append({
744
+ 'label': 'APNEA',
745
+ 'start_sec': round(candidate['start'], 2),
746
+ 'end_sec': round(candidate['end'], 2),
747
+ 'duration': round(candidate['duration'], 2),
748
+ 'risk_level': risk_level,
749
+ 'confidence': {
750
+ 'before_snoring': candidate['before_snore']['confidence'],
751
+ 'gap_silence': round(1 - gap_prob, 3),
752
+ 'after_snoring': candidate['after_snore']['confidence']
753
+ }
754
+ })
755
+ print(f" ✅ VALIDATED: {candidate['start']:.1f}s → {candidate['end']:.1f}s "
756
+ f"(Gap silence: {gap_prob:.2f}) - Risk: {risk_level}")
757
+ else:
758
+ print(f" ❌ REJECTED: {candidate['start']:.1f}s → {candidate['end']:.1f}s "
759
+ f"(Gap has snoring: {gap_prob:.2f})")
760
+
761
+ except Exception as e:
762
+ print(f" ⚠️ Validation error for candidate {idx+1}: {e}")
763
+
764
+ # On error, be conservative - still accept if gap is long enough
765
+ if candidate['duration'] >= 15:
766
+ duration = candidate['duration']
767
+ if duration < 15:
768
+ risk_level = "LOW"
769
+ elif duration < 20:
770
+ risk_level = "MEDIUM"
771
+ else:
772
+ risk_level = "HIGH"
773
+
774
+ validated_apneas.append({
775
+ 'label': 'APNEA',
776
+ 'start_sec': round(candidate['start'], 2),
777
+ 'end_sec': round(candidate['end'], 2),
778
+ 'duration': round(candidate['duration'], 2),
779
+ 'risk_level': risk_level
780
+ })
781
+ print(f" ⚠️ ACCEPTED (validation skipped): {candidate['start']:.1f}s → {candidate['end']:.1f}s - Risk: {risk_level}")
782
+ continue
783
+ else:
784
+ # No model - accept all candidates between snoring periods
785
+ print(" Stage 3: No ML model, accepting all gaps between snoring...")
786
+ for candidate in apnea_candidates:
787
  duration = candidate['duration']
788
  if duration < 15:
789
  risk_level = "LOW"
790
  elif duration < 20:
791
  risk_level = "MEDIUM"
792
+ else:
793
  risk_level = "HIGH"
794
 
795
  validated_apneas.append({
 
797
  'start_sec': round(candidate['start'], 2),
798
  'end_sec': round(candidate['end'], 2),
799
  'duration': round(candidate['duration'], 2),
800
+ 'risk_level': risk_level
801
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
 
803
  print(f" ✅ Final: {len(validated_apneas)} validated apnea events\n")
804