| # 06 — Stable Audio 3 Integration |
|
|
| ## What we're using Stable Audio 3 for |
|
|
| PatternTalk uses SA3 for two things, both explicitly listed in the Stability AI challenge brief: |
|
|
| 1. **One-shot sample generation** — "give me a trashy 18-inch china" |
| 2. **Loop preview generation** — 5–15 second previews of patterns with brutal drum styling |
|
|
| Plus one bonus use case: |
|
|
| 3. **LoRA fine-tuning** — train a small adapter on brutal drum samples so the model actually understands the genre |
|
|
| The MIDI patterns themselves are NOT generated by SA3. They're hand-coded templates. This is a deliberate split: SA3's strength is sample quality, not structured MIDI generation. |
|
|
| ## Why local inference, not API |
|
|
| The Stability AI challenge explicitly rewards *"showing the strengths of local open models."* Using the open weights locally: |
|
|
| - Proves we actually use the open model, not a black-box API |
| - Eliminates rate limits and surprise outages during the demo |
| - Lets us fine-tune and ship the LoRA weights as a deliverable |
| - Makes the inference reproducible and inspectable |
|
|
| We pay for this with slower inference on consumer hardware. Mitigation: cloud GPU for the heavy work. |
|
|
| ## Hardware reality |
|
|
| ### Vega 56 (your machine) — honest assessment |
|
|
| **Specs:** 8GB HBM2, GCN 5th gen, ROCm-supported but old. |
|
|
| **Inference (SA3 small):** |
| - ✅ Fits in 8GB VRAM in fp16 |
| - ⚠️ Tight memory headroom — no concurrent inference, no batching |
| - ⚠️ ROCm + PyTorch + audio models = some setup friction |
| - ⚠️ Throughput roughly **1/4 to 1/6 of an RTX 4090** on diffusion models |
| - ⚠️ A 30-second sample at 100 denoising steps: expect 2–10 minutes wall time |
|
|
| **Fine-tuning (LoRA):** |
| - ❌ Not practical. Gradient buffers + optimizer state blow past 8GB. |
| - ⏱️ If forced: 10–50× slower than cloud. Full LoRA training: hours to days, not feasible in a hackathon. |
|
|
| **Verdict:** Use Vega for development and testing the inference path. Use cloud GPU for fine-tuning and demo-day inference. |
|
|
| ### Cloud GPU (RunPod / Vast.ai / Modal) |
|
|
| **Recommended for the hackathon:** |
|
|
| | Provider | GPU | Cost/hr | Notes | |
| |---|---|---|---| |
| | RunPod | RTX 4090 | $0.40 | Easiest UX, instant deploy | |
| | RunPod | A4000 | $0.30 | Slightly slower than 4090 | |
| | Vast.ai | RTX 3090 | $0.20 | Cheaper, more setup | |
| | Modal | A10G | $0.50 | Serverless, pay per second | |
| | Lambda Labs | A100 | $1.10 | Overkill but fast | |
|
|
| **Recommendation:** RunPod with a 4090 for ~6 hours total = **~$3**. Fine-tune during one session, run demo inference during another. |
|
|
| ## Setup |
|
|
| ### Step 1: Get the model weights |
|
|
| ```bash |
| # Clone the repo |
| git clone https://github.com/Stability-AI/stable-audio-3.git |
| cd stable-audio-3 |
| |
| # Request access on HuggingFace (gated) |
| # https://huggingface.co/stabilityai/stable-audio-3-small |
| # https://huggingface.co/stabilityai/stable-audio-3-medium |
| |
| # Login |
| huggingface-cli login |
| |
| # Download weights (use small on Vega, medium on cloud) |
| python scripts/download_weights.py --model small |
| ``` |
|
|
| ### Step 2: Install dependencies (local, Vega) |
|
|
| ROCm is finicky. Use a pre-built PyTorch container if possible. |
|
|
| ```bash |
| # Option A: Use the official Stability AI Docker image |
| docker pull stabilityai/stable-audio-tools:latest |
| |
| # Option B: Manual install (if Docker fails) |
| pip install torch==2.4.0 --index-url https://download.pytorch.org/whl/rocm5.7 |
| pip install stable-audio-tools |
| pip install fastapi uvicorn |
| ``` |
|
|
| **Reality check:** On Vega, expect 1–3 hours of yak-shaving to get inference running the first time. Budget for this. |
|
|
| ### Step 3: Verify inference works |
|
|
| ```python |
| # scripts/smoke_test.py |
| from stable_audio_tools import get_pretrained_model |
| from stable_audio_tools.inference.generation import generate_diffusion_cond |
| |
| model, config = get_pretrained_model("stabilityai/stable-audio-3-small") |
| |
| # Time this — establishes your baseline |
| import time |
| start = time.time() |
| output = generate_diffusion_cond( |
| model, |
| steps=100, |
| cfg_scale=7, |
| conditioning=[{"prompt": "d-beat drum loop, 180 BPM, brutal", "seconds_start": 0, "seconds_total": 8}], |
| batch_size=1, |
| sample_size=44100 * 8, |
| device="cuda", |
| ) |
| elapsed = time.time() - start |
| print(f"Generated in {elapsed:.1f}s") |
| # Save output |
| import torchaudio |
| torchaudio.save("smoke_test.wav", output.squeeze().cpu(), 44100) |
| ``` |
|
|
| If this takes < 2 minutes on Vega, you're fine. If it takes > 10 minutes, commit to cloud GPU for the demo. |
|
|
| ## Fine-tuning: brutal-drum LoRA |
|
|
| ### Why fine-tune |
|
|
| Off-the-shelf SA3 doesn't know "brutal" drums. Your testing confirmed it. Fine-tuning on a curated dataset makes the model actually understand the genre vocabulary. |
|
|
| ### Training data curation |
|
|
| **Minimum viable dataset (start with this):** |
|
|
| - 20–40 one-shots: |
| - 5–10 kicks (tight, clicky, triggered) |
| - 5–10 snares (snappy, trashy, mid-range) |
| - 3–5 chinas (trashy, dark, bright variants) |
| - 3–5 crashes (various sizes/washes) |
| - 2–3 hi-hats (closed, open, stack) |
| - 2–3 rides (dry, washy, bell-forward) |
| - 10–20 loops (3–10 seconds each): |
| - 3–5 d-beat loops at various tempos |
| - 3–5 blast beat loops (traditional, hammer, hyperblast) |
| - 2–3 half-time grooves |
| - 2–3 punk rock / hardcore loops |
| - 2–3 djent polyrhythm loops |
|
|
| **Sources (in order of preference):** |
|
|
| 1. **Your own recordings** — best, no licensing issues, your taste |
| 2. **Freesound.org** — filter to CC0 or CC-BY, document the user |
| 3. **Splice / Loopcloud** — if you have a subscription, export with licensing documented |
| 4. **Bandcamp / label sample packs** — check license terms, some are CC-BY |
| 5. **Your band's existing recordings** — if you produced them, you own them |
|
|
| **Discipline:** Every file in `data/training/` has a corresponding entry in `data/training/manifest.yaml` with source, license, duration, BPM (for loops), and tags. |
|
|
| ### Pre-processing |
|
|
| All samples normalized to: |
| - WAV format |
| - 44100 Hz sample rate (or 48000 — match SA3's expected rate) |
| - Mono (drums don't need stereo for LoRA training; stereo adds noise) |
| - Loudness normalized to ~ -14 LUFS |
| - Trimmed silence at start/end |
| - Loops: aligned to bar boundaries, ideally with a single downbeat |
|
|
| ```python |
| # services/audio/training/preprocess.py |
| import torchaudio |
| import pyloudnorm as pyln |
| |
| def preprocess(input_path: str, output_path: str): |
| waveform, sr = torchaudio.load(input_path) |
| # Resample |
| if sr != 44100: |
| waveform = torchaudio.functional.resample(waveform, sr, 44100) |
| # To mono |
| waveform = waveform.mean(dim=0, keepdim=True) |
| # Loudness normalize |
| meter = pyln.Meter(44100) |
| loudness = meter.integrated_loudness(waveform.numpy().T) |
| normalized = pyln.normalize.loudness(waveform.numpy().T, loudness, -14.0) |
| waveform = torch.from_numpy(normalized.T).unsqueeze(0) |
| torchaudio.save(output_path, waveform, 44100) |
| ``` |
|
|
| ### LoRA training script |
|
|
| Stability's repo supports LoRA via `stable_audio_tools.training.lora`. Use their example as a base. |
|
|
| ```python |
| # services/audio/training/train_lora.py |
| import torch |
| from stable_audio_tools.models import create_model_from_config |
| from stable_audio_tools.training.lora import LoRADataset, LoRATrainer |
| from stable_audio_tools.data.utils import load_training_manifest |
| |
| # Load base model |
| model, config = create_model_from_config("model_config.json") |
| model.load_state_dict(torch.load("stable_audio_3_small.safetensors")) |
| |
| # Load training manifest |
| dataset = LoRADataset( |
| manifest_path="data/training/manifest.yaml", |
| audio_dir="data/training/", |
| ) |
| |
| # LoRA config |
| lora_config = { |
| "rank": 32, |
| "alpha": 32, |
| "dropout": 0.05, |
| "target_modules": ["to_q", "to_k", "to_v", "to_out.0"], # SA3 specifics |
| } |
| |
| # Train |
| trainer = LoRATrainer( |
| model=model, |
| dataset=dataset, |
| lora_config=lora_config, |
| learning_rate=1e-4, |
| batch_size=2, |
| gradient_accumulation=4, |
| max_steps=1500, |
| save_every=500, |
| output_dir="loras/brutal-drums", |
| device="cuda", |
| ) |
| |
| trainer.train() |
| ``` |
|
|
| **Expected training time on RTX 4090:** 30–90 minutes for 1500 steps with rank 32. |
|
|
| **Output:** |
| - `loras/brutal-drums/adapter.safetensors` — the LoRA weights (~50MB) |
| - `loras/brutal-drums/checkpoints/` — intermediate checkpoints |
| - `loras/brutal-drums/training_log.json` — loss curve |
|
|
| ### Publishing the LoRA |
|
|
| After training, push to HuggingFace: |
|
|
| ```python |
| # services/audio/training/publish.py |
| from huggingface_hub import HfApi |
| |
| api = HfApi() |
| api.create_repo("your-username/patterntalk-brutal-drums", repo_type="model") |
| |
| api.upload_folder( |
| folder_path="loras/brutal-drums/", |
| repo_id="your-username/patterntalk-brutal-drums", |
| commit_message="Initial LoRA trained on brutal drum samples", |
| ) |
| |
| # Upload model card |
| api.upload_file( |
| path_or_fileobj="loras/brutal-drums/README.md", |
| path_in_repo="README.md", |
| repo_id="your-username/patterntalk-brutal-drums", |
| ) |
| ``` |
|
|
| **Model card must include:** |
| - Base model reference |
| - Training data summary (with manifest link) |
| - License |
| - Intended use |
| - Limitations (it's a LoRA, not general-purpose) |
| - Citation to Stability AI and PatternTalk |
|
|
| ## Inference service |
|
|
| ### Architecture |
|
|
| ```python |
| # services/audio/sa3/server.py |
| from fastapi import FastAPI, HTTPException |
| from pydantic import BaseModel |
| import torch |
| from stable_audio_tools import get_pretrained_model |
| from stable_audio_tools.inference.generation import generate_diffusion_cond |
| import torchaudio |
| import io |
| import hashlib |
| |
| app = FastAPI() |
| |
| # Load model once at startup |
| model, config = get_pretrained_model("stabilityai/stable-audio-3-small") |
| model = model.to("cuda") |
| model.eval() |
| |
| # Load LoRA adapter |
| model.load_adapter("loras/brutal-drums/adapter.safetensors", adapter_name="brutal") |
| |
| # Sample cache (LRU + disk) |
| cache_dir = Path("cache/") |
| cache_dir.mkdir(exist_ok=True) |
| |
| class GenerateRequest(BaseModel): |
| prompt: str |
| duration_seconds: float = 8.0 |
| intensity: str = "medium" # soft | medium | brutal | brutal-max |
| use_lora: bool = True |
| cfg_scale: float = 7.0 |
| steps: int = 100 |
| |
| class GenerateResponse(BaseModel): |
| audio_url: str # URL to download the WAV |
| prompt: str |
| duration: float |
| cached: bool |
| |
| @app.post("/generate", response_model=GenerateResponse) |
| async def generate(req: GenerateRequest): |
| # Cache key |
| cache_key = hashlib.sha256( |
| f"{req.prompt}|{req.duration_seconds}|{req.intensity}|{req.use_lora}".encode() |
| ).hexdigest()[:16] |
| |
| cache_path = cache_dir / f"{cache_key}.wav" |
| |
| if cache_path.exists(): |
| return GenerateResponse( |
| audio_url=f"/cache/{cache_key}.wav", |
| prompt=req.prompt, |
| duration=req.duration_seconds, |
| cached=True, |
| ) |
| |
| # Build conditioning |
| conditioning = [{ |
| "prompt": req.prompt, |
| "seconds_start": 0, |
| "seconds_total": req.duration_seconds, |
| }] |
| |
| # Apply LoRA |
| if req.use_lora: |
| model.set_adapter("brutal") |
| else: |
| model.disable_adapters() |
| |
| # Generate |
| try: |
| output = generate_diffusion_cond( |
| model, |
| steps=req.steps, |
| cfg_scale=req.cfg_scale, |
| conditioning=conditioning, |
| sample_size=int(44100 * req.duration_seconds), |
| device="cuda", |
| ) |
| except Exception as e: |
| raise HTTPException(500, f"Generation failed: {e}") |
| |
| # Save |
| torchaudio.save(str(cache_path), output.squeeze().cpu(), 44100) |
| |
| return GenerateResponse( |
| audio_url=f"/cache/{cache_key}.wav", |
| prompt=req.prompt, |
| duration=req.duration_seconds, |
| cached=False, |
| ) |
| |
| @app.get("/cache/{key}.wav") |
| async def get_cached(key: str): |
| path = cache_dir / f"{key}.wav" |
| if not path.exists(): |
| raise HTTPException(404) |
| return FileResponse(path) |
| ``` |
|
|
| ### Intensity → prompt engineering |
|
|
| Map user-friendly intensity to model-friendly prompts: |
|
|
| ```python |
| INTENSITY_PROMPTS = { |
| "soft": "clean studio drums, polished, tight, controlled", |
| "medium": "live room drums, punchy, present", |
| "brutal": "brutal drums, trashy, aggressive, raw, distorted", |
| "brutal-max": "ultra-brutal drums, completely destroyed, blown-out, panic-attack intensity", |
| } |
| |
| def build_audio_prompt(req: AudioPrompt) -> str: |
| parts = [ |
| req.pattern, |
| req.styleHints.join(", "), |
| INTENSITY_PROMPTS[req.intensity], |
| "drum recording, close-mic'd", |
| ] |
| if req.limb: |
| parts.append(f"single {req.limb} hit") |
| return ", ".join(filter(None, parts)) |
| ``` |
|
|
| ## Prompting strategy for SA3 |
|
|
| Stable Audio 3 responds well to: |
| - Genre terms ("d-beat", "black metal", "brutal") |
| - Tempo ("180 BPM") |
| - Recording style ("close-mic'd", "room mic", "triggered") |
| - Specific instrument descriptors ("tight snare", "trashy china") |
| - Intensity adjectives ("brutal", "raw", "polished") |
|
|
| SA3 responds poorly to: |
| - Vague aesthetic terms ("cool", "interesting") |
| - Mixing many genres ("jazz-black-metal-funk") |
| - Trying to specify timing ("snare on the and of 2") |
|
|
| Our brutal-drum LoRA biases the model toward: |
| - Recognizing metal subgenres |
| - Generating physically realistic drum sounds |
| - Avoiding pop/EDM patterns |
|
|
| ## Demo strategy |
|
|
| For the live demo, we want generation to be fast and reliable. Pre-generate a few showcase samples: |
|
|
| ```bash |
| # Pre-generate demo cache |
| python scripts/pregenerate_demo.py |
| # Generates: |
| # cache/dbeat-180.wav |
| # cache/blast-traditional-200.wav |
| # cache/china-trashy.wav |
| # cache/kick-brutal.wav |
| # cache/skank-120.wav |
| # cache/variation-1.wav ... variation-4.wav |
| ``` |
|
|
| During the demo, hit the live API for the "regenerate" command (to show inference happening), but rely on cache for the initial generation to keep the demo snappy. |
|
|
| ## What can go wrong |
|
|
| | Failure | Mitigation | |
| |---|---| |
| | Cloud GPU instance dies | Pre-generated cache + fallback to Vega inference | |
| | SA3 produces generic output | LoRA adapter handles this; fallback is to label output "demo variation" | |
| | Generation exceeds 30s | Show progress indicator + have pre-generated fallback | |
| | Audio quality is bad | Iterate on LoRA training data; have multiple variations to pick from | |
| | ROCm issues on Vega | Skip local dev, use cloud-only | |
|
|
| ## Cost summary |
|
|
| | Item | Cost | |
| |---|---| |
| | Cloud GPU (RunPod 4090, 6 hours total) | ~$3 | |
| | HuggingFace Pro (if needed for gated weights) | $0 (free tier sufficient) | |
| | Total cloud spend | **~$3** | |
|
|
| If your budget is truly $0, do everything on Vega and accept 2–10 minute inference times. The product still works, the demo just needs more buffer time. |
|
|
| ## Post-hackathon |
|
|
| - Quantize the model (int8) for faster inference |
| - Train additional LoRAs: rock, jazz, funk (one per genre) |
| - Host inference on Modal/Replicate for public use |
| - Build a "train your own LoRA" UI — the "personal LoRA trainer" use case from the brief |
|
|