Download main.py from thepaul/kicks-api: direct link, hf CLI and curl.
- Browser
- Download file 13.8 kB
-
https://huggingface.co/spaces/thepaul/kicks-api/resolve/main/main.py
- Command line
-
hf download hf://spaces/thepaul/kicks-api/main.py
-
curl -L -o main.py https://huggingface.co/spaces/thepaul/kicks-api/resolve/main/main.py
13.8 kB
| """Kicks API β VAE kick drum synthesizer deployed on Hugging Face Spaces. | |
| Uses BigVGAN vocoder on GPU (falls back to Griffin-LIM on CPU). | |
| PCA parameters are pre-computed and loaded from pca_params.npz. | |
| """ | |
| import glob | |
| import io | |
| import os | |
| import time | |
| from collections import defaultdict | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torchaudio | |
| import soundfile as sf | |
| from fastapi import FastAPI, HTTPException, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import StreamingResponse | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Rate limiter | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| class RateLimiter: | |
| """Simple in-memory sliding-window rate limiter.""" | |
| def __init__(self, max_requests: int = 10, window_seconds: float = 60.0): | |
| self.max_requests = max_requests | |
| self.window = window_seconds | |
| self._clients: dict[str, list[float]] = defaultdict(list) | |
| def _prune(self, ip: str, now: float) -> None: | |
| cutoff = now - self.window | |
| self._clients[ip] = [t for t in self._clients[ip] if t > cutoff] | |
| if not self._clients[ip]: | |
| del self._clients[ip] | |
| def is_allowed(self, ip: str) -> bool: | |
| now = time.time() | |
| self._prune(ip, now) | |
| if len(self._clients[ip]) >= self.max_requests: | |
| return False | |
| self._clients[ip].append(now) | |
| return True | |
| def _get_client_ip(request: Request) -> str: | |
| forwarded = request.headers.get("X-Forwarded-For") | |
| if forwarded: | |
| return forwarded.split(",")[0].strip() | |
| return request.client.host if request.client else "unknown" | |
| # Per-endpoint rate limiters (stricter for expensive endpoints) | |
| _generate_limiter = RateLimiter(max_requests=10, window_seconds=60) | |
| _spectrogram_limiter = RateLimiter(max_requests=30, window_seconds=60) | |
| _config_limiter = RateLimiter(max_requests=60, window_seconds=60) | |
| async def rate_limit_generate(request: Request) -> None: | |
| ip = _get_client_ip(request) | |
| if not _generate_limiter.is_allowed(ip): | |
| raise HTTPException(status_code=429, detail="Too many requests. Wait before generating again.") | |
| async def rate_limit_spectrogram(request: Request) -> None: | |
| ip = _get_client_ip(request) | |
| if not _spectrogram_limiter.is_allowed(ip): | |
| raise HTTPException(status_code=429, detail="Too many requests. Wait before requesting again.") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Constants (mirrors kicks/model.py) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| SAMPLE_RATE = 44100 | |
| AUDIO_LENGTH = 65536 # ~1.49 seconds | |
| N_FFT = 1024 | |
| HOP_LENGTH = 256 | |
| WIN_SIZE = 1024 | |
| N_MELS = 128 | |
| FMIN = 0 | |
| FMAX = None | |
| LOG_MEL_MIN = -11.5129 # ln(1e-5) β silence floor | |
| LOG_MEL_MAX = 2.5 # headroom above observed max | |
| N_PCS = 5 | |
| LATENT_DIM = 128 | |
| VOCODER_DIR = "models/vocoder" # fine-tuned BigVGAN weights | |
| BIGVGAN_MODEL = "nvidia/bigvgan_v2_44khz_128band_256x" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # VAE model (identical architecture to kicks/model.py) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| class VAE(nn.Module): | |
| """2D Conv VAE for log-mel spectrograms (1, 128, 256).""" | |
| def __init__(self, latent_dim: int = 128): | |
| super().__init__() | |
| self.latent_dim = latent_dim | |
| self.encoder = nn.Sequential( | |
| nn.Conv2d(1, 32, 3, stride=2, padding=1), nn.BatchNorm2d(32), nn.ReLU(), | |
| nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.BatchNorm2d(64), nn.ReLU(), | |
| nn.Conv2d(64, 128, 3, stride=2, padding=1), nn.BatchNorm2d(128), nn.ReLU(), | |
| nn.Conv2d(128, 256, 3, stride=2, padding=1), nn.BatchNorm2d(256), nn.ReLU(), | |
| ) | |
| self.fc_mu = nn.Linear(256 * 8 * 16, latent_dim) | |
| self.fc_logvar = nn.Linear(256 * 8 * 16, latent_dim) | |
| self.fc_decode = nn.Linear(latent_dim, 256 * 8 * 16) | |
| self.decoder = nn.Sequential( | |
| nn.ConvTranspose2d(256, 128, 3, stride=2, padding=1, output_padding=1), | |
| nn.BatchNorm2d(128), nn.ReLU(), | |
| nn.ConvTranspose2d(128, 64, 3, stride=2, padding=1, output_padding=1), | |
| nn.BatchNorm2d(64), nn.ReLU(), | |
| nn.ConvTranspose2d(64, 32, 3, stride=2, padding=1, output_padding=1), | |
| nn.BatchNorm2d(32), nn.ReLU(), | |
| nn.ConvTranspose2d(32, 1, 3, stride=2, padding=1, output_padding=1), | |
| nn.Sigmoid(), | |
| ) | |
| def decode(self, z: torch.Tensor) -> torch.Tensor: | |
| h = self.fc_decode(z) | |
| h = h.view(-1, 256, 8, 16) | |
| return self.decoder(h) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # BigVGAN vocoder | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_bigvgan(device: torch.device): | |
| """Load BigVGAN from HF Hub, optionally with fine-tuned weights.""" | |
| import bigvgan as _bigvgan | |
| # Patch for huggingface_hub >= 1.0 compatibility | |
| original = _bigvgan.BigVGAN._from_pretrained.__func__ | |
| def _patched(cls, **kwargs): | |
| kwargs.setdefault("proxies", None) | |
| kwargs.setdefault("resume_download", False) | |
| return original(cls, **kwargs) | |
| _bigvgan.BigVGAN._from_pretrained = _patched | |
| model = _bigvgan.BigVGAN.from_pretrained(BIGVGAN_MODEL, use_cuda_kernel=False) | |
| # Load fine-tuned weights if available | |
| pth_files = sorted(glob.glob(os.path.join(VOCODER_DIR, "*.pth"))) | |
| if pth_files: | |
| pth_path = pth_files[0] | |
| checkpoint = torch.load(pth_path, map_location=device, weights_only=True) | |
| model.load_state_dict(checkpoint["generator"]) | |
| print(f"Loaded fine-tuned BigVGAN from {pth_path}") | |
| else: | |
| print("Using pretrained BigVGAN from HuggingFace Hub") | |
| model.remove_weight_norm() | |
| return model.eval().to(device) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Griffin-LIM fallback vocoder | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| class GriffinLimVocoder: | |
| """Mel spectrogram β audio via pseudo-inverse + Griffin-LIM.""" | |
| def __init__(self, n_iter: int = 64): | |
| n_stft = N_FFT // 2 + 1 | |
| fb = torchaudio.functional.melscale_fbanks( | |
| n_freqs=n_stft, | |
| f_min=FMIN, | |
| f_max=FMAX if FMAX is not None else SAMPLE_RATE / 2.0, | |
| n_mels=N_MELS, | |
| sample_rate=SAMPLE_RATE, | |
| ) | |
| self.fb_pinv = torch.linalg.pinv(fb.T) | |
| self.griffin_lim = torchaudio.transforms.GriffinLim( | |
| n_fft=N_FFT, win_length=WIN_SIZE, | |
| hop_length=HOP_LENGTH, n_iter=n_iter, | |
| ) | |
| def __call__(self, log_mel: torch.Tensor) -> torch.Tensor: | |
| mel_linear = torch.exp(log_mel).cpu() | |
| linear_spec = torch.clamp(self.fb_pinv @ mel_linear, min=0.0) | |
| linear_spec = linear_spec + 1e-4 | |
| return self.griffin_lim(linear_spec) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Normalization helpers | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def denormalize(spec: torch.Tensor) -> torch.Tensor: | |
| """Convert [0, 1] normalized spectrogram back to log-mel scale.""" | |
| return spec * (LOG_MEL_MAX - LOG_MEL_MIN) + LOG_MEL_MIN | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Application state | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| class AppState: | |
| device: torch.device | |
| model: VAE | |
| vocoder: object | |
| vocoder_type: str | |
| pca_components: np.ndarray | |
| pca_mean: np.ndarray | |
| pc_names: list[str] | |
| pc_mins: list[float] | |
| pc_maxs: list[float] | |
| state = AppState() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FastAPI app | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI(title="Kicks API") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def startup(): | |
| # Auto-detect device | |
| if torch.cuda.is_available(): | |
| state.device = torch.device("cuda") | |
| print("Using CUDA GPU") | |
| else: | |
| state.device = torch.device("cpu") | |
| print("CUDA not available β falling back to CPU") | |
| # Load PCA parameters | |
| pca_path = os.environ.get("PCA_PARAMS_PATH", "pca_params.npz") | |
| pca_data = np.load(pca_path, allow_pickle=False) | |
| state.pca_components = pca_data["components"] | |
| state.pca_mean = pca_data["mean"] | |
| state.pc_names = list(pca_data["pc_names"]) | |
| state.pc_mins = list(pca_data["pc_mins"]) | |
| state.pc_maxs = list(pca_data["pc_maxs"]) | |
| print(f"PCA loaded: {state.pc_names}") | |
| # Load VAE checkpoint | |
| ckpt_path = os.environ.get("VAE_CHECKPOINT_PATH", "vae_best.pth") | |
| checkpoint = torch.load(ckpt_path, map_location=state.device, weights_only=True) | |
| latent_dim = checkpoint.get("latent_dim", LATENT_DIM) | |
| state.model = VAE(latent_dim=latent_dim) | |
| state.model.load_state_dict(checkpoint["model"]) | |
| state.model.to(state.device) | |
| state.model.eval() | |
| print(f"VAE loaded (latent_dim={latent_dim})") | |
| # Load vocoder β try BigVGAN with CUDA, fall back to Griffin-LIM | |
| if state.device.type == "cuda": | |
| try: | |
| state.vocoder = load_bigvgan(state.device) | |
| state.vocoder_type = "bigvgan" | |
| print("BigVGAN vocoder ready") | |
| except Exception as e: | |
| print(f"BigVGAN loading failed ({e}), falling back to Griffin-LIM") | |
| state.vocoder = GriffinLimVocoder() | |
| state.vocoder_type = "griffinlim" | |
| else: | |
| state.vocoder = GriffinLimVocoder() | |
| state.vocoder_type = "griffinlim" | |
| print("API ready") | |
| def _parse_pc_values(request: Request) -> list[float]: | |
| values = [] | |
| for i in range(N_PCS): | |
| raw = float(request.query_params.get(f"pc{i + 1}", "0.5")) | |
| val = state.pc_mins[i] + raw * (state.pc_maxs[i] - state.pc_mins[i]) | |
| values.append(max(state.pc_mins[i], min(state.pc_maxs[i], val))) | |
| return values | |
| def _pc_to_latent(pc_values: list[float]) -> torch.Tensor: | |
| z_np = (np.array(pc_values) @ state.pca_components) + state.pca_mean | |
| return torch.tensor(z_np, dtype=torch.float32).to(state.device).unsqueeze(0) | |
| def _spec_to_audio(spec: torch.Tensor) -> np.ndarray: | |
| """Convert normalized spectrogram to waveform using the loaded vocoder.""" | |
| log_mel = denormalize(spec) | |
| log_mel = log_mel.squeeze(1) # (1, 128, 256) | |
| if state.vocoder_type == "bigvgan": | |
| with torch.no_grad(): | |
| waveform = state.vocoder(log_mel.to(state.device)) # (1, 1, T) | |
| waveform = waveform.squeeze(1).cpu() # (1, T) | |
| waveform = torchaudio.functional.highpass_biquad(waveform, SAMPLE_RATE, cutoff_freq=25.0) | |
| waveform = torchaudio.functional.lowpass_biquad(waveform, SAMPLE_RATE, cutoff_freq=20000.0) | |
| waveform = waveform / (waveform.abs().max() + 1e-8) | |
| else: | |
| waveform = state.vocoder(log_mel) # (1, T) | |
| waveform = waveform / (waveform.abs().max() + 1e-8) | |
| return waveform.squeeze(0).numpy() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Endpoints | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def get_config(): | |
| return { | |
| "sliders": [ | |
| { | |
| "id": i + 1, | |
| "name": state.pc_names[i], | |
| "min": 0.0, "max": 1.0, | |
| "default": 0.5, "step": 0.01, | |
| } | |
| for i in range(N_PCS) | |
| ], | |
| "vocoder": state.vocoder_type, | |
| } | |
| async def generate(request: Request): | |
| await rate_limit_generate(request) | |
| pc_values = _parse_pc_values(request) | |
| z = _pc_to_latent(pc_values) | |
| with torch.no_grad(): | |
| spec = state.model.decode(z) | |
| audio = _spec_to_audio(spec) | |
| buf = io.BytesIO() | |
| sf.write(buf, audio, SAMPLE_RATE, format="WAV") | |
| buf.seek(0) | |
| return StreamingResponse(buf, media_type="audio/wav") | |
| async def spectrogram_data(request: Request): | |
| await rate_limit_spectrogram(request) | |
| pc_values = _parse_pc_values(request) | |
| z = _pc_to_latent(pc_values) | |
| with torch.no_grad(): | |
| spec = state.model.decode(z) | |
| return {"data": spec.squeeze().cpu().numpy().tolist()} | |
| async def health(): | |
| return {"status": "ok", "vocoder": state.vocoder_type, "device": str(state.device)} | |