#!/usr/bin/env python3 """ Gradio app to convert one or more MIDI files into Geometry Dash .gmd files (using midi_to_gd.py). For each uploaded MIDI the app will create a .gmd file (plist wrapper with compressed k4 level string) and return a zip containing all generated .gmd files. """ import os import sys import tempfile import zipfile import traceback from typing import List, Tuple HERE = os.path.dirname(os.path.abspath(__file__)) if HERE not in sys.path: sys.path.insert(0, HERE) try: import gradio as gr except Exception as e: raise RuntimeError("Gradio is required. Install with: pip install gradio") from e # Import functions from the provided script try: from midi_to_gd import ( parse_midi_notes, generate_gd_level_objects, build_full_level_string, encrypt_level_string, ) except Exception as e: raise RuntimeError("Failed to import from midi_to_gd.py. Ensure the file is in the same directory.") from e def _get_local_path_from_gr_file(f) -> str: """ Accepts the various shapes Gradio may pass: - a filesystem path string - a dict like {'name': '/tmp/....'} - a list of the above (we only call this per-file) Returns a single filesystem path string or empty if not found. """ if not f: return "" if isinstance(f, str): return f if isinstance(f, (list, tuple)) and f: # take first element return _get_local_path_from_gr_file(f[0]) if isinstance(f, dict): # Gradio sometimes returns {'name': '/tmp/...'} return f.get("name") or f.get("file") or "" # Fallback: try attribute .name return getattr(f, "name", "") def convert_midis_to_zip(midi_files: List, beats_scale: float = 2.0, name_prefix: str = "") -> Tuple[str, str]: """ Process uploaded midi files (list), generate .gmd files for each, and return a path to a zip archive and a status text message. Returns: (zip_file_path, status_message) """ try: if not midi_files: return None, "No MIDI files uploaded." out_dir = tempfile.mkdtemp(prefix="gd_midis_") created_files = [] # midi_files is expected to be a list of file paths (type="filepath") for idx, f in enumerate(midi_files, start=1): midi_path = _get_local_path_from_gr_file(f) if not os.path.exists(midi_path): return None, f"Uploaded file not found on disk: {midi_path}" base = os.path.splitext(os.path.basename(midi_path))[0] level_name = f"{name_prefix}{'_' if name_prefix else ''}{base}" if name_prefix else base notes = parse_midi_notes(midi_path) raw_objects = generate_gd_level_objects(notes, blocks_per_beat=beats_scale) full_level_data = build_full_level_string(raw_objects) encrypted_k4 = encrypt_level_string(full_level_data) gmd_content = f""" kCEK4 k2{level_name} k3MIDI converted Piano Roll k4{encrypted_k4} k5Player """ out_path = os.path.join(out_dir, f"{level_name}.gmd") with open(out_path, "w", encoding="utf-8") as wf: wf.write(gmd_content) created_files.append(out_path) zip_path = os.path.join(out_dir, "gd_levels.zip") with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: for p in created_files: zf.write(p, arcname=os.path.basename(p)) status_lines = [ f"Generated {len(created_files)} .gmd file(s).", f"ZIP archive: {os.path.basename(zip_path)}", "Files:", ] + [f"- {os.path.basename(p)}" for p in created_files] return zip_path, "\n".join(status_lines) except Exception: tb = traceback.format_exc() return None, f"Error during conversion:\n{tb}" def launch_ui(): with gr.Blocks() as demo: gr.Markdown("# MIDI → Geometry Dash (.gmd) Converter") gr.Markdown( "Upload one or more MIDI files. The app will convert each MIDI into a Geometry Dash .gmd " "file (plist wrapper with compressed level string) and return a ZIP archive containing all generated .gmd files." ) with gr.Row(): # NOTE: type="filepath" is the correct modern API. 'file' is invalid. midi_input = gr.File(label="Upload MIDI files (.mid/.midi)", file_count="multiple", type="filepath") settings_col = gr.Column(scale=1) beats_scale = gr.Number(value=2.0, label="Blocks per beat (blocks/quarter-note)", precision=2) name_prefix = gr.Textbox(label="Optional level name prefix", placeholder="Prefix (optional)") convert_btn = gr.Button("Convert and Download ZIP") output_zip = gr.File(label="Download ZIP of .gmd files") status = gr.Textbox(label="Status", interactive=False, lines=6) def _on_click(files, beats, prefix): zip_path, status_text = convert_midis_to_zip(files, beats, prefix) return zip_path, status_text convert_btn.click(fn=_on_click, inputs=[midi_input, beats_scale, name_prefix], outputs=[output_zip, status]) gr.Markdown("Notes: mido is required to parse MIDI files. Install with `pip install mido`.") gr.Markdown("If the app cannot find your local CCLocalLevels.dat or you just want the .gmd files, use the ZIP to import them with your editor.") demo.launch() if __name__ == "__main__": launch_ui()