| from fastapi import FastAPI, UploadFile, File, HTTPException |
| from pathlib import Path |
| import numpy as np |
| import tensorflow_hub as hub |
| import tensorflow as tf |
| import librosa |
| import io |
| import joblib |
| from loguru import logger |
| from pydub import AudioSegment |
| from rich.logging import RichHandler |
| from fastapi.middleware.cors import CORSMiddleware |
|
|
| |
| |
| |
| logger.remove() |
| logger.add(RichHandler(), level="INFO") |
|
|
| |
| |
| |
| MODEL_DIR = Path("models") |
| VGGISH_MODEL_URL = "https://tfhub.dev/google/vggish/1" |
| SR = 16000 |
|
|
| |
| |
| |
| app = FastAPI(title="Audio Embedding & Classification API") |
|
|
| |
| |
| |
|
|
| origins = [ |
| "https://zane-dev16.github.io/MJ-Cat-Frontend/", |
| ] |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=origins, |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| |
| |
| logger.info("Loading VGGish model from TF Hub...") |
| vggish_model = hub.load(VGGISH_MODEL_URL) |
| logger.info("VGGish model loaded successfully.") |
|
|
| lgbm_model_path = MODEL_DIR / "lgbm_model.pkl" |
| if not lgbm_model_path.exists(): |
| raise FileNotFoundError(f"LightGBM model not found at {lgbm_model_path}") |
| logger.info("Loading LightGBM model...") |
| lgbm_model = joblib.load(lgbm_model_path) |
| logger.info("LightGBM model loaded successfully.") |
|
|
| |
| |
| |
| def preprocess_audio(file_bytes: bytes) -> tf.Tensor: |
| waveform, _ = librosa.load(io.BytesIO(file_bytes), sr=SR, mono=True) |
| return tf.convert_to_tensor(np.array(waveform, dtype=np.float32)) |
|
|
| def extract_embedding(tensor: tf.Tensor) -> np.ndarray: |
| embedding = vggish_model(tensor).numpy() |
| feature_vector = np.concatenate([ |
| np.mean(embedding, axis=0), |
| np.std(embedding, axis=0), |
| np.max(embedding, axis=0) |
| ]) |
| return feature_vector |
|
|
| |
| |
| |
|
|
| @app.post("/predict") |
| async def predict(file: UploadFile = File(...)): |
| logger.info(f"Received file: {file.filename}") |
|
|
| file_bytes = await file.read() |
|
|
| if file.filename.endswith(".webm"): |
| try: |
| audio = AudioSegment.from_file(io.BytesIO(file_bytes), format="webm") |
| wav_io = io.BytesIO() |
| audio.export(wav_io, format="wav") |
| wav_io.seek(0) |
| file_bytes = wav_io.read() |
| logger.info(f"Converted WebM to WAV: {file.filename}") |
| except Exception as e: |
| logger.error(f"Failed to convert WebM to WAV: {e}") |
| raise HTTPException(status_code=500, detail="Failed to convert WebM to WAV") |
|
|
| elif not file.filename.endswith(".wav"): |
| raise HTTPException(status_code=400, detail="Only WAV or WebM files are supported") |
|
|
| try: |
| tensor = preprocess_audio(file_bytes) |
| features = extract_embedding(tensor).reshape(1, -1) |
| proba = lgbm_model.predict(features)[0] |
| prediction = int(proba >= 0.5) |
| logger.info(f"Prediction: {prediction} (prob={proba:.2f})") |
| return {"prediction": prediction, "probability": float(proba)} |
| except Exception as e: |
| logger.error(f"Failed to process file {file.filename}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @app.post("/extract_embedding") |
| async def get_embedding(file: UploadFile = File(...)): |
| if not file.filename.endswith(".wav"): |
| raise HTTPException(status_code=400, detail="Only WAV files are supported") |
| try: |
| file_bytes = await file.read() |
| tensor = preprocess_audio(file_bytes) |
| embedding = extract_embedding(tensor) |
| return {"embedding": embedding.tolist()} |
| except Exception as e: |
| logger.error(f"Failed to process file {file.filename}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @app.get("/") |
| def health_check(): |
| return {"status": "ok", "message": "Service running with VGGish preloaded."} |
|
|
|
|