Spaces:
Sleeping
Sleeping
| from io import BytesIO | |
| from contextlib import asynccontextmanager | |
| import librosa | |
| import numpy as np | |
| import torch | |
| from fastapi import FastAPI, File, HTTPException, UploadFile | |
| from transformers import AutoFeatureExtractor, AutoModelForAudioClassification, AutoProcessor | |
| MODEL_NAME = "garystafford/wav2vec2-deepfake-voice-detector" | |
| TARGET_SAMPLE_RATE = 16000 | |
| THRESHOLD = 0.5 | |
| DEVICE = "cpu" | |
| ALLOWED_EXTENSIONS = {".wav", ".mp3"} | |
| async def lifespan(app: FastAPI): | |
| global processor, model | |
| try: | |
| processor = AutoProcessor.from_pretrained(MODEL_NAME) | |
| except TypeError: | |
| # Some audio checkpoints do not ship tokenizer files expected by AutoProcessor. | |
| processor = AutoFeatureExtractor.from_pretrained(MODEL_NAME) | |
| model = AutoModelForAudioClassification.from_pretrained(MODEL_NAME) | |
| model.to(DEVICE) | |
| model.eval() | |
| yield | |
| app = FastAPI(lifespan=lifespan) | |
| processor = None | |
| model = None | |
| def _resolve_label_indices(id2label: dict[int, str]) -> tuple[int, int]: | |
| real_idx = None | |
| fake_idx = None | |
| for idx, label in id2label.items(): | |
| normalized = label.strip().lower() | |
| if normalized == "real": | |
| real_idx = idx | |
| elif normalized == "fake": | |
| fake_idx = idx | |
| if real_idx is None or fake_idx is None: | |
| raise RuntimeError("Model labels must include both 'real' and 'fake'.") | |
| return real_idx, fake_idx | |
| def health() -> dict[str, str]: | |
| return {"status": "ok"} | |
| async def predict(file: UploadFile = File(...)) -> dict: | |
| if processor is None or model is None: | |
| raise HTTPException(status_code=503, detail="Model not loaded yet.") | |
| filename = file.filename or "uploaded_audio.wav" | |
| lowered = filename.lower() | |
| if not any(lowered.endswith(ext) for ext in ALLOWED_EXTENSIONS): | |
| raise HTTPException(status_code=400, detail="Only .wav and .mp3 files are supported.") | |
| try: | |
| audio_bytes = await file.read() | |
| if not audio_bytes: | |
| raise ValueError("Uploaded file is empty.") | |
| # librosa loads and converts to mono/16kHz in one step. | |
| audio, _ = librosa.load(BytesIO(audio_bytes), sr=TARGET_SAMPLE_RATE, mono=True) | |
| if audio.size == 0: | |
| raise ValueError("Audio content is empty.") | |
| audio = np.asarray(audio, dtype=np.float32) | |
| inputs = processor( | |
| audio, | |
| sampling_rate=TARGET_SAMPLE_RATE, | |
| return_tensors="pt", | |
| padding=True, | |
| ) | |
| inputs = {key: value.to(DEVICE) for key, value in inputs.items()} | |
| with torch.no_grad(): | |
| logits = model(**inputs).logits | |
| probabilities = torch.softmax(logits, dim=-1).cpu().numpy()[0] | |
| id2label = {int(k): v for k, v in model.config.id2label.items()} | |
| real_idx, fake_idx = _resolve_label_indices(id2label) | |
| real_score = float(probabilities[real_idx]) | |
| fake_score = float(probabilities[fake_idx]) | |
| is_fake = fake_score > THRESHOLD | |
| predicted_label = "fake" if is_fake else "real" | |
| predicted_index = fake_idx if is_fake else real_idx | |
| confidence = fake_score if is_fake else real_score | |
| return { | |
| "source": filename, | |
| "predicted_label": predicted_label, | |
| "predicted_index": int(predicted_index), | |
| "confidence": round(confidence, 6), | |
| "is_fake": is_fake, | |
| "fake_score": round(fake_score, 6), | |
| "real_score": round(real_score, 6), | |
| "threshold": THRESHOLD, | |
| "scores": { | |
| "real": round(real_score, 6), | |
| "fake": round(fake_score, 6), | |
| }, | |
| "model_name": MODEL_NAME, | |
| "sample_rate": TARGET_SAMPLE_RATE, | |
| "device": DEVICE, | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as exc: | |
| raise HTTPException(status_code=400, detail=f"Failed to process audio: {exc}") from exc | |