Spaces:
Runtime error
Runtime error
| """ | |
| FastAPI backend + frontend server mapped for Hugging Face Spaces. | |
| Models are loaded ONCE at startup and reused for every request. | |
| """ | |
| import os | |
| import sys | |
| import shutil | |
| import tempfile | |
| import subprocess | |
| import gradio as gr | |
| from fastapi import FastAPI, File, UploadFile | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from contextlib import asynccontextmanager | |
| # Dynamically compute paths relative to this file's location | |
| _here = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.insert(0, os.path.join(_here, "scripts")) | |
| import torch | |
| from feature_extractors import ( | |
| _load_audio_model, _load_text_model, | |
| _load_whisper, _load_face_model, | |
| extract_all | |
| ) | |
| from fusion_model import AttentionFusion, ConcatFusion, EMBED_DIM | |
| # Safe cross-platform path resolution | |
| WEIGHTS_PATH = os.environ.get("FUSION_WEIGHTS", os.path.join(_here, "models", "fusion.pt")) | |
| LABELS = ["low risk", "moderate risk", "high risk"] | |
| # Global model handle -- set at startup | |
| _fusion_model = None | |
| def build_model(kind="attention", **kwargs): | |
| if kind == "concat": | |
| return ConcatFusion(**kwargs) | |
| return AttentionFusion(**kwargs) | |
| def get_device(): | |
| if torch.cuda.is_available(): | |
| return "cuda" | |
| return "cpu" | |
| async def lifespan(app): | |
| """Load ALL models once when the server starts.""" | |
| global _fusion_model | |
| device = get_device() | |
| print(f"\n[startup] Loading models on {device} ...") | |
| print("[startup] 1/4 wav2vec2 audio model...") | |
| _load_audio_model() | |
| print("[startup] 2/4 Whisper ASR...") | |
| _load_whisper() | |
| print("[startup] 3/4 Text model (MentalBERT / RoBERTa)...") | |
| _load_text_model() | |
| print("[startup] 4/4 ViT face model...") | |
| _load_face_model() | |
| print("[startup] 5/5 Fusion head...") | |
| checkpoint = torch.load(WEIGHTS_PATH, map_location=device) | |
| _fusion_model = build_model(checkpoint["model_type"], embed_dim=EMBED_DIM).to(device) | |
| _fusion_model.load_state_dict(checkpoint["state_dict"]) | |
| _fusion_model.eval() | |
| print("[startup] All models loaded β ready to serve requests.\n") | |
| yield | |
| # cleanup on shutdown (nothing needed for torch models) | |
| app = FastAPI(title="Mental Health Multimodal Fusion API", lifespan=lifespan) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def extract_audio_from_video(video_path: str, audio_path: str): | |
| subprocess.run([ | |
| "ffmpeg", "-y", "-i", video_path, | |
| "-vn", "-acodec", "pcm_s16le", | |
| "-ar", "16000", "-ac", "1", audio_path | |
| ], check=True) | |
| # try: | |
| # import noisereduce as nr | |
| # import soundfile as sf | |
| # import librosa | |
| # y, sr = librosa.load(raw_path, sr=16000) | |
| # y_clean = nr.reduce_noise(y=y, sr=sr, stationary=True) | |
| # sf.write(audio_path, y_clean, sr) | |
| # os.remove(raw_path) | |
| # except Exception: | |
| # os.rename(raw_path, audio_path) | |
| def predict_with_loaded_models(audio_path: str, video_path: str) -> dict: | |
| """Run inference using already-loaded global models -- no reload.""" | |
| device = get_device() | |
| audio_emb, text_emb, face_emb, transcript = extract_all(audio_path, video_path) | |
| a = torch.from_numpy(audio_emb).unsqueeze(0).to(device) | |
| t = torch.from_numpy(text_emb).unsqueeze(0).to(device) | |
| f = torch.from_numpy(face_emb).unsqueeze(0).to(device) | |
| with torch.no_grad(): | |
| if hasattr(_fusion_model, "pool_weights"): | |
| logits, modality_weights = _fusion_model(a, t, f, return_weights=True) | |
| modality_weights = modality_weights.squeeze(0).cpu().tolist() | |
| else: | |
| logits = _fusion_model(a, t, f) | |
| modality_weights = None | |
| probs = torch.softmax(logits, dim=-1).squeeze(0).cpu().tolist() | |
| pred_idx = int(torch.argmax(logits, dim=-1).item()) | |
| return { | |
| "transcript": transcript, | |
| "prediction": LABELS[pred_idx], | |
| "probabilities": dict(zip(LABELS, probs)), | |
| "modality_contribution": dict(zip(["audio", "text", "face"], modality_weights)) | |
| if modality_weights else None, | |
| } | |
| # ββ API routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "weights_loaded": os.path.exists(WEIGHTS_PATH), | |
| "model_ready": _fusion_model is not None, | |
| "device": get_device(), | |
| } | |
| async def predict_endpoint(video: UploadFile = File(...)): | |
| with tempfile.TemporaryDirectory() as tmp: | |
| video_path = os.path.join(tmp, video.filename) | |
| with open(video_path, "wb") as f: | |
| shutil.copyfileobj(video.file, f) | |
| audio_path = os.path.join(tmp, "extracted_audio.wav") | |
| extract_audio_from_video(video_path, audio_path) | |
| result = predict_with_loaded_models(audio_path, video_path) | |
| return result | |
| # ββ Static files mount ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| STATIC_DIR = os.path.join(_here, "scripts") | |
| app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="frontend") | |
| # ββ Gradio Mount Gateway Definition βββββββββββββββββββββββββββββββββββββββββ | |
| # DO NOT reassign 'app = gr.mount_gradio_app(...)'. | |
| # Instead, keep 'app' and 'demo' as separate global objects. | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# MindSense API Gateway Core Active") | |
| gr.Markdown("Your custom HTML frontend is running at the root `/` URL pathway.") | |
| # Mount your FastAPI app *into* the Gradio interface structure securely | |
| gr.mount_gradio_app(app, demo, path="/gradio_backend") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("app:app", host="0.0.0.0", port=7860) |