AuralGuard / src /dataset.py
AyoPrince's picture
Upload folder using huggingface_hub
127b976 verified
Raw History Blame Contribute Delete
4.62 kB
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Dict, Any
import pandas as pd
import torch
import torch.nn.functional as F
import torchaudio
from torch.utils.data import Dataset
from .labels import ATTACK_TO_ID, normalize_attack_type, make_explanation_label
@dataclass
class AudioConfig:
sample_rate: int = 16000
duration_sec: float = 4.0
random_crop: bool = False
@property
def num_samples(self) -> int:
return int(self.sample_rate * self.duration_sec)
def _to_float(value: Any, default: float = -1.0) -> float:
if pd.isna(value):
return default
value = str(value).strip().lower()
if value == "full":
return default
try:
return float(value)
except ValueError:
return default
def load_audio_fixed(
file_path: str | Path,
sample_rate: int = 16000,
num_samples: Optional[int] = None,
random_crop: bool = False,
) -> torch.Tensor:
"""Load audio as mono waveform, resample, then pad/crop to num_samples.
Returns:
wav: Tensor with shape [num_samples] if num_samples is provided,
otherwise [num_audio_samples].
"""
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"Audio file not found: {path}")
wav, sr = torchaudio.load(str(path))
# Convert channels x samples -> mono samples
if wav.ndim == 2:
wav = wav.mean(dim=0)
else:
wav = wav.flatten()
if sr != sample_rate:
wav = torchaudio.functional.resample(wav, sr, sample_rate)
wav = wav.float()
if num_samples is None:
return wav
if wav.numel() < num_samples:
return F.pad(wav, (0, num_samples - wav.numel()))
if wav.numel() == num_samples:
return wav
if random_crop:
max_start = wav.numel() - num_samples
start = torch.randint(0, max_start + 1, (1,)).item()
else:
start = 0
return wav[start : start + num_samples]
class AuralGuardDataset(Dataset):
"""Dataset for AuralGuard-AASIST++.
Required CSV columns:
file_path,binary_label,attack_type
Optional columns:
start_fake,end_fake,dataset,split
"""
def __init__(
self,
metadata_csv: str | Path,
audio_config: Optional[AudioConfig] = None,
root_dir: str | Path | None = None,
) -> None:
self.metadata_csv = Path(metadata_csv)
self.df = pd.read_csv(self.metadata_csv)
self.audio_config = audio_config or AudioConfig()
self.root_dir = Path(root_dir) if root_dir is not None else None
required = {"file_path", "binary_label", "attack_type"}
missing = required - set(self.df.columns)
if missing:
raise ValueError(f"Metadata CSV is missing required columns: {sorted(missing)}")
def __len__(self) -> int:
return len(self.df)
def _resolve_path(self, path_value: str) -> Path:
path = Path(path_value)
if path.is_absolute():
return path
if self.root_dir is not None:
return self.root_dir / path
return path
def __getitem__(self, idx: int) -> Dict[str, Any]:
row = self.df.iloc[idx]
file_path = self._resolve_path(row["file_path"])
wav = load_audio_fixed(
file_path=file_path,
sample_rate=self.audio_config.sample_rate,
num_samples=self.audio_config.num_samples,
random_crop=self.audio_config.random_crop,
)
binary_label = int(row["binary_label"])
attack_type = normalize_attack_type(row["attack_type"])
attack_label = ATTACK_TO_ID[attack_type]
explanation_label = make_explanation_label(binary_label, attack_type)
item: Dict[str, Any] = {
"wav": wav,
"binary_label": torch.tensor(binary_label, dtype=torch.long),
"attack_label": torch.tensor(attack_label, dtype=torch.long),
"explanation_label": torch.tensor(explanation_label, dtype=torch.long),
"file_path": str(file_path),
"attack_type": attack_type,
"dataset": str(row["dataset"]) if "dataset" in row else "unknown",
"start_fake": _to_float(row["start_fake"]) if "start_fake" in row else -1.0,
"end_fake": _to_float(row["end_fake"]) if "end_fake" in row else -1.0,
}
return item