phoner45 commited on
Commit
1886358
·
verified ·
1 Parent(s): 900b065

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +102 -0
  2. feature_extract.py +82 -0
  3. requirements.txt +10 -0
app.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ import torch
5
+ from torchvision import models, transforms
6
+ import torch.nn.functional as F
7
+ import librosa, soundfile as sf, tempfile
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ import librosa.display
11
+ from PIL import Image
12
+ import io
13
+ from feature_extract import AudioFeatureExtractor
14
+ import requests, os
15
+
16
+ # === CONFIG ===
17
+ MODEL_REPO = "Chula-PD/voice-mobilenet-pd"
18
+ MODEL_FILE = "MobileNet_Model.pth"
19
+ MODEL_URL = f"https://huggingface.co/{MODEL_REPO}/resolve/main/{MODEL_FILE}"
20
+
21
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
22
+
23
+ # === FastAPI Init ===
24
+ app = FastAPI(title="CheckPD Voice API", version="1.0")
25
+
26
+ # Allow CORS (for React frontend)
27
+ app.add_middleware(
28
+ CORSMiddleware,
29
+ allow_origins=["*"], # ปรับให้เฉพาะ domain ได้ภายหลัง
30
+ allow_credentials=True,
31
+ allow_methods=["*"],
32
+ allow_headers=["*"],
33
+ )
34
+
35
+ # === Load Model ===
36
+ def load_model():
37
+ if not os.path.exists(MODEL_FILE):
38
+ print("Downloading model weights from Hugging Face...")
39
+ weights_bytes = requests.get(MODEL_URL)
40
+ with open(MODEL_FILE, "wb") as f:
41
+ f.write(weights_bytes.content)
42
+ model = models.mobilenet_v3_small(weights=None)
43
+ in_features = model.classifier[-1].in_features
44
+ model.classifier[-1] = torch.nn.Linear(in_features, 2)
45
+ model.load_state_dict(torch.load(MODEL_FILE, map_location=device))
46
+ model.eval()
47
+ return model
48
+
49
+ model = load_model()
50
+ classes = ["HC", "PD"]
51
+
52
+ # === Image Transform ===
53
+ transform = transforms.Compose([
54
+ transforms.Resize((224, 224)),
55
+ transforms.ToTensor(),
56
+ transforms.Normalize(
57
+ [0.485, 0.456, 0.406],
58
+ [0.229, 0.224, 0.225]
59
+ ),
60
+ ])
61
+
62
+ @app.get("/")
63
+ def home():
64
+ return {"message": "CheckPD Voice API is running."}
65
+
66
+ @app.post("/predict")
67
+ async def predict(file: UploadFile = File(...)):
68
+ try:
69
+ # Load and preprocess audio
70
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
71
+ tmp.write(await file.read())
72
+ tmp.flush()
73
+ wav_path = tmp.name
74
+
75
+ extractor = AudioFeatureExtractor(wav_path, sr=16000)
76
+ mel_db = extractor.get_melspectrogram()
77
+
78
+ # Convert mel to image
79
+ fig, ax = plt.subplots(figsize=(6, 3))
80
+ librosa.display.specshow(mel_db, sr=16000, hop_length=51, cmap="viridis")
81
+ plt.axis("off")
82
+ buf = io.BytesIO()
83
+ plt.savefig(buf, format='png', bbox_inches="tight", pad_inches=0)
84
+ plt.close()
85
+ buf.seek(0)
86
+ image = Image.open(buf).convert("RGB")
87
+
88
+ # Predict
89
+ input_tensor = transform(image).unsqueeze(0).to(device)
90
+ with torch.no_grad():
91
+ outputs = model(input_tensor)
92
+ probs = F.softmax(outputs, dim=1)
93
+ pred_idx = torch.argmax(probs, dim=1).item()
94
+ confidence = probs[0][pred_idx].item()
95
+
96
+ return {
97
+ "label": classes[pred_idx],
98
+ "confidence": round(confidence, 4)
99
+ }
100
+
101
+ except Exception as e:
102
+ raise HTTPException(status_code=500, detail=str(e))
feature_extract.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ import librosa
4
+ import librosa.display
5
+ import matplotlib.pyplot as plt
6
+
7
+
8
+ class AudioFeatureExtractor:
9
+ def __init__(self, wavfile, sr=16000, n_fft=1024, hop_length=51, n_mels=256):
10
+ self.wavfile = wavfile
11
+ self.target_sr = sr
12
+ self.n_fft = n_fft
13
+ self.hop_length = hop_length
14
+ self.n_mels = n_mels
15
+
16
+ # ✅ โหลดเสียงด้วย librosa (resample อัตโนมัติ)
17
+ waveform, _ = librosa.load(self.wavfile, sr=self.target_sr)
18
+ waveform = torch.tensor(waveform).unsqueeze(0)
19
+ self.waveform = waveform
20
+ self.sr = self.target_sr
21
+
22
+ def get_spectrogram(self, to_db=True):
23
+ """สร้าง spectrogram แบบธรรมดา"""
24
+ spec = np.abs(librosa.stft(
25
+ self.waveform.squeeze(0).numpy(),
26
+ n_fft=self.n_fft,
27
+ hop_length=self.hop_length
28
+ )) ** 2
29
+ if to_db:
30
+ spec = librosa.power_to_db(spec, ref=np.max)
31
+ return spec
32
+
33
+ def get_melspectrogram(self):
34
+ """สร้าง Mel-spectrogram"""
35
+ mel_spec = librosa.feature.melspectrogram(
36
+ y=self.waveform.squeeze(0).numpy(),
37
+ sr=self.sr,
38
+ n_fft=self.n_fft,
39
+ hop_length=self.hop_length,
40
+ n_mels=self.n_mels,
41
+ power=2.0
42
+ )
43
+ mel_db = librosa.power_to_db(mel_spec, ref=np.max)
44
+ return mel_db
45
+
46
+ def normalize(self, spec):
47
+ """ปรับค่าสีให้อยู่ในช่วง 0–1"""
48
+ spec_min, spec_max = spec.min(), spec.max()
49
+ return (spec - spec_min) / (spec_max - spec_min + 1e-6)
50
+
51
+ def to_grayscale(self, spec):
52
+ """แปลงให้เป็น 1-channel"""
53
+ return np.expand_dims(spec, axis=0)
54
+
55
+ def get_normalized_melspec(self):
56
+ mel_db = self.get_melspectrogram()
57
+ mel_norm = self.normalize(mel_db)
58
+ return self.to_grayscale(mel_norm)
59
+
60
+ def plot_melspectrogram(self, save_path=None):
61
+ mel_db = self.get_melspectrogram()
62
+ plt.figure(figsize=(10, 4))
63
+ librosa.display.specshow(mel_db, sr=self.sr, hop_length=self.hop_length, cmap="viridis")
64
+ plt.axis("off")
65
+ plt.tight_layout()
66
+ if save_path:
67
+ plt.savefig(save_path, bbox_inches="tight", pad_inches=0)
68
+ plt.close()
69
+ else:
70
+ plt.show()
71
+
72
+ def save_melspectrogram(self, out_path="melspec.png"):
73
+ melspec = self.get_melspectrogram()
74
+ plt.figure(figsize=(10, 4))
75
+ import librosa.display
76
+
77
+ librosa.display.specshow(melspec, sr=self.sr, hop_length=self.hop_length)
78
+ plt.axis("off")
79
+ plt.tight_layout()
80
+ plt.savefig(out_path, bbox_inches="tight", pad_inches=0)
81
+ plt.close()
82
+ return out_path
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ torch
4
+ torchvision
5
+ librosa
6
+ soundfile
7
+ matplotlib
8
+ pillow
9
+ numpy
10
+ requests