trixy194t commited on
Commit
a82e9eb
·
verified ·
1 Parent(s): 304a56f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +148 -0
app.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import numpy as np
4
+ import librosa
5
+ import noisereduce as nr
6
+ import scipy.signal as signal
7
+ from fastapi import FastAPI, UploadFile, File, HTTPException
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+
10
+ app = FastAPI()
11
+
12
+ # Enable CORS for Flutter mobile access
13
+ app.add_middleware(
14
+ CORSMiddleware,
15
+ allow_origins=["*"],
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ )
19
+
20
+ # --- CONFIGURATION ---
21
+ SR = 16000
22
+ SILENCE_DB = 25
23
+ APNEA_MIN = 10.0
24
+ APNEA_MAX = 120.0
25
+
26
+ # --- CORE LOGIC FUNCTIONS ---
27
+
28
+ def clean_audio_stream(y, sr):
29
+ """Clean audio with noise reduction and bandpass filter"""
30
+ y_denoised = nr.reduce_noise(y=y, sr=sr)
31
+ b, a = signal.butter(4, [200/(sr/2), 2000/(sr/2)], btype='band')
32
+ y_filtered = signal.filtfilt(b, a, y_denoised)
33
+ return y_filtered
34
+
35
+ def is_snoring_sound(y_segment, sr):
36
+ """Spectral analysis for snoring detection"""
37
+ if len(y_segment) < sr * 0.15:
38
+ return False, 0.0
39
+
40
+ rms_energy = np.sqrt(np.mean(y_segment**2))
41
+ if rms_energy < 0.008 or rms_energy > 0.95:
42
+ return False, 0.0
43
+
44
+ stft = np.abs(librosa.stft(y_segment))
45
+ freq_bins = librosa.fft_frequencies(sr=sr)
46
+
47
+ total_energy = np.sum(stft) + 1e-10
48
+ snoring_ratio = np.sum(stft[(freq_bins >= 200) & (freq_bins <= 800), :]) / total_energy
49
+
50
+ zcr = np.mean(librosa.feature.zero_crossing_rate(y_segment)[0])
51
+ centroid = np.mean(librosa.feature.spectral_centroid(y=y_segment, sr=sr)[0])
52
+
53
+ confidence = 0.0
54
+ if rms_energy >= 0.008: confidence += 0.25
55
+ confidence += min(snoring_ratio / 0.15, 1.0) * 0.30
56
+ if centroid <= 2200: confidence += 0.15
57
+ if zcr <= 0.25: confidence += 0.10
58
+
59
+ return confidence >= 0.40, round(confidence, 2)
60
+
61
+ def is_human_breathing_sound(y_segment, sr):
62
+ """Detect quiet breathing"""
63
+ rms_energy = np.sqrt(np.mean(y_segment**2))
64
+ if rms_energy < 0.002 or rms_energy > 0.5:
65
+ return False
66
+ centroid = np.mean(librosa.feature.spectral_centroid(y=y_segment, sr=sr)[0])
67
+ return centroid <= 2000
68
+
69
+ # --- API ROUTES ---
70
+
71
+ @app.get("/")
72
+ def home():
73
+ return {"status": "Sleep API is Live", "model": "Signal Processing v2"}
74
+
75
+ @app.post("/analyze")
76
+ async def analyze_endpoint(file: UploadFile = File(...)):
77
+ # 1. Save file temporarily
78
+ temp_path = f"temp_{file.filename}"
79
+ with open(temp_path, "wb") as buffer:
80
+ shutil.copyfileobj(file.file, buffer)
81
+
82
+ try:
83
+ # 2. Load Audio
84
+ y_orig, sr = librosa.load(temp_path, sr=SR)
85
+ duration = len(y_orig) / sr
86
+
87
+ # 3. Quick Validation
88
+ if duration < 5:
89
+ raise HTTPException(status_code=400, detail="Audio too short for analysis")
90
+
91
+ # 4. Process and Detect
92
+ y_clean = clean_audio_stream(y_orig, sr)
93
+ intervals = librosa.effects.split(y_clean, top_db=SILENCE_DB)
94
+
95
+ events = []
96
+ prev_end = 0
97
+
98
+ for idx, (start, end) in enumerate(intervals):
99
+ # Check for Apnea Gap
100
+ gap_dur = (start - prev_end) / sr
101
+ if APNEA_MIN <= gap_dur <= APNEA_MAX:
102
+ events.append({
103
+ "label": "APNEA",
104
+ "start": round(prev_end/sr, 2),
105
+ "end": round(start/sr, 2),
106
+ "duration": round(gap_dur, 2)
107
+ })
108
+
109
+ # Check Sound for Snoring
110
+ seg = y_orig[start:end]
111
+ snore_bool, conf = is_snoring_sound(seg, sr)
112
+ if snore_bool:
113
+ events.append({
114
+ "label": "SNORING",
115
+ "start": round(start/sr, 2),
116
+ "end": round(end/sr, 2),
117
+ "confidence": conf
118
+ })
119
+ prev_end = end
120
+
121
+ # 5. Finalize Statistics
122
+ apnea_list = [e for e in events if e["label"] == "APNEA"]
123
+ ahi = len(apnea_list) / (duration / 3600) if duration > 0 else 0
124
+
125
+ severity = "NORMAL"
126
+ if ahi >= 30: severity = "SEVERE"
127
+ elif ahi >= 15: severity = "MODERATE"
128
+ elif ahi >= 5: severity = "MILD"
129
+
130
+ return {
131
+ "success": True,
132
+ "filename": file.filename,
133
+ "duration": round(duration, 2),
134
+ "ahi": round(ahi, 2),
135
+ "severity": severity,
136
+ "total_events": len(events),
137
+ "events": events
138
+ }
139
+
140
+ except Exception as e:
141
+ return {"success": False, "error": str(e)}
142
+ finally:
143
+ if os.path.exists(temp_path):
144
+ os.remove(temp_path)
145
+
146
+ if __name__ == "__main__":
147
+ import uvicorn
148
+ uvicorn.run(app, host="0.0.0.0", port=7860)