Spaces:
Sleeping
Sleeping
| import os | |
| import tempfile | |
| import warnings | |
| from flask import Flask, request, jsonify | |
| from inference_service import ( | |
| load_classifier, | |
| preprocess_audio, | |
| generate_embeddings, | |
| predict_pneumonia, | |
| aggregate_predictions, | |
| SAMPLE_RATE, | |
| CLIP_DURATION, | |
| CLIP_OVERLAP_PERCENT, | |
| CLIP_IGNORE_SILENT_CLIPS, | |
| SILENCE_RMS_THRESHOLD_DB | |
| ) | |
| warnings.filterwarnings("ignore", category=UserWarning, module="soundfile") | |
| warnings.filterwarnings("ignore", module="librosa") | |
| app = Flask(__name__) | |
| # Load classifier globally | |
| classifier_model = load_classifier() | |
| def home(): | |
| """API documentation homepage""" | |
| html_content = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Pneumonia Risk Assessment API</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| line-height: 1.6; | |
| color: #333; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 20px; | |
| box-shadow: 0 20px 60px rgba(0,0,0,0.3); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 40px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .header .emoji { | |
| font-size: 3em; | |
| margin-bottom: 10px; | |
| } | |
| .header p { | |
| font-size: 1.2em; | |
| opacity: 0.9; | |
| } | |
| .content { | |
| padding: 40px; | |
| } | |
| .section { | |
| margin-bottom: 40px; | |
| } | |
| .section h2 { | |
| color: #667eea; | |
| font-size: 1.8em; | |
| margin-bottom: 15px; | |
| padding-bottom: 10px; | |
| border-bottom: 3px solid #667eea; | |
| } | |
| .endpoint { | |
| background: #f8f9fa; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| border-left: 5px solid #667eea; | |
| } | |
| .endpoint h3 { | |
| color: #764ba2; | |
| margin-bottom: 10px; | |
| } | |
| .endpoint code { | |
| background: #e9ecef; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| font-family: 'Courier New', monospace; | |
| color: #d63384; | |
| } | |
| .code-block { | |
| background: #2d3748; | |
| color: #fff; | |
| padding: 20px; | |
| border-radius: 8px; | |
| overflow-x: auto; | |
| margin: 15px 0; | |
| font-family: 'Courier New', monospace; | |
| } | |
| .code-block pre { | |
| margin: 0; | |
| } | |
| .warning { | |
| background: #fff3cd; | |
| border-left: 5px solid #ffc107; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin: 20px 0; | |
| } | |
| .warning strong { | |
| color: #856404; | |
| } | |
| .test-section { | |
| background: #e7f3ff; | |
| padding: 30px; | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| } | |
| .test-section h3 { | |
| color: #0066cc; | |
| margin-bottom: 15px; | |
| } | |
| .file-input-wrapper { | |
| position: relative; | |
| display: inline-block; | |
| margin: 15px 0; | |
| } | |
| .file-input-wrapper input[type="file"] { | |
| position: absolute; | |
| opacity: 0; | |
| width: 100%; | |
| height: 100%; | |
| cursor: pointer; | |
| } | |
| .file-input-label { | |
| display: inline-block; | |
| padding: 12px 25px; | |
| background: #667eea; | |
| color: white; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .file-input-label:hover { | |
| background: #764ba2; | |
| transform: translateY(-2px); | |
| } | |
| .submit-btn { | |
| background: #28a745; | |
| color: white; | |
| padding: 12px 30px; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 1em; | |
| margin-top: 10px; | |
| transition: all 0.3s; | |
| } | |
| .submit-btn:hover { | |
| background: #218838; | |
| transform: translateY(-2px); | |
| } | |
| .submit-btn:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| #result { | |
| margin-top: 20px; | |
| padding: 20px; | |
| border-radius: 8px; | |
| display: none; | |
| } | |
| .result-success { | |
| background: #d4edda; | |
| border: 1px solid #c3e6cb; | |
| color: #155724; | |
| } | |
| .result-error { | |
| background: #f8d7da; | |
| border: 1px solid #f5c6cb; | |
| color: #721c24; | |
| } | |
| .selected-file { | |
| margin-top: 10px; | |
| color: #666; | |
| font-style: italic; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <div class="emoji">🫁</div> | |
| <h1>Pneumonia Risk Assessment API</h1> | |
| <p>AI-powered respiratory audio analysis</p> | |
| </div> | |
| <div class="content"> | |
| <div class="warning"> | |
| <strong>⚠️ Medical Disclaimer:</strong> This is an AI assessment tool, not a medical diagnostic device. | |
| Results should not be used as the sole basis for medical decisions. Always consult qualified healthcare professionals. | |
| </div> | |
| <div class="section"> | |
| <h2>📡 API Endpoint</h2> | |
| <div class="endpoint"> | |
| <h3>POST /predict_pneumonia</h3> | |
| <p><strong>Description:</strong> Analyze respiratory audio file for pneumonia risk assessment</p> | |
| <p><strong>Content-Type:</strong> <code>multipart/form-data</code></p> | |
| <p><strong>Parameter:</strong> <code>audio_file</code> - Audio file (.wav, .mp3, etc.)</p> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h2>📥 Response Format</h2> | |
| <div class="code-block"> | |
| <pre>{ | |
| "filename": "recording.wav", | |
| "pneumonia_risk_score": 0.7234, | |
| "risk_level": "High", | |
| "note": "This is an AI assessment, not a medical diagnosis..." | |
| }</pre> | |
| </div> | |
| <p><strong>Risk Levels:</strong></p> | |
| <ul style="margin-left: 20px; margin-top: 10px;"> | |
| <li><strong>Low:</strong> Risk score < 0.4</li> | |
| <li><strong>Moderate:</strong> Risk score 0.4 - 0.7</li> | |
| <li><strong>High:</strong> Risk score > 0.7</li> | |
| </ul> | |
| </div> | |
| <div class="section"> | |
| <h2>💻 Usage Example</h2> | |
| <p>Using cURL:</p> | |
| <div class="code-block"> | |
| <pre>curl -X POST \\ | |
| -F "audio_file=@recording.wav" \\ | |
| https://root16285-pneumonia-space.hf.space/predict_pneumonia</pre> | |
| </div> | |
| <p>Using Python:</p> | |
| <div class="code-block"> | |
| <pre>import requests | |
| url = "https://root16285-pneumonia-space.hf.space/predict_pneumonia" | |
| files = {"audio_file": open("recording.wav", "rb")} | |
| response = requests.post(url, files=files) | |
| print(response.json())</pre> | |
| </div> | |
| </div> | |
| <div class="test-section"> | |
| <h3>🧪 Test the API</h3> | |
| <p>Upload an audio file to test the pneumonia risk assessment:</p> | |
| <form id="testForm"> | |
| <div class="file-input-wrapper"> | |
| <label class="file-input-label"> | |
| 📁 Choose Audio File | |
| <input type="file" id="audioFile" accept="audio/*" required> | |
| </label> | |
| </div> | |
| <div class="selected-file" id="selectedFile"></div> | |
| <br> | |
| <button type="submit" class="submit-btn" id="submitBtn">Analyze Audio</button> | |
| </form> | |
| <div id="result"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const form = document.getElementById('testForm'); | |
| const fileInput = document.getElementById('audioFile'); | |
| const selectedFile = document.getElementById('selectedFile'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const result = document.getElementById('result'); | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| selectedFile.textContent = `Selected: ${e.target.files[0].name}`; | |
| } | |
| }); | |
| form.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const file = fileInput.files[0]; | |
| if (!file) { | |
| alert('Please select an audio file'); | |
| return; | |
| } | |
| submitBtn.disabled = true; | |
| submitBtn.textContent = 'Analyzing...'; | |
| result.style.display = 'none'; | |
| const formData = new FormData(); | |
| formData.append('audio_file', file); | |
| try { | |
| const response = await fetch('/predict_pneumonia', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| result.className = 'result-success'; | |
| result.innerHTML = ` | |
| <h4>✅ Analysis Complete</h4> | |
| <p><strong>File:</strong> ${data.filename}</p> | |
| <p><strong>Pneumonia Risk Score:</strong> ${(data.pneumonia_risk_score * 100).toFixed(2)}%</p> | |
| <p><strong>Risk Level:</strong> ${data.risk_level}</p> | |
| <p style="margin-top: 10px; font-size: 0.9em;"><em>${data.note}</em></p> | |
| `; | |
| } else { | |
| result.className = 'result-error'; | |
| result.innerHTML = ` | |
| <h4>❌ Error</h4> | |
| <p>${data.error || 'An error occurred during analysis'}</p> | |
| `; | |
| } | |
| result.style.display = 'block'; | |
| } catch (error) { | |
| result.className = 'result-error'; | |
| result.innerHTML = ` | |
| <h4>❌ Error</h4> | |
| <p>Failed to connect to the API: ${error.message}</p> | |
| `; | |
| result.style.display = 'block'; | |
| } finally { | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'Analyze Audio'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return html_content | |
| def predict_pneumonia_endpoint(): | |
| if 'audio_file' not in request.files: | |
| return jsonify({"error": "No audio_file part in the request"}), 400 | |
| audio_file = request.files['audio_file'] | |
| if audio_file.filename == '': | |
| return jsonify({"error": "No selected file"}), 400 | |
| temp_dir = tempfile.mkdtemp() | |
| temp_audio_path = os.path.join(temp_dir, audio_file.filename) | |
| audio_file.save(temp_audio_path) | |
| try: | |
| if classifier_model is None: | |
| return jsonify({"error": "Classifier not loaded"}), 500 | |
| audio_clips = preprocess_audio( | |
| temp_audio_path, SAMPLE_RATE, CLIP_DURATION, CLIP_OVERLAP_PERCENT, | |
| CLIP_IGNORE_SILENT_CLIPS, SILENCE_RMS_THRESHOLD_DB | |
| ) | |
| if audio_clips.size == 0: | |
| return jsonify({"result": "No valid audio clips", "risk_score": None}), 200 | |
| # Generate embeddings using OpenL3 | |
| embeddings = generate_embeddings(audio_clips) | |
| if embeddings.size == 0: | |
| return jsonify({"result": "No embeddings generated", "risk_score": None}), 200 | |
| # Predict pneumonia risk | |
| clip_predictions, clip_probabilities = predict_pneumonia(embeddings, classifier_model) | |
| if clip_predictions is None or clip_probabilities is None: | |
| return jsonify({"result": "Prediction failed", "risk_score": None}), 200 | |
| final_prediction, risk_score = aggregate_predictions(clip_predictions, clip_probabilities) | |
| return jsonify({ | |
| "filename": audio_file.filename, | |
| "pneumonia_risk_score": round(float(risk_score), 4), | |
| "risk_level": "High" if risk_score > 0.7 else "Moderate" if risk_score > 0.4 else "Low", | |
| "note": "This is an AI assessment, not a medical diagnosis. Consult a healthcare professional." | |
| }), 200 | |
| finally: | |
| os.remove(temp_audio_path) | |
| os.rmdir(temp_dir) | |
| if __name__ == '__main__': | |
| app.run(debug=True, host='0.0.0.0', port=5000) | |