import math import numpy as np import os import re import tempfile import torch from suno_utils.audio import Audio from suno_utils.audio.conversion import change_audio_speed from suno_utils.utils.clip import SunoClip from suno_utils.tasks.mert_25 import encode as encode_semantic, preload_models as preload_semantic_models from suno_utils.tasks.dac_vae_fixed_25hz import preload_models as preload_codec_models, decode_stream_to_full_audio from suno_utils.gpt.generation import GenerationConfig, CfgGenerationConfig from suno_utils.gpt.engine import Engine from suno_utils.gpt.generation_engine import make_request from suno_utils.gpt.generation_prompt import ALL_AUDIO_PROMPTS from suno_utils.diffusion import generation as diffusion_gen from suno_utils.diffusion.generation import preload_tokenizer from suno_utils.tasks.upsample_engine import UpsampleEngine, Request, Job global engine, diffusion_engine # gpt_model_fp = "/app/suno/checkpoints/2025-04-28_15-12-21/last_ckpt_infer.pt" # bct 6b # gpt_model_fp = "/app/suno/checkpoints/2025-04-23_16-23-43/last_ckpt_infer.pt" # bct finetune # gpt_model_fp = "/app/suno/checkpoints/2025-04-26_23-38-19/last_ckpt_infer.pt" # auk-d (new) # gpt_model_fp = "/app/suno/checkpoints/2025-04-20_05-36-49/last_ckpt_infer.pt" # auk-d # gpt_model_fp = "/app/suno/checkpoints/2025-04-08_19-36-46/last_ckpt_infer.pt" # (old auk-d) # gpt_model_fp = "/app/suno/checkpoints/2025-04-05_04-09-06/last_ckpt_infer.pt" # (old 1-round auk-d) # gpt_model_fp = "/app/suno/checkpoints/2025-02-04_21-04-31/last_ckpt_infer.pt" # (6b base) # gpt_model_fp = "/app/suno/checkpoints/2025-01-28_12-59-39/last_ckpt_infer.pt" # (3b orig) # gpt_model_fp = "/app/suno/checkpoints/2024-12-05_22-01-59/last_ckpt_infer.pt" # (old chef) def _get_engine(): global engine return engine def _get_diffusion_engine(): global diffusion_engine return diffusion_engine def setup_models(gpt_model_fp, diff_model_fp): global engine, diffusion_engine engine = Engine( gpt_model_fp, "/app/suno/models/chirp_v2/tokenizer_60k.json", max_sequences=8, compile=True, ) _ = diffusion_gen.preload_dit_model( dit_model_filepath=diff_model_fp, use_ema_if_exists=True, compile=True, weights_precision=torch.bfloat16, ) _ = preload_tokenizer("/app/suno/models/chirp_v2/tokenizer_60k.json") _ = preload_semantic_models( "/home/georg/notebooks/gpu_nb/tmp/mert_25.pt", "/home/georg/notebooks/gpu_nb/tmp/mert_25_2x4k.npy", ) _ = preload_codec_models("/home/georg/notebooks/gpu_nb/tmp/dac_vae_fixed_25hz_2.pth") diffusion_engine = UpsampleEngine(min_chunk_size=750) # initiate compile _ = generate("", "", max_gen_duration_s=9) def load_audio(audio_fp): if re.match(r"^[0-9a-z\-]{36}$", audio_fp): a = SunoClip(audio_fp).audio() else: a = Audio.from_file(audio_fp, sample_rate=48_000, n_channels=2) return a def load_clip(suno_uuid): assert re.match(r"^[0-9a-z\-]{36}$", suno_uuid) clip = SunoClip(suno_uuid) return clip.full_arr()[:,:1], clip.audio().normalize_volume(-14) def adjust_speed(audio, factor=1.0): with tempfile.TemporaryDirectory() as tmp_dir: tmp_path = os.path.join(tmp_dir, "audio.wav") audio.convert(48_000, 2, 2).to_wav(tmp_path) out_audio = change_audio_speed(tmp_path, factor, True, 48_000, 2, 2) return out_audio def generate( text, tags, gpt_cfg_coef=1.0, gpt_cfg_coef_tags=2.5, gpt_cfg_coef_max_steps=25*60*2, gpt_cfg_coef_tags_max_steps=25*60*2, gpt_cfg_coef_audio=0.0, diff_n_steps=10, control_tags=None, #"{min_duration:60;max_duration:480}", max_gen_duration_s=3*60, n_skip_semantic=1, audio_history=None, audio_artist=None, audio_cover=None, audio_vocals=None, audio_instrumental=None, audio_playlist=None, vocals_noise_lvl=0.2, gpt_cfg_streams=None, gpt_gen_cfg=None, diff_gen_cfg=None, return_semantic=False, seed=0, ): global engine, diffusion_engine text = text.strip() if text is None else text.strip() tags = tags.strip() if tags is None else tags.strip() if len(text.strip()) == 0 and max_gen_duration_s >= 10: text = "[instrumental]" if "instrumental" not in tags.lower(): if len(tags) == 0: tags = "instrumental" else: tags = tags + "; instrumental" if gpt_gen_cfg is None: if audio_history is not None: # TODO: diffusion continue not yet implemented audio_history = encode_semantic(audio_history)[:,:1] if audio_artist is not None: audio_artist = encode_semantic(audio_artist.normalize_volume())[:,:1] if audio_cover is not None: audio_cover = encode_semantic(audio_cover.normalize_volume())[:,:1] if audio_vocals is not None: if vocals_noise_lvl > 0: audio_vocals = audio_vocals.normalize_volume() arr = audio_vocals.array_float arr = arr + np.random.normal(0, vocals_noise_lvl, arr.shape).astype(arr.dtype) audio_vocals = Audio.from_array_float(np.clip(arr, -1.1, 1.1), 48_000) audio_vocals = encode_semantic(audio_vocals)[:,:1] if audio_instrumental is not None: audio_instrumental = encode_semantic(audio_instrumental.normalize_volume())[:,:1] if audio_playlist is not None: if not isinstance(audio_playlist, list): audio_playlist = [audio_playlist] tmp_audio_playlist = encode_semantic(audio_playlist[0].normalize_volume())[:,:1] for n in range(1, len(audio_playlist)): tmp_audio_playlist = np.concatenate([ tmp_audio_playlist, np.full((1, 1), engine.model.config.semantic_playlist_token, dtype=tmp_audio_playlist.dtype), encode_semantic(audio_playlist[n].normalize_volume())[:,:1] ], axis=0) audio_playlist = tmp_audio_playlist if gpt_cfg_streams is None: gpt_cfg_streams = [ CfgGenerationConfig( stream_type="tag", prompts=["tag"] + ALL_AUDIO_PROMPTS, null_prompts=ALL_AUDIO_PROMPTS, weight=gpt_cfg_coef_tags, max_steps=gpt_cfg_coef_tags_max_steps, ), ] if gpt_cfg_coef_audio > 0: gpt_cfg_streams.append(CfgGenerationConfig( stream_type="custom", prompts=["lyrics", "tag"] + ALL_AUDIO_PROMPTS, null_prompts=["lyrics", "tag"], weight=gpt_cfg_coef_audio, max_steps=None, )) gpt_gen_cfg = GenerationConfig( text=text, text_tags=tags, history_arr=audio_history, artist_arr=audio_artist, cover_arr=audio_cover, underpaint_arr=audio_vocals, overpaint_arr=audio_instrumental, playlist_arr=audio_playlist, cfg_coef=gpt_cfg_coef, cfg_coef_tags=gpt_cfg_coef_tags, cfg_coef_max_steps=gpt_cfg_coef_max_steps, cfg_coef_tags_max_steps=gpt_cfg_coef_tags_max_steps, n_repeat_tags=3 if len(tags) < 50 else 1, n_repeat_neg_tags=1, n_skip_semantic=n_skip_semantic, text_start_control_tags=control_tags, cfg_coef_neg_tags=-1.0, text_neg_tags="repetitive, loop", temp_semantic=0.9, top_k_semantic=1500, top_p_semantic=None, min_p_semantic=0.005, max_tag_len=512, allow_control_replace_end=False, n_batch=1, min_eos_p=0.1, min_text_offset=0, eos_pad_duration_s=0, max_gen_duration_s=max_gen_duration_s, random_seed=seed, custom_null_fields=ALL_AUDIO_PROMPTS, cfg_streams=gpt_cfg_streams, ) requests = [ make_request(f"{i}", gpt_gen_cfg, engine.model.config, engine.tokenizer) for i in range(2) ] jobs = engine.run_request(requests, tqdm_enabled=max_gen_duration_s>=10) out_gpt = [] for n, job in enumerate(jobs): stream = engine.token_generator(job) arr = torch.stack(list(stream))[:,1] if arr[-1] == 4000: arr = arr[:-1] print(f"{round(arr.shape[-1]/25*n_skip_semantic)}s for track {n}") arr2 = torch.zeros(arr.shape[0]*n_skip_semantic, dtype=arr.dtype) + engine.model.config.semantic_pad_token arr2[::n_skip_semantic] = arr out_gpt.append(arr2) if diff_gen_cfg is None: diff_gen_cfg = diffusion_gen.DiffusionGenerationConfig( lyrics=text, tags=tags, steps=diff_n_steps, text_cfg_coef=2.0, codec_scale_factor=0.4, scale_ctx_vector=True, noise_ctx_level=0.75, noise_ctx_pad_len=0, drop_semantic_tokens=False, seed=24+seed, ) out_audio = [] for in_sem_arr in out_gpt: request = Request( id="dummy", generation_config=diff_gen_cfg, tokens=in_sem_arr, input_tokens_finished=True, ) result = diffusion_engine.run_request(request, tqdm_enabled=max_gen_duration_s>=10) vae_latents = torch.concat(result.vae_latents) a = decode_stream_to_full_audio(vae_latents).normalize_volume(target_db=-14) out_audio.append(a) if max_gen_duration_s >= 10: a.play() if return_semantic: return out_gpt, out_audio return out_audio def upsample( sem_array_list, text="", tags="", n_steps=16, text_cfg_coef=2.0, noise_ctx_level=0.75, ignore_history_every=0, gen_cfg=None, seed=0, ): # https://github.com/suno-ai/glockenspiel/pull/9525/files assert isinstance(sem_array_list, list) if gen_cfg is None: gen_cfg = diffusion_gen.DiffusionGenerationConfig( lyrics=text, tags=tags, steps=n_steps, text_cfg_coef=text_cfg_coef, codec_scale_factor=0.4, scale_ctx_vector=True, noise_ctx_level=noise_ctx_level, noise_ctx_pad_len=0, drop_semantic_tokens=False, seed=24+seed, ) out_audio = [] for in_sem_arr in sem_array_list: in_kwargs = { "id": "dummy", "generation_config": gen_cfg, "tokens": in_sem_arr, "input_tokens_finished": True, } if ignore_history_every > 0: in_kwargs["ignore_history_every"] = ignore_history_every request = Request(**in_kwargs) result = diffusion_engine.run_request(request, tqdm_enabled=True) vae_latents = torch.concat(result.vae_latents) a = decode_stream_to_full_audio(vae_latents).normalize_volume(target_db=-14) out_audio.append(a) a.play() return out_audio def _crossfade(arr_1, arr_2, sr=48000, duration=20): assert arr_1.shape[0] == 2 and arr_2.shape[0] == 2 # stereo fade_len = sr * duration assert arr_1.shape[1] >= fade_len and arr_2.shape[1] >= fade_len, \ "Each array must be at least as long as the fade duration" # Sine-based fade curves t = np.linspace(0, np.pi, fade_len) fade_out = np.cos(t / 2) fade_in = np.sin(t / 2) # Crossfade only the overlapping parts crossfaded = ( arr_1[:, -fade_len:] * fade_out + arr_2[:, :fade_len] * fade_in ) # Concatenate: arr_1 up to overlap + crossfaded + arr_2 after overlap merged = np.concatenate([ arr_1[:, :-fade_len], crossfaded, arr_2[:, fade_len:] ], axis=1) return merged def upsample_crossfade( sem_array_list, text="", tags="", n_steps=16, text_cfg_coef=2.0, noise_ctx_level=0.75, window_duration_s=60, fade_duration_s=20, min_pred_duration_s=30, gen_cfg=None, seed=0, ): assert isinstance(sem_array_list, list) if gen_cfg is None: gen_cfg = diffusion_gen.DiffusionGenerationConfig( lyrics=text, tags=tags, steps=n_steps, text_cfg_coef=text_cfg_coef, codec_scale_factor=0.4, scale_ctx_vector=True, noise_ctx_level=noise_ctx_level, noise_ctx_pad_len=0, drop_semantic_tokens=False, seed=24+seed, ) window_len = 25 * window_duration_s shift_len = 25 * (window_duration_s - fade_duration_s) min_window = 25 * min_pred_duration_s overlap_len = window_len - shift_len out_audio = [] for in_sem_arr in sem_array_list: outs = [] num_samples = len(in_sem_arr) n_chunks = max(1, math.ceil((num_samples - min_window - window_len) / shift_len) + 1) for n in range(n_chunks): start = n * shift_len end = start + window_len if n < n_chunks - 1 else num_samples arr_chunk = in_sem_arr[start:end] in_kwargs = { "id": "dummy", "generation_config": gen_cfg, "tokens": arr_chunk, "input_tokens_finished": True, } request = Request(**in_kwargs) result = diffusion_engine.run_request(request, tqdm_enabled=False) vae_latents = torch.concat(result.vae_latents) a = decode_stream_to_full_audio(vae_latents) outs.append(a) out_merged = outs[0].array_float for out in outs[1:]: out_merged = _crossfade(out_merged, out.array_float) a = Audio.from_array_float(out_merged, 48_000).normalize_volume(target_db=-14) out_audio.append(a) a.play() return out_audio ################### # extra functions # ################### import math import random import re def _space_repl(m): s = m.group() n_newline = s.count("\n") if n_newline >= 2: return "\n\n" elif n_newline == 1: return "\n" return " " def _simplify_whitespace(text, retain_newlines=True): """simplify while respecting up to 2 newlines""" if retain_newlines: text = re.sub(r"\s+", _space_repl, text).strip() else: text = re.sub(r"\s+", " ", text).strip() return text def simplify_text(text): text = re.sub(r"\[.*?\]", " ", text) text = re.sub(r"\s+", " ", text).strip().lower() # text = _simplify_whitespace(re.sub(r"\[.*?\]", " ", text), retain_newlines=True) return text def shuffle_lyrics(lyrics, n_grams=1): lyrics = lyrics.lower() lyrics = re.sub(r"\[.*?\]", " ", lyrics) lyrics = re.sub(r"[\s+\,\;\(\)]", " ", lyrics).strip() words = lyrics.split() fragments = [ " ".join(words[n*n_grams:(n+1)*n_grams]) for n in range(math.ceil(len(words)/n_grams)) ] fragments = list(set(fragments)) random.shuffle(fragments) return "; ".join(fragments) #### # upsample #### #### # diff source sep #### STEM_INSTRUMENT_CATEGORIES = [ "Vocals", "Drums", "Bass", "Guitar", "Keyboard", "Percussion", "Strings", "Synth", "FX", "Brass", "Woodwinds", ] STEM_TYPE_ID_TO_NAME = { "0": "Lead Vocal", "1": "Electric Guitar", "2": "Backing Vocals", "3": "Drum Kit", "4": "Bass", "5": "Piano", "6": "Acoustic Guitar", "7": "Percussion", "8": "String Section", "9": "Synth Pad", "10": "Synthesizer", "11": "Organ", "12": "Synth Bass", "13": "Lead Electric Guitar", "14": "Synth Keys", "15": "Rhythm Electric Guitar", "16": "Electronic Drum Kit", "17": "Noise effects", "18": "Electric Piano", "19": "Arr. Electric Guitar", "20": "Brass section", "21": "Distorted Electric Guitar", "22": "Upright Bass", "23": "Synth Strings", "24": "Synth Lead", "25": "Rhythm Acoustic Guitar", "26": "Intro Count (Click + Key)", "27": "Flute", "28": "Arpeggiator", "29": "Tambourine", "30": "Trumpet", "31": "Harp", "32": "Synth Voice", "33": "Accordion", "34": "Fiddle", "35": "Synth Brass", "36": "Violin", "37": "Pedal Steel Guitar", "38": "Sound effects", "39": "Brass Instruments", "40": "Digital Piano", "41": "Tenor Saxophone", "42": "Mandolin", "43": "Trombone", "44": "Clarinet", "45": "French Horn", "46": "Banjo", "47": "Lead Acoustic Guitar", "48": "Rhythm Electric Guitar (Arpeggio)", "49": "Glockenspiel", "50": "Shaker", "51": "Electric Bass", "52": "Timpani", "53": "Vibes", "54": "Harmonica", "55": "Hand Clap", "56": "Guitar Synth", "57": "Lap Steel Guitar", "58": "Arr. Acoustic Guitar", "59": "Cello", "60": "Electronic Percussion", "61": "Woodwinds", "62": "Oboe", "63": "Rhythm Acoustic Guitar (Arpeggio)", "64": "Saxophone", "65": "Drums and Percussion", "66": "Female Backing Vocals", "67": "Marimba", "68": "Male Backing Vocals", "69": "Alto Saxophone", "70": "Bells", "71": "Pitched Percussion", "72": "Synth Ambiant", "73": "Dobro", "74": "Celesta", "75": "Congas", "76": "Synth Flute", "77": "Harpsichord", "78": "Baritone Saxophone", "79": "Orchestra Hit", "80": "Female Lead Vocal", "81": "Male Lead Vocal", "82": "Keyboard", "83": "Wind Chimes", "84": "Wind Instruments", "85": "Double Bass", "86": "Acoustic Drum Kit", "87": "Orchestral Percussion", "88": "Xylophone", "89": "Ukulele", "90": "Orchestra", "91": "Sound Effects", "92": "Cymbals", "93": "Tuba", "94": "Clap", "95": "Latin Percussion", "96": "Intro Count (Click)", "97": "Bassoon", "98": "Triangle", "99": "Snap (Fingers)", "100": "Sitar", "101": "Tubular Bell", "102": "Bongos", "103": "Cowbell", "104": "Sample", "105": "Metal bars", "106": "Steel Drums", "107": "Guitar", "108": "Viola", "109": "Soprano Saxophone", "110": "Claves/Woodblock", "111": "Whistle", "112": "English Horn", "113": "Jingle Bells", "114": "Thumb Piano", "115": "Piccolo", "116": "Intro Count (Key)", "117": "Bagpipes", "118": "Voice", "119": "Bass Drum", "120": "Music Box", "121": "Rhodes", "122": "Appalachian Dulcimer", "123": "Human Beatbox", "124": "Flugelhorn", "125": "Fretless Bass", "126": "Theremin", "127": "Vocoder", } import os import modal import json import tempfile from suno_utils.audio import Audio from suno_utils.utils.s3 import _upload_s3_file from uuid import uuid4 from contextlib import redirect_stderr, redirect_stdout import requests def split_stems(fp): uid = str(uuid4()) s3_filepath = f"s3://suno-data/georg/tmp/modal/{uid}.mp3" with tempfile.TemporaryDirectory() as tmp_dir: local_filepath = os.path.join(tmp_dir, f"{uid}.mp3") _ = Audio.from_file(fp, sample_rate=48_000, n_channels=2).to_hq_mp3(local_filepath) _upload_s3_file(local_filepath, s3_filepath) in_info = { "id": uid, "prompt_audio": s3_filepath, "model_name": "stems", "callback_url": "https://api-staging.suno.ai/api/generate/finish-clip/", "metadata": { "stem_type_group_name": "Vocals", # "stem_type_id": 0, "multi_ids": [f"{uid}_stem", f"{uid}_complement"], }, } with redirect_stderr(open(os.devnull, "w")): with redirect_stdout(open(os.devnull, "w")): model_f_stem = modal.Cls.lookup("stems-stems_v1-dev", "StemStub") model_f_stem.stem.remote(json.dumps(in_info)) out_stem_fp = f"https://cdn1.suno.ai/{uid}_stem.opus" out_complement_fp = f"https://cdn1.suno.ai/{uid}_complement.opus" r_stem = requests.get(out_stem_fp) r_complement = requests.get(out_complement_fp) with tempfile.TemporaryDirectory() as tmp_dir: out_local_stem_fp = os.path.join(tmp_dir, f"{uid}_stem.opus") out_local_complement_fp = os.path.join(tmp_dir, f"{uid}_complement.opus") with open(out_local_stem_fp, "wb") as f: f.write(r_stem.content) with open(out_local_complement_fp, "wb") as f: f.write(r_complement.content) audio_stem = Audio.read_opus(out_local_stem_fp) audio_complement = Audio.read_opus(out_local_complement_fp) return audio_stem, audio_complement