Spaces:
Sleeping
Sleeping
Download app.py from teamup-tech/SonicMaster: direct link, hf CLI and curl.
- Browser
- Download file 8.1 kB
-
https://huggingface.co/spaces/teamup-tech/SonicMaster/resolve/main/app.py
- Command line
-
hf download hf://spaces/teamup-tech/SonicMaster/app.py
-
curl -L -o app.py https://huggingface.co/spaces/teamup-tech/SonicMaster/resolve/main/app.py
8.1 kB
| """ | |
| SonicMaster on HARP: text-controlled music restoration and mastering. | |
| This Space is a frontend for the authors' own Space, amaai-lab/SonicMaster, which runs the model on | |
| ZeroGPU. The model decodes through Stable Audio Open's VAE, whose weights are gated, so serving it | |
| from here would hand it to callers who never accepted their terms (see "Calling Another Space" in | |
| HARP's deployment guidelines). Each request is forwarded with the caller's x-ip-token instead, | |
| which bills it to the caller's own ZeroGPU allowance, as a direct call to the authors' Space would. | |
| """ | |
| import tempfile | |
| from pathlib import Path | |
| import gradio as gr | |
| import numpy as np | |
| import soundfile as sf | |
| import soxr | |
| from gradio.context import LocalContext | |
| from gradio_client import Client, handle_file | |
| from gradio_client.exceptions import AppError | |
| from pyharp import ModelCard, Subcategory, build_endpoint | |
| BACKEND = "amaai-lab/SonicMaster" | |
| # Connected to by URL. Given the repository id, gradio_client looks the host up through the Hub's | |
| # API without a token, which the Hub rate-limits from the address a Space shares with others. | |
| BACKEND_URL = "https://amaai-lab-sonicmaster.hf.space" | |
| API_NAME = "/enhance_audio_ui" | |
| # The parameters of API_NAME, in order, as the backend declared them when this frontend was written | |
| BACKEND_PARAMETERS = ["audio_path", "prompt"] | |
| # A lossless input keeps its container. A lossy one comes back as WAV, since encoding the result | |
| # lossily again would expose the artifacts of the first encode and add a second set. | |
| LOSSLESS = {".wav", ".flac"} | |
| def check_backend(): | |
| """ | |
| Stops the Space from starting if the backend's API no longer matches what process_fn sends. | |
| An upstream change then shows up as a Space that fails to start, rather than as a puzzle on | |
| every request. A backend that cannot be reached is only reported, since a sleeping or | |
| restarting Space is no reason to keep this one down. | |
| """ | |
| try: | |
| api = Client(BACKEND_URL, verbose=False).view_api(return_format="dict", print_info=False) | |
| except Exception as e: | |
| print(f"SonicMaster: could not reach {BACKEND} to check its API: {e}", flush=True) | |
| return | |
| endpoint = api["named_endpoints"].get(API_NAME) | |
| names = [p.get("parameter_name") for p in endpoint["parameters"]] if endpoint else None | |
| if names != BACKEND_PARAMETERS: | |
| raise RuntimeError(f"{BACKEND}{API_NAME} now takes {names}, but this frontend sends " | |
| f"{BACKEND_PARAMETERS}. Update process_fn to match.") | |
| model_card = ModelCard( | |
| name="SonicMaster", | |
| description=( | |
| "Text-controlled all-in-one music restoration and mastering: one instruction in plain " | |
| "language fixes reverb, clipping, tonal balance, dynamics, or the stereo image. The " | |
| "model is a flow-matching transformer over Stable Audio's latent space, conditioned on " | |
| "a FLAN-T5 encoding of the instruction, so the wording of the prompt is what selects the " | |
| "treatment. It runs on the authors' Space (amaai-lab/SonicMaster), to which this Space " | |
| "forwards each request, so a request spends the caller's own ZeroGPU allowance and " | |
| "waits in that Space's queue. The model works at 44.1 kHz stereo, and the result is " | |
| "returned at the sample rate of the input. SonicMaster itself is Apache 2.0, but it " | |
| "decodes through Stable Audio Open's VAE, so results are also governed by the Stability " | |
| "AI Community License: " | |
| "https://huggingface.co/stabilityai/stable-audio-open-1.0/blob/main/LICENSE.md " | |
| "Repository: https://github.com/AMAAI-Lab/SonicMaster Paper: " | |
| "https://arxiv.org/abs/2508.03448" | |
| ), | |
| author="Jan Melechovsky, Ambuj Mehrish, Dorien Herremans", | |
| tags=[Subcategory.POST_PROCESSING, "text-guided", "zerogpu", "domain:music"], | |
| ) | |
| def _caller_client(): | |
| """A client of the backend carrying the caller's x-ip-token, so that ZeroGPU bills the caller.""" | |
| # The Hub adds x-ip-token to every request a Space receives, naming the caller. PyHARP | |
| # restores the request inside its worker, so it is read here. | |
| request = LocalContext.request.get(None) | |
| ip_token = request.headers.get("x-ip-token") if request is not None else None | |
| print(f"SonicMaster: forwarding to {BACKEND} " | |
| f"{'with' if ip_token else 'without'} the caller's x-ip-token", flush=True) | |
| return Client(BACKEND_URL, headers={"x-ip-token": ip_token} if ip_token else None, | |
| verbose=False) | |
| def _in_the_format_of(output_path, input_path, seconds): | |
| """ | |
| The backend's audio at the rate and in the container of the input, cut to `seconds`, as | |
| 16-bit within full scale. The channels stay as the model made them. | |
| """ | |
| audio, rate = sf.read(output_path, dtype="float32", always_2d=True) | |
| input_rate = sf.info(input_path).samplerate | |
| if rate != input_rate: | |
| audio = soxr.resample(audio, rate, input_rate) | |
| rate = input_rate | |
| audio = audio[: int(round(seconds * rate))] | |
| peak = float(np.abs(audio).max()) if audio.size else 0.0 | |
| if peak > 1.0: | |
| audio = audio / peak | |
| ext = Path(input_path).suffix.lower() | |
| path = Path(tempfile.mkdtemp()) / f"sonicmaster{ext if ext in LOSSLESS else '.wav'}" | |
| sf.write(path, audio, rate, subtype="PCM_16") | |
| return str(path) | |
| def process_fn(input_audio_path, prompt): | |
| """ | |
| Apply the instruction to the recording. | |
| Args: | |
| input_audio_path (str): The recording to edit. | |
| prompt (str): What to change about it. | |
| Returns: | |
| output_path (str): The edited audio. | |
| """ | |
| # Checked before forwarding, so a request the model cannot take waits in no queue and spends | |
| # none of the caller's allowance | |
| if not input_audio_path: | |
| raise gr.Error("Provide the recording to treat.") | |
| try: | |
| info = sf.info(input_audio_path) | |
| except Exception as exc: | |
| raise gr.Error(f"Could not read the audio file: {exc}") from exc | |
| if info.frames == 0: | |
| raise gr.Error("The audio file is empty.") | |
| if not (prompt or "").strip(): | |
| raise gr.Error("Describe what to change about the audio.") | |
| try: | |
| output, status = _caller_client().predict(handle_file(input_audio_path), prompt.strip(), | |
| api_name=API_NAME) | |
| except AppError as e: | |
| # The backend's own message, a quota error included, so the caller sees why | |
| raise gr.Error(str(e)) | |
| if not output: | |
| raise gr.Error(status or "SonicMaster returned no audio.") | |
| return _in_the_format_of(output, input_audio_path, float(info.duration)) | |
| # The processing worker imports this file, so the app must not be built there | |
| if __name__ == "__main__": | |
| check_backend() | |
| with gr.Blocks() as demo: | |
| input_components = [ | |
| gr.Audio(type="filepath", label="Input audio").harp_required(True).set_info(( | |
| "The recording to treat. The model works at 44.1 kHz stereo, and a mono input " | |
| "is heard on both channels." | |
| )), | |
| gr.Textbox(label="Instruction", value="Remove the reverb from this recording.", | |
| info=( | |
| "What to change, in plain language, for example " | |
| "\"reduce the reverb\", \"brighten the high end\", " | |
| "\"even out the loudness\", or \"widen the stereo " | |
| "image\". One instruction at a time works better " | |
| "than several combined." | |
| )), | |
| ] | |
| output_components = [ | |
| gr.Audio(type="filepath", label="Treated audio").set_info(( | |
| "The result, at 16-bit, at the sample rate and in the container of the input." | |
| )), | |
| ] | |
| build_endpoint( | |
| model_card=model_card, | |
| input_components=input_components, | |
| output_components=output_components, | |
| process_fn=process_fn, | |
| ) | |
| demo.queue().launch(show_error=True, pwa=True, ssr_mode=False) | |