mariox07/bird-sound-api
0
1from fastapi import FastAPI, UploadFile, File, WebSocket, WebSocketDisconnect
2from fastapi.middleware.cors import CORSMiddleware
3import uuid
4import tensorflow as tf
5import numpy as np
6import os
7import librosa
8import soundfile as sf
9import logging
10import asyncio
11
12# Setup logging
13logging.basicConfig(level=logging.INFO)
14logger = logging.getLogger(__name__)
15
16from preprocessing import preprocess_audio
17
18app = FastAPI()
19
20# Buat folder temp otomatis
21os.makedirs("temp", exist_ok=True)
22
23# CORS middleware
24app.add_middleware(
25 CORSMiddleware,
26 allow_origins=["*"],
27 allow_credentials=True,
28 allow_methods=["*"],
29 allow_headers=["*"],
30)
31
32# Load model burung
33try:
34 model = tf.keras.models.load_model("bird_sound_model.keras")
35 logger.info("✅ Model berhasil dimuat")
36except Exception as e:
37 logger.error(f"❌ Gagal memuat model: {e}")
38 model = None
39
40CLASSES = [
41 "Burung_gereja",
42 "Cabak_kota",
43 "Cucak_kutilang",
44 "Kipasan_belang",
45 "Tekukur biasa"
46]
47
48
49def adjust_audio_duration(file_path, target_duration=5.0, sr=22050):
50 """
51 Memaksa durasi audio menjadi tepat target_duration detik
52 """
53 try:
54 # Load audio
55 y, current_sr = librosa.load(file_path, sr=sr, duration=target_duration)
56
57 target_samples = int(target_duration * sr)
58
59 if len(y) > target_samples:
60 # Potong jika kepanjangan
61 y = y[:target_samples]
62 elif len(y) < target_samples:
63 # Padding jika kurang
64 y = np.pad(y, (0, target_samples - len(y)), mode='constant')
65
66 # Simpan sebagai WAV
67 sf.write(file_path, y, sr, format='WAV', subtype='PCM_16')
68 logger.info(f"✅ Audio diproses: durasi={len(y)/sr:.2f}s")
69 return True
70 except Exception as e:
71 logger.error(f"❌ Gagal adjust audio: {repr(e)}")
72 logger.error(traceback.format_exc())
73 raise
74
75@app.websocket("/ws-test-socket")
76async def websocket_test(websocket: WebSocket):
77 await websocket.accept()
78 await websocket.send_text("connected")
79
80 while True:
81 data = await websocket.receive_text()
82 await websocket.send_text(f"echo: {data}")
83
84# ==================== ENDPOINT UPLOAD FILE ====================
85@app.post("/predict")
86async def predict(file: UploadFile = File(...)):
87 if model is None:
88 return {"error": "Model belum dimuat. Silakan periksa kembali model."}
89
90 file_ext = os.path.splitext(file.filename)[1] or ".wav"
91 temp_audio = f"temp/upload_{uuid.uuid4().hex}{file_ext}"
92
93 try:
94 # Simpan file
95 content = await file.read()
96 if len(content) == 0:
97 return {"error": "File audio kosong"}
98
99 with open(temp_audio, "wb") as buffer:
100 buffer.write(content)
101
102 logger.info(f"📁 File diterima: {file.filename}, ukuran={len(content)} bytes")
103
104 # Proses durasi
105 adjust_audio_duration(temp_audio, target_duration=5.0)
106
107 # Preprocessing
108 image = preprocess_audio(temp_audio)
109 image = np.expand_dims(image, axis=0)
110
111 # Prediksi
112 prediction = model.predict(image, verbose=0)
113 predicted_index = np.argmax(prediction)
114 confidence = float(np.max(prediction) * 100)
115 predicted_label = CLASSES[predicted_index]
116
117 logger.info(f"🎯 Prediksi: {predicted_label} ({confidence:.2f}%)")
118
119 return {
120 "prediction": predicted_label,
121 "confidence": confidence
122 }
123
124 except Exception as e:
125 logger.error(f"❌ Error pada /predict: {str(e)}")
126 return {"error": f"Gagal memproses audio: {str(e)}"}
127
128 finally:
129 if os.path.exists(temp_audio):
130 try:
131 os.remove(temp_audio)
132 except:
133 pass
134
135
136# ==================== WEBSOCKET ENDPOINT REAL-TIME ====================
137@app.websocket("/ws/realtime")
138async def websocket_endpoint(websocket: WebSocket):
139 await websocket.accept()
140 logger.info("🔌 Koneksi WebSocket terbuka")
141
142 if model is None:
143 await websocket.send_json({"error": "Model belum dimuat"})
144 await websocket.close()
145 return
146
147 try:
148 while True:
149 # Terima data audio dengan timeout
150 try:
151 # Gunakan receive_bytes dengan try-except untuk WebSocketDisconnect
152 audio_bytes = await asyncio.wait_for(
153 websocket.receive_bytes(),
154 timeout=30.0
155 )
156 except asyncio.TimeoutError:
157 logger.warning("Timeout menerima data, tetap mendengarkan...")
158 continue
159 except WebSocketDisconnect:
160 logger.info("WebSocket disconnected by client")
161 break
162 except Exception as e:
163 logger.error(f"Error receive bytes: {e}")
164 break
165
166 if not audio_bytes or len(audio_bytes) < 2000:
167 logger.warning(f"Data terlalu kecil: {len(audio_bytes)} bytes, skip")
168 continue
169
170 temp_filename = f"temp/ws_{uuid.uuid4().hex}.webm"
171
172 try:
173 # Simpan data mentah
174 with open(temp_filename, "wb") as f:
175 f.write(audio_bytes)
176
177 logger.info(f"📥 Menerima audio: {len(audio_bytes)} bytes")
178
179 # Cek header file (validasi)
180 with open(temp_filename, "rb") as f:
181 header = f.read(12)
182 is_webm = header[:4] == b'\x1a\x45\xdf\xa3'
183 is_wav = header[:4] == b'RIFF'
184
185 if not is_webm and not is_wav:
186 logger.warning(f"Format tidak dikenal: {header[:4]}")
187 await websocket.send_json({
188 "error": "Format audio tidak didukung. Kirim dalam format WebM atau WAV."
189 })
190 continue
191
192 # Proses audio
193 adjust_audio_duration(temp_filename, target_duration=5.0)
194
195 # Preprocessing
196 image = preprocess_audio(temp_filename)
197 image = np.expand_dims(image, axis=0)
198
199 # Prediksi
200 prediction = model.predict(image, verbose=0)
201 predicted_index = np.argmax(prediction)
202 confidence = float(np.max(prediction) * 100)
203 predicted_label = CLASSES[predicted_index]
204
205 logger.info(f"🎯 Real-time prediksi: {predicted_label} ({confidence:.2f}%)")
206
207 # Kirim hasil
208 try:
209 await websocket.send_json({
210 "prediction": predicted_label,
211 "confidence": confidence
212 })
213 except Exception as e:
214 logger.error(f"Error sending response: {e}")
215 break
216
217 except Exception as e:
218 logger.error(f"Error processing audio: {repr(e)}")
219 logger.error(traceback.format_exc())
220 try:
221 await websocket.send_json({
222 "error": f"Gagal memproses audio: {str(e)}"
223 })
224 except:
225 pass
226
227 finally:
228 if os.path.exists(temp_filename):
229 try:
230 os.remove(temp_filename)
231 except:
232 pass
233
234 except WebSocketDisconnect:
235 logger.info("🔌 Koneksi WebSocket terputus secara normal")
236 except Exception as e:
237 logger.error(f"❌ Terjadi kesalahan koneksi WebSocket: {str(e)}")
238 finally:
239 logger.info("WebSocket connection closed")
240
241
242# ==================== HEALTH CHECK ====================
243@app.get("/")
244@app.get("/health")
245async def health_check():
246 return {
247 "status": "ok",
248 "model_loaded": model is not None,
249 "classes": CLASSES
250 }
251
252
253if __name__ == "__main__":
254 import uvicorn
255 uvicorn.run(
256 app,
257 host="127.0.0.1",
258 port=8000,
259 log_level="info",
260 ws_ping_interval=20,
261 ws_ping_timeout=60
262 )