Update app.py
Browse files
app.py
CHANGED
|
@@ -154,31 +154,33 @@ try:
|
|
| 154 |
|
| 155 |
except Exception as e1:
|
| 156 |
print(f" Method 1 failed: {str(e1)[:150]}")
|
| 157 |
-
print(" Trying Method 2:
|
| 158 |
|
| 159 |
try:
|
| 160 |
-
# Build
|
| 161 |
new_counting_model = models.Sequential([
|
| 162 |
-
|
| 163 |
-
layers.Conv2D(32, (3, 3), activation='relu',
|
| 164 |
-
layers.BatchNormalization(),
|
| 165 |
layers.MaxPooling2D((2, 2)),
|
| 166 |
-
|
| 167 |
-
layers.Conv2D(64, (3, 3), activation='relu', padding='same'),
|
| 168 |
layers.BatchNormalization(),
|
|
|
|
|
|
|
|
|
|
| 169 |
layers.MaxPooling2D((2, 2)),
|
| 170 |
-
|
| 171 |
-
layers.Conv2D(128, (3, 3), activation='relu', padding='same'),
|
| 172 |
layers.BatchNormalization(),
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
| 174 |
|
|
|
|
| 175 |
layers.Dense(128, activation='relu'),
|
| 176 |
-
layers.Dropout(0.
|
| 177 |
layers.Dense(1, activation='sigmoid')
|
| 178 |
])
|
| 179 |
|
| 180 |
new_counting_model.load_weights(NEW_MODEL_PATH)
|
| 181 |
-
print("✅ New Counting Model Loaded Successfully (Method 2 -
|
| 182 |
print(f" Model input shape: {new_counting_model.input_shape}")
|
| 183 |
print(f" Model output shape: {new_counting_model.output_shape}")
|
| 184 |
|
|
@@ -269,36 +271,39 @@ def predict_snoring_segment(y_segment, sr, model):
|
|
| 269 |
# ========================================
|
| 270 |
|
| 271 |
def extract_spectrogram_for_new_model(y_segment, sr):
|
| 272 |
-
"""Extract spectrogram for new model
|
| 273 |
-
# Resample to 16kHz
|
| 274 |
if sr != NEW_MODEL_SAMPLE_RATE:
|
| 275 |
y_segment = librosa.resample(y_segment, orig_sr=sr, target_sr=NEW_MODEL_SAMPLE_RATE)
|
| 276 |
sr = NEW_MODEL_SAMPLE_RATE
|
| 277 |
|
| 278 |
-
# Ensure exact length
|
| 279 |
-
target_len = NEW_MODEL_SAMPLE_RATE * NEW_MODEL_DURATION
|
| 280 |
if len(y_segment) < target_len:
|
| 281 |
y_segment = np.pad(y_segment, (0, target_len - len(y_segment)))
|
| 282 |
else:
|
| 283 |
y_segment = y_segment[:target_len]
|
| 284 |
|
| 285 |
-
#
|
| 286 |
-
|
| 287 |
-
log_spec = librosa.power_to_db(
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
| 290 |
return log_spec[..., np.newaxis]
|
| 291 |
|
| 292 |
def count_snores_in_segment(y_segment, sr, model, threshold=0.7):
|
| 293 |
"""
|
| 294 |
Count individual snores within a detected snoring segment
|
| 295 |
Uses sliding 2-second window to detect multiple snore events
|
|
|
|
| 296 |
"""
|
| 297 |
if model is None:
|
| 298 |
return 0
|
| 299 |
|
| 300 |
try:
|
| 301 |
-
# Resample to 16kHz for new model
|
| 302 |
if sr != NEW_MODEL_SAMPLE_RATE:
|
| 303 |
y_segment = librosa.resample(y_segment, orig_sr=sr, target_sr=NEW_MODEL_SAMPLE_RATE)
|
| 304 |
sr = NEW_MODEL_SAMPLE_RATE
|
|
@@ -307,16 +312,18 @@ def count_snores_in_segment(y_segment, sr, model, threshold=0.7):
|
|
| 307 |
is_snoring = False
|
| 308 |
|
| 309 |
# Slide a 2-second window across the segment
|
| 310 |
-
window_size = 2 * sr
|
| 311 |
step_size = int(0.5 * sr) # 0.5s steps for high resolution
|
| 312 |
|
| 313 |
for start in range(0, len(y_segment) - window_size, step_size):
|
| 314 |
chunk = y_segment[start : start + window_size]
|
| 315 |
|
| 316 |
-
# Process chunk to spectrogram
|
| 317 |
-
|
| 318 |
-
log_spec = librosa.power_to_db(
|
| 319 |
-
|
|
|
|
|
|
|
| 320 |
|
| 321 |
# Predict
|
| 322 |
prediction = model.predict(log_spec[np.newaxis, ..., np.newaxis], verbose=0)
|
|
@@ -766,7 +773,7 @@ async def analyze_audio(file: UploadFile = File(...)):
|
|
| 766 |
|
| 767 |
annotations.append(annotation)
|
| 768 |
|
| 769 |
-
# Apnea detection -
|
| 770 |
for i in range(len(segment_results) - 1):
|
| 771 |
current = segment_results[i]
|
| 772 |
if current['is_snoring'] and current['confidence_level'] in ['HIGH', 'MEDIUM']:
|
|
@@ -774,35 +781,27 @@ async def analyze_audio(file: UploadFile = File(...)):
|
|
| 774 |
if segment_results[j]['is_snoring'] and segment_results[j]['confidence_level'] in ['HIGH', 'MEDIUM']:
|
| 775 |
gap_duration = segment_results[j]['start_time'] - current['end_time']
|
| 776 |
|
| 777 |
-
#
|
| 778 |
-
if 10.0 <= gap_duration <=
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
"label": "APNEA",
|
| 787 |
-
"start_sec": round(current['end_time'], 2),
|
| 788 |
-
"end_sec": round(segment_results[j]['start_time'], 2),
|
| 789 |
-
"duration": round(gap_duration, 2),
|
| 790 |
-
"risk_level": risk,
|
| 791 |
-
"note": "Detected between high/medium confidence snoring segments"
|
| 792 |
-
})
|
| 793 |
-
print(f" ⚠️ Apnea: {gap_duration:.1f}s gap (between confident snoring segments)")
|
| 794 |
else:
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
break
|
| 807 |
|
| 808 |
# 7. STATISTICS
|
|
@@ -837,13 +836,14 @@ async def analyze_audio(file: UploadFile = File(...)):
|
|
| 837 |
"valid_recording": True,
|
| 838 |
"validation_warning": validation_warning,
|
| 839 |
"snore_count": snore_count,
|
|
|
|
| 840 |
"high_confidence_snores": high_conf_count,
|
| 841 |
"medium_confidence_snores": medium_conf_count,
|
| 842 |
"rejected_potential_snores": rejected_count,
|
| 843 |
"total_segments": total_segments,
|
| 844 |
"snoring_percentage": round(snoring_percentage, 2),
|
| 845 |
"apnea_count": apnea_count,
|
| 846 |
-
"
|
| 847 |
"ahi_score": round(ahi, 1),
|
| 848 |
"detection_mode": "STRICT: Only HIGH and MEDIUM confidence snoring detection",
|
| 849 |
"detection_note": detection_note,
|
|
|
|
| 154 |
|
| 155 |
except Exception as e1:
|
| 156 |
print(f" Method 1 failed: {str(e1)[:150]}")
|
| 157 |
+
print(" Trying Method 2: Rebuild EXACT architecture from training code...")
|
| 158 |
|
| 159 |
try:
|
| 160 |
+
# Build EXACT architecture from your training code
|
| 161 |
new_counting_model = models.Sequential([
|
| 162 |
+
# First Block: Detects basic edges in sound
|
| 163 |
+
layers.Conv2D(32, (3, 3), activation='relu', input_shape=(128, 431, 1)),
|
|
|
|
| 164 |
layers.MaxPooling2D((2, 2)),
|
|
|
|
|
|
|
| 165 |
layers.BatchNormalization(),
|
| 166 |
+
|
| 167 |
+
# Second Block: Detects complex patterns (snore textures)
|
| 168 |
+
layers.Conv2D(64, (3, 3), activation='relu'),
|
| 169 |
layers.MaxPooling2D((2, 2)),
|
|
|
|
|
|
|
| 170 |
layers.BatchNormalization(),
|
| 171 |
+
|
| 172 |
+
# Third Block
|
| 173 |
+
layers.Conv2D(128, (3, 3), activation='relu'),
|
| 174 |
+
layers.MaxPooling2D((2, 2)),
|
| 175 |
|
| 176 |
+
layers.Flatten(),
|
| 177 |
layers.Dense(128, activation='relu'),
|
| 178 |
+
layers.Dropout(0.5),
|
| 179 |
layers.Dense(1, activation='sigmoid')
|
| 180 |
])
|
| 181 |
|
| 182 |
new_counting_model.load_weights(NEW_MODEL_PATH)
|
| 183 |
+
print("✅ New Counting Model Loaded Successfully (Method 2 - EXACT Architecture Rebuild)!")
|
| 184 |
print(f" Model input shape: {new_counting_model.input_shape}")
|
| 185 |
print(f" Model output shape: {new_counting_model.output_shape}")
|
| 186 |
|
|
|
|
| 271 |
# ========================================
|
| 272 |
|
| 273 |
def extract_spectrogram_for_new_model(y_segment, sr):
|
| 274 |
+
"""Extract spectrogram for new model - EXACT from training code"""
|
| 275 |
+
# Resample to 16kHz as in training
|
| 276 |
if sr != NEW_MODEL_SAMPLE_RATE:
|
| 277 |
y_segment = librosa.resample(y_segment, orig_sr=sr, target_sr=NEW_MODEL_SAMPLE_RATE)
|
| 278 |
sr = NEW_MODEL_SAMPLE_RATE
|
| 279 |
|
| 280 |
+
# Ensure exact length: 160,000 samples for 10s @ 16kHz
|
| 281 |
+
target_len = NEW_MODEL_SAMPLE_RATE * NEW_MODEL_DURATION # 160,000
|
| 282 |
if len(y_segment) < target_len:
|
| 283 |
y_segment = np.pad(y_segment, (0, target_len - len(y_segment)))
|
| 284 |
else:
|
| 285 |
y_segment = y_segment[:target_len]
|
| 286 |
|
| 287 |
+
# Convert to Mel Spectrogram - EXACT same as training
|
| 288 |
+
spectrogram = librosa.feature.melspectrogram(y=y_segment, sr=sr, n_mels=128)
|
| 289 |
+
log_spec = librosa.power_to_db(spectrogram, ref=np.max)
|
| 290 |
+
|
| 291 |
+
# Normalize between 0 and 1 - EXACT same as training
|
| 292 |
+
log_spec = (log_spec - log_spec.min()) / (log_spec.max() - log_spec.min())
|
| 293 |
+
|
| 294 |
return log_spec[..., np.newaxis]
|
| 295 |
|
| 296 |
def count_snores_in_segment(y_segment, sr, model, threshold=0.7):
|
| 297 |
"""
|
| 298 |
Count individual snores within a detected snoring segment
|
| 299 |
Uses sliding 2-second window to detect multiple snore events
|
| 300 |
+
USES EXACT PREPROCESSING FROM TRAINING CODE
|
| 301 |
"""
|
| 302 |
if model is None:
|
| 303 |
return 0
|
| 304 |
|
| 305 |
try:
|
| 306 |
+
# Resample to 16kHz for new model (as in training)
|
| 307 |
if sr != NEW_MODEL_SAMPLE_RATE:
|
| 308 |
y_segment = librosa.resample(y_segment, orig_sr=sr, target_sr=NEW_MODEL_SAMPLE_RATE)
|
| 309 |
sr = NEW_MODEL_SAMPLE_RATE
|
|
|
|
| 312 |
is_snoring = False
|
| 313 |
|
| 314 |
# Slide a 2-second window across the segment
|
| 315 |
+
window_size = 2 * sr # 2 seconds
|
| 316 |
step_size = int(0.5 * sr) # 0.5s steps for high resolution
|
| 317 |
|
| 318 |
for start in range(0, len(y_segment) - window_size, step_size):
|
| 319 |
chunk = y_segment[start : start + window_size]
|
| 320 |
|
| 321 |
+
# Process chunk to spectrogram - EXACT same as training code
|
| 322 |
+
spectrogram = librosa.feature.melspectrogram(y=chunk, sr=sr, n_mels=128)
|
| 323 |
+
log_spec = librosa.power_to_db(spectrogram, ref=np.max)
|
| 324 |
+
|
| 325 |
+
# Normalize between 0 and 1 - EXACT same as training
|
| 326 |
+
log_spec = (log_spec - log_spec.min()) / (log_spec.max() - log_spec.min())
|
| 327 |
|
| 328 |
# Predict
|
| 329 |
prediction = model.predict(log_spec[np.newaxis, ..., np.newaxis], verbose=0)
|
|
|
|
| 773 |
|
| 774 |
annotations.append(annotation)
|
| 775 |
|
| 776 |
+
# Apnea detection - FOCUS: Silence gaps of 10-120 seconds between snoring events
|
| 777 |
for i in range(len(segment_results) - 1):
|
| 778 |
current = segment_results[i]
|
| 779 |
if current['is_snoring'] and current['confidence_level'] in ['HIGH', 'MEDIUM']:
|
|
|
|
| 781 |
if segment_results[j]['is_snoring'] and segment_results[j]['confidence_level'] in ['HIGH', 'MEDIUM']:
|
| 782 |
gap_duration = segment_results[j]['start_time'] - current['end_time']
|
| 783 |
|
| 784 |
+
# APNEA: Silence gap between 10-120 seconds
|
| 785 |
+
if 10.0 <= gap_duration <= 120.0:
|
| 786 |
+
apnea_count += 1
|
| 787 |
+
|
| 788 |
+
# Risk classification based on gap duration
|
| 789 |
+
if gap_duration < 20.0:
|
| 790 |
+
risk = "LOW"
|
| 791 |
+
elif gap_duration < 40.0:
|
| 792 |
+
risk = "MEDIUM"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
else:
|
| 794 |
+
risk = "HIGH"
|
| 795 |
+
|
| 796 |
+
annotations.append({
|
| 797 |
+
"label": "APNEA",
|
| 798 |
+
"start_sec": round(current['end_time'], 2),
|
| 799 |
+
"end_sec": round(segment_results[j]['start_time'], 2),
|
| 800 |
+
"duration": round(gap_duration, 2),
|
| 801 |
+
"risk_level": risk,
|
| 802 |
+
"note": f"Silence gap of {gap_duration:.1f}s between snoring events"
|
| 803 |
+
})
|
| 804 |
+
print(f" ⚠️ Apnea: {gap_duration:.1f}s silence gap (Risk: {risk})")
|
| 805 |
break
|
| 806 |
|
| 807 |
# 7. STATISTICS
|
|
|
|
| 836 |
"valid_recording": True,
|
| 837 |
"validation_warning": validation_warning,
|
| 838 |
"snore_count": snore_count,
|
| 839 |
+
"total_snore_counts": total_individual_snores, # NEW: Total individual snores counted by new model
|
| 840 |
"high_confidence_snores": high_conf_count,
|
| 841 |
"medium_confidence_snores": medium_conf_count,
|
| 842 |
"rejected_potential_snores": rejected_count,
|
| 843 |
"total_segments": total_segments,
|
| 844 |
"snoring_percentage": round(snoring_percentage, 2),
|
| 845 |
"apnea_count": apnea_count,
|
| 846 |
+
"overall_risk": risk_summary,
|
| 847 |
"ahi_score": round(ahi, 1),
|
| 848 |
"detection_mode": "STRICT: Only HIGH and MEDIUM confidence snoring detection",
|
| 849 |
"detection_note": detection_note,
|