Spaces:
Paused
Paused
Download app.py from launch-calcium/Geode-Music: direct link, hf CLI and curl.
- Browser
- Download file 5.75 kB
-
https://huggingface.co/spaces/launch-calcium/Geode-Music/resolve/main/app.py
- Command line
-
hf download hf://spaces/launch-calcium/Geode-Music/app.py
-
curl -L -o app.py https://huggingface.co/spaces/launch-calcium/Geode-Music/resolve/main/app.py
5.75 kB
| #!/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"""<?xml version="1.0"?> | |
| <plist version="1.0" gjver="2.0"> | |
| <dict> | |
| <key>kCEK</key><i>4</i> | |
| <key>k2</key><s>{level_name}</s> | |
| <key>k3</key><s>MIDI converted Piano Roll</s> | |
| <key>k4</key><s>{encrypted_k4}</s> | |
| <key>k5</key><s>Player</s> | |
| </dict> | |
| </plist>""" | |
| 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() | |