Geode-Music / app.py
launch-calcium's picture
Update app.py
52ad754 verified
Raw History Blame Contribute Delete
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()