Spaces:
Running on Zero
Running on Zero
| """ | |
| Virtual MIDI Keyboard - Gradio Application | |
| A browser-based MIDI keyboard that can: | |
| - Play notes with various synthesizer sounds | |
| - Record MIDI events with timestamps | |
| - Export recordings as .mid files | |
| - Support computer keyboard input | |
| - Monitor MIDI events in real-time | |
| """ | |
| import base64 | |
| import json | |
| import os | |
| import re | |
| from threading import Thread | |
| import traceback | |
| import gradio as gr | |
| from huggingface_hub import login | |
| from config import INSTRUMENTS, KEYBOARD_KEYS, KEYBOARD_SHORTCUTS | |
| from midi import events_to_midbytes | |
| from midi_model import preload_godzilla_assets, preload_godzilla_model | |
| from engines import EngineRegistry | |
| import spaces | |
| # ============================================================================= | |
| # API ENDPOINTS | |
| # ============================================================================= | |
| def save_midi_api(events): | |
| """Export recorded MIDI events to .mid file""" | |
| if not isinstance(events, list) or len(events) == 0: | |
| return {"error": "No events provided"} | |
| mid_bytes = events_to_midbytes(events) | |
| midi_b64 = base64.b64encode(mid_bytes).decode("ascii") | |
| return {"midi_base64": midi_b64} | |
| def _parse_json_payload(payload_text: str | None, default): | |
| if payload_text is None or payload_text == "": | |
| return default | |
| try: | |
| return json.loads(payload_text) | |
| except json.JSONDecodeError: | |
| return default | |
| def get_config(): | |
| """Provide frontend with instruments and keyboard layout""" | |
| return { | |
| "instruments": INSTRUMENTS, | |
| "keyboard_keys": KEYBOARD_KEYS, | |
| "keyboard_shortcuts": KEYBOARD_SHORTCUTS, | |
| "engines": [ | |
| {"id": engine_id, "name": EngineRegistry.get_engine_info(engine_id)["name"]} | |
| for engine_id in EngineRegistry.list_engines() | |
| ], | |
| } | |
| def process_with_engine( | |
| engine_id: str, | |
| events: list, | |
| options: dict | None = None, | |
| request: "gr.Request | None" = None, | |
| device: str = "auto", | |
| ): | |
| """Process MIDI events through selected engine""" | |
| if not engine_id or not events: | |
| return {"error": "Missing engine_id or events"} | |
| x_ip_token = ( | |
| request.headers.get("x-ip-token") | |
| if request is not None and hasattr(request, "headers") | |
| else None | |
| ) | |
| print( | |
| "process_engine auth:", | |
| { | |
| "engine_id": engine_id, | |
| "has_x_ip_token": bool(x_ip_token), | |
| }, | |
| ) | |
| try: | |
| engine = EngineRegistry.get_engine(engine_id) | |
| processed = engine.process( | |
| events, | |
| options=options, | |
| request=request, | |
| device=device, | |
| ) | |
| return {"success": True, "events": processed} | |
| except ValueError as e: | |
| traceback.print_exc() | |
| return {"error": str(e)} | |
| except Exception as e: | |
| traceback.print_exc() | |
| return {"error": f"Processing error: {str(e)}"} | |
| def process_engine_payload( | |
| payload: dict, | |
| request: "gr.Request | None" = None, | |
| device: str = "auto", | |
| ): | |
| if not isinstance(payload, dict): | |
| return {"error": "Invalid payload"} | |
| return process_with_engine( | |
| payload.get("engine_id"), | |
| payload.get("events", []), | |
| options=payload.get("options"), | |
| request=request, | |
| device=device, | |
| ) | |
| def config_event_bridge(_payload_text: str) -> str: | |
| return json.dumps(get_config()) | |
| def save_midi_event_bridge(payload_text: str) -> str: | |
| events = _parse_json_payload(payload_text, []) | |
| result = save_midi_api(events) | |
| return json.dumps(result) | |
| def process_engine_event_bridge_cpu( | |
| payload_text: str, | |
| request: "gr.Request | None" = None, | |
| ) -> str: | |
| payload = _parse_json_payload(payload_text, {}) | |
| result = process_engine_payload(payload, request=request, device="cpu") | |
| return json.dumps(result) | |
| def process_engine_event_bridge_gpu( | |
| payload_text: str, | |
| request: "gr.Request | None" = None, | |
| ) -> str: | |
| payload = _parse_json_payload(payload_text, {}) | |
| result = process_engine_payload(payload, request=request, device="cuda") | |
| return json.dumps(result) | |
| def start_background_preload() -> None: | |
| def _run() -> None: | |
| try: | |
| checkpoint_path = preload_godzilla_assets() | |
| print(f"Godzilla assets preloaded: {checkpoint_path}") | |
| model_info = preload_godzilla_model(device="cpu") | |
| print(f"Godzilla model warmed in memory: {model_info}") | |
| except Exception: | |
| print("Godzilla preload failed:") | |
| traceback.print_exc() | |
| Thread(target=_run, daemon=True).start() | |
| def login_huggingface_from_env() -> None: | |
| """Authenticate with Hugging Face if HF_TOKEN is available.""" | |
| token = os.environ.get("HF_TOKEN") | |
| if not token: | |
| print("HF_TOKEN not set; skipping huggingface_hub.login()") | |
| return | |
| try: | |
| login(token=token, add_to_git_credential=False) | |
| print("Authenticated with Hugging Face using HF_TOKEN") | |
| except Exception: | |
| print("huggingface_hub login failed:") | |
| traceback.print_exc() | |
| # ============================================================================= | |
| # GRADIO UI | |
| # ============================================================================= | |
| login_huggingface_from_env() | |
| start_background_preload() | |
| # Load HTML and CSS | |
| with open("keyboard.html", "r", encoding="utf-8") as f: | |
| html_content = f.read() | |
| with open("static/styles.css", "r", encoding="utf-8") as f: | |
| css_content = f.read() | |
| with open("static/keyboard.js", "r", encoding="utf-8") as f: | |
| js_content = f.read() | |
| body_match = re.search(r"<body[^>]*>(.*)</body>", html_content, flags=re.IGNORECASE | re.DOTALL) | |
| keyboard_markup = body_match.group(1) if body_match else html_content | |
| keyboard_markup = re.sub(r"<script\b[^>]*>.*?</script>", "", keyboard_markup, flags=re.IGNORECASE | re.DOTALL) | |
| keyboard_markup = re.sub(r"<link\b[^>]*>", "", keyboard_markup, flags=re.IGNORECASE) | |
| # Make logo rendering robust by embedding local repo logo bytes directly. | |
| logo_path = "synthia_logo.png" | |
| if os.path.exists(logo_path): | |
| try: | |
| with open(logo_path, "rb") as logo_file: | |
| logo_b64 = base64.b64encode(logo_file.read()).decode("ascii") | |
| keyboard_markup = keyboard_markup.replace( | |
| 'src="/file=synthia_logo.png"', | |
| f'src="data:image/png;base64,{logo_b64}"', | |
| ) | |
| except Exception: | |
| print("Failed to embed synthia_logo.png; keeping original src path.") | |
| traceback.print_exc() | |
| else: | |
| print("synthia_logo.png not found; logo image may not render.") | |
| hidden_bridge_css = "\n.vk-hidden { display: none !important; }\n" | |
| head_html = "\n".join( | |
| [ | |
| '<script src="https://unpkg.com/tone@next/build/Tone.js"></script>', | |
| f"<script>{js_content}</script>", | |
| ] | |
| ) | |
| # Create Gradio app | |
| with gr.Blocks(title="Virtual MIDI Keyboard", css=css_content + hidden_bridge_css, head=head_html) as demo: | |
| gr.HTML(keyboard_markup) | |
| # Hidden bridges for direct Gradio event calls from frontend JS | |
| with gr.Group(elem_classes=["vk-hidden"]): | |
| config_input = gr.Textbox(value="{}", elem_id="vk_config_input", show_label=False) | |
| config_output = gr.Textbox(elem_id="vk_config_output", show_label=False) | |
| config_btn = gr.Button("get_config", elem_id="vk_config_btn") | |
| config_btn.click( | |
| fn=config_event_bridge, | |
| inputs=config_input, | |
| outputs=config_output, | |
| ) | |
| midi_input = gr.Textbox(value="[]", elem_id="vk_save_input", show_label=False) | |
| midi_output = gr.Textbox(elem_id="vk_save_output", show_label=False) | |
| midi_btn = gr.Button("save_midi", elem_id="vk_save_btn") | |
| midi_btn.click( | |
| fn=save_midi_event_bridge, | |
| inputs=midi_input, | |
| outputs=midi_output, | |
| ) | |
| engine_input = gr.Textbox(value="{}", elem_id="vk_engine_input", show_label=False) | |
| engine_cpu_output = gr.Textbox(elem_id="vk_engine_cpu_output", show_label=False) | |
| engine_cpu_btn = gr.Button("process_engine_cpu", elem_id="vk_engine_cpu_btn") | |
| engine_cpu_btn.click( | |
| fn=process_engine_event_bridge_cpu, | |
| inputs=engine_input, | |
| outputs=engine_cpu_output, | |
| ) | |
| engine_gpu_output = gr.Textbox(elem_id="vk_engine_gpu_output", show_label=False) | |
| engine_gpu_btn = gr.Button("process_engine_gpu", elem_id="vk_engine_gpu_btn") | |
| engine_gpu_btn.click( | |
| fn=process_engine_event_bridge_gpu, | |
| inputs=engine_input, | |
| outputs=engine_gpu_output, | |
| ) | |
| # ============================================================================= | |
| # MAIN | |
| # ============================================================================= | |
| if __name__ == "__main__": | |
| demo.launch() | |