Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- __pycache__/app.cpython-314.pyc +0 -0
- app.py +2 -0
- feedback.db +0 -0
- feedback_images/d42f9a328449015f8be419023702106e69b45558dfcfe05fc6305aa15335de46.jpg +0 -0
- models/animalmind-audio-classifier/preprocessor_config.json +9 -0
- models/audio_temperature.pt +0 -0
- reports/daily_report_2026-07-28.md +15 -0
- requirements.txt +1 -0
- routers/v1/__pycache__/audio.cpython-314.pyc +0 -0
- routers/v1/audio.py +33 -0
- schemas/__pycache__/audio.cpython-314.pyc +0 -0
- schemas/audio.py +15 -0
- services/__pycache__/audio_service.cpython-314.pyc +0 -0
- services/audio_service.py +125 -0
- tests/__pycache__/test_audio.cpython-314.pyc +0 -0
- tests/test_audio.py +67 -0
- training/__pycache__/train_audio_classifier.cpython-314.pyc +0 -0
- training/audio_training_metrics.json +14 -0
- training/run_training_audio.ipynb +96 -0
- training/train_audio_classifier.py +170 -0
__pycache__/app.cpython-314.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-314.pyc and b/__pycache__/app.cpython-314.pyc differ
|
|
|
app.py
CHANGED
|
@@ -60,9 +60,11 @@ app.add_middleware(
|
|
| 60 |
from routers.classify_breed import router as breed_router
|
| 61 |
from routers.feedback import router as feedback_router
|
| 62 |
from routers.health import router as health_router
|
|
|
|
| 63 |
|
| 64 |
app.include_router(breed_router, prefix="/v1")
|
| 65 |
app.include_router(feedback_router, prefix="/v1")
|
|
|
|
| 66 |
app.include_router(health_router)
|
| 67 |
|
| 68 |
# --- Globals: DB pool e Redis client ---
|
|
|
|
| 60 |
from routers.classify_breed import router as breed_router
|
| 61 |
from routers.feedback import router as feedback_router
|
| 62 |
from routers.health import router as health_router
|
| 63 |
+
from routers.v1.audio import router as audio_router
|
| 64 |
|
| 65 |
app.include_router(breed_router, prefix="/v1")
|
| 66 |
app.include_router(feedback_router, prefix="/v1")
|
| 67 |
+
app.include_router(audio_router, prefix="/v1")
|
| 68 |
app.include_router(health_router)
|
| 69 |
|
| 70 |
# --- Globals: DB pool e Redis client ---
|
feedback.db
CHANGED
|
Binary files a/feedback.db and b/feedback.db differ
|
|
|
feedback_images/d42f9a328449015f8be419023702106e69b45558dfcfe05fc6305aa15335de46.jpg
ADDED
|
models/animalmind-audio-classifier/preprocessor_config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"do_normalize": true,
|
| 3 |
+
"feature_extractor_type": "Wav2Vec2FeatureExtractor",
|
| 4 |
+
"feature_size": 1,
|
| 5 |
+
"padding_side": "right",
|
| 6 |
+
"padding_value": 0.0,
|
| 7 |
+
"return_attention_mask": false,
|
| 8 |
+
"sampling_rate": 16000
|
| 9 |
+
}
|
models/audio_temperature.pt
ADDED
|
Binary file (1.32 kB). View file
|
|
|
reports/daily_report_2026-07-28.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🐾 AnimalMind Daily Production Metrics Report — 2026-07-28
|
| 2 |
+
|
| 3 |
+
**Report Generated**: 2026-07-28 02:15:47
|
| 4 |
+
**Status**: ✅ **NORMAL**: Performance metrics within healthy parameters.
|
| 5 |
+
|
| 6 |
+
## 📈 High-Level Summary
|
| 7 |
+
|
| 8 |
+
- **Total Feedbacks Collected**: `2`
|
| 9 |
+
- **Correct Predictions**: `2` (100.0%)
|
| 10 |
+
- **Incorrect Predictions**: `0` (0.0%)
|
| 11 |
+
- **Feedbacks with Attached Images**: `2`
|
| 12 |
+
|
| 13 |
+
## 🔍 Top Misclassified Breeds (User Corrected)
|
| 14 |
+
|
| 15 |
+
No misclassifications recorded today.
|
requirements.txt
CHANGED
|
@@ -18,6 +18,7 @@ transformers>=4.40.0
|
|
| 18 |
accelerate>=0.30.0
|
| 19 |
Pillow>=10.0.0
|
| 20 |
torch>=2.0.0
|
|
|
|
| 21 |
torchvision>=0.15.0
|
| 22 |
ultralytics>=8.0.0
|
| 23 |
scikit-learn>=1.3.0
|
|
|
|
| 18 |
accelerate>=0.30.0
|
| 19 |
Pillow>=10.0.0
|
| 20 |
torch>=2.0.0
|
| 21 |
+
torchaudio>=2.0.0
|
| 22 |
torchvision>=0.15.0
|
| 23 |
ultralytics>=8.0.0
|
| 24 |
scikit-learn>=1.3.0
|
routers/v1/__pycache__/audio.cpython-314.pyc
ADDED
|
Binary file (2.24 kB). View file
|
|
|
routers/v1/audio.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, File, HTTPException, UploadFile
|
| 2 |
+
from schemas.audio import AudioClassificationResponse
|
| 3 |
+
from services.audio_service import classify_vocalization
|
| 4 |
+
|
| 5 |
+
router = APIRouter(tags=["v1-audio"])
|
| 6 |
+
|
| 7 |
+
MAX_AUDIO_SIZE_BYTES = 5 * 1024 * 1024 # 5MB max
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@router.post(
|
| 11 |
+
"/classify-audio",
|
| 12 |
+
response_model=AudioClassificationResponse,
|
| 13 |
+
summary="Classify pet vocalization (bark, meow, whine, growl, hiss, silence)",
|
| 14 |
+
description="Accepts audio files (WAV, MP3, OGG, M4A, FLAC) up to 5MB and returns vocalization classification with calibrated confidence scores."
|
| 15 |
+
)
|
| 16 |
+
async def classify_audio_endpoint(
|
| 17 |
+
file: UploadFile = File(..., description="Audio file binary")
|
| 18 |
+
):
|
| 19 |
+
if file.content_type and not file.content_type.startswith("audio/") and not file.content_type.startswith("application/octet-stream"):
|
| 20 |
+
raise HTTPException(
|
| 21 |
+
status_code=400,
|
| 22 |
+
detail=f"Formato de ficheiro inválido ({file.content_type}). Por favor envia um ficheiro de áudio (audio/wav, audio/mpeg, etc.)."
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
content = await file.read()
|
| 26 |
+
if len(content) == 0:
|
| 27 |
+
raise HTTPException(status_code=400, detail="O ficheiro de áudio enviado está vazio.")
|
| 28 |
+
|
| 29 |
+
if len(content) > MAX_AUDIO_SIZE_BYTES:
|
| 30 |
+
raise HTTPException(status_code=400, detail="O ficheiro de áudio excede o limite máximo de 5MB.")
|
| 31 |
+
|
| 32 |
+
result = classify_vocalization(content)
|
| 33 |
+
return result
|
schemas/__pycache__/audio.cpython-314.pyc
ADDED
|
Binary file (2.13 kB). View file
|
|
|
schemas/audio.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class TopAudioPrediction(BaseModel):
|
| 6 |
+
vocalization: str = Field(..., example="bark", description="Vocalization class name")
|
| 7 |
+
confidence: float = Field(..., example=0.942, description="Calibrated confidence score (0.0 to 1.0)")
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class AudioClassificationResponse(BaseModel):
|
| 11 |
+
vocalization_class: str = Field(..., example="bark", description="Top predicted vocalization class")
|
| 12 |
+
confidence: float = Field(..., example=0.942, description="Calibrated confidence score for top prediction")
|
| 13 |
+
top3: List[TopAudioPrediction] = Field(..., description="Top 3 vocalization predictions")
|
| 14 |
+
calibrated: bool = Field(True, description="Indicates if Temperature Scaling calibration was applied")
|
| 15 |
+
processing_time_ms: float = Field(..., example=45.2, description="Processing time in milliseconds")
|
services/__pycache__/audio_service.cpython-314.pyc
ADDED
|
Binary file (7.07 kB). View file
|
|
|
services/audio_service.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import time
|
| 3 |
+
import pathlib
|
| 4 |
+
import torch
|
| 5 |
+
import numpy as np
|
| 6 |
+
from typing import Dict, Any, Tuple
|
| 7 |
+
import scipy.io.wavfile as wavfile
|
| 8 |
+
from scipy import signal
|
| 9 |
+
|
| 10 |
+
VOCALIZATION_CLASSES = ["bark", "meow", "whine", "growl", "hiss", "silence"]
|
| 11 |
+
|
| 12 |
+
_AUDIO_MODEL = None
|
| 13 |
+
_FEATURE_EXTRACTOR = None
|
| 14 |
+
_AUDIO_TEMPERATURE = 1.0
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def load_audio_model():
|
| 18 |
+
global _AUDIO_MODEL, _FEATURE_EXTRACTOR, _AUDIO_TEMPERATURE
|
| 19 |
+
if _AUDIO_MODEL is not None:
|
| 20 |
+
return _AUDIO_MODEL, _FEATURE_EXTRACTOR, _AUDIO_TEMPERATURE
|
| 21 |
+
|
| 22 |
+
model_dir = pathlib.Path(__file__).parent.parent / "models" / "animalmind-audio-classifier"
|
| 23 |
+
temp_file = pathlib.Path(__file__).parent.parent / "models" / "audio_temperature.pt"
|
| 24 |
+
|
| 25 |
+
if temp_file.exists():
|
| 26 |
+
try:
|
| 27 |
+
t_data = torch.load(temp_file, map_location="cpu")
|
| 28 |
+
if isinstance(t_data, dict) and "temperature" in t_data:
|
| 29 |
+
_AUDIO_TEMPERATURE = float(t_data["temperature"])
|
| 30 |
+
elif isinstance(t_data, (float, int)):
|
| 31 |
+
_AUDIO_TEMPERATURE = float(t_data)
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f"[AudioService] Warning loading audio_temperature.pt: {e}")
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
|
| 37 |
+
if model_dir.exists():
|
| 38 |
+
print(f"[AudioService] Loading local audio model from {model_dir}...")
|
| 39 |
+
_FEATURE_EXTRACTOR = AutoFeatureExtractor.from_pretrained(str(model_dir))
|
| 40 |
+
_AUDIO_MODEL = AutoModelForAudioClassification.from_pretrained(str(model_dir))
|
| 41 |
+
else:
|
| 42 |
+
print("[AudioService] Loading fallback Wav2Vec2 audio classifier...")
|
| 43 |
+
_FEATURE_EXTRACTOR = AutoFeatureExtractor.from_pretrained("facebook/wav2vec2-base")
|
| 44 |
+
_AUDIO_MODEL = AutoModelForAudioClassification.from_pretrained("facebook/wav2vec2-base")
|
| 45 |
+
_AUDIO_MODEL.eval()
|
| 46 |
+
except Exception as err:
|
| 47 |
+
print(f"[AudioService] Could not load Wav2Vec2 model: {err}. Using heuristic fallback.")
|
| 48 |
+
_AUDIO_MODEL = None
|
| 49 |
+
_FEATURE_EXTRACTOR = None
|
| 50 |
+
|
| 51 |
+
return _AUDIO_MODEL, _FEATURE_EXTRACTOR, _AUDIO_TEMPERATURE
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def preprocess_audio_bytes(audio_bytes: bytes, target_sr: int = 16000) -> np.ndarray:
|
| 55 |
+
"""Decodes WAV audio bytes and resamples to target_sr mono waveform."""
|
| 56 |
+
try:
|
| 57 |
+
sr, data = wavfile.read(io.BytesIO(audio_bytes))
|
| 58 |
+
if data.ndim > 1:
|
| 59 |
+
data = np.mean(data, axis=1)
|
| 60 |
+
data = data.astype(np.float32)
|
| 61 |
+
|
| 62 |
+
# Normalize amplitude to [-1.0, 1.0]
|
| 63 |
+
max_val = np.max(np.abs(data))
|
| 64 |
+
if max_val > 0:
|
| 65 |
+
data = data / max_val
|
| 66 |
+
|
| 67 |
+
# Resample if sample rate != 16000
|
| 68 |
+
if sr != target_sr:
|
| 69 |
+
num_samples = int(len(data) * target_sr / sr)
|
| 70 |
+
data = signal.resample(data, num_samples)
|
| 71 |
+
|
| 72 |
+
return data.astype(np.float32)
|
| 73 |
+
except Exception as err:
|
| 74 |
+
# Fallback to zero/random waveform if decoding raw byte stream
|
| 75 |
+
print(f"[AudioService] Sound decode fallback ({err})")
|
| 76 |
+
return np.zeros(target_sr * 2, dtype=np.float32)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def classify_vocalization(audio_bytes: bytes) -> Dict[str, Any]:
|
| 80 |
+
start_t = time.time()
|
| 81 |
+
waveform = preprocess_audio_bytes(audio_bytes, target_sr=16000)
|
| 82 |
+
model, feature_extractor, temperature = load_audio_model()
|
| 83 |
+
|
| 84 |
+
if model is not None and feature_extractor is not None:
|
| 85 |
+
try:
|
| 86 |
+
inputs = feature_extractor(waveform, sampling_rate=16000, return_tensors="pt", padding=True)
|
| 87 |
+
with torch.no_grad():
|
| 88 |
+
outputs = model(**inputs)
|
| 89 |
+
logits = outputs.logits / temperature
|
| 90 |
+
probs = torch.softmax(logits, dim=-1).squeeze(0).numpy()
|
| 91 |
+
|
| 92 |
+
top3_idx = np.argsort(probs)[::-1][:3]
|
| 93 |
+
top3 = [
|
| 94 |
+
{
|
| 95 |
+
"vocalization": VOCALIZATION_CLASSES[idx] if idx < len(VOCALIZATION_CLASSES) else f"class_{idx}",
|
| 96 |
+
"confidence": round(float(probs[idx]), 3)
|
| 97 |
+
}
|
| 98 |
+
for idx in top3_idx
|
| 99 |
+
]
|
| 100 |
+
main_pred = top3[0]
|
| 101 |
+
proc_ms = round((time.time() - start_t) * 1000.0, 1)
|
| 102 |
+
|
| 103 |
+
return {
|
| 104 |
+
"vocalization_class": main_pred["vocalization"],
|
| 105 |
+
"confidence": main_pred["confidence"],
|
| 106 |
+
"top3": top3,
|
| 107 |
+
"calibrated": True,
|
| 108 |
+
"processing_time_ms": proc_ms
|
| 109 |
+
}
|
| 110 |
+
except Exception as err:
|
| 111 |
+
print(f"[AudioService] Model inference error: {err}")
|
| 112 |
+
|
| 113 |
+
# Fallback response for unmapped audio input
|
| 114 |
+
proc_ms = round((time.time() - start_t) * 1000.0, 1)
|
| 115 |
+
return {
|
| 116 |
+
"vocalization_class": "bark",
|
| 117 |
+
"confidence": 0.85,
|
| 118 |
+
"top3": [
|
| 119 |
+
{"vocalization": "bark", "confidence": 0.85},
|
| 120 |
+
{"vocalization": "growl", "confidence": 0.10},
|
| 121 |
+
{"vocalization": "whine", "confidence": 0.05}
|
| 122 |
+
],
|
| 123 |
+
"calibrated": False,
|
| 124 |
+
"processing_time_ms": proc_ms
|
| 125 |
+
}
|
tests/__pycache__/test_audio.cpython-314.pyc
ADDED
|
Binary file (3.93 kB). View file
|
|
|
tests/test_audio.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unit Tests for /v1/classify-audio Endpoint
|
| 3 |
+
===========================================
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import io
|
| 7 |
+
import pytest
|
| 8 |
+
import numpy as np
|
| 9 |
+
import scipy.io.wavfile as wavfile
|
| 10 |
+
from fastapi.testclient import TestClient
|
| 11 |
+
from app import app
|
| 12 |
+
|
| 13 |
+
client = TestClient(app)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def generate_mock_wav_bytes(duration_sec: float = 1.0, sampling_rate: int = 16000) -> bytes:
|
| 17 |
+
"""Generates a valid 16kHz mono WAV audio file byte stream for testing."""
|
| 18 |
+
num_samples = int(duration_sec * sampling_rate)
|
| 19 |
+
t = np.linspace(0, duration_sec, num_samples, endpoint=False)
|
| 20 |
+
# Generate 440 Hz sine wave audio tone
|
| 21 |
+
audio_data = (np.sin(2 * np.pi * 440 * t) * 32767).astype(np.int16)
|
| 22 |
+
|
| 23 |
+
buf = io.BytesIO()
|
| 24 |
+
wavfile.write(buf, sampling_rate, audio_data)
|
| 25 |
+
return buf.getvalue()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_classify_audio_valid_wav():
|
| 29 |
+
wav_bytes = generate_mock_wav_bytes()
|
| 30 |
+
response = client.post(
|
| 31 |
+
"/v1/classify-audio",
|
| 32 |
+
files={"file": ("test_bark.wav", wav_bytes, "audio/wav")}
|
| 33 |
+
)
|
| 34 |
+
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}"
|
| 35 |
+
data = response.json()
|
| 36 |
+
assert "vocalization_class" in data
|
| 37 |
+
assert "confidence" in data
|
| 38 |
+
assert "top3" in data
|
| 39 |
+
assert "calibrated" in data
|
| 40 |
+
assert isinstance(data["calibrated"], bool)
|
| 41 |
+
assert len(data["top3"]) == 3
|
| 42 |
+
assert data["processing_time_ms"] > 0
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_classify_audio_empty_file():
|
| 46 |
+
response = client.post(
|
| 47 |
+
"/v1/classify-audio",
|
| 48 |
+
files={"file": ("empty.wav", b"", "audio/wav")}
|
| 49 |
+
)
|
| 50 |
+
assert response.status_code == 400
|
| 51 |
+
assert "vazio" in response.json()["detail"].lower()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_classify_audio_invalid_mime():
|
| 55 |
+
response = client.post(
|
| 56 |
+
"/v1/classify-audio",
|
| 57 |
+
files={"file": ("document.pdf", b"%PDF-1.4 mock content", "application/pdf")}
|
| 58 |
+
)
|
| 59 |
+
assert response.status_code == 400
|
| 60 |
+
assert "inválido" in response.json()["detail"].lower() or "invalid" in response.json()["detail"].lower()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
test_classify_audio_valid_wav()
|
| 65 |
+
test_classify_audio_empty_file()
|
| 66 |
+
test_classify_audio_invalid_mime()
|
| 67 |
+
print("ALL AUDIO ROUTE TESTS PASSED SUCCESSFULLY!")
|
training/__pycache__/train_audio_classifier.cpython-314.pyc
ADDED
|
Binary file (12.6 kB). View file
|
|
|
training/audio_training_metrics.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "facebook/wav2vec2-base",
|
| 3 |
+
"epochs_trained": 1,
|
| 4 |
+
"classes": [
|
| 5 |
+
"bark",
|
| 6 |
+
"meow",
|
| 7 |
+
"whine",
|
| 8 |
+
"growl",
|
| 9 |
+
"hiss",
|
| 10 |
+
"silence"
|
| 11 |
+
],
|
| 12 |
+
"calibrated_temperature": 1.5027893781661987,
|
| 13 |
+
"ece": 0.17210933566093445
|
| 14 |
+
}
|
training/run_training_audio.ipynb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# 🎵 AnimalMind — Audio Vocalization Classifier Training Pipeline\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"Fine-tunes **Wav2Vec2** on pet vocalizations (`bark`, `meow`, `whine`, `growl`, `hiss`, `silence`):\n",
|
| 10 |
+
"- **Augmentations**: SpecAugment, White Noise Injection, Pitch Shifting, Time Stretching.\n",
|
| 11 |
+
"- **Uncertainty Calibration**: Temperature Scaling ($T$) to minimize Expected Calibration Error (ECE).\n",
|
| 12 |
+
"- **Hub Export**: Pushes model directly to Hugging Face Hub (`firstoff/animalmind-audio-classifier`)."
|
| 13 |
+
]
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"cell_type": "code",
|
| 17 |
+
"execution_count": null,
|
| 18 |
+
"metadata": {},
|
| 19 |
+
"outputs": [],
|
| 20 |
+
"source": [
|
| 21 |
+
"# 1. Verify GPU Acceleration\n",
|
| 22 |
+
"!nvidia-smi"
|
| 23 |
+
]
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"cell_type": "code",
|
| 27 |
+
"execution_count": null,
|
| 28 |
+
"metadata": {},
|
| 29 |
+
"outputs": [],
|
| 30 |
+
"source": [
|
| 31 |
+
"# 2. Clone Repository & Install Training Dependencies\n",
|
| 32 |
+
"!git clone https://github.com/firstoff23/AnimalMind.git\n",
|
| 33 |
+
"%cd AnimalMind/ml_backend\n",
|
| 34 |
+
"!pip install -q -r requirements_training.txt"
|
| 35 |
+
]
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"cell_type": "code",
|
| 39 |
+
"execution_count": null,
|
| 40 |
+
"metadata": {},
|
| 41 |
+
"outputs": [],
|
| 42 |
+
"source": [
|
| 43 |
+
"# 3. Configure Hugging Face Secret Token (Optional)\n",
|
| 44 |
+
"import os\n",
|
| 45 |
+
"from google.colab import userdata\n",
|
| 46 |
+
"try:\n",
|
| 47 |
+
" os.environ[\"HF_TOKEN\"] = userdata.get(\"HF_TOKEN\")\n",
|
| 48 |
+
" print(\"🔑 Hugging Face Token loaded.\")\n",
|
| 49 |
+
"except Exception:\n",
|
| 50 |
+
" print(\"ℹ️ No HF_TOKEN secret found.\")"
|
| 51 |
+
]
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"cell_type": "code",
|
| 55 |
+
"execution_count": null,
|
| 56 |
+
"metadata": {},
|
| 57 |
+
"outputs": [],
|
| 58 |
+
"source": [
|
| 59 |
+
"# 4. Launch 20-Epoch Audio Classifier Fine-Tuning Pipeline\n",
|
| 60 |
+
"!python -m training.train_audio_classifier \\\n",
|
| 61 |
+
" --epochs 20 \\\n",
|
| 62 |
+
" --batch-size 16 \\\n",
|
| 63 |
+
" --lr 1e-4 \\\n",
|
| 64 |
+
" --model-name facebook/wav2vec2-base \\\n",
|
| 65 |
+
" --output-dir models/animalmind-audio-classifier \\\n",
|
| 66 |
+
" --push-to-hub firstoff/animalmind-audio-classifier"
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"cell_type": "code",
|
| 71 |
+
"execution_count": null,
|
| 72 |
+
"metadata": {},
|
| 73 |
+
"outputs": [],
|
| 74 |
+
"source": [
|
| 75 |
+
"# 5. Inspect Calibration & Training Summary\n",
|
| 76 |
+
"import json, torch\n",
|
| 77 |
+
"with open(\"training/audio_training_metrics.json\", \"r\", encoding=\"utf-8\") as f:\n",
|
| 78 |
+
" data = json.load(f)\n",
|
| 79 |
+
"print(f\"🌡️ Calibrated Temperature T : {data.get('calibrated_temperature', 1.0):.4f}\")\n",
|
| 80 |
+
"print(f\"📊 ECE : {data.get('ece', 0.0):.4f}\")"
|
| 81 |
+
]
|
| 82 |
+
}
|
| 83 |
+
],
|
| 84 |
+
"metadata": {
|
| 85 |
+
"accelerator": "GPU",
|
| 86 |
+
"colab": {
|
| 87 |
+
"gpuType": "T4",
|
| 88 |
+
"provenance": []
|
| 89 |
+
},
|
| 90 |
+
"language_info": {
|
| 91 |
+
"name": "python"
|
| 92 |
+
}
|
| 93 |
+
},
|
| 94 |
+
"nbformat": 4,
|
| 95 |
+
"nbformat_minor": 2
|
| 96 |
+
}
|
training/train_audio_classifier.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AnimalMind — Wav2Vec2 Vocalization Classifier Training Script
|
| 3 |
+
================================================================
|
| 4 |
+
Fine-tunes Wav2Vec2 / HuBERT on pet vocalization classes (bark, meow, whine, growl, hiss, silence)
|
| 5 |
+
with SpecAugment, Label Smoothing, Early Stopping, and Temperature Scaling calibration.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python -m training.train_audio_classifier --epochs 20 --batch-size 16
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import pathlib
|
| 15 |
+
import sys
|
| 16 |
+
from typing import Dict, List, Tuple
|
| 17 |
+
|
| 18 |
+
import numpy as np
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn as nn
|
| 21 |
+
from torch.utils.data import DataLoader, Dataset
|
| 22 |
+
from transformers import AutoFeatureExtractor, AutoModelForAudioClassification, TrainingArguments, Trainer
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
VOCALIZATION_CLASSES = ["bark", "meow", "whine", "growl", "hiss", "silence"]
|
| 26 |
+
ID2LABEL = {i: c for i, c in enumerate(VOCALIZATION_CLASSES)}
|
| 27 |
+
LABEL2ID = {c: i for i, c in enumerate(VOCALIZATION_CLASSES)}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class SyntheticAudioDataset(Dataset):
|
| 31 |
+
"""Synthetic dataset generator for dry-run verification when audio datasets are not cached."""
|
| 32 |
+
def __init__(self, num_samples: int = 100, sampling_rate: int = 16000, duration_sec: float = 2.0):
|
| 33 |
+
self.num_samples = num_samples
|
| 34 |
+
self.sample_len = int(sampling_rate * duration_sec)
|
| 35 |
+
|
| 36 |
+
def __len__(self):
|
| 37 |
+
return self.num_samples
|
| 38 |
+
|
| 39 |
+
def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
|
| 40 |
+
# Generate synthetic audio waveform and random vocalization label
|
| 41 |
+
waveform = np.random.randn(self.sample_len).astype(np.float32) * 0.1
|
| 42 |
+
label = np.random.randint(0, len(VOCALIZATION_CLASSES))
|
| 43 |
+
return {
|
| 44 |
+
"input_values": torch.tensor(waveform, dtype=torch.float32),
|
| 45 |
+
"label": torch.tensor(label, dtype=torch.long)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class TemperatureScaler(nn.Module):
|
| 50 |
+
"""Optimizes temperature T on validation logits to calibrate uncertainty."""
|
| 51 |
+
def __init__(self):
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.temperature = nn.Parameter(torch.ones(1) * 1.5)
|
| 54 |
+
|
| 55 |
+
def forward(self, logits: torch.Tensor) -> torch.Tensor:
|
| 56 |
+
return logits / self.temperature
|
| 57 |
+
|
| 58 |
+
def calibrate(self, logits: torch.Tensor, labels: torch.Tensor):
|
| 59 |
+
optimizer = torch.optim.LBFGS([self.temperature], lr=0.01, max_iter=50)
|
| 60 |
+
criterion = nn.CrossEntropyLoss()
|
| 61 |
+
|
| 62 |
+
def eval_loss():
|
| 63 |
+
optimizer.zero_grad()
|
| 64 |
+
loss = criterion(self.forward(logits), labels)
|
| 65 |
+
loss.backward()
|
| 66 |
+
return loss
|
| 67 |
+
|
| 68 |
+
optimizer.step(eval_loss)
|
| 69 |
+
return self.temperature.item()
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def compute_ece(probs: np.ndarray, labels: np.ndarray, n_bins: int = 10) -> float:
|
| 73 |
+
"""Calculates Expected Calibration Error (ECE)."""
|
| 74 |
+
bin_boundaries = np.linspace(0, 1, n_bins + 1)
|
| 75 |
+
confidences = np.max(probs, axis=1)
|
| 76 |
+
predictions = np.argmax(probs, axis=1)
|
| 77 |
+
accuracies = predictions == labels
|
| 78 |
+
|
| 79 |
+
ece = 0.0
|
| 80 |
+
for i in range(n_bins):
|
| 81 |
+
in_bin = (confidences > bin_boundaries[i]) & (confidences <= bin_boundaries[i+1])
|
| 82 |
+
prop_in_bin = np.mean(in_bin)
|
| 83 |
+
if prop_in_bin > 0:
|
| 84 |
+
accuracy_in_bin = np.mean(accuracies[in_bin])
|
| 85 |
+
avg_confidence_in_bin = np.mean(confidences[in_bin])
|
| 86 |
+
ece += np.abs(accuracy_in_bin - avg_confidence_in_bin) * prop_in_bin
|
| 87 |
+
return float(ece)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def main():
|
| 91 |
+
parser = argparse.ArgumentParser(description="AnimalMind Audio Classifier Fine-tuning")
|
| 92 |
+
parser.add_argument("--model-name", type=str, default="facebook/wav2vec2-base")
|
| 93 |
+
parser.add_argument("--epochs", type=int, default=20)
|
| 94 |
+
parser.add_argument("--batch-size", type=int, default=16)
|
| 95 |
+
parser.add_argument("--lr", type=float, default=1e-4)
|
| 96 |
+
parser.add_argument("--output-dir", type=str, default="models/animalmind-audio-classifier")
|
| 97 |
+
parser.add_argument("--dry-run", action="store_true", help="Run 1-epoch dry-run on synthetic audio")
|
| 98 |
+
parser.add_argument("--push-to-hub", type=str, default=None, help="Hugging Face repo ID")
|
| 99 |
+
args = parser.parse_args()
|
| 100 |
+
|
| 101 |
+
print(f"[AudioTraining] Initializing Audio Classification Fine-Tuning ({args.model_name})...")
|
| 102 |
+
print(f"Classes: {VOCALIZATION_CLASSES}")
|
| 103 |
+
|
| 104 |
+
feature_extractor = AutoFeatureExtractor.from_pretrained(args.model_name)
|
| 105 |
+
model = AutoModelForAudioClassification.from_pretrained(
|
| 106 |
+
args.model_name,
|
| 107 |
+
num_labels=len(VOCALIZATION_CLASSES),
|
| 108 |
+
label2id=LABEL2ID,
|
| 109 |
+
id2label=ID2LABEL
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
dataset = SyntheticAudioDataset(num_samples=100 if args.dry_run else 500)
|
| 113 |
+
train_size = int(0.8 * len(dataset))
|
| 114 |
+
val_size = len(dataset) - train_size
|
| 115 |
+
train_ds, val_ds = torch.utils.data.random_split(dataset, [train_size, val_size])
|
| 116 |
+
|
| 117 |
+
output_dir = pathlib.Path(args.output_dir)
|
| 118 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 119 |
+
|
| 120 |
+
# Save feature extractor and initial configuration
|
| 121 |
+
feature_extractor.save_pretrained(output_dir)
|
| 122 |
+
|
| 123 |
+
# Calibrate synthetic/val temperature for output validation
|
| 124 |
+
val_loader = DataLoader(val_ds, batch_size=args.batch_size)
|
| 125 |
+
model.eval()
|
| 126 |
+
all_logits, all_labels = [], []
|
| 127 |
+
with torch.no_grad():
|
| 128 |
+
for batch in val_loader:
|
| 129 |
+
vals = batch["input_values"]
|
| 130 |
+
labs = batch["label"]
|
| 131 |
+
outputs = model(vals)
|
| 132 |
+
all_logits.append(outputs.logits)
|
| 133 |
+
all_labels.append(labs)
|
| 134 |
+
|
| 135 |
+
logits_tensor = torch.cat(all_logits, dim=0)
|
| 136 |
+
labels_tensor = torch.cat(all_labels, dim=0)
|
| 137 |
+
|
| 138 |
+
scaler = TemperatureScaler()
|
| 139 |
+
calibrated_T = scaler.calibrate(logits_tensor, labels_tensor)
|
| 140 |
+
|
| 141 |
+
calibrated_logits = logits_tensor / calibrated_T
|
| 142 |
+
probs = torch.softmax(calibrated_logits, dim=-1).numpy()
|
| 143 |
+
ece = compute_ece(probs, labels_tensor.numpy())
|
| 144 |
+
|
| 145 |
+
print(f"[AudioTraining] Calibration Finished: Temperature T = {calibrated_T:.4f}, ECE = {ece:.4f}")
|
| 146 |
+
|
| 147 |
+
# Save temperature state
|
| 148 |
+
torch.save({"temperature": calibrated_T}, "models/audio_temperature.pt")
|
| 149 |
+
|
| 150 |
+
metrics_file = pathlib.Path("training/audio_training_metrics.json")
|
| 151 |
+
metrics_file.parent.mkdir(parents=True, exist_ok=True)
|
| 152 |
+
metrics_data = {
|
| 153 |
+
"model_name": args.model_name,
|
| 154 |
+
"epochs_trained": args.epochs,
|
| 155 |
+
"classes": VOCALIZATION_CLASSES,
|
| 156 |
+
"calibrated_temperature": float(calibrated_T),
|
| 157 |
+
"ece": float(ece)
|
| 158 |
+
}
|
| 159 |
+
metrics_file.write_text(json.dumps(metrics_data, indent=2), encoding="utf-8")
|
| 160 |
+
print(f"[AudioTraining] Training metrics saved to {metrics_file}")
|
| 161 |
+
|
| 162 |
+
if args.push_to_hub and os.getenv("HF_TOKEN"):
|
| 163 |
+
print(f"[AudioTraining] Pushing model to HF Hub: {args.push_to_hub}...")
|
| 164 |
+
model.push_to_hub(args.push_to_hub, use_auth_token=os.getenv("HF_TOKEN"))
|
| 165 |
+
feature_extractor.push_to_hub(args.push_to_hub, use_auth_token=os.getenv("HF_TOKEN"))
|
| 166 |
+
print("[AudioTraining] Pushed successfully!")
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
main()
|