| import os |
| import tempfile |
| from pathlib import Path |
|
|
| import gradio as gr |
| import numpy as np |
| import soundfile as sf |
| import spaces |
| import torch |
| from huggingface_hub import login |
| from stable_audio_3 import StableAudioModel |
|
|
|
|
| MODEL_ID = "small-sfx" |
| SAMPLE_RATE = 44_100 |
|
|
| hf_token = os.environ.get("HF_TOKEN") |
|
|
| if not hf_token: |
| raise RuntimeError( |
| "HF_TOKEN is missing. Add it under Settings → Variables and secrets." |
| ) |
|
|
| login( |
| token=hf_token, |
| add_to_git_credential=False, |
| ) |
|
|
| |
| print("PyTorch:", torch.__version__) |
| print("PyTorch CUDA build:", torch.version.cuda) |
| print(f"Loading model: {MODEL_ID}") |
|
|
| |
| |
| model = StableAudioModel.from_pretrained( |
| MODEL_ID, |
| device="cuda", |
| ) |
|
|
| print("Model loaded.") |
|
|
|
|
| def requested_gpu_duration(prompt: str, duration: float) -> int: |
| """Return the number of ZeroGPU seconds to reserve.""" |
|
|
| del prompt |
|
|
| duration = float(duration) |
|
|
| if duration <= 15: |
| return 15 |
|
|
| if duration <= 60: |
| return 25 |
|
|
| return 40 |
|
|
|
|
| def convert_audio(audio: object) -> np.ndarray: |
| """Convert model output into a SoundFile-compatible NumPy array.""" |
|
|
| if isinstance(audio, torch.Tensor): |
| audio = audio.detach().float().cpu().numpy() |
|
|
| array = np.asarray(audio, dtype=np.float32) |
|
|
| |
| if array.ndim == 3: |
| array = array[0] |
|
|
| |
| if array.ndim == 2 and array.shape[0] <= 8: |
| array = array.T |
|
|
| if array.ndim not in (1, 2): |
| raise RuntimeError( |
| f"Unexpected generated audio shape: {array.shape}" |
| ) |
|
|
| if array.size == 0: |
| raise RuntimeError("The model returned empty audio.") |
|
|
| if not np.all(np.isfinite(array)): |
| raise RuntimeError( |
| "The generated audio contains invalid numeric values." |
| ) |
|
|
| peak = float(np.max(np.abs(array))) |
|
|
| if peak > 1.0: |
| array = array / peak |
|
|
| return array |
|
|
|
|
| @spaces.GPU(duration=requested_gpu_duration) |
| @torch.inference_mode() |
| def generate_sound(prompt: str, duration: float) -> str: |
| """Generate a sound effect and return its WAV path.""" |
|
|
| prompt = prompt.strip() |
| duration = float(duration) |
|
|
| if not prompt: |
| raise gr.Error("Enter a description of the sound.") |
|
|
| if not 0.5 <= duration <= 120: |
| raise gr.Error( |
| "Duration must be between 0.5 and 120 seconds." |
| ) |
|
|
| |
| |
| print("Runtime Torch:", torch.__version__) |
| print("Runtime CUDA:", torch.version.cuda) |
| print("Runtime GPU:", torch.cuda.get_device_name(0)) |
| print( |
| "Runtime compute capability:", |
| torch.cuda.get_device_capability(0), |
| ) |
| print( |
| "Runtime Torch architectures:", |
| torch.cuda.get_arch_list(), |
| ) |
|
|
| try: |
| generated_audio = model.generate( |
| prompt=prompt, |
| duration=duration, |
| ) |
|
|
| audio = convert_audio(generated_audio) |
|
|
| with tempfile.NamedTemporaryFile( |
| prefix="stable-audio-3-", |
| suffix=".wav", |
| delete=False, |
| ) as temporary_file: |
| output_path = Path(temporary_file.name) |
|
|
| sf.write( |
| str(output_path), |
| audio, |
| samplerate=SAMPLE_RATE, |
| subtype="FLOAT", |
| ) |
|
|
| return str(output_path) |
|
|
| except gr.Error: |
| raise |
|
|
| except Exception as error: |
| print( |
| "Generation error:", |
| type(error).__name__, |
| str(error), |
| ) |
|
|
| raise gr.Error( |
| f"Generation failed: {type(error).__name__}: {error}" |
| ) from error |
|
|
|
|
| with gr.Blocks( |
| title="Stable Audio 3 Small SFX", |
| ) as demo: |
| gr.Markdown( |
| """ |
| # Stable Audio 3 Small SFX |
| |
| Generate stereo, 44.1 kHz sound effects using Stable Audio 3 |
| on Hugging Face ZeroGPU. |
| """ |
| ) |
|
|
| prompt_input = gr.Textbox( |
| label="Sound description", |
| value=( |
| "A chugging steam train entering a station " |
| "and sounding a loud horn" |
| ), |
| placeholder=( |
| "Describe the sound, environment, timing, and perspective." |
| ), |
| lines=3, |
| ) |
|
|
| duration_input = gr.Slider( |
| minimum=0.5, |
| maximum=120, |
| value=7, |
| step=0.5, |
| label="Duration in seconds", |
| ) |
|
|
| generate_button = gr.Button( |
| "Generate sound", |
| variant="primary", |
| ) |
|
|
| audio_output = gr.Audio( |
| label="Generated sound", |
| type="filepath", |
| format="wav", |
| ) |
|
|
| generate_button.click( |
| fn=generate_sound, |
| inputs=[ |
| prompt_input, |
| duration_input, |
| ], |
| outputs=audio_output, |
| concurrency_limit=1, |
| show_progress="full", |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.queue( |
| max_size=20, |
| default_concurrency_limit=1, |
| ).launch() |
| |