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()