Update app.py
Browse files
app.py
CHANGED
|
@@ -719,7 +719,7 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=1, max_apnea=60):
|
|
| 719 |
# Check B: ML model score (gap must score < 0.40)
|
| 720 |
# BOTH must pass. This fixes the padding problem and reduces false positives.
|
| 721 |
print(" Stage 3: DUAL validation (RMS energy + ML score)...")
|
| 722 |
-
print(" Rules: gap RMS ratio < 0.
|
| 723 |
|
| 724 |
validated_apneas = []
|
| 725 |
|
|
@@ -752,7 +752,7 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=1, max_apnea=60):
|
|
| 752 |
# Avoid division by zero
|
| 753 |
rms_ratio = gap_rms / (avg_snore_rms + 1e-10)
|
| 754 |
|
| 755 |
-
rms_ok = rms_ratio < 0.
|
| 756 |
|
| 757 |
except Exception as e:
|
| 758 |
print(f" ⚠️ RMS check error for candidate {idx+1}: {e}")
|
|
@@ -834,7 +834,7 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=1, max_apnea=60):
|
|
| 834 |
else:
|
| 835 |
reasons = []
|
| 836 |
if not rms_ok:
|
| 837 |
-
reasons.append(f"gap not quiet enough (RMS ratio {rms_ratio:.3f} >= 0.
|
| 838 |
if not ml_ok:
|
| 839 |
reasons.append(f"ML says snoring in gap (score {ml_score:.3f} >= 0.40)")
|
| 840 |
print(f" → REJECTED: {', '.join(reasons)}")
|
|
@@ -847,7 +847,7 @@ def count_precise_apneas(y, sr, segment_results, min_apnea=1, max_apnea=60):
|
|
| 847 |
# VALIDATION
|
| 848 |
# ========================================
|
| 849 |
def validate_sleep_recording(y, sr):
|
| 850 |
-
"""Validate that this is a sleep recording"""
|
| 851 |
duration = len(y) / sr
|
| 852 |
|
| 853 |
if duration < 20:
|
|
@@ -855,12 +855,11 @@ def validate_sleep_recording(y, sr):
|
|
| 855 |
|
| 856 |
rms_energy = np.sqrt(np.mean(y**2))
|
| 857 |
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
if rms_energy > 0.6:
|
| 862 |
-
return False, 0.0, "Audio too loud (not a sleep recording)"
|
| 863 |
|
|
|
|
| 864 |
stft = np.abs(librosa.stft(y))
|
| 865 |
freq_bins = librosa.fft_frequencies(sr=sr)
|
| 866 |
|
|
@@ -871,11 +870,27 @@ def validate_sleep_recording(y, sr):
|
|
| 871 |
low_freq_ratio = low_freq_energy / total_energy
|
| 872 |
high_freq_ratio = high_freq_energy / total_energy
|
| 873 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
if low_freq_ratio < 0.4:
|
| 875 |
-
return False, 0.0, "Frequency distribution doesn't match sleep audio"
|
| 876 |
|
| 877 |
if high_freq_ratio > 0.3:
|
| 878 |
-
return False, 0.0, "Too much high-frequency content (
|
|
|
|
|
|
|
|
|
|
| 879 |
|
| 880 |
return True, 1.0, "Valid sleep recording detected"
|
| 881 |
|
|
|
|
| 719 |
# Check B: ML model score (gap must score < 0.40)
|
| 720 |
# BOTH must pass. This fixes the padding problem and reduces false positives.
|
| 721 |
print(" Stage 3: DUAL validation (RMS energy + ML score)...")
|
| 722 |
+
print(" Rules: gap RMS ratio < 0.55 AND ML score < 0.40")
|
| 723 |
|
| 724 |
validated_apneas = []
|
| 725 |
|
|
|
|
| 752 |
# Avoid division by zero
|
| 753 |
rms_ratio = gap_rms / (avg_snore_rms + 1e-10)
|
| 754 |
|
| 755 |
+
rms_ok = rms_ratio < 0.55 # Relaxed from 0.5 to 0.55 to account for noise reduction
|
| 756 |
|
| 757 |
except Exception as e:
|
| 758 |
print(f" ⚠️ RMS check error for candidate {idx+1}: {e}")
|
|
|
|
| 834 |
else:
|
| 835 |
reasons = []
|
| 836 |
if not rms_ok:
|
| 837 |
+
reasons.append(f"gap not quiet enough (RMS ratio {rms_ratio:.3f} >= 0.55)")
|
| 838 |
if not ml_ok:
|
| 839 |
reasons.append(f"ML says snoring in gap (score {ml_score:.3f} >= 0.40)")
|
| 840 |
print(f" → REJECTED: {', '.join(reasons)}")
|
|
|
|
| 847 |
# VALIDATION
|
| 848 |
# ========================================
|
| 849 |
def validate_sleep_recording(y, sr):
|
| 850 |
+
"""Validate that this is a sleep recording with detailed diagnostics"""
|
| 851 |
duration = len(y) / sr
|
| 852 |
|
| 853 |
if duration < 20:
|
|
|
|
| 855 |
|
| 856 |
rms_energy = np.sqrt(np.mean(y**2))
|
| 857 |
|
| 858 |
+
# Calculate zero-crossing rate
|
| 859 |
+
zcr = librosa.feature.zero_crossing_rate(y)[0]
|
| 860 |
+
zcr_mean = np.mean(zcr)
|
|
|
|
|
|
|
| 861 |
|
| 862 |
+
# Calculate spectral characteristics
|
| 863 |
stft = np.abs(librosa.stft(y))
|
| 864 |
freq_bins = librosa.fft_frequencies(sr=sr)
|
| 865 |
|
|
|
|
| 870 |
low_freq_ratio = low_freq_energy / total_energy
|
| 871 |
high_freq_ratio = high_freq_energy / total_energy
|
| 872 |
|
| 873 |
+
# DIAGNOSTIC LOGGING
|
| 874 |
+
print(f" 📊 AUDIO QUALITY DIAGNOSTICS:")
|
| 875 |
+
print(f" RMS Energy: {rms_energy:.4f} (normal: 0.001-0.6)")
|
| 876 |
+
print(f" Zero-Crossing Rate: {zcr_mean:.4f} (speech-like if > 0.2)")
|
| 877 |
+
print(f" Low Freq (<2kHz): {low_freq_ratio:.3f} (should be > 0.4)")
|
| 878 |
+
print(f" High Freq (>4kHz): {high_freq_ratio:.3f} (should be < 0.3)")
|
| 879 |
+
|
| 880 |
+
if rms_energy < 0.001:
|
| 881 |
+
return False, 0.0, "Audio is blank/silent"
|
| 882 |
+
|
| 883 |
+
if rms_energy > 0.6:
|
| 884 |
+
return False, 0.0, "Audio too loud (not a sleep recording)"
|
| 885 |
+
|
| 886 |
if low_freq_ratio < 0.4:
|
| 887 |
+
return False, 0.0, f"Frequency distribution doesn't match sleep audio (low_freq={low_freq_ratio:.3f}, needs >0.4) - FRONTEND AUDIO QUALITY ISSUE"
|
| 888 |
|
| 889 |
if high_freq_ratio > 0.3:
|
| 890 |
+
return False, 0.0, f"Too much high-frequency content (high_freq={high_freq_ratio:.3f}, needs <0.3) - FRONTEND RECORDING HAS NOISE/ARTIFACTS"
|
| 891 |
+
|
| 892 |
+
if zcr_mean > 0.2:
|
| 893 |
+
return False, 0.0, f"Zero-crossing rate too high ({zcr_mean:.3f}) - Audio sounds like speech/noise, not snoring - FRONTEND RECORDING ISSUE"
|
| 894 |
|
| 895 |
return True, 1.0, "Valid sleep recording detected"
|
| 896 |
|