| import random |
| import argparse |
| import json |
| import os |
| import time |
| from concurrent.futures import ThreadPoolExecutor |
|
|
| import gradio as gr |
| import numpy as np |
| import torch |
| import torch.nn.functional as F |
| import tqdm |
| from huggingface_hub import hf_hub_download |
| from transformers import DynamicCache |
|
|
| import MIDI |
| from midi_model import MIDIModel |
| from midi_synthesizer import MidiSynthesizer |
|
|
| MAX_SEED = np.iinfo(np.int32).max |
| in_space = os.getenv("SYSTEM") == "spaces" |
|
|
| |
| FIXED_JS = """ |
| const MIDI_OUTPUT_BATCH_SIZE=2; |
| function deepQuerySelector(selector){ |
| function deepSearch(root,selector){ |
| let el=root.querySelector(selector); |
| if(el)return el; |
| const hosts=root.querySelectorAll('*'); |
| for(let h of hosts){ |
| if(h.shadowRoot){ |
| el=deepSearch(h.shadowRoot,selector); |
| if(el)return el; |
| } |
| } |
| return null; |
| } |
| return deepSearch(this,selector); |
| } |
| Element.prototype.deepQuerySelector=deepQuerySelector; |
| Document.prototype.deepQuerySelector=deepQuerySelector; |
| function gradioApp(){ |
| const elems=document.getElementsByTagName('gradio-app'); |
| const elem=elems.length==0?document:elems[0]; |
| return elem.shadowRoot?elem.shadowRoot:elem; |
| } |
| window.onUiLoaded=window.onUiLoaded||function(cb){ |
| if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',cb); |
| else cb(); |
| }; |
| const msgReceiveCallbacks=[]; |
| function onMsgReceive(cb){msgReceiveCallbacks.push(cb);} |
| function executeCallbacks(cbs,...args){ |
| cbs.forEach(cb=>{try{cb(...args);}catch(e){console.error(e);}}); |
| } |
| (function(){ |
| let midi_visualizers=[],audio_players=[]; |
| onUiLoaded(()=>{ |
| const c=gradioApp(); |
| for(let i=0;i<MIDI_OUTPUT_BATCH_SIZE;i++){ |
| let div=c.deepQuerySelector(`#midi_visualizer_container_${i}`); |
| if(!div)continue; |
| div.innerHTML=`<div class="progressDiv"><div class="progress">0/0</div></div><midi-visualizer class="midi-visualizer" id="midi_visualizer_${i}"></midi-visualizer>`; |
| let vis=c.deepQuerySelector(`#midi_visualizer_${i}`); |
| let aud=c.deepQuerySelector(`#midi_audio_${i} audio`); |
| if(vis){ |
| vis.config={noteHeight:2,pixelsPerTimeStep:40,noteSpacing:1,noteRGB:'136,212,152',activeNoteRGB:'168,230,207',windowless:true}; |
| if(aud)vis.bindAudioPlayer(aud); |
| midi_visualizers.push(vis); |
| audio_players.push(aud); |
| } |
| } |
| }); |
| function setProgress(p,t){ |
| let par=gradioApp().deepQuerySelector(".progressDiv"); |
| if(!par)return; |
| let inner=par.querySelector(".progress"); |
| if(t===0)t=1; |
| inner.style.width=`${(p/t)*100}%`; |
| inner.textContent=`${p}/${t}`; |
| } |
| onMsgReceive(msgs=>{ |
| for(let m of msgs){ |
| if(m instanceof Array)m.forEach(o=>handleMsg(o)); |
| else handleMsg(m); |
| } |
| }); |
| function handleMsg(msg){ |
| let idx; |
| switch(msg.name){ |
| case"visualizer_clear": idx=msg.data[0];let ver=msg.data[1];if(midi_visualizers[idx]){midi_visualizers[idx].clearMidiEvents(false);midi_visualizers[idx].version=ver;}break; |
| case"visualizer_append": idx=msg.data[0];let evs=msg.data[1];if(midi_visualizers[idx]){evs.forEach(v=>midi_visualizers[idx].appendMidiEvent(v));}break; |
| case"visualizer_end": idx=msg.data;if(midi_visualizers[idx]){midi_visualizers[idx].finishAppendMidiEvent();midi_visualizers[idx].setPlayTime(0);}break; |
| case"progress": setProgress(msg.data[0],msg.data[1]);break; |
| } |
| } |
| })(); |
| """ |
|
|
| |
| css = """ |
| :root { |
| --primary-600: #a8e6cf !important; |
| --accent-600: #a8e6cf !important; |
| --ring-color: #a8e6cf !important; |
| --checkbox-label-background-selected: #e1f0e5 !important; |
| --button-primary-background-fill: #a8e6cf !important; |
| --button-primary-background-fill-hover: #88d498 !important; |
| } |
| .gradio-container, .gradio-container * { --loader-color: #dcedc1 !important; box-shadow: none !important; } |
| *:focus { border-color: #a8e6cf !important; box-shadow: 0 0 0 2px #e1f0e5 !important; } |
| .ba-header-container { border: 1.5px solid #e1f0e5; border-radius: 12px; margin-bottom: 12px; background: white; overflow: hidden; line-height: 0; } |
| .ba-header-container img { width: 100%; height: auto; } |
| .status-container { border: 1.5px solid #e1f0e5; border-radius: 12px; padding: 15px 22px; margin-bottom: 20px; background: white; } |
| .status-title { color: #88d498 !important; font-weight: 800; font-size: 16px; margin-bottom: 8px; } |
| .text-green-bold { color: #88d498 !important; font-weight: 900 !important; } |
| .dot { height: 8px; width: 8px; background-color: #88d498; border-radius: 50%; display: inline-block; margin-right: 5px; box-shadow: 0 0 0 0 rgba(136,212,152,1); animation: pulse-green 2s infinite; } |
| @keyframes pulse-green { 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(136,212,152,0.7); } 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(136,212,152,0); } 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(136,212,152,0); } } |
| .slim-card { max-width: 700px; margin: 0 auto; background: transparent; padding: 10px; } |
| |
| /* Grid untuk Select Model - horizontal scroll (ke samping) */ |
| .model-horizontal-grid { |
| display: flex; |
| flex-direction: row; |
| gap: 10px; |
| padding: 12px; |
| background: white; |
| border: 1px solid #e9f0ec; |
| border-radius: 14px; |
| overflow-x: auto; |
| overflow-y: hidden; |
| white-space: nowrap; |
| box-sizing: border-box; |
| margin-bottom: 16px; |
| box-shadow: 0 1px 2px rgba(0,0,0,0.02); |
| scrollbar-width: thin; |
| } |
| .model-card { |
| flex: 0 0 auto; |
| width: 220px; |
| border: 1px solid #e2ece6; |
| border-radius: 12px; |
| padding: 8px 12px; |
| background: #fff; |
| display: inline-flex; |
| flex-direction: column; |
| justify-content: center; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| text-align: left; |
| font-family: inherit; |
| box-sizing: border-box; |
| } |
| .model-card:hover { |
| background: #f7fdf9; |
| border-color: #b8decb; |
| transform: translateY(-1px); |
| } |
| .model-card.selected { |
| background: #ecf6f1; |
| border-left: 3px solid #88d498; |
| border-color: #c8e6d9; |
| } |
| .model-title { |
| font-weight: 600; |
| color: #3a5e4f; |
| font-size: 12px; |
| margin-bottom: 2px; |
| white-space: normal; |
| word-break: break-word; |
| } |
| .model-sub { |
| color: #9bb7a8; |
| font-size: 9px; |
| font-weight: 500; |
| letter-spacing: 0.3px; |
| } |
| |
| /* Grid untuk Instruments - vertikal scroll (1 kolom) */ |
| .compact-grid { |
| display: grid; |
| grid-template-columns: 1fr; |
| gap: 8px; |
| padding: 12px; |
| background: white; |
| border: 1px solid #e9f0ec; |
| border-radius: 14px; |
| max-height: 260px; |
| overflow-y: auto; |
| box-sizing: border-box; |
| margin-bottom: 16px; |
| box-shadow: 0 1px 2px rgba(0,0,0,0.02); |
| } |
| |
| /* Grid untuk Drum Kit - 2 kolom (vertical scroll) */ |
| .drum-grid { |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 8px; |
| padding: 12px; |
| background: white; |
| border: 1px solid #e9f0ec; |
| border-radius: 14px; |
| max-height: 200px; |
| overflow-y: auto; |
| box-sizing: border-box; |
| margin-bottom: 16px; |
| box-shadow: 0 1px 2px rgba(0,0,0,0.02); |
| } |
| |
| .card-btn { |
| border: 1px solid #e2ece6; |
| border-radius: 12px; |
| padding: 8px 12px; |
| background: #fff; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| text-align: left; |
| width: 100%; |
| font-family: inherit; |
| box-sizing: border-box; |
| } |
| .card-btn:hover { |
| background: #f7fdf9; |
| border-color: #b8decb; |
| transform: translateY(-1px); |
| } |
| .card-btn.selected { |
| background: #ecf6f1; |
| border-left: 3px solid #88d498; |
| border-color: #c8e6d9; |
| } |
| .card-title { |
| font-weight: 600; |
| color: #3a5e4f; |
| font-size: 12px; |
| margin-bottom: 2px; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| .card-sub { |
| color: #9bb7a8; |
| font-size: 9px; |
| font-weight: 500; |
| letter-spacing: 0.3px; |
| } |
| .section-header { |
| font-size: 13px; |
| font-weight: 700; |
| color: #5c7c6b; |
| margin: 8px 0 6px 0; |
| letter-spacing: 0.5px; |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| } |
| .warning-card { background: #fffcf0; border: 2px dashed #ffd3b6; border-radius: 10px; padding: 12px; margin-bottom: 15px; text-align: center; } |
| .gen-btn { background: #a8e6cf !important; color: #4a5568 !important; font-weight: 700 !important; border-radius: 12px !important; height: 45px !important; width: 100%; border: none !important; cursor: pointer; } |
| .credit-footer { margin-top: 25px; padding: 15px; background: white; border-radius: 12px; text-align: center; border-bottom: 4px solid #a8e6cf; color: #94a3b8; font-weight: 700; font-size: 12px; letter-spacing: 2px; } |
| .diary-container { display: flex; flex-direction: column; align-items: center; margin: 20px 0 15px; padding: 12px; background: #fefef7; border-radius: 24px; border: 1px solid #e1f0e5; } |
| .diary-text { font-family: 'Georgia', serif; font-size: 18px; font-weight: 500; color: #88d498; margin-bottom: 8px; letter-spacing: 1px; } |
| .gif-wrapper { display: flex; justify-content: center; margin-top: 5px; } |
| .gif-wrapper img { max-width: 100%; border-radius: 20px; border: 1px solid #e1f0e5; } |
| |
| /* Tombol Undo */ |
| .undo-btn { |
| background: #a8e6cf !important; |
| color: white !important; |
| font-weight: 700 !important; |
| border-radius: 10px !important; |
| border: none !important; |
| padding: 8px 16px !important; |
| margin-top: 8px !important; |
| } |
| .undo-btn:hover { |
| background: #88d498 !important; |
| } |
| """ |
|
|
| |
| @torch.inference_mode() |
| def generate(model, prompt=None, batch_size=1, max_len=512, temp=1.0, top_p=0.98, top_k=20, |
| disable_patch_change=False, disable_control_change=False, disable_channels=None, generator=None): |
| tokenizer = model.tokenizer |
| if disable_channels is not None: |
| disable_channels = [tokenizer.parameter_ids["channel"][c] for c in disable_channels] |
| else: |
| disable_channels = [] |
| max_token_seq = tokenizer.max_token_seq |
| if prompt is None: |
| input_tensor = torch.full((1, max_token_seq), tokenizer.pad_id, dtype=torch.long, device=model.device) |
| input_tensor[0,0] = tokenizer.bos_id |
| input_tensor = input_tensor.unsqueeze(0).repeat(batch_size,1,1) |
| else: |
| if len(prompt.shape)==2: |
| prompt = prompt[None,:] |
| prompt = np.repeat(prompt, repeats=batch_size, axis=0) |
| elif prompt.shape[0]==1: |
| prompt = np.repeat(prompt, repeats=batch_size, axis=0) |
| elif len(prompt.shape)!=3 or prompt.shape[0]!=batch_size: |
| raise ValueError(f"invalid shape for prompt, {prompt.shape}") |
| prompt = prompt[..., :max_token_seq] |
| if prompt.shape[-1] < max_token_seq: |
| prompt = np.pad(prompt, ((0,0),(0,0),(0, max_token_seq - prompt.shape[-1])), |
| mode="constant", constant_values=tokenizer.pad_id) |
| input_tensor = torch.from_numpy(prompt).to(dtype=torch.long, device=model.device) |
| cur_len = input_tensor.shape[1] |
| bar = tqdm.tqdm(desc="generating", total=max_len - cur_len, disable=in_space) |
| cache1 = DynamicCache() |
| past_len = 0 |
| with bar: |
| while cur_len < max_len: |
| end = [False]*batch_size |
| hidden = model.forward(input_tensor[:, past_len:], cache=cache1)[:,-1] |
| next_token_seq = None |
| event_names = [""]*batch_size |
| cache2 = DynamicCache() |
| for i in range(max_token_seq): |
| mask = torch.zeros((batch_size, tokenizer.vocab_size), dtype=torch.int64, device=model.device) |
| for b in range(batch_size): |
| if end[b]: |
| mask[b, tokenizer.pad_id] = 1 |
| continue |
| if i == 0: |
| mask_ids = list(tokenizer.event_ids.values()) + [tokenizer.eos_id] |
| if disable_patch_change: mask_ids.remove(tokenizer.event_ids["patch_change"]) |
| if disable_control_change: mask_ids.remove(tokenizer.event_ids["control_change"]) |
| mask[b, mask_ids] = 1 |
| else: |
| param_names = tokenizer.events[event_names[b]] |
| if i > len(param_names): |
| mask[b, tokenizer.pad_id] = 1 |
| continue |
| param_name = param_names[i-1] |
| mask_ids = tokenizer.parameter_ids[param_name] |
| if param_name == "channel": |
| mask_ids = [i for i in mask_ids if i not in disable_channels] |
| mask[b, mask_ids] = 1 |
| mask = mask.unsqueeze(1) |
| x = next_token_seq |
| if i != 0: |
| hidden = None |
| x = x[:, -1:] |
| logits = model.forward_token(hidden, x, cache=cache2)[:,-1:] |
| scores = torch.softmax(logits/temp, dim=-1) * mask |
| samples = model.sample_top_p_k(scores, top_p, top_k, generator=generator) |
| if i == 0: |
| next_token_seq = samples |
| for b in range(batch_size): |
| if end[b]: continue |
| eid = samples[b].item() |
| if eid == tokenizer.eos_id: |
| end[b] = True |
| else: |
| event_names[b] = tokenizer.id_events[eid] |
| else: |
| next_token_seq = torch.cat([next_token_seq, samples], dim=1) |
| if all([len(tokenizer.events[event_names[b]]) == i for b in range(batch_size) if not end[b]]): |
| break |
| if next_token_seq.shape[1] < max_token_seq: |
| next_token_seq = F.pad(next_token_seq, (0, max_token_seq - next_token_seq.shape[1]), "constant", value=tokenizer.pad_id) |
| next_token_seq = next_token_seq.unsqueeze(1) |
| input_tensor = torch.cat([input_tensor, next_token_seq], dim=1) |
| past_len = cur_len |
| cur_len += 1 |
| bar.update(1) |
| yield next_token_seq[:,0].cpu().numpy() |
| if all(end): break |
|
|
| def create_msg(name,data): return {"name":name,"data":data} |
| def send_msgs(msgs): return json.dumps(msgs) |
|
|
| |
| def run(model_name, tab, mid_seq, continuation_state, continuation_select, |
| selected_instruments, drum_kit, bpm, time_sig, key_sig_str, |
| mid, midi_events, reduce_cc_st, remap_track_channel, add_default_instr, remove_empty_channels, |
| seed, seed_rand, gen_events, temp, top_p, top_k, allow_cc, progress=gr.Progress()): |
| try: |
| progress(0, desc="Memuat model...") |
| model = models[model_name] |
| model.to(device=opt.device) |
| tokenizer = model.tokenizer |
| bpm = int(bpm) |
| if time_sig == "auto": |
| time_sig = None; time_sig_nn = 4; time_sig_dd = 2 |
| else: |
| time_sig_nn, time_sig_dd = time_sig.split('/'); time_sig_nn = int(time_sig_nn); time_sig_dd = {2:1,4:2,8:3}[int(time_sig_dd)] |
| |
| key_sig = key_sig_str |
| if key_sig == "auto": |
| key_sig = None |
| key_sig_sf = 0; key_sig_mi = 0 |
| else: |
| key_index = key_sig_to_index[key_sig] |
| key_sig = key_index |
| key_sig_sf = (key_sig - 1) // 2 - 7 |
| key_sig_mi = (key_sig - 1) % 2 |
| |
| gen_events = int(gen_events) |
| max_len = gen_events |
| if seed_rand: seed = random.randint(0, MAX_SEED) |
| generator = torch.Generator(opt.device).manual_seed(seed) |
| disable_patch_change = False |
| disable_channels = None |
| |
| continuation_select_idx = 0 if continuation_select == "all" else int(continuation_select.replace("output", "")) |
| |
| progress(0.05, desc="Menyiapkan prompt...") |
| if tab == 0: |
| i = 0 |
| mid = [[tokenizer.bos_id] + [tokenizer.pad_id] * (tokenizer.max_token_seq - 1)] |
| if tokenizer.version == "v2": |
| if time_sig is not None: mid.append(tokenizer.event2tokens(["time_signature",0,0,0,time_sig_nn-1,time_sig_dd-1])) |
| if key_sig is not None: mid.append(tokenizer.event2tokens(["key_signature",0,0,0,key_sig_sf+7,key_sig_mi])) |
| if bpm != 0: mid.append(tokenizer.event2tokens(["set_tempo",0,0,0,bpm])) |
| patches = {} |
| instruments = selected_instruments |
| for instr in instruments: patches[i] = patch2number[instr]; i = (i+1) if i != 8 else 10 |
| if drum_kit != "None": patches[9] = drum_kits2number[drum_kit] |
| for i,(c,p) in enumerate(patches.items()): mid.append(tokenizer.event2tokens(["patch_change",0,0,i+1,c,p])) |
| mid = np.asarray([mid] * OUTPUT_BATCH_SIZE, dtype=np.int64) |
| mid_seq = mid.tolist() |
| if len(instruments) > 0: disable_patch_change = True; disable_channels = [i for i in range(16) if i not in patches] |
| elif tab == 1 and mid is not None: |
| eps = 4 if reduce_cc_st else 0 |
| mid = tokenizer.tokenize(MIDI.midi2score(mid), cc_eps=eps, tempo_eps=eps, |
| remap_track_channel=remap_track_channel, |
| add_default_instr=add_default_instr, remove_empty_channels=remove_empty_channels) |
| mid = mid[:int(midi_events)]; mid = np.asarray([mid] * OUTPUT_BATCH_SIZE, dtype=np.int64); mid_seq = mid.tolist() |
| elif tab == 2 and mid_seq is not None: |
| mid = np.asarray(mid_seq, dtype=np.int64) |
| if continuation_select_idx > 0: |
| continuation_state.append(mid_seq); mid = np.repeat(mid[continuation_select_idx-1:continuation_select_idx], repeats=OUTPUT_BATCH_SIZE, axis=0); mid_seq = mid.tolist() |
| else: continuation_state.append(mid.shape[1]) |
| else: |
| continuation_state = [0]; mid = [[tokenizer.bos_id] + [tokenizer.pad_id] * (tokenizer.max_token_seq - 1)]; mid = np.asarray([mid] * OUTPUT_BATCH_SIZE, dtype=np.int64); mid_seq = mid.tolist() |
| if mid is not None: max_len += mid.shape[1] |
| |
| init_msgs = [create_msg("progress", [0, gen_events])] |
| if not (tab == 2 and continuation_select_idx == 0): |
| for i in range(OUTPUT_BATCH_SIZE): |
| events = [tokenizer.tokens2event(tokens) for tokens in mid_seq[i]] |
| init_msgs += [create_msg("visualizer_clear", [i, tokenizer.version]), create_msg("visualizer_append", [i, events])] |
| yield mid_seq, continuation_state, seed, send_msgs(init_msgs) |
| |
| progress(0.1, desc=f"Generating {gen_events} events...") |
| midi_generator = generate(model, mid, batch_size=OUTPUT_BATCH_SIZE, max_len=max_len, temp=temp, |
| top_p=top_p, top_k=top_k, disable_patch_change=disable_patch_change, |
| disable_control_change=not allow_cc, disable_channels=disable_channels, generator=generator) |
| events = [list() for _ in range(OUTPUT_BATCH_SIZE)] |
| t = time.time() + 0.2 |
| total_steps = gen_events |
| for step, token_seqs in enumerate(midi_generator): |
| token_seqs = token_seqs.tolist() |
| for j in range(OUTPUT_BATCH_SIZE): |
| token_seq = token_seqs[j]; mid_seq[j].append(token_seq); events[j].append(tokenizer.tokens2event(token_seq)) |
| if time.time() - t > 0.2: |
| progress((step+1)/total_steps, desc=f"Generating {step+1}/{total_steps}") |
| msgs = [create_msg("progress", [step+1, gen_events])] |
| for j in range(OUTPUT_BATCH_SIZE): |
| msgs += [create_msg("visualizer_append", [j, events[j]])] |
| events[j] = [] |
| yield mid_seq, continuation_state, seed, send_msgs(msgs) |
| t = time.time() |
| yield mid_seq, continuation_state, seed, send_msgs([]) |
| progress(1.0, desc="Selesai!") |
| print("Generation finished.") |
| except Exception as e: |
| print(f"ERROR in run: {e}") |
| import traceback |
| traceback.print_exc() |
| yield mid_seq if 'mid_seq' in locals() else None, continuation_state if 'continuation_state' in locals() else [0], seed if 'seed' in locals() else 0, send_msgs([create_msg("progress", [0,0])]) |
|
|
| |
| def finish_run(model_name, mid_seq): |
| print("Starting finish_run...") |
| if mid_seq is None: |
| print("mid_seq is None") |
| outputs = [None] * OUTPUT_BATCH_SIZE |
| return *outputs, [] |
| tokenizer = models[model_name].tokenizer |
| outputs = [] |
| end_msgs = [create_msg("progress",[0,0])] |
| out_dir = "outputs" |
| if not os.path.exists(out_dir): |
| os.mkdir(out_dir) |
| print(f"Created outputs directory: {out_dir}") |
| for i in range(OUTPUT_BATCH_SIZE): |
| events = [tokenizer.tokens2event(tokens) for tokens in mid_seq[i]] |
| mid = tokenizer.detokenize(mid_seq[i]) |
| out_path = os.path.join(out_dir, f"output{i+1}.mid") |
| with open(out_path, 'wb') as f: |
| f.write(MIDI.score2midi(mid)) |
| outputs.append(out_path) |
| print(f"Saved MIDI to {out_path}") |
| end_msgs += [create_msg("visualizer_clear", [i, tokenizer.version]), create_msg("visualizer_append", [i, events]), create_msg("visualizer_end", i)] |
| print("finish_run completed, returning outputs:", outputs) |
| return *outputs, send_msgs(end_msgs) |
|
|
| def synthesis_task(mid): |
| return synthesizer.synthesis(MIDI.score2opus(mid)) |
|
|
| def render_audio(model_name, mid_seq, should_render_audio): |
| print(f"render_audio called, should_render_audio={should_render_audio}, mid_seq is {type(mid_seq)}") |
| if (not should_render_audio) or mid_seq is None: |
| outputs = [None] * OUTPUT_BATCH_SIZE |
| print("No audio to render") |
| return tuple(outputs) |
| tokenizer = models[model_name].tokenizer |
| outputs = [] |
| audio_futures = [] |
| for i in range(OUTPUT_BATCH_SIZE): |
| mid = tokenizer.detokenize(mid_seq[i]) |
| audio_futures.append(thread_pool.submit(synthesis_task, mid)) |
| for future in audio_futures: |
| audio_data = future.result() |
| if isinstance(audio_data, np.ndarray): |
| audio_data = audio_data.astype(np.float32) |
| outputs.append((44100, audio_data)) |
| print(f"Rendered {len(outputs)} audio files") |
| if OUTPUT_BATCH_SIZE == 1: |
| return outputs[0] |
| return tuple(outputs) |
|
|
| def undo_continuation(model_name, mid_seq, continuation_state): |
| if mid_seq is None or len(continuation_state) < 2: |
| return mid_seq, continuation_state, send_msgs([]) |
| tokenizer = models[model_name].tokenizer |
| if isinstance(continuation_state[-1], list): |
| mid_seq = continuation_state[-1] |
| else: |
| mid_seq = [ms[:continuation_state[-1]] for ms in mid_seq] |
| continuation_state = continuation_state[:-1] |
| end_msgs = [create_msg("progress",[0,0])] |
| for i in range(OUTPUT_BATCH_SIZE): |
| events = [tokenizer.tokens2event(tokens) for tokens in mid_seq[i]] |
| end_msgs += [create_msg("visualizer_clear", [i, tokenizer.version]), create_msg("visualizer_append", [i, events]), create_msg("visualizer_end", i)] |
| return mid_seq, continuation_state, send_msgs(end_msgs) |
|
|
| def hf_hub_download_retry(repo_id, filename): |
| print(f"downloading {repo_id} {filename}") |
| retry=0; err=None |
| while retry<30: |
| try: return hf_hub_download(repo_id=repo_id, filename=filename) |
| except Exception as e: err=e; retry+=1 |
| if err: raise err |
|
|
| |
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--share", action="store_true", default=False) |
| parser.add_argument("--port", type=int, default=7860) |
| default_device = "cuda" if torch.cuda.is_available() else "cpu" |
| parser.add_argument("--device", type=str, default=default_device) |
| parser.add_argument("--batch", type=int, default=2) |
| parser.add_argument("--max-gen", type=int, default=512) |
| opt = parser.parse_args() |
| |
| if opt.device == "cuda" and not torch.cuda.is_available(): |
| print("⚠️ CUDA tidak tersedia, beralih ke CPU") |
| opt.device = "cpu" |
| |
| OUTPUT_BATCH_SIZE = opt.batch |
| print(f"Using device: {opt.device}, batch size: {OUTPUT_BATCH_SIZE}") |
| |
| if opt.device == "cpu" and "LD_LIBRARY_PATH" in os.environ: |
| print("⚠️ Menghapus LD_LIBRARY_PATH karena menggunakan CPU") |
| |
| soundfont_path = hf_hub_download_retry(repo_id="skytnt/midi-model", filename="soundfont.sf2") |
| thread_pool = ThreadPoolExecutor(max_workers=OUTPUT_BATCH_SIZE) |
| synthesizer = MidiSynthesizer(soundfont_path) |
|
|
| |
| models_info = { |
| "generic pretrain model (tv2o-medium) by skytnt": [ |
| "skytnt/midi-model-tv2o-medium", { |
| "jpop": "skytnt/midi-model-tv2om-jpop-lora", |
| "touhou": "skytnt/midi-model-tv2om-touhou-lora" |
| } |
| ], |
| "generic pretrain model (tv2o-large) by asigalov61": ["asigalov61/Music-Llama", {}], |
| "generic pretrain model (tv2o-medium) by asigalov61": ["asigalov61/Music-Llama-Medium", {}], |
| "generic pretrain model (tv1-medium) by skytnt": ["skytnt/midi-model", {}] |
| } |
| models = {} |
| if opt.device == "cuda": |
| torch.backends.cudnn.deterministic = True |
| torch.backends.cudnn.benchmark = False |
| torch.backends.cuda.matmul.allow_tf32 = True |
| torch.backends.cudnn.allow_tf32 = True |
| torch.backends.cuda.enable_mem_efficient_sdp(True) |
| torch.backends.cuda.enable_flash_sdp(True) |
| for name, (repo_id, loras) in models_info.items(): |
| print(f"Loading {name}...") |
| model = MIDIModel.from_pretrained(repo_id) |
| model.to(device="cpu", dtype=torch.float32) |
| models[name] = model |
| for lora_name, lora_repo in loras.items(): |
| print(f"loading lora {lora_repo} for {name}") |
| model = MIDIModel.from_pretrained(repo_id) |
| model = model.load_merge_lora(lora_repo) |
| model.to(device="cpu", dtype=torch.float32) |
| models[f"{name} with {lora_name} lora"] = model |
|
|
| number2drum_kits = {-1:"None",0:"Standard",8:"Room",16:"Power",24:"Electric",25:"TR-808",32:"Jazz",40:"Blush",48:"Orchestra"} |
| patch2number = {v:k for k,v in MIDI.Number2patch.items()} |
| drum_kits2number = {v:k for k,v in number2drum_kits.items()} |
| key_signatures = ['C♭','A♭m','G♭','E♭m','D♭','B♭m','A♭','Fm','E♭','Cm','B♭','Gm','F','Dm','C','Am','G','Em','D','Bm','A','F♯m','E','C♯m','B','G♯m','F♯','D♯m','C♯','A♯m'] |
| key_sig_to_index = {name: i+1 for i, name in enumerate(key_signatures)} |
| |
| instrument_list = list(patch2number.keys()) |
| drum_kit_list = list(number2drum_kits.values()) |
|
|
| |
| with gr.Blocks(css=css, title="Midi Music Generator") as app: |
| gr.HTML(f"<script>{FIXED_JS}</script>") |
| |
| with gr.Column(elem_classes="slim-card"): |
| gr.HTML(""" |
| <div class="ba-header-container"> |
| <img src="https://huggingface.co/spaces/Plana-Archive/MOE-TTS/resolve/main/Mutsumi.jpg" alt="Banner"> |
| </div> |
| <div class="status-container"> |
| <div class="status-title">System Status</div> |
| <div><span style="color:#4a5568">Engine :</span> <span class="text-green-bold">LOADED ACTIVE ✅</span></div> |
| <div><span style="color:#4a5568">System :</span> <span style="color:#88d498; font-weight:700;"><span class="dot"></span>Online</span></div> |
| <div><span style="color:#4a5568">Status :</span> <span style="color:#88d498; font-weight:700;">Free</span></div> |
| </div> |
| """) |
| gr.Markdown("### 🎹 Midi Music Generator") |
| |
| js_msg = gr.Textbox(elem_id="msg_receiver", visible=False) |
| js_msg.change(None, [js_msg], [], js="(msg_json) => { let msgs = JSON.parse(msg_json); executeCallbacks(msgReceiveCallbacks, msgs); return []; }") |
| |
| |
| gr.HTML('<div class="section-header"><span>🎼</span> Select Model</div>') |
| model_state = gr.State(value=list(models.keys())[0]) |
| model_grid_html = gr.HTML("") |
| |
| def build_model_horizontal_grid(): |
| html = '<div class="model-horizontal-grid" id="model-grid">' |
| model_names = list(models.keys()) |
| for i, name in enumerate(model_names): |
| selected_attr = 'selected' if i == 0 else '' |
| html += f''' |
| <div data-model="{name}" class="model-card {selected_attr}" onclick=" |
| document.querySelectorAll('#model-grid .model-card').forEach(card => card.classList.remove('selected')); |
| this.classList.add('selected'); |
| const modelName = this.getAttribute('data-model'); |
| const gradioApp = document.querySelector('gradio-app').shadowRoot || document; |
| const modelStateInput = gradioApp.querySelector('#model_state_input'); |
| if(modelStateInput) modelStateInput.value = modelName; |
| modelStateInput.dispatchEvent(new Event('input', {{bubbles: true}})); |
| "> |
| <div class="model-title">{name}</div> |
| <div class="model-sub">click to select</div> |
| </div> |
| ''' |
| html += '</div>' |
| return html |
| |
| model_grid_html.value = build_model_horizontal_grid() |
| model_state_input = gr.Textbox(visible=False, elem_id="model_state_input") |
| model_state_input.change(fn=lambda x: x, inputs=[model_state_input], outputs=[model_state]) |
| |
| |
| gr.HTML('<div class="section-header"><span>🎸</span> Instruments (multiselect)</div>') |
| instr_state = gr.State(value=[]) |
| instr_grid_html = gr.HTML("") |
| |
| def build_instr_grid(selected_list): |
| html = '<div class="compact-grid" id="instr-grid">' |
| for name in instrument_list: |
| selected_class = ' selected' if name in selected_list else '' |
| html += f''' |
| <div data-instr="{name}" class="card-btn{selected_class}" onclick=" |
| let btn = this; |
| let isSelected = btn.classList.contains('selected'); |
| if(isSelected) btn.classList.remove('selected'); |
| else btn.classList.add('selected'); |
| const selectedNames = Array.from(document.querySelectorAll('#instr-grid .card-btn.selected')).map(b => b.getAttribute('data-instr')); |
| const gradioApp = document.querySelector('gradio-app').shadowRoot || document; |
| const instrInput = gradioApp.querySelector('#instr_state_input'); |
| if(instrInput) instrInput.value = JSON.stringify(selectedNames); |
| instrInput.dispatchEvent(new Event('input', {{bubbles: true}})); |
| "> |
| <div class="card-title">{name}</div> |
| <div class="card-sub">instrument</div> |
| </div> |
| ''' |
| html += '</div>' |
| return html |
| |
| instr_grid_html.value = build_instr_grid([]) |
| instr_state_input = gr.Textbox(visible=False, elem_id="instr_state_input") |
| instr_state_input.change(fn=lambda x: json.loads(x) if x else [], inputs=[instr_state_input], outputs=[instr_state]) |
| |
| |
| gr.HTML('<div class="section-header"><span>🥁</span> Drum Kit</div>') |
| drum_state = gr.State(value="None") |
| drum_grid_html = gr.HTML("") |
| |
| def build_drum_grid(selected): |
| html = '<div class="drum-grid" id="drum-grid">' |
| for name in drum_kit_list: |
| selected_class = ' selected' if name == selected else '' |
| html += f''' |
| <div data-drum="{name}" class="card-btn{selected_class}" onclick=" |
| document.querySelectorAll('#drum-grid .card-btn').forEach(btn => btn.classList.remove('selected')); |
| this.classList.add('selected'); |
| const drumName = this.getAttribute('data-drum'); |
| const gradioApp = document.querySelector('gradio-app').shadowRoot || document; |
| const drumInput = gradioApp.querySelector('#drum_state_input'); |
| if(drumInput) drumInput.value = drumName; |
| drumInput.dispatchEvent(new Event('input', {{bubbles: true}})); |
| "> |
| <div class="card-title">{name}</div> |
| <div class="card-sub">drum kit</div> |
| </div> |
| ''' |
| html += '</div>' |
| return html |
| |
| drum_grid_html.value = build_drum_grid("None") |
| drum_state_input = gr.Textbox(visible=False, elem_id="drum_state_input") |
| drum_state_input.change(fn=lambda x: x, inputs=[drum_state_input], outputs=[drum_state]) |
| |
| |
| tab_select = gr.State(value=0) |
| with gr.Tabs(): |
| with gr.TabItem("custom prompt") as tab1: |
| input_bpm = gr.Slider(label="BPM (0=auto)", minimum=0, maximum=255, step=1, value=0) |
| input_time_sig = gr.Radio(label="time signature", choices=["auto","4/4","2/4","3/4","6/4","7/4","2/2","3/2","4/2","3/8","5/8","6/8","7/8","9/8","12/8"], value="auto") |
| input_key_sig = gr.Dropdown(label="key signature", choices=["auto"] + key_signatures, value="auto") |
| with gr.TabItem("midi prompt") as tab2: |
| input_midi = gr.File(label="input midi", file_types=[".midi",".mid"], type="binary") |
| input_midi_events = gr.Slider(label="use first n events", minimum=1, maximum=512, step=1, value=128) |
| input_reduce_cc_st = gr.Checkbox(label="reduce cc & tempo events", value=True) |
| input_remap_track_channel = gr.Checkbox(label="remap tracks/channels", value=True) |
| input_add_default_instr = gr.Checkbox(label="add default instrument", value=True) |
| input_remove_empty_channels = gr.Checkbox(label="remove empty channels", value=False) |
| with gr.TabItem("last output prompt") as tab3: |
| gr.Markdown("Continue from last output") |
| input_continuation_select = gr.Radio(label="select output", choices=["all"]+[f"output{i+1}" for i in range(OUTPUT_BATCH_SIZE)], value="all") |
| undo_btn = gr.Button("Undo last continuation", elem_classes="undo-btn") |
| |
| tab1.select(lambda:0, None, tab_select, queue=False) |
| tab2.select(lambda:1, None, tab_select, queue=False) |
| tab3.select(lambda:2, None, tab_select, queue=False) |
|
|
| input_seed = gr.Slider(label="seed", minimum=0, maximum=2**31-1, step=1, value=0) |
| input_seed_rand = gr.Checkbox(label="random seed", value=True) |
| input_gen_events = gr.Slider(label="generate max events", minimum=1, maximum=opt.max_gen, step=1, value=opt.max_gen//2) |
| |
| with gr.Accordion("options", open=False): |
| input_temp = gr.Slider(label="temperature", minimum=0.1, maximum=1.2, step=0.01, value=1) |
| input_top_p = gr.Slider(label="top p", minimum=0.1, maximum=1, step=0.01, value=0.95) |
| input_top_k = gr.Slider(label="top k", minimum=1, maximum=128, step=1, value=20) |
| input_allow_cc = gr.Checkbox(label="allow midi cc", value=True) |
| input_render_audio = gr.Checkbox(label="render audio after generation", value=True) |
| |
| gr.HTML('<div class="warning-card"><div style="color:#f5a623;font-weight:800;">✨ PETUNJUK GENERASI ✨</div><div style="color:#855d1a;font-size:11px;">Pilih model, instrumen (bisa banyak), drum kit, lalu klik tombol hijau.</div></div>') |
| |
| run_btn = gr.Button("🫒 GENERATE MUSIC 🫒", elem_classes="gen-btn") |
| output_midi_seq = gr.State() |
| output_continuation_state = gr.State([0]) |
| midi_outputs = [] |
| audio_outputs = [] |
| |
| with gr.Tabs(elem_id="output_tabs"): |
| for i in range(OUTPUT_BATCH_SIZE): |
| with gr.TabItem(f"output {i+1}"): |
| gr.HTML(elem_id=f"midi_visualizer_container_{i}") |
| audio_outputs.append(gr.Audio(label="output audio", format="mp3", elem_id=f"midi_audio_{i}")) |
| midi_outputs.append(gr.File(label="output midi", file_types=[".mid"])) |
| |
| |
| gr.HTML(""" |
| <div class="diary-container"> |
| <div class="diary-text">SYSTEM WAKABA</div> |
| <div class="gif-wrapper"> |
| <img src="https://huggingface.co/spaces/Plana-Archive/MOE-TTS/resolve/main/kurumi-tokisaki.gif" alt="aesthetic gif"> |
| </div> |
| <div style="font-size: 12px; color:#9bb7a8; margin-top: 8px;">every melody tells a story</div> |
| </div> |
| """) |
| |
| |
| run_event = run_btn.click( |
| run, |
| [model_state, tab_select, output_midi_seq, output_continuation_state, input_continuation_select, |
| instr_state, drum_state, input_bpm, input_time_sig, input_key_sig, |
| input_midi, input_midi_events, input_reduce_cc_st, input_remap_track_channel, |
| input_add_default_instr, input_remove_empty_channels, input_seed, input_seed_rand, |
| input_gen_events, input_temp, input_top_p, input_top_k, input_allow_cc], |
| [output_midi_seq, output_continuation_state, input_seed, js_msg], |
| queue=True |
| ) |
| |
| finish_run_event = run_event.then( |
| finish_run, |
| [model_state, output_midi_seq], |
| midi_outputs + [js_msg], |
| queue=False |
| ) |
| finish_run_event.then( |
| render_audio, |
| [model_state, output_midi_seq, input_render_audio], |
| audio_outputs, |
| queue=False |
| ) |
| |
| undo_btn.click( |
| undo_continuation, |
| [model_state, output_midi_seq, output_continuation_state], |
| [output_midi_seq, output_continuation_state, js_msg], |
| queue=False |
| ) |
| |
| gr.HTML('<div class="credit-footer">🍏 CREATED BY MUTSUMI 🍏</div>') |
|
|
| app.queue(default_concurrency_limit=2).launch(server_port=opt.port, share=opt.share, ssr_mode=False) |
| thread_pool.shutdown() |