import os import sys os.chdir(os.path.join("src", "SongFormer")) sys.path.append(os.path.join("..", "third_party")) sys.path.append(".") # monkey patch to fix issues in msaf import scipy import numpy as np scipy.inf = np.inf import gradio as gr import torch import librosa import json import math import importlib import matplotlib matplotlib.use("Agg") # non-interactive backend: safe for rendering plots off the main thread import matplotlib.pyplot as plt import matplotlib.ticker as ticker from pathlib import Path from argparse import Namespace from omegaconf import OmegaConf from ema_pytorch import EMA from muq import MuQ from musicfm.model.musicfm_25hz import MusicFM25Hz from postprocessing.functional import postprocess_functional_structure from dataset.label2id import DATASET_ID_ALLOWED_LABEL_IDS, DATASET_LABEL_TO_DATASET_ID from utils.fetch_pretrained import download_all import export_utils # ZeroGPU (Hugging Face Spaces). Preinstalled on the Space; this branch # is Space-only and never runs locally. import spaces # Constants MUSICFM_HOME_PATH = os.path.join("ckpts", "MusicFM") BEFORE_DOWNSAMPLING_FRAME_RATES = 25 AFTER_DOWNSAMPLING_FRAME_RATES = 8.333 DATASET_LABEL = "SongForm-HX-8Class" DATASET_IDS = [5] TIME_DUR = 420 INPUT_SAMPLING_RATE = 24000 # Hardware-aware usage note shown on both tabs. ZeroGPU containers set # SPACES_ZERO_GPU; without it the Space is on plain CPU hardware. if os.environ.get("SPACES_ZERO_GPU"): USAGE_NOTE = ( "*Running on ZeroGPU: each analyzed file consumes your daily GPU " "quota — anonymous visitors 2 min, free accounts 5 min, PRO 40 min, " "Team/Enterprise members 40/60 min. Remaining quota also sets your " "queue priority.*" ) else: USAGE_NOTE = ( "*Running on CPU hardware: analysis takes a few minutes per song. " "On ZeroGPU hardware each file would consume daily GPU quota " "(anonymous 2 min, free 5 min, PRO 40 min).*" ) # Global model variables muq_model = None musicfm_model = None msa_model = None device = None def get_device(): """Select the best available device: MPS (Apple Silicon), CUDA, or CPU.""" if torch.cuda.is_available(): return torch.device("cuda") if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): return torch.device("mps") return torch.device("cpu") def clear_device_cache(device): """Clear GPU memory cache for the given device type.""" if device.type == "cuda": torch.cuda.empty_cache() elif device.type == "mps": torch.mps.empty_cache() def load_checkpoint(checkpoint_path, device=None): """Load checkpoint from path""" if device is None: device = "cpu" if checkpoint_path.endswith(".pt"): checkpoint = torch.load(checkpoint_path, map_location=device) elif checkpoint_path.endswith(".safetensors"): from safetensors.torch import load_file checkpoint = {"model_ema": load_file(checkpoint_path, device=device)} else: raise ValueError("Unsupported checkpoint format. Use .pt or .safetensors") return checkpoint def initialize_models(model_name: str, checkpoint: str, config_path: str): """Initialize all models""" global muq_model, musicfm_model, msa_model, device # Set device device = get_device() # Load MuQ muq_model = MuQ.from_pretrained("OpenMuQ/MuQ-large-msd-iter") muq_model = muq_model.to(device).eval() # Load MusicFM musicfm_model = MusicFM25Hz( is_flash=False, stat_path=os.path.join(MUSICFM_HOME_PATH, "msd_stats.json"), model_path=os.path.join(MUSICFM_HOME_PATH, "pretrained_msd.pt"), ) musicfm_model = musicfm_model.to(device).eval() # Load MSA model module = importlib.import_module("models." + str(model_name)) Model = getattr(module, "Model") hp = OmegaConf.load(os.path.join("configs", config_path)) msa_model = Model(hp) ckpt = load_checkpoint(checkpoint_path=os.path.join("ckpts", checkpoint)) if ckpt.get("model_ema", None) is not None: model_ema = EMA(msa_model, include_online_model=False) model_ema.load_state_dict(ckpt["model_ema"]) msa_model.load_state_dict(model_ema.ema_model.state_dict()) else: msa_model.load_state_dict(ckpt["model"]) msa_model.to(device).eval() return hp def _gpu_duration(audio_path, win_size=420, hop_size=420, num_classes=128): """Estimate GPU seconds for one file (ZeroGPU dynamic duration). Conservative: 30s base + 0.2s per audio second, clamped to [60, 300]. Tune the constants from observed Space timings. """ try: audio_secs = librosa.get_duration(path=audio_path) except Exception: return 120 return int(min(300, max(60, 30 + 0.2 * audio_secs))) @spaces.GPU(duration=_gpu_duration) def process_audio(audio_path, win_size=420, hop_size=420, num_classes=128): """Process audio file and return structure analysis results""" global muq_model, musicfm_model, msa_model, device if muq_model is None: hp = initialize_models() else: hp = OmegaConf.load(os.path.join("configs", "SongFormer.yaml")) # Load audio wav, sr = librosa.load(audio_path, sr=INPUT_SAMPLING_RATE) audio = torch.tensor(wav).to(device) # Prepare output total_len = ( (audio.shape[0] // INPUT_SAMPLING_RATE) // TIME_DUR * TIME_DUR ) + TIME_DUR total_frames = math.ceil(total_len * AFTER_DOWNSAMPLING_FRAME_RATES) logits = { "function_logits": np.zeros([total_frames, num_classes]), "boundary_logits": np.zeros([total_frames]), } logits_num = { "function_logits": np.zeros([total_frames, num_classes]), "boundary_logits": np.zeros([total_frames]), } # Prepare label masks dataset_id2label_mask = {} for key, allowed_ids in DATASET_ID_ALLOWED_LABEL_IDS.items(): dataset_id2label_mask[key] = np.ones(num_classes, dtype=bool) dataset_id2label_mask[key][allowed_ids] = False lens = 0 i = 0 with torch.no_grad(): while True: start_idx = i * INPUT_SAMPLING_RATE end_idx = min((i + win_size) * INPUT_SAMPLING_RATE, audio.shape[-1]) if start_idx >= audio.shape[-1]: break if end_idx - start_idx <= 1024: continue audio_seg = audio[start_idx:end_idx] # Get embeddings muq_output = muq_model(audio_seg.unsqueeze(0), output_hidden_states=True) muq_embd_420s = muq_output["hidden_states"][10] del muq_output clear_device_cache(device) _, musicfm_hidden_states = musicfm_model.get_predictions( audio_seg.unsqueeze(0) ) musicfm_embd_420s = musicfm_hidden_states[10] del musicfm_hidden_states clear_device_cache(device) # Process 30-second segments wraped_muq_embd_30s = [] wraped_musicfm_embd_30s = [] for idx_30s in range(i, i + hop_size, 30): start_idx_30s = idx_30s * INPUT_SAMPLING_RATE end_idx_30s = min( (idx_30s + 30) * INPUT_SAMPLING_RATE, audio.shape[-1], (i + hop_size) * INPUT_SAMPLING_RATE, ) if start_idx_30s >= audio.shape[-1]: break if end_idx_30s - start_idx_30s <= 1024: continue wraped_muq_embd_30s.append( muq_model( audio[start_idx_30s:end_idx_30s].unsqueeze(0), output_hidden_states=True, )["hidden_states"][10] ) clear_device_cache(device) wraped_musicfm_embd_30s.append( musicfm_model.get_predictions( audio[start_idx_30s:end_idx_30s].unsqueeze(0) )[1][10] ) clear_device_cache(device) if wraped_muq_embd_30s: wraped_muq_embd_30s = torch.concatenate(wraped_muq_embd_30s, dim=1) wraped_musicfm_embd_30s = torch.concatenate( wraped_musicfm_embd_30s, dim=1 ) all_embds = [ wraped_musicfm_embd_30s, wraped_muq_embd_30s, musicfm_embd_420s, muq_embd_420s, ] # Align embedding lengths if len(all_embds) > 1: embd_lens = [x.shape[1] for x in all_embds] min_embd_len = min(embd_lens) for idx in range(len(all_embds)): all_embds[idx] = all_embds[idx][:, :min_embd_len, :] embd = torch.concatenate(all_embds, axis=-1) # Inference dataset_ids = torch.Tensor(DATASET_IDS).to(device, dtype=torch.long) msa_info, chunk_logits = msa_model.infer( input_embeddings=embd, dataset_ids=dataset_ids, label_id_masks=torch.Tensor( dataset_id2label_mask[ DATASET_LABEL_TO_DATASET_ID[DATASET_LABEL] ] ) .to(device, dtype=bool) .unsqueeze(0) .unsqueeze(0), with_logits=True, ) # Accumulate logits start_frame = int(i * AFTER_DOWNSAMPLING_FRAME_RATES) end_frame = start_frame + min( math.ceil(hop_size * AFTER_DOWNSAMPLING_FRAME_RATES), chunk_logits["boundary_logits"][0].shape[0], ) logits["function_logits"][start_frame:end_frame, :] += ( chunk_logits["function_logits"][0].detach().cpu().numpy() ) logits["boundary_logits"][start_frame:end_frame] = ( chunk_logits["boundary_logits"][0].detach().cpu().numpy() ) logits_num["function_logits"][start_frame:end_frame, :] += 1 logits_num["boundary_logits"][start_frame:end_frame] += 1 lens += end_frame - start_frame i += hop_size # Average logits logits["function_logits"] /= np.maximum(logits_num["function_logits"], 1) logits["boundary_logits"] /= np.maximum(logits_num["boundary_logits"], 1) logits["function_logits"] = torch.from_numpy( logits["function_logits"][:lens] ).unsqueeze(0) logits["boundary_logits"] = torch.from_numpy( logits["boundary_logits"][:lens] ).unsqueeze(0) # Post-process msa_infer_output = postprocess_functional_structure(logits, hp) return logits, msa_infer_output def format_as_segments(msa_output): """Format as list of segments""" segments = [] for idx in range(len(msa_output) - 1): segments.append( { "start": str(round(msa_output[idx][0], 2)), "end": str(round(msa_output[idx + 1][0], 2)), "label": msa_output[idx][1], } ) return segments def format_as_msa(msa_output): """Format as MSA format""" lines = [] for time, label in msa_output: lines.append(f"{time:.2f} {label}") return "\n".join(lines) def format_as_json(segments): """Format as JSON""" return json.dumps(segments, indent=2, ensure_ascii=False) def create_visualization( logits, msa_output, label_num=8, frame_rates=AFTER_DOWNSAMPLING_FRAME_RATES ): """Create visualization plot""" # Assume ID_TO_LABEL mapping exists try: from dataset.label2id import ID_TO_LABEL except: ID_TO_LABEL = {i: f"Class_{i}" for i in range(128)} function_vals = logits["function_logits"].squeeze().cpu().numpy() boundary_vals = logits["boundary_logits"].squeeze().cpu().numpy() top_classes = np.argsort(function_vals.mean(axis=0))[-label_num:] T = function_vals.shape[0] time_axis = np.arange(T) / frame_rates fig, ax = plt.subplots(2, 1, figsize=(15, 8), sharex=True) # Plot function logits for cls in top_classes: ax[1].plot( time_axis, function_vals[:, cls], label=f"{ID_TO_LABEL.get(cls, f'Class_{cls}')}", ) ax[1].set_title("Top 8 Function Logits by Mean Activation") ax[1].set_xlabel("Time (seconds)") ax[1].set_ylabel("Logit") ax[1].xaxis.set_major_locator(ticker.MultipleLocator(20)) ax[1].xaxis.set_minor_locator(ticker.MultipleLocator(5)) ax[1].xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f")) ax[1].legend() ax[1].grid(True) # Plot boundary logits ax[0].plot(time_axis, boundary_vals, label="Boundary Logit", color="orange") ax[0].set_title("Boundary Logits") ax[0].set_ylabel("Logit") ax[0].legend() ax[0].grid(True) # Add vertical lines for markers for t_sec, label in msa_output: for a in ax: a.axvline(x=t_sec, color="red", linestyle="--", linewidth=0.8, alpha=0.7) if label != "end": ax[1].text( t_sec + 0.3, ax[1].get_ylim()[1] * 0.85, label, rotation=90, fontsize=8, color="red", ) plt.suptitle("Music Structure Analysis - Logits Overview", fontsize=16) plt.tight_layout() return fig def rule_post_processing(msa_list): if len(msa_list) <= 2: return msa_list result = msa_list.copy() while len(result) > 2: first_duration = result[1][0] - result[0][0] if first_duration < 1.0 and len(result) > 2: result[0] = (result[0][0], result[1][1]) result = [result[0]] + result[2:] else: break while len(result) > 2: last_label_duration = result[-1][0] - result[-2][0] if last_label_duration < 1.0: result = result[:-2] + [result[-1]] else: break while len(result) > 2: if result[0][1] == result[1][1] and result[1][0] <= 10.0: result = [(result[0][0], result[0][1])] + result[2:] else: break while len(result) > 2: last_duration = result[-1][0] - result[-2][0] if result[-2][1] == result[-3][1] and last_duration <= 10.0: result = result[:-2] + [result[-1]] else: break return result def analyze_one(audio_file, out_dir, stem=None): """Run the full per-file analysis pipeline and write export files. Shared by the single-file and batch handlers so the two paths cannot drift. Returns (segments, json_str, msa_str, fig, export_paths). The caller owns the returned figure (single-file displays it via gr.Plot; batch saves+closes it); on a write failure the figure is closed here before re-raising so it never leaks. """ logits, msa_output = process_audio(audio_file) # Apply rule-based post-processing, if not needed, use in cli infer msa_output = rule_post_processing(msa_output) segments = format_as_segments(msa_output) msa_str = format_as_msa(msa_output) json_str = format_as_json(segments) fig = create_visualization(logits, msa_output) try: export_paths = export_utils.write_exports( audio_file, segments, json_str, msa_str, fig, out_dir, stem=stem ) except Exception: plt.close(fig) raise return segments, json_str, msa_str, fig, export_paths def process_and_analyze(audio_file): """Main processing function""" if audio_file is None: return None, "", "", None, None, None, None, None, None, None try: # Shared pipeline; exports land in a fresh per-run temp directory # (stale runs are swept automatically by the bootstrap). out_dir = export_utils.new_run_dir() segments, json_format, msa_format, fig, export_paths = analyze_one( audio_file, out_dir ) # Create table data table_data = export_utils.segments_to_table(segments) zip_path = os.path.join( out_dir, export_utils.stem_of(audio_file) + "_songformer.zip" ) export_utils.make_zip(list(export_paths.values()), zip_path) return ( table_data, json_format, msa_format, fig, export_paths["json"], export_paths["msa"], export_paths["csv"], export_paths["audacity"], export_paths["png"], zip_path, ) except Exception as e: import traceback error_msg = f"Error: {str(e)}\n{traceback.format_exc()}" print(error_msg) # 在命令行输出完整错误 return None, "", error_msg, None, None, None, None, None, None, None def process_batch(files): """Analyze multiple files sequentially, yielding live status. The status table itself is the progress display: every file is listed as queued upfront, flips to processing, then to done/failed. Dropdown choices update as files finish so completed results can be inspected while the rest of the batch is still running. Outputs (per yield): status rows, ZIP download update, file-selector update, per-file results dict (for the detail viewer). """ if not files: yield ( [["(no files uploaded)", "", "", ""]], gr.update(value=None), gr.update(choices=[], value=None), {}, ) return run_dir = export_utils.new_run_dir() bundle = os.path.join(run_dir, "bundle") os.makedirs(bundle, exist_ok=True) # De-duplicate stems upfront so same-named uploads don't overwrite each # other and the queued list shows the final names. used_stems = set() queue = [] for audio_file in files: base = export_utils.stem_of(audio_file) stem = base n = 2 while stem in used_stems: stem = f"{base}_{n}" n += 1 used_stems.add(stem) queue.append((audio_file, stem)) status_rows = [[stem, "⏳ queued", "", ""] for _, stem in queue] results = {} zipped_count = 0 # how many files the on-disk ZIP actually contains zip_path = os.path.join(run_dir, "songformer_batch.zip") def _rebuild_bundle_zip(): """Rewrite manifests and atomically swap in an updated ZIP. Called after each completed file so the download button always serves "everything so far". os.replace is atomic, so a click can never observe a half-written archive. The (stem, segments) pairs are derived from `results` — the single source of truth. """ named = [(s, r["segments"]) for s, r in results.items()] with open( os.path.join(bundle, "summary.csv"), "w", encoding="utf-8", newline="" ) as f: f.write(export_utils.segments_to_combined_csv(named)) with open( os.path.join(bundle, "combined.json"), "w", encoding="utf-8" ) as f: f.write(export_utils.combined_json(named)) part = zip_path + ".part" export_utils.zip_dir(bundle, part) os.replace(part, zip_path) # List every file as queued; clear any previous run's results yield ( status_rows, gr.update(value=None, interactive=False, label="⬇️ Download all (ZIP)"), gr.update(choices=[], value=None), {}, ) for idx, (audio_file, stem) in enumerate(queue): status_rows[idx] = [stem, "🔄 processing…", "", ""] yield status_rows, gr.update(), gr.update(), results try: file_dir = os.path.join(bundle, stem) os.makedirs(file_dir, exist_ok=True) segments, json_str, msa_str, fig, paths = analyze_one( audio_file, file_dir, stem=stem ) plt.close(fig) duration = ( export_utils.format_time(float(segments[-1]["end"])) if segments else "" ) status_rows[idx] = [stem, "✅", len(segments), duration] results[stem] = { "segments": segments, "json": json_str, "msa": msa_str, "png": paths["png"], "audio": audio_file, } except Exception as e: import traceback print(f"Batch error for {stem}:\n{traceback.format_exc()}") status_rows[idx] = [stem, "❌ " + str(e)[:80], 0, ""] # ZeroGPU quota exhausted: every remaining file would fail the # same way, so skip them. (Message heuristic — ZeroGPU does not # document a stable exception class.) if "quota" in str(e).lower(): for j in range(idx + 1, len(queue)): status_rows[j] = [queue[j][1], "⏭️ skipped (GPU quota)", "", ""] yield ( status_rows, gr.update(), gr.update(choices=list(results.keys())), results, ) break else: # A ZIP rebuild failure must NOT mark the analyzed file as # failed: its exports exist and the next successful rebuild # will include it (pairs derive from `results`). try: # Keep the ZIP downloadable mid-run with everything so far _rebuild_bundle_zip() zipped_count = len(results) except Exception: import traceback print(f"ZIP rebuild error after {stem}:\n{traceback.format_exc()}") if zipped_count: zip_update = gr.update( value=zip_path, interactive=True, label=f"⬇️ Download all (ZIP) — {zipped_count}/{len(queue)} files", ) else: zip_update = gr.update() # Completed files become inspectable while the batch continues yield status_rows, zip_update, gr.update(choices=list(results.keys())), results # Manifests + ZIP were rebuilt incrementally per file; just normalize # the button label now that the batch is complete. The button is only # active if at least one rebuild actually produced a ZIP on disk. yield ( status_rows, gr.update( value=zip_path if zipped_count else None, interactive=bool(zipped_count), label="⬇️ Download all (ZIP)", ), gr.update(choices=list(results.keys())), results, ) def on_select_file(stem, results): """Render a previously-computed file's result in the batch detail viewer.""" # A selection can race an in-flight batch iteration under rare scheduler # timings (choices reach the browser just before the state lands); the # guard degrades to an empty view, recoverable by re-selecting. results = results or {} if not stem or stem not in results: return None, "", "", None, None r = results[stem] return ( export_utils.segments_to_table(r["segments"]), r["json"], r["msa"], r["png"], r.get("audio"), ) # Create Gradio interface with gr.Blocks( title="Music Structure Analysis", css=""" .logo-container { text-align: center; margin-bottom: 20px; } .links-container { display: flex; justify-content: center; column-gap: 10px; margin-bottom: 10px; } .model-title { text-align: center; font-size: 24px; font-weight: bold; margin-bottom: 30px; } """, ) as demo: # Top Logo gr.HTML("""
""") # Model title gr.HTML("""
SongFormer: Scaling Music Structure Analysis with Heterogeneous Supervision
""") # Links gr.HTML(""" """) with gr.Tabs(): with gr.Tab("Single File"): gr.Markdown(USAGE_NOTE) # Main input area with gr.Row(): with gr.Column(scale=3): audio_input = gr.Audio( label="Upload Audio File", type="filepath", elem_id="audio-input" ) with gr.Column(scale=1): gr.Markdown("### 📌 Examples") gr.Examples( examples=[ ["examples/BC_5cd6a6.mp3"], ["examples/BC_282ece.mp3"], ["examples/BHX_0158_letitrock.wav"], ["examples/BHX_0374_drunkonyou.wav"], ], inputs=[audio_input], label="Click to load example", ) # Analyze button with gr.Row(): analyze_btn = gr.Button( "🚀 Analyze Music Structure", variant="primary", scale=1 ) # Results display area with gr.Row(): with gr.Column(scale=13): segments_table = gr.Dataframe( headers=["Start / s (m:s.ms)", "End / s (m:s.ms)", "Label"], label="Detected Music Segments", interactive=False, elem_id="result-table", ) with gr.Column(scale=8): with gr.Row(): with gr.Accordion("📄 JSON Output", open=False): json_output = gr.Textbox( label="JSON Format", lines=15, max_lines=20, interactive=False, show_copy_button=True, ) with gr.Row(): with gr.Accordion("📋 MSA Text Output", open=False): msa_output = gr.Textbox( label="MSA Format", lines=15, max_lines=20, interactive=False, show_copy_button=True, ) # Visualization plot with gr.Row(): plot_output = gr.Plot(label="Activation Curves Visualization") # Export / download buttons (populated after analysis) with gr.Row(): download_json_btn = gr.DownloadButton("⬇️ JSON") download_msa_btn = gr.DownloadButton("⬇️ MSA (.txt)") download_csv_btn = gr.DownloadButton("⬇️ CSV") download_audacity_btn = gr.DownloadButton("⬇️ Audacity (.txt)") download_png_btn = gr.DownloadButton("⬇️ Plot (.png)") download_zip_btn = gr.DownloadButton( "⬇️ Download all (ZIP)", variant="primary" ) with gr.Tab("Batch"): gr.Markdown( "Upload multiple audio files, analyze them sequentially, " "and download all results as a single ZIP — it always " "contains everything analyzed so far, so you can download " "mid-run." ) gr.Markdown(USAGE_NOTE) with gr.Row(): with gr.Column(scale=3): batch_files = gr.File( label="Upload Audio Files", file_count="multiple", type="filepath", ) with gr.Column(scale=1): batch_analyze_btn = gr.Button( "🚀 Analyze Batch", variant="primary" ) batch_zip_btn = gr.DownloadButton( "⬇️ Download all (ZIP)", variant="primary", interactive=False ) with gr.Row(): batch_status = gr.Dataframe( headers=["File", "Status", "Segments", "Duration"], label="Batch Status", interactive=False, ) batch_results_state = gr.State({}) gr.Markdown("### Inspect a file") with gr.Row(): with gr.Column(scale=1): batch_file_selector = gr.Dropdown( label="Processed File", choices=[], interactive=True ) with gr.Column(scale=2): batch_detail_audio = gr.Audio( label="Listen", type="filepath", interactive=False ) with gr.Row(): with gr.Column(scale=13): batch_detail_table = gr.Dataframe( headers=["Start / s (m:s.ms)", "End / s (m:s.ms)", "Label"], label="Detected Music Segments", interactive=False, ) with gr.Column(scale=8): with gr.Row(): with gr.Accordion("📄 JSON Output", open=False): batch_detail_json = gr.Textbox( label="JSON Format", lines=15, max_lines=20, interactive=False, show_copy_button=True, ) with gr.Row(): with gr.Accordion("📋 MSA Text Output", open=False): batch_detail_msa = gr.Textbox( label="MSA Format", lines=15, max_lines=20, interactive=False, show_copy_button=True, ) with gr.Row(): batch_detail_plot = gr.Image(label="Activation Curves Visualization") gr.HTML("""
""") # Set event handlers analyze_btn.click( fn=process_and_analyze, inputs=[audio_input], outputs=[ segments_table, json_output, msa_output, plot_output, download_json_btn, download_msa_btn, download_csv_btn, download_audacity_btn, download_png_btn, download_zip_btn, ], ) batch_analyze_btn.click( fn=process_batch, inputs=[batch_files], outputs=[ batch_status, batch_zip_btn, batch_file_selector, batch_results_state, ], show_progress="minimal", ) batch_file_selector.change( fn=on_select_file, inputs=[batch_file_selector, batch_results_state], outputs=[ batch_detail_table, batch_detail_json, batch_detail_msa, batch_detail_plot, batch_detail_audio, ], ) if __name__ == "__main__": # Download pretrained models if not exist download_all(use_mirror=False) # Initialize models print("Initializing models...") initialize_models( model_name="SongFormer", checkpoint="SongFormer.safetensors", config_path="SongFormer.yaml", ) print("Models loaded successfully!") # Launch interface (Spaces injects its own server settings; an explicit # port would break the platform health check) demo.launch()