Foundation-1 / app.py
multimodalart's picture
multimodalart HF Staff
Claude Opus 4.6 (1M context)
Fix audio distortion by normalizing to -1 dBFS with headroom
4e71cc6
Raw History Blame
9.38 kB
import subprocess
subprocess.run('pip install flash-attn==2.7.4.post1 pandas --no-build-isolation', env={'FLASH_ATTENTION_SKIP_CUDA_BUILD': "TRUE"}, shell=True)
subprocess.run('pip install git+https://github.com/RoyalCities/RC-stable-audio-tools.git --no-deps', shell=True)
import spaces
import os
import json
import math
import random
import tempfile
import numpy as np
import torch
import torchaudio
import gradio as gr
from einops import rearrange
from huggingface_hub import hf_hub_download
# Stable Audio Tools imports
from stable_audio_tools.models.factory import create_model_from_config
from stable_audio_tools.models.utils import load_ckpt_state_dict
from stable_audio_tools.inference.generation import generate_diffusion_cond
from stable_audio_tools.interface.prompts.master_prompt_map import prompt_generator_foundation
# ==========================================
# 1. GLOBAL INITIALIZATION & MODEL PRELOADING
# ==========================================
MODEL_REPO = "RoyalCities/Foundation-1"
MODEL_FILE = "Foundation_1.safetensors" # Explicitly match the file in the repo
# Automatically choose CUDA if available, fallback to MPS or CPU
if torch.cuda.is_available():
DEVICE = torch.device("cuda")
DTYPE = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
elif torch.backends.mps.is_available():
DEVICE = torch.device("mps")
DTYPE = torch.float16
else:
DEVICE = torch.device("cpu")
DTYPE = torch.float32
print(f"Loading {MODEL_REPO} onto {DEVICE} ({DTYPE})...")
def load_custom_model(repo_id, filename):
"""Custom loader to handle non-standard model filenames."""
# 1. Download config
config_path = hf_hub_download(repo_id, filename="model_config.json", repo_type='model')
with open(config_path) as f:
model_config = json.load(f)
# 2. Create model from config
model = create_model_from_config(model_config)
# 3. Download and load weights
ckpt_path = hf_hub_download(repo_id, filename=filename, repo_type='model')
model.load_state_dict(load_ckpt_state_dict(ckpt_path))
return model, model_config
# Preload model globally
model, model_config = load_custom_model(MODEL_REPO, MODEL_FILE)
model = model.to(DEVICE).to(DTYPE).eval().requires_grad_(False)
SAMPLE_RATE = model_config.get("sample_rate", 44100)
MIN_INPUT_LENGTH = getattr(model, "min_input_length", None)
print("Model successfully loaded and ready for inference!")
# ==========================================
# 2. HELPER FUNCTIONS
# ==========================================
def calculate_target_samples(bars: int, bpm: float):
"""Calculates the exact sample length and padded model input size."""
# Calculate exact clip length in seconds and samples
clip_seconds = (60.0 / float(bpm)) * 4.0 * float(bars) # 4 beats per bar
clip_samples = int(round(clip_seconds * SAMPLE_RATE))
# Calculate padded length for the model (must be aligned to min_input_length)
seconds_total_int = int(math.ceil(clip_samples / SAMPLE_RATE))
target_samples = int(seconds_total_int * SAMPLE_RATE)
if isinstance(MIN_INPUT_LENGTH, int) and MIN_INPUT_LENGTH > 0 and (target_samples % MIN_INPUT_LENGTH) != 0:
target_samples += (MIN_INPUT_LENGTH - (target_samples % MIN_INPUT_LENGTH))
return clip_samples, seconds_total_int, target_samples
def generate_random_prompt(is_experimental: bool):
"""Uses the built-in prompt generator to create a Foundation-style prompt."""
variant = "T1" if is_experimental else "M1"
prompt = prompt_generator_foundation(
variant=variant,
mode="standard",
allow_timbre_mix=is_experimental
)
bars = random.choice([4, 8])
bpm = random.choice([100, 110, 120, 128, 130, 140, 150])
note = random.choice(["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"])
scale = random.choice(["major", "minor"])
return prompt, bars, bpm, note, scale
# ==========================================
# 3. CORE GENERATION LOGIC
# ==========================================
@spaces.GPU
def generate_audio(prompt, negative_prompt, bars, bpm, note, scale, steps, cfg_scale, seed):
try:
seed_val = int(seed) if seed != -1 else random.randint(0, 2**32 - 1)
amended_prompt = f"{prompt}, {note} {scale}, {bars} bars, {bpm}BPM,"
clip_samples, seconds_total_int, input_sample_size = calculate_target_samples(bars, bpm)
conditioning = [{"prompt": amended_prompt, "seconds_start": 0.0, "seconds_total": float(seconds_total_int)}]
neg_conditioning = [{"prompt": negative_prompt, "seconds_start": 0.0, "seconds_total": float(seconds_total_int)}] if negative_prompt else None
audio = generate_diffusion_cond(
model,
conditioning=conditioning,
negative_conditioning=neg_conditioning,
steps=steps,
cfg_scale=cfg_scale,
batch_size=1,
sample_size=input_sample_size,
sample_rate=SAMPLE_RATE,
seed=seed_val,
device=DEVICE,
sampler_type="dpmpp-3m-sde",
sigma_min=0.03,
sigma_max=1000,
)
# 1. Rearrange to [channels, samples] and ensure float32
audio = rearrange(audio, "b d n -> d (b n)").to(torch.float32)
# 2. PEAK NORMALIZATION — prevents clipping distortion
# The diffusion model outputs can far exceed [-1, 1].
# Hard-clamping (the repo default) chops those peaks → audible distortion.
# Instead, scale the whole waveform so the loudest peak sits at -1 dBFS
# (≈0.89), leaving headroom to avoid inter-sample clipping in DAWs.
max_amp = torch.abs(audio).max()
if max_amp > 1e-8:
audio = audio / max_amp * 0.89125 # -1 dBFS headroom
# 3. Trim to the exact deterministic grid length
end = min(int(audio.shape[-1]), int(clip_samples))
audio = audio[:, :max(1, end)].contiguous()
# 4. Apply a tiny 15ms fade-out to avoid clicks at the end
fade_ms = 15.0
fade_len = int(round((fade_ms / 1000.0) * SAMPLE_RATE))
if fade_len > 1 and audio.shape[-1] > 1:
fade_len = min(fade_len, audio.shape[-1])
ramp = torch.linspace(1.0, 0.0, steps=fade_len, device=audio.device)
audio[:, -fade_len:] *= ramp
# 5. Save as float32 WAV — let torchaudio handle bit-depth encoding
audio = audio.clamp(-1, 1).cpu()
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
output_path = tmp.name
torchaudio.save(output_path, audio, SAMPLE_RATE)
return output_path
except Exception as e:
import traceback
traceback.print_exc()
raise gr.Error(f"An error occurred during generation: {str(e)}")
finally:
if torch.cuda.is_available():
torch.cuda.empty_cache()
# ==========================================
# 4. GRADIO UI
# ==========================================
with gr.Blocks(title="Foundation-1 Generator", theme=gr.themes.Default()) as demo:
gr.Markdown("# 🎶 RoyalCities / Foundation-1 \nGenerate music using the Foundation-1 model. The model is already loaded and ready to go.")
with gr.Row():
with gr.Column(scale=2):
prompt = gr.Textbox(label="Prompt", placeholder="Describe the instrumentation, genre, and mood...", lines=3)
negative_prompt = gr.Textbox(label="Negative Prompt", placeholder="Optional: elements to avoid", value="")
with gr.Row():
bars = gr.Dropdown(choices=[4, 8], value=8, label="Bars")
bpm = gr.Dropdown(choices=[100, 110, 120, 128, 130, 140, 150], value=128, label="BPM")
with gr.Row():
note = gr.Dropdown(choices=["A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#"], value="C", label="Key")
scale = gr.Dropdown(choices=["major", "minor"], value="minor", label="Scale")
with gr.Accordion("Advanced Settings", open=False):
steps = gr.Slider(minimum=10, maximum=150, value=75, step=1, label="Inference Steps")
cfg_scale = gr.Slider(minimum=1.0, maximum=15.0, value=7.0, step=0.1, label="CFG Scale")
seed = gr.Number(value=-1, label="Seed (-1 for random)", precision=0)
with gr.Row():
experimental_mode = gr.Checkbox(label="Experimental Prompts (More Timbre Mix)", value=False)
random_btn = gr.Button("🎲 Generate Random Prompt", variant="secondary")
generate_btn = gr.Button("🚀 Generate Audio", variant="primary", size="lg")
with gr.Column(scale=1):
audio_out = gr.Audio(label="Generated Audio", type="filepath")
# Wire up buttons
random_btn.click(
fn=generate_random_prompt,
inputs=[experimental_mode],
outputs=[prompt, bars, bpm, note, scale]
)
generate_btn.click(
fn=generate_audio,
inputs=[prompt, negative_prompt, bars, bpm, note, scale, steps, cfg_scale, seed],
outputs=[audio_out]
)
# Start the app
if __name__ == "__main__":
demo.queue().launch()