Spaces:
Running
Running
File size: 8,813 Bytes
c427c8f 055747e c427c8f 5b51485 cee0097 5b51485 cee0097 c427c8f 055747e cee0097 055747e cee0097 5b51485 c427c8f 055747e c427c8f 5b51485 055747e 5b51485 cee0097 055747e 43a0d3f cee0097 055747e 43a0d3f cee0097 43a0d3f cee0097 43a0d3f cee0097 43a0d3f cee0097 43a0d3f cee0097 c427c8f 055747e c427c8f cee0097 055747e c427c8f cee0097 5b51485 cee0097 5b51485 055747e cee0097 055747e cee0097 055747e 43a0d3f cee0097 055747e cee0097 055747e 43a0d3f cee0097 43a0d3f cee0097 43a0d3f 5b51485 c427c8f 5b51485 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | """
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)
@spaces.GPU(duration=120)
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()
|