# app.py import json from pathlib import Path import gradio as gr from huggingface_hub import snapshot_download TEMPLATE_FILENAME = "workflow_template.json" TEMPLATE_PATH = Path(__file__).parent / TEMPLATE_FILENAME # Minimal embedded template (used if workflow_template.json is missing). # This mirrors the flattened template structure used earlier but kept compact. DEFAULT_TEMPLATE = { "id": "minimax-music3-template", "revision": 1, "last_node_id": 43, "last_link_id": 63, "nodes": [ { "id": 6, "type": "UNETLoader", "pos": [700, 380], "size": [480, 90], "widgets_values": ["minimax_music3_dit_fp16.safetensors","default"], "widgets_values_named": {"unet_name":"minimax_music3_dit_fp16.safetensors","weight_dtype":"default"} }, { "id": 3, "type": "CLIPLoader", "pos": [700, 550], "size": [480, 120], "widgets_values": ["minimax_music3_text_encoder_pruned_int8_convrot.safetensors","minimax","default"], "widgets_values_named": {"clip_name":"minimax_music3_text_encoder_pruned_int8_convrot.safetensors","type":"minimax","device":"default"} }, { "id": 7, "type": "VAELoader", "pos": [700, 750], "size": [480, 70], "widgets_values": ["minimax_music3_dav.safetensors"], "widgets_values_named": {"vae_name":"minimax_music3_dav.safetensors"} }, { "id": 13, "type": "MiniMaxMusic3TextEncode", "pos": [1230, 380], "size": [500, 750], "showAdvanced": True, "widgets_values": ["", "", 222, "fixed", 60, 1.7, 50], "widgets_values_named": {"caption":"","lyrics":"","seed":222,"control_after_generate":"fixed","max_duration":60,"cfg_scale":1.7,"top_k":50} }, { "id": 15, "type": "EmptyMiniMaxMusic3LatentAudio", "pos": [1780, 380], "size": [350, 90], "widgets_values": [60, 1], "widgets_values_named": {"seconds": 60, "batch_size": 1} }, { "id": 10, "type": "ConditioningZeroOut", "pos": [1820, 540], "size": [230, 40] }, { "id": 9, "type": "KSampler", "pos": [2170, 380], "size": [370, 270], "widgets_values": [1111111112, "fixed", 30, 1.7, "euler", "simple", 1], "widgets_values_named": {"seed":1111111112,"control_after_generate":"fixed","steps":30,"cfg":1.7,"sampler_name":"euler","scheduler":"simple","denoise":1} }, { "id": 12, "type": "VAEDecodeAudio", "pos": [2590, 380], "size": [230, 60] }, { "id": 42, "type": "VAEDecodeAudioTiled", "pos": [2580, 500], "size": [280, 110], "widgets_values": [1536, 64], "widgets_values_named": {"tile_size":1536,"overlap":64} }, { "id": 43, "type": "ComfySwitchNode", "pos": [2600, 690], "size": [270, 110], "widgets_values": [True], "widgets_values_named": {"switch": True} }, { "id": 38, "type": "SeedNode", "pos": [890, 970], "size": [270, 90], "widgets_values": [1040288205448534, "fixed"], "widgets_values_named": {"seed": 1040288205448534, "fixed": "fixed"} }, { "id": 35, "type": "SaveAudioAdvanced", "pos": [2150, 630], "size": [750, 160], "widgets_values": ["audio/audio_minimax_music3","mp3","V0"], "widgets_values_named": {"filename_prefix":"audio/audio_minimax_music3","format":"mp3","format.quality":"V0"} } ], "links": [ [18, 3, 0, 13, 0, "CLIP"], [16, 13, 0, 10, 0, "CONDITIONING"], [20, 13, 1, 15, 0, "FLOAT"], [6, 6, 0, 9, 0, "MODEL"], [15, 13, 0, 9, 1, "CONDITIONING"], [8, 10, 0, 9, 2, "CONDITIONING"], [21, 15, 0, 9, 3, "LATENT"], [12, 9, 0, 12, 0, "LATENT"], [13, 7, 0, 12, 1, "VAE"], [52, 38, 0, 13, 3, "INT"], [53, 38, 0, 9, 4, "INT"], [58, 9, 0, 42, 0, "LATENT"], [59, 7, 0, 42, 1, "VAE"], [60, 12, 0, 43, 0, "AUDIO"], [61, 42, 0, 43, 1, "AUDIO"], [62, 43, 0, 35, 0, "AUDIO"] ], "extra": {"frontendVersion": "1.49.6"}, "version": 0.4 } def load_template(): # Try reading an external template file first if TEMPLATE_PATH.exists(): try: return json.loads(TEMPLATE_PATH.read_text(encoding="utf-8")) except Exception as e: # fallback to embedded template if parsing fails print("Failed to parse workflow_template.json, using embedded template:", e) return DEFAULT_TEMPLATE.copy() def generate_workflow(caption, lyrics, max_duration, seed, unet_name, clip_name, vae_name, tiled_decode, filename_prefix="audio/audio_minimax_music3"): # Load the template JSON (dict) template = load_template() # Update loaders' widget filenames for node in template.get("nodes", []): if node.get("type") == "UNETLoader": node.setdefault("widgets_values", [unet_name, "default"]) node.setdefault("widgets_values_named", {})["unet_name"] = unet_name if node.get("type") == "CLIPLoader": node.setdefault("widgets_values", [clip_name, "minimax", "default"]) node.setdefault("widgets_values_named", {})["clip_name"] = clip_name if node.get("type") == "VAELoader": node.setdefault("widgets_values", [vae_name]) node.setdefault("widgets_values_named", {})["vae_name"] = vae_name # MiniMax text encode node if node.get("type") == "MiniMaxMusic3TextEncode": node.setdefault("widgets_values_named", {})["caption"] = caption node["widgets_values_named"]["lyrics"] = lyrics node["widgets_values_named"]["max_duration"] = float(max_duration) node["widgets_values_named"]["seed"] = int(seed) # keep widgets_values aligned if present node["widgets_values"] = [ caption, lyrics, int(seed), node.get("widgets_values", [None,])[3] if len(node.get("widgets_values", []))>3 else "fixed", float(max_duration), node.get("widgets_values", [None,])[5] if len(node.get("widgets_values", []))>5 else 1.7, node.get("widgets_values", [None,])[6] if len(node.get("widgets_values", []))>6 else 50 ] # Empty latent seconds if node.get("type") == "EmptyMiniMaxMusic3LatentAudio": node.setdefault("widgets_values_named", {})["seconds"] = float(max_duration) node["widgets_values"] = [float(max_duration), 1] # ComfySwitch: switch widget if node.get("type") == "ComfySwitchNode": node.setdefault("widgets_values", [bool(tiled_decode)]) node.setdefault("widgets_values_named", {})["switch"] = bool(tiled_decode) # SaveAudioAdvanced filename if node.get("type") == "SaveAudioAdvanced": node.setdefault("widgets_values_named", {})["filename_prefix"] = filename_prefix node["widgets_values"] = [filename_prefix, "mp3", "V0"] # Write out file out_dir = Path("outputs") out_dir.mkdir(exist_ok=True) out_path = out_dir / f"minimax_workflow_{int(seed)}.json" out_path.write_text(json.dumps(template, indent=2, ensure_ascii=False), encoding="utf-8") return str(out_path) def download_models(repo_id="Comfy-Org/MiniMax-Music-3", revision="main"): out_dir = Path("models_download") out_dir.mkdir(exist_ok=True) try: path = snapshot_download(repo_id=repo_id, revision=revision, cache_dir=str(out_dir)) return f"Downloaded to {path}" except Exception as e: return f"Failed to download models: {e}" with gr.Blocks(title="MiniMax Music 3 — ComfyUI workflow generator") as demo: gr.Markdown("# MiniMax Music 3 — ComfyUI workflow generator") with gr.Row(): with gr.Column(): caption = gr.Textbox(label="Caption (Global metadata → Vocal details → Arrangement)", value="Global Metadata: Mainstream pop... ", lines=6) lyrics = gr.Textbox(label="Lyrics (use [Intro]/[Verse]/[Chorus] tags)", value="[Intro]\nOh-oh-oh...", lines=8) max_duration = gr.Number(label="Max duration (seconds)", value=60) seed = gr.Number(label="Seed (integer)", value=1040288205448534) unet_name = gr.Textbox(label="UNET model filename", value="minimax_music3_dit_fp16.safetensors") clip_name = gr.Textbox(label="Text encoder filename", value="minimax_music3_text_encoder_pruned_int8_convrot.safetensors") vae_name = gr.Textbox(label="VAE filename", value="minimax_music3_dav.safetensors") tiled_decode = gr.Checkbox(label="Tiled decode (reduces VRAM)", value=True) filename_prefix = gr.Textbox(label="SaveAudio filename prefix", value="audio/audio_minimax_music3") generate_btn = gr.Button("Generate workflow JSON") download_models_btn = gr.Button("Download model weights to runner (optional)") with gr.Column(): out_file = gr.File(label="Generated workflow JSON") status = gr.Textbox(label="Status / Messages", interactive=False) def on_generate(caption, lyrics, max_duration, seed, unet_name, clip_name, vae_name, tiled_decode, filename_prefix): path = generate_workflow(caption, lyrics, max_duration, seed, unet_name, clip_name, vae_name, tiled_decode, filename_prefix) return path, f"Workflow generated: {path} — download and open in ComfyUI / tensor.art." generate_btn.click(on_generate, inputs=[caption, lyrics, max_duration, seed, unet_name, clip_name, vae_name, tiled_decode, filename_prefix], outputs=[out_file, status]) download_models_btn.click(lambda: download_models(), outputs=[status]) if __name__ == "__main__": demo.launch()