QnxprU69yCNg8XJ commited on
Commit
5879023
·
1 Parent(s): b98052a

Add pneumonia detection Flask API with inference service and Docker support

Browse files
Files changed (4) hide show
  1. Dockerfile +18 -0
  2. app.py +67 -5
  3. inference_service.py +116 -0
  4. requirements.txt +7 -2
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim-buster
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ ffmpeg libsndfile1 \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY app.py .
13
+ COPY inference_service.py .
14
+ COPY pneumonia_classifier.joblib .
15
+
16
+ EXPOSE 5000
17
+
18
+ CMD ["python", "app.py"]
app.py CHANGED
@@ -1,7 +1,69 @@
1
- import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import warnings
4
+ from flask import Flask, request, jsonify
5
+ from inference_service import (
6
+ load_hear_model,
7
+ load_classifier,
8
+ preprocess_audio,
9
+ generate_embeddings,
10
+ predict_pneumonia,
11
+ aggregate_predictions,
12
+ SAMPLE_RATE,
13
+ CLIP_DURATION,
14
+ CLIP_OVERLAP_PERCENT,
15
+ CLIP_IGNORE_SILENT_CLIPS,
16
+ SILENCE_RMS_THRESHOLD_DB
17
+ )
18
 
19
+ warnings.filterwarnings("ignore", category=UserWarning, module="soundfile")
20
+ warnings.filterwarnings("ignore", module="librosa")
21
 
22
+ app = Flask(__name__)
23
+
24
+ # Load models globally
25
+ hear_infer_fn = load_hear_model()
26
+ classifier_model = load_classifier()
27
+
28
+ @app.route('/predict_pneumonia', methods=['POST'])
29
+ def predict_pneumonia_endpoint():
30
+ if 'audio_file' not in request.files:
31
+ return jsonify({"error": "No audio_file part in the request"}), 400
32
+
33
+ audio_file = request.files['audio_file']
34
+ if audio_file.filename == '':
35
+ return jsonify({"error": "No selected file"}), 400
36
+
37
+ temp_dir = tempfile.mkdtemp()
38
+ temp_audio_path = os.path.join(temp_dir, audio_file.filename)
39
+ audio_file.save(temp_audio_path)
40
+
41
+ try:
42
+ if hear_infer_fn is None or classifier_model is None:
43
+ return jsonify({"error": "Models not loaded"}), 500
44
+
45
+ audio_clips = preprocess_audio(
46
+ temp_audio_path, SAMPLE_RATE, CLIP_DURATION, CLIP_OVERLAP_PERCENT,
47
+ CLIP_IGNORE_SILENT_CLIPS, SILENCE_RMS_THRESHOLD_DB
48
+ )
49
+
50
+ if audio_clips.size == 0:
51
+ return jsonify({"result": "No valid audio clips", "pneumonia_status": "Undetermined"}), 200
52
+
53
+ embeddings = generate_embeddings(audio_clips, hear_infer_fn)
54
+
55
+ if embeddings.size == 0:
56
+ return jsonify({"result": "No embeddings generated", "pneumonia_status": "Undetermined"}), 200
57
+
58
+ clip_predictions = predict_pneumonia(embeddings, classifier_model)
59
+ final_prediction = aggregate_predictions(clip_predictions)
60
+
61
+ result_label = 'Pneumonia' if final_prediction == 1 else 'No Pneumonia'
62
+ return jsonify({"filename": audio_file.filename, "pneumonia_status": result_label}), 200
63
+
64
+ finally:
65
+ os.remove(temp_audio_path)
66
+ os.rmdir(temp_dir)
67
+
68
+ if __name__ == '__main__':
69
+ app.run(debug=True, host='0.0.0.0', port=5000)
inference_service.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import librosa
4
+ import joblib
5
+ import soundfile as sf
6
+ from huggingface_hub import from_pretrained_keras
7
+ import warnings
8
+
9
+ # Suppress warnings
10
+ warnings.filterwarnings("ignore", category=UserWarning, module="soundfile")
11
+ warnings.filterwarnings("ignore", module="librosa")
12
+
13
+ # HeAR Parameters
14
+ SAMPLE_RATE = 16000
15
+ CLIP_DURATION = 2
16
+ CLIP_LENGTH = SAMPLE_RATE * CLIP_DURATION
17
+ CLIP_OVERLAP_PERCENT = 10
18
+ CLIP_IGNORE_SILENT_CLIPS = True
19
+ SILENCE_RMS_THRESHOLD_DB = -50
20
+
21
+ def create_dummy_audio(filename='dummy_audio.wav', duration=5, sr=SAMPLE_RATE):
22
+ t = np.linspace(0, duration, int(sr * duration), endpoint=False)
23
+ dummy_audio = 0.5 * np.sin(2 * np.pi * 440 * t) + 0.3 * np.sin(2 * np.pi * 880 * t)
24
+ dummy_audio = dummy_audio.astype(np.float32)
25
+ sf.write(filename, dummy_audio, sr)
26
+ print(f"Dummy audio file '{filename}' created with duration {duration}s.")
27
+ return filename
28
+
29
+ def load_hear_model():
30
+ print("Loading HeAR model from Hugging Face...")
31
+ try:
32
+ loaded_model = from_pretrained_keras("google/hear")
33
+ infer_fn = loaded_model.signatures["serving_default"]
34
+ print("HeAR model loaded successfully.")
35
+ return infer_fn
36
+ except Exception as e:
37
+ print(f"Error loading HeAR model: {e}")
38
+ return None
39
+
40
+ def load_classifier(model_path='pneumonia_classifier.joblib'):
41
+ print(f"Loading classifier from '{model_path}'...")
42
+ try:
43
+ classifier_model = joblib.load(model_path)
44
+ print("Classifier loaded successfully.")
45
+ return classifier_model
46
+ except Exception as e:
47
+ print(f"Error loading classifier: {e}")
48
+ return None
49
+
50
+ def preprocess_audio(audio_path, sample_rate, clip_duration, clip_overlap_percent, ignore_silent_clips, silence_rms_threshold_db):
51
+ print(f"Preprocessing audio file: {audio_path}")
52
+ try:
53
+ audio, sr = librosa.load(audio_path, sr=sample_rate, mono=True)
54
+ except Exception as e:
55
+ print(f"Error loading audio file {audio_path}: {e}")
56
+ return np.array([])
57
+
58
+ clip_batch = []
59
+ clip_length = sr * clip_duration
60
+ overlap_samples = int(clip_length * (clip_overlap_percent / 100))
61
+ step_size = clip_length - overlap_samples
62
+ num_clips = max(1, (len(audio) - overlap_samples) // step_size)
63
+
64
+ for i in range(num_clips):
65
+ start_sample = i * step_size
66
+ end_sample = start_sample + clip_length
67
+ clip = audio[start_sample:end_sample]
68
+
69
+ if end_sample > len(audio):
70
+ clip = np.pad(clip, (0, clip_length - len(clip)), 'constant')
71
+
72
+ rms_loudness = round(20 * np.log10(np.sqrt(np.mean(clip**2)) + 1e-10))
73
+ if ignore_silent_clips and rms_loudness < silence_rms_threshold_db:
74
+ continue
75
+
76
+ clip_batch.append(clip)
77
+
78
+ if not clip_batch:
79
+ print("No valid (non-silent) audio clips generated.")
80
+ return np.array([])
81
+
82
+ return np.asarray(clip_batch)
83
+
84
+ def generate_embeddings(audio_clips, infer_fn):
85
+ if audio_clips.size == 0:
86
+ print("No audio clips to generate embeddings from.")
87
+ return np.array([])
88
+
89
+ print(f"Generating embeddings for {len(audio_clips)} clips...")
90
+ try:
91
+ embedding_batch = infer_fn(x=audio_clips)['output_0'].numpy()
92
+ print(f"Generated {embedding_batch.shape[0]} embeddings of dimension {embedding_batch.shape[1]}.")
93
+ return embedding_batch
94
+ except Exception as e:
95
+ print(f"Error generating embeddings: {e}")
96
+ return np.array([])
97
+
98
+ def predict_pneumonia(embeddings, classifier_model):
99
+ if embeddings.size == 0:
100
+ print("No embeddings for prediction.")
101
+ return np.array([])
102
+
103
+ print(f"Making predictions for {len(embeddings)} embeddings...")
104
+ predictions = classifier_model.predict(embeddings)
105
+ return predictions
106
+
107
+ def aggregate_predictions(predictions):
108
+ if predictions.size == 0:
109
+ print("No predictions to aggregate.")
110
+ return None
111
+
112
+ unique, counts = np.unique(predictions, return_counts=True)
113
+ final_prediction = unique[np.argmax(counts)]
114
+ result_label = 'Pneumonia' if final_prediction == 1 else 'No Pneumonia'
115
+ print(f"Aggregated prediction: {result_label}")
116
+ return final_prediction
requirements.txt CHANGED
@@ -1,2 +1,7 @@
1
- # Add any additional dependencies here
2
- # gradio is already pre-installed by Hugging Face Spaces
 
 
 
 
 
 
1
+ flask
2
+ numpy
3
+ librosa
4
+ joblib
5
+ soundfile
6
+ huggingface_hub
7
+ scikit-learn