CoolFace
Apppublic

mariox07/bird-sound-api

sourceHugging Facemitupdated 3mo agoView on Hugging Face
0likes
main.py262 linesDownload Raw Back to root
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    )