pneumonia_space / app.py
QnxprU69yCNg8XJ
Add beautiful HTML homepage with interactive API testing interface
03e3132
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()
@app.route('/')
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 &lt; 0.4</li>
<li><strong>Moderate:</strong> Risk score 0.4 - 0.7</li>
<li><strong>High:</strong> Risk score &gt; 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
@app.route('/predict_pneumonia', methods=['POST'])
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)