CoolFace
Apppublic

hugging-apps/diffsynth-music-demo

sourceHugging Faceupdated 11d agoView on Hugging Face
4likes
app.py638 linesDownload Raw Back to root
1import os2 3os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")4 5import spaces  # noqa: E402  (must precede torch / CUDA-touching imports)6 7import math  # noqa: E4028import tempfile  # noqa: E4029import time  # noqa: E40210 11import gradio as gr  # noqa: E40212import librosa  # noqa: E40213import numpy as np  # noqa: E40214import soundfile as sf  # noqa: E40215import torch  # noqa: E40216from einops import repeat  # noqa: E40217from huggingface_hub import snapshot_download  # noqa: E40218from scipy.signal import butter, filtfilt  # noqa: E40219 20from diffsynth.diffusion.template import TemplatePipeline  # noqa: E40221from diffsynth.pipelines.diffsynth_music import (  # noqa: E40222    DiffSynthMusicPipeline,23    ModelConfig,24)25from diffsynth.utils.music_tools import generate_click  # noqa: E40226 27REPO_ID = "DiffSynth-Studio/DiffSynth-Music"28SAMPLE_RATE = 4800029DIVISION_FACTOR = 3840  # 1920 (VAE hop) x 2 (latent token pairing)30MAX_REFERENCE_SECONDS = 60  # reference template scans for its loudest window31 32# Template model ids, matching the order they are registered in TemplatePipeline below.33TEMPLATE_CONTROL = 034TEMPLATE_PROSODY = 135TEMPLATE_REFERENCE = 236 37MODE_TEXT = "Text to Music"38MODE_BEATS = "Beats"39MODE_VOCALS = "Vocals"40MODE_ACCOMP = "Accompaniment"41MODE_PROSODY = "Prosody"42MODE_REFERENCE = "Reference"43MODES = [MODE_TEXT, MODE_BEATS, MODE_VOCALS, MODE_ACCOMP, MODE_PROSODY, MODE_REFERENCE]44AUDIO_MODES = (MODE_VOCALS, MODE_ACCOMP, MODE_PROSODY, MODE_REFERENCE)45 46# ----------------------------------------------------------------------------------47# Model loading (module scope, then eagerly onto "cuda" -- ZeroGPU packs the weights)48#49# DiffSynth's loader hardcodes `use_disk_map=True` and hands the *computation device*50# straight to `safetensors.safe_open(..., device=...)`, which allocates on CUDA51# outside of any patchable torch call and blows up with "No CUDA GPUs are available"52# in the ZeroGPU main process. The VAE's `remove_weight_norm()` is real arithmetic on53# the loaded weights and has the same problem. So: build both pipelines on CPU, then54# move them to CUDA through `.to()`, which is the call ZeroGPU intercepts.55# ----------------------------------------------------------------------------------56 57MODEL_DIR = snapshot_download(58    REPO_ID,59    allow_patterns=[60        "transformer/*",61        "conditioner/*",62        "text_encoder/*",63        "vae/*",64        "track_separator/*",65        "template_control/*",66        "template_prosody/*",67        "template_reference/*",68    ],69)70print(f"Weights downloaded to {MODEL_DIR}", flush=True)71 72 73def _p(*parts):74    return os.path.join(MODEL_DIR, *parts)75 76 77pipe = DiffSynthMusicPipeline.from_pretrained(78    torch_dtype=torch.bfloat16,79    device="cpu",80    model_configs=[81        ModelConfig(path=_p("transformer", "model.safetensors")),82        ModelConfig(path=_p("conditioner", "model.safetensors")),83        ModelConfig(path=_p("text_encoder", "model.safetensors")),84        ModelConfig(path=_p("vae", "model.safetensors")),85        ModelConfig(path=_p("track_separator", "model.safetensors"), computation_dtype=torch.float32),86    ],87    tokenizer_config=ModelConfig(path=_p("text_encoder")),88)89template = TemplatePipeline.from_pretrained(90    torch_dtype=torch.bfloat16,91    device="cpu",92    model_configs=[93        ModelConfig(path=_p("template_control")),94        ModelConfig(path=_p("template_prosody")),95        ModelConfig(path=_p("template_reference")),96    ],97)98print("Models built on CPU, moving to CUDA...", flush=True)99 100# `BasePipeline.to` keeps `self.device` in sync but not `self.device_type`; the plain101# `TemplatePipeline` tracks neither, so both are fixed up by hand. Note the dtype is102# deliberately left alone: the Demucs track separator is loaded in float32.103pipe.to("cuda")104pipe.device = "cuda"105pipe.device_type = "cuda"106template.models.to("cuda")107template.device = "cuda"108 109DEFAULT_NEGATIVE_PROMPT = pipe.default_negative_prompt110print("All models loaded.", flush=True)111 112 113# ----------------------------------------------------------------------------------114# Audio helpers115# ----------------------------------------------------------------------------------116 117 118def load_audio(path: str, max_seconds: float = None) -> torch.Tensor:119    """Load an audio file as a stereo 48 kHz float tensor of shape [2, L]."""120    wav, _ = librosa.load(path, sr=SAMPLE_RATE, mono=False)121    wav = np.atleast_2d(wav)122    if wav.shape[0] == 1:123        wav = np.repeat(wav, 2, axis=0)124    elif wav.shape[0] > 2:125        wav = wav[:2]126    audio = torch.from_numpy(np.ascontiguousarray(wav)).float()127    if max_seconds is not None:128        audio = audio[:, : int(max_seconds * SAMPLE_RATE)]129    return audio130 131 132def align_length(audio: torch.Tensor, max_seconds: float = None) -> torch.Tensor:133    """Crop to `max_seconds` and to a whole number of VAE frames."""134    if max_seconds is not None:135        audio = audio[:, : int(max_seconds * SAMPLE_RATE)]136    length = audio.shape[1] // DIVISION_FACTOR * DIVISION_FACTOR137    return audio[:, :length]138 139 140def crop_loudest(audio: torch.Tensor, max_seconds: float = None) -> torch.Tensor:141    """Crop to the loudest `max_seconds` window, aligned to whole VAE frames.142 143    Songs -- and isolated stems especially -- routinely open with a near-silent intro:144    the vocal stem of the authors' own reference track sits at -49 dBFS over its first145    20 s. That matters because `fuse_track` balances the generated audio down to the146    control track's RMS, so naively keeping the *first* N seconds can scale an entire147    render into inaudibility. Picking the loudest window keeps short renders usable.148    """149    total = audio.shape[1] // DIVISION_FACTOR * DIVISION_FACTOR150    audio = audio[:, :total]151    if max_seconds is None or total == 0:152        return audio153    want = int(max_seconds * SAMPLE_RATE) // DIVISION_FACTOR * DIVISION_FACTOR154    if want <= 0 or total <= want:155        return audio156    n = want // DIVISION_FACTOR157    frames = audio.pow(2).mean(dim=0).reshape(-1, DIVISION_FACTOR).mean(dim=1)158    cumulative = torch.cat([torch.zeros(1), frames.cumsum(0)])159    energy = cumulative[n:] - cumulative[:-n]160    start = int(torch.argmax(energy)) * DIVISION_FACTOR161    return audio[:, start : start + want]162 163 164def to_gradio_audio(audio: torch.Tensor):165    return SAMPLE_RATE, audio.float().cpu().numpy().T166 167 168def save_audio(audio: torch.Tensor) -> str:169    path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name170    sf.write(path, audio.float().cpu().numpy().T, SAMPLE_RATE)171    return path172 173 174def extract_prosody(audio_tensor: torch.Tensor, sr: int = SAMPLE_RATE) -> torch.Tensor:175    """Prosody signal: every syllable's pronunciation is blurred out, timing + pitch kept.176 177    Port of `diffsynth.utils.music_tools.extract_prosody`. The only change is that the178    pYIN pitch track is estimated on a 16 kHz down-sample (identical f0 range, ~10x179    faster) before being interpolated back onto the 48 kHz grid -- the envelope and the180    synthesised carrier still run at the full 48 kHz.181    """182    audio_tensor = audio_tensor.squeeze()183    if audio_tensor.dim() > 1:184        audio_tensor = audio_tensor.mean(dim=0)185    y = audio_tensor.float().cpu().numpy()186 187    # Smooth amplitude envelope (identical to the reference implementation).188    abs_y = np.abs(y)189    nyq = 0.5 * sr190    b1, a1 = butter(4, 30.0 / nyq, btype="low")191    env = filtfilt(b1, a1, abs_y)192    b2, a2 = butter(2, 80.0 / nyq, btype="low")193    env = filtfilt(b2, a2, env)194    envelope = env / (np.max(env) + 1e-8)195 196    # f0 track.197    pyin_sr = 16000198    y_small = librosa.resample(y, orig_sr=sr, target_sr=pyin_sr)199    hop_length = 160200    f0, _, _ = librosa.pyin(201        y_small, fmin=65, fmax=1000, sr=pyin_sr, hop_length=hop_length, frame_length=1024202    )203    # forward/backward fill of unvoiced frames204    mask = np.isnan(f0)205    if np.any(mask):206        idx = np.where(~mask, np.arange(mask.shape[0]), 0)207        np.maximum.accumulate(idx, out=idx)208        f0 = f0[idx]209        mask2 = np.isnan(f0)210        if np.any(mask2):211            idx2 = np.where(~mask2, np.arange(mask2.shape[0]), mask2.shape[0] - 1)212            np.minimum.accumulate(idx2[::-1], out=idx2[::-1])213            f0 = f0[idx2]214    if np.all(np.isnan(f0)):215        f0 = np.full_like(f0, 220.0)216 217    t_frames = librosa.frames_to_time(np.arange(len(f0)), sr=pyin_sr, hop_length=hop_length)218    t_samples = np.arange(len(y)) / sr219    f0_samples = np.interp(t_samples, t_frames, f0)220 221    phase = 2 * np.pi * np.cumsum(f0_samples) / sr222    carrier = np.sin(phase)223    out = carrier * envelope224    out = out * (np.max(np.abs(y)) / (np.max(np.abs(out)) + 1e-8))225    out = torch.from_numpy(out.astype(np.float32))226    return repeat(out, "l -> n l", n=2)227 228 229# ----------------------------------------------------------------------------------230# Step 1 (optional): turn a full song into a control signal231# ----------------------------------------------------------------------------------232 233 234# ZeroGPU reservations below are fitted to times measured on this Space (see the235# constants' comments) and then given the recommended 1.4x margin, so visitors are236# only charged quota for what the call actually costs.237GPU_ENTRY_OVERHEAD = 5.0  # streaming the packed weights into VRAM on a cold worker238GPU_MARGIN = 1.4239 240 241def _reserve(core_seconds: float, floor: int, cap: int) -> int:242    return int(min(cap, max(floor, math.ceil((core_seconds + GPU_ENTRY_OVERHEAD) * GPU_MARGIN))))243 244 245def _prepare_duration(mode=MODE_VOCALS, source_audio=None, duration=60, *args, **kwargs):246    duration = int(duration or 60)247    core = 2.0 + 0.02 * duration          # Demucs separation248    if mode == MODE_PROSODY:249        core += 0.05 * duration           # + pYIN / envelope extraction250    return _reserve(core, floor=20, cap=90)251 252 253@spaces.GPU(duration=_prepare_duration)254def prepare_control(mode: str, source_audio: str, duration: int = 60):255    """Turn a full song into the control signal for the selected control mode.256 257    Runs Demucs source separation (Vocals / Accompaniment / Prosody) and, for Prosody,258    the pitch+envelope extraction on top of the isolated vocal track.259    """260    if mode not in AUDIO_MODES:261        raise gr.Error(262            f"'{mode}' does not take a control audio. Use Vocals, Accompaniment, "263            "Prosody or Reference."264        )265    if not source_audio:266        raise gr.Error("Please upload a song first.")267 268    if mode == MODE_REFERENCE:269        audio = crop_loudest(load_audio(source_audio), MAX_REFERENCE_SECONDS)270        return to_gradio_audio(audio)271 272    audio = crop_loudest(load_audio(source_audio), duration)273    if mode == MODE_ACCOMP:274        control = pipe.extract_track(audio, track=["drums", "bass", "other"])275    else:276        control = pipe.extract_track(audio, track="vocals")277    if mode == MODE_PROSODY:278        control = extract_prosody(control)279    control = align_length(control.float().cpu(), duration)280    return to_gradio_audio(control)281 282 283# ----------------------------------------------------------------------------------284# Step 2: generation285# ----------------------------------------------------------------------------------286 287 288def _generate_duration(289    mode=MODE_TEXT,290    prompt="",291    lyrics="",292    duration=60,293    bpm=120,294    control_audio=None,295    num_inference_steps=50,296    *args,297    **kwargs,298):299    duration = int(duration or 60)300    steps = int(num_inference_steps or 50)301    # Denoising dominates and scales with steps x sequence length. Measured on this302    # Space: 3.4s at 20 steps/20s, 9.5s at 50/60, 33.9s at 100/150 -> ~0.00215 s per303    # step-second on top of ~3s of fixed template + VAE work.304    core = 3.0 + 0.00215 * steps * duration305    if mode in (MODE_VOCALS, MODE_ACCOMP):306        core += 0.07 * duration   # fuse_track runs Demucs twice (+9s at 150s measured)307    elif mode == MODE_PROSODY:308        core += 0.01 * duration309    return _reserve(core, floor=25, cap=120)310 311 312def _run(313    mode,314    prompt,315    lyrics,316    duration,317    bpm,318    control_audio,319    num_inference_steps,320    cfg_scale,321    seed,322    negative_prompt,323):324    t0 = time.perf_counter()325    duration = int(duration)326    num_inference_steps = int(num_inference_steps)327    seed = int(seed)328    negative_prompt = negative_prompt if negative_prompt is not None else ""329 330    kwargs = dict(331        prompt=prompt,332        negative_prompt=negative_prompt,333        lyrics=lyrics or "",334        duration=duration,335        seed=seed,336        tiled=True,337        cfg_scale=float(cfg_scale),338        num_inference_steps=num_inference_steps,339    )340 341    if mode == MODE_TEXT:342        pass343 344    elif mode == MODE_BEATS:345        beats = generate_click(int(bpm), duration=duration)346        kwargs.update(347            bpm=int(bpm),348            template_inputs=[{"model_id": TEMPLATE_CONTROL, "audio": beats}],349            negative_template_inputs=[{"model_id": TEMPLATE_CONTROL, "audio": beats * 0}],350        )351 352    elif mode in (MODE_VOCALS, MODE_ACCOMP, MODE_PROSODY, MODE_REFERENCE):353        if not control_audio:354            raise gr.Error(355                f"The '{mode}' mode needs a control audio. Upload one, or use "356                "'Make a control signal from a song' to build it from a full track."357            )358        if mode == MODE_REFERENCE:359            control = crop_loudest(load_audio(control_audio), MAX_REFERENCE_SECONDS)360            # The reference template carries the timbre, so the reference recipe drops361            # the negative prompt entirely.362            kwargs.update(363                negative_prompt="",364                template_inputs=[{"model_id": TEMPLATE_REFERENCE, "audio": control}],365            )366        else:367            control = crop_loudest(load_audio(control_audio), duration)368            if control.shape[1] < DIVISION_FACTOR:369                raise gr.Error("The control audio is too short.")370            kwargs["duration"] = control.shape[1] / SAMPLE_RATE371            model_id = TEMPLATE_PROSODY if mode == MODE_PROSODY else TEMPLATE_CONTROL372            kwargs.update(373                template_inputs=[{"model_id": model_id, "audio": control}],374                negative_template_inputs=[{"model_id": model_id, "audio": control}],375            )376            if mode == MODE_VOCALS:377                kwargs.update(target_audio=control, target_track="vocals")378            elif mode == MODE_ACCOMP:379                kwargs.update(target_audio=control, target_track=["drums", "bass", "other"])380    else:381        raise gr.Error(f"Unknown mode: {mode}")382 383    audio = template(pipe, **kwargs)384    print(f"[{mode}] generated in {time.perf_counter() - t0:.1f}s", flush=True)385    return save_audio(audio)386 387 388@spaces.GPU(duration=_generate_duration)389def generate(390    mode: str = MODE_TEXT,391    prompt: str = "",392    lyrics: str = "",393    duration: int = 60,394    bpm: int = 120,395    control_audio: str = None,396    num_inference_steps: int = 50,397    cfg_scale: float = 4.0,398    seed: int = 42,399    negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,400    progress=gr.Progress(track_tqdm=True),401) -> str:402    """Generate music with DiffSynth-Music.403 404    Args:405        mode: control mode -- Text to Music, Beats, Vocals, Accompaniment, Prosody or Reference.406        prompt: text description of the music to generate.407        lyrics: lyrics, optionally with [Intro] / [Verse] / [Chorus] structure tags.408        duration: length of the generated track in seconds.409        bpm: beats per minute, used by the Beats mode to build the click track.410        control_audio: path to the control audio (Vocals / Accompaniment / Prosody / Reference).411        num_inference_steps: number of flow-matching steps.412        cfg_scale: classifier-free guidance scale.413        seed: random seed.414        negative_prompt: text description of what to avoid.415 416    Returns:417        Path to the generated 48 kHz stereo audio file.418    """419    return _run(420        mode, prompt, lyrics, duration, bpm, control_audio,421        num_inference_steps, cfg_scale, seed, negative_prompt,422    )423 424 425# Dedicated entry points for `gr.Examples`. Gradio fills example values positionally426# and then inserts its Progress object at the index where it is declared, so an427# example set has to line up with a signature of exactly its own arity.428 429 430@spaces.GPU(duration=_generate_duration)431def generate_from_text_example(432    mode: str = MODE_TEXT,433    prompt: str = "",434    lyrics: str = "",435    duration: int = 60,436    bpm: int = 120,437    progress=gr.Progress(track_tqdm=True),438) -> str:439    """Generate music from a prompt, lyrics and (for the Beats mode) a BPM."""440    return _run(mode, prompt, lyrics, duration, bpm, None, 50, 4.0, 42, DEFAULT_NEGATIVE_PROMPT)441 442 443@spaces.GPU(duration=_generate_duration)444def generate_from_audio_example(445    mode: str = MODE_VOCALS,446    prompt: str = "",447    lyrics: str = "",448    duration: int = 60,449    bpm: int = 120,450    control_audio: str = None,451    progress=gr.Progress(track_tqdm=True),452) -> str:453    """Generate music conditioned on a control audio track."""454    return _run(455        mode, prompt, lyrics, duration, bpm, control_audio, 50, 4.0, 42, DEFAULT_NEGATIVE_PROMPT456    )457 458 459# ----------------------------------------------------------------------------------460# UI461# ----------------------------------------------------------------------------------462 463LYRICS_ZH = """[Intro]464 465清新海风里有我们旅途466漆黑海浪上有帆依呀远征467风暴的咆哮不把恐惧藏水手的胸襟468祈祷你像无畏的领航人469懂也不懂的守护航程470你在甲板上留下的刻痕471是我梦的风景472 473我要送你永不沉的信念474升起代表勇的黑旗幡475我要送你永不沉的誓言476锚连着锚把七海踏遍477你就是烈焰478你就是烈焰479我的血未寒480不灭的烽火燃在你身边481我的血未寒482 483怒海的狂涛总是起了又平484凝望指着罗盘的星辰485我要把酒全都灌进骨里486陪我一起远行"""487 488LYRICS_EN = """[Verse]489Neon rain on a midnight street490Every puddle keeps a second sky491I am counting every heartbeat492Waiting for the dark to say goodbye493 494[Chorus]495Hold the line, hold the line496We are running out of night497Hold the line, hold the line498Till the morning gets it right"""499 500PROMPT_ZH = "An explosive, high-energy pop-rock track with a strong anime theme song feel."501PROMPT_EN = "A dreamy synth-pop ballad with warm analog pads, a soft female voice and a steady 4/4 groove."502 503CSS = """504.small-note { font-size: 0.9em; opacity: 0.75; }505.dark .gradio-container { color: var(--body-text-color); }506"""507 508with gr.Blocks(title="DiffSynth-Music") as demo:509    gr.Markdown(510        """511        # 🎼 DiffSynth-Music — controllable music generation512 513        [DiffSynth-Music](https://huggingface.co/DiffSynth-Studio/DiffSynth-Music) adds514        **audio-conditioned KV-cache adapters** ("Diffusion Templates") on top of515        ACE-Step-1.5, so a reference audio track can steer generation without touching516        the backbone. Pick a control mode, write a prompt and lyrics, and generate.517        """518    )519 520    with gr.Row():521        with gr.Column(scale=1):522            mode = gr.Radio(MODES, value=MODE_TEXT, label="Control mode")523            prompt = gr.Textbox(524                label="Prompt",525                value=PROMPT_EN,526                lines=3,527                placeholder="A dreamy synth-pop ballad with warm analog pads…",528            )529            lyrics = gr.Textbox(530                label="Lyrics",531                value=LYRICS_EN,532                lines=8,533                placeholder="[Verse]\n…\n\n[Chorus]\n…",534            )535            duration = gr.Slider(536                20, 150, value=60, step=10, label="Duration (seconds)",537                info="For Vocals / Accompaniment / Prosody the control audio is cropped to this length.",538            )539            bpm = gr.Slider(60, 200, value=120, step=1, label="BPM (Beats mode)")540 541        with gr.Column(scale=1):542            control_audio = gr.Audio(543                label="Control audio (Vocals / Accompaniment / Prosody / Reference)",544                type="filepath",545                sources=["upload", "microphone"],546            )547            with gr.Accordion("Make a control signal from a full song", open=False):548                gr.Markdown(549                    "Upload a complete song and let Demucs isolate the track the "550                    "selected mode needs (vocals, accompaniment, or the prosody "551                    "contour of the vocals). The result lands in the control-audio "552                    "box above.",553                    elem_classes="small-note",554                )555                source_audio = gr.Audio(label="Full song", type="filepath", sources=["upload"])556                prepare_btn = gr.Button("Extract control signal", variant="secondary")557 558            run_btn = gr.Button("🎵 Generate music", variant="primary", size="lg")559            output_audio = gr.Audio(label="Generated music", type="filepath")560 561    with gr.Accordion("Advanced options", open=False):562        with gr.Row():563            num_inference_steps = gr.Slider(20, 100, value=50, step=5, label="Inference steps")564            cfg_scale = gr.Slider(1.0, 8.0, value=4.0, step=0.5, label="CFG scale")565            seed = gr.Number(value=42, precision=0, label="Seed")566        negative_prompt = gr.Textbox(567            label="Negative prompt",568            value=DEFAULT_NEGATIVE_PROMPT,569            lines=3,570        )571 572    gen_inputs = [573        mode,574        prompt,575        lyrics,576        duration,577        bpm,578        control_audio,579        num_inference_steps,580        cfg_scale,581        seed,582        negative_prompt,583    ]584 585    run_btn.click(fn=generate, inputs=gen_inputs, outputs=output_audio)586    prepare_btn.click(587        fn=prepare_control,588        inputs=[mode, source_audio, duration],589        outputs=control_audio,590    )591 592    gr.Markdown("### Examples")593    gr.Markdown(594        "Text and beat driven generation — no control audio needed.",595        elem_classes="small-note",596    )597    gr.Examples(598        examples=[599            [MODE_TEXT, PROMPT_ZH, LYRICS_ZH, 60, 120],600            [MODE_TEXT, PROMPT_EN, LYRICS_EN, 60, 120],601            [MODE_BEATS, PROMPT_ZH, LYRICS_ZH, 60, 120],602        ],603        inputs=[mode, prompt, lyrics, duration, bpm],604        outputs=output_audio,605        fn=generate_from_text_example,606        cache_examples=True,607        cache_mode="lazy",608        label="Text / Beats",609    )610    gr.Markdown(611        "Audio-conditioned generation. The control clips below are the authors' own "612        "examples from the model card (Apache-2.0).",613        elem_classes="small-note",614    )615    gr.Examples(616        examples=[617            [MODE_VOCALS, PROMPT_ZH, "", 60, 120, "examples/audio_3_input.mp3"],618            [MODE_ACCOMP, PROMPT_ZH, LYRICS_ZH, 60, 120, "examples/audio_4_input.mp3"],619            [MODE_PROSODY, PROMPT_ZH, LYRICS_ZH, 60, 120, "examples/audio_5_input.mp3"],620            [MODE_REFERENCE, "Music", LYRICS_ZH, 60, 120, "examples/audio_reference.mp3"],621        ],622        inputs=[mode, prompt, lyrics, duration, bpm, control_audio],623        outputs=output_audio,624        fn=generate_from_audio_example,625        cache_examples=True,626        cache_mode="lazy",627        label="Vocals / Accompaniment / Prosody / Reference",628    )629 630    gr.Markdown(631        "Model: [DiffSynth-Studio/DiffSynth-Music](https://huggingface.co/DiffSynth-Studio/DiffSynth-Music) · "632        "Paper: [arXiv:2609.12774](https://huggingface.co/papers/2609.12774) · "633        "Code: [DiffSynth-Studio](https://github.com/modelscope/DiffSynth-Studio)",634        elem_classes="small-note",635    )636 637demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS)638