ZeroGPU support, Game loop, UI updates

#1
by karlhajal - opened
Files changed (11) hide show
  1. .gitattributes +2 -0
  2. README.md +4 -3
  3. app.py +176 -41
  4. engines.py +53 -8
  5. keyboard.html +111 -50
  6. midi_model.py +613 -64
  7. pyproject.toml +1 -1
  8. requirements.txt +2 -1
  9. static/keyboard.js +1190 -248
  10. static/styles.css +606 -302
  11. synthia_logo.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ Synthia[[:space:]]Logo[[:space:]]v2.png filter=lfs diff=lfs merge=lfs -text
37
+ synthia_logo.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🎹
4
  colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.5.1
8
  app_file: app.py
9
  pinned: false
10
  short_description: Browser-based MIDI keyboard with recording and synthesis
@@ -61,14 +61,15 @@ git remote add hf git@hf.co:spaces/YOUR_USERNAME/synthia
61
  git push hf main
62
  ```
63
 
 
 
64
  ## 🔧 Technology
65
 
66
  - **Frontend**: Tone.js v6+ (Web Audio API)
67
- - **Backend**: Gradio 6.x + Python 3.10+
68
  - **MIDI**: mido library
69
  - **Model**: Godzilla Piano Transformer (via Hugging Face)
70
 
71
  ## 📝 License
72
 
73
  Open source - free to use and modify.
74
-
 
4
  colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
  short_description: Browser-based MIDI keyboard with recording and synthesis
 
61
  git push hf main
62
  ```
63
 
64
+ Current configuration runs Godzilla generation on CPU for reliability.
65
+
66
  ## 🔧 Technology
67
 
68
  - **Frontend**: Tone.js v6+ (Web Audio API)
69
+ - **Backend**: Gradio 5.x + Python 3.10+
70
  - **MIDI**: mido library
71
  - **Model**: Godzilla Piano Transformer (via Hugging Face)
72
 
73
  ## 📝 License
74
 
75
  Open source - free to use and modify.
 
app.py CHANGED
@@ -10,12 +10,19 @@ A browser-based MIDI keyboard that can:
10
  """
11
 
12
  import base64
13
- import html
 
 
 
 
14
  import gradio as gr
 
15
 
16
  from config import INSTRUMENTS, KEYBOARD_KEYS, KEYBOARD_SHORTCUTS
17
  from midi import events_to_midbytes
 
18
  from engines import EngineRegistry
 
19
 
20
 
21
  # =============================================================================
@@ -33,6 +40,15 @@ def save_midi_api(events):
33
  return {"midi_base64": midi_b64}
34
 
35
 
 
 
 
 
 
 
 
 
 
36
  def get_config():
37
  """Provide frontend with instruments and keyboard layout"""
38
  return {
@@ -45,26 +61,128 @@ def get_config():
45
  ],
46
  }
47
 
48
-
49
- def process_with_engine(engine_id: str, events: list):
 
 
 
 
 
50
  """Process MIDI events through selected engine"""
51
  if not engine_id or not events:
52
  return {"error": "Missing engine_id or events"}
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  try:
55
  engine = EngineRegistry.get_engine(engine_id)
56
- processed = engine.process(events)
 
 
 
 
 
57
  return {"success": True, "events": processed}
58
  except ValueError as e:
 
59
  return {"error": str(e)}
60
  except Exception as e:
 
61
  return {"error": f"Processing error: {str(e)}"}
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  # =============================================================================
65
  # GRADIO UI
66
  # =============================================================================
67
 
 
 
 
68
  # Load HTML and CSS
69
  with open("keyboard.html", "r", encoding="utf-8") as f:
70
  html_content = f.read()
@@ -75,58 +193,75 @@ with open("static/styles.css", "r", encoding="utf-8") as f:
75
  with open("static/keyboard.js", "r", encoding="utf-8") as f:
76
  js_content = f.read()
77
 
78
- # Inject CSS and JS into HTML
79
- keyboard_html = html_content.replace(
80
- '<link rel="stylesheet" href="/file=static/styles.css" />',
81
- f"<style>{css_content}</style>",
82
- ).replace(
83
- '<script src="/file=static/keyboard.js"></script>', f"<script>{js_content}</script>"
84
- )
85
 
86
- iframe_html = (
87
- '<iframe srcdoc="'
88
- + html.escape(keyboard_html, quote=True)
89
- + '" style="width:100%;height:750px;border:0;"></iframe>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  )
91
 
92
  # Create Gradio app
93
- with gr.Blocks(title="Virtual MIDI Keyboard") as demo:
94
- gr.HTML(iframe_html)
95
-
96
- # Hidden config API
97
- with gr.Group(visible=False):
98
- config_input = gr.Textbox(label="_")
99
- config_output = gr.JSON(label="_")
100
- config_btn = gr.Button("get_config", visible=False)
101
  config_btn.click(
102
- fn=lambda x: get_config(),
103
  inputs=config_input,
104
  outputs=config_output,
105
- api_name="config",
106
  )
107
 
108
- # MIDI save API
109
- midi_input = gr.JSON(label="_")
110
- midi_output = gr.JSON(label="_")
111
- midi_btn = gr.Button("save_midi", visible=False)
112
  midi_btn.click(
113
- fn=save_midi_api,
114
  inputs=midi_input,
115
  outputs=midi_output,
116
- api_name="save_midi",
117
  )
118
 
119
- # Engine processing API
120
- engine_input = gr.JSON(label="_")
121
- engine_output = gr.JSON(label="_")
122
- engine_btn = gr.Button("process_engine", visible=False)
123
- engine_btn.click(
124
- fn=lambda payload: process_with_engine(
125
- payload.get("engine_id"), payload.get("events", [])
126
- ),
 
 
 
 
 
 
127
  inputs=engine_input,
128
- outputs=engine_output,
129
- api_name="process_engine",
130
  )
131
 
132
 
 
10
  """
11
 
12
  import base64
13
+ import json
14
+ import os
15
+ import re
16
+ from threading import Thread
17
+ import traceback
18
  import gradio as gr
19
+ from huggingface_hub import login
20
 
21
  from config import INSTRUMENTS, KEYBOARD_KEYS, KEYBOARD_SHORTCUTS
22
  from midi import events_to_midbytes
23
+ from midi_model import preload_godzilla_assets, preload_godzilla_model
24
  from engines import EngineRegistry
25
+ import spaces
26
 
27
 
28
  # =============================================================================
 
40
  return {"midi_base64": midi_b64}
41
 
42
 
43
+ def _parse_json_payload(payload_text: str | None, default):
44
+ if payload_text is None or payload_text == "":
45
+ return default
46
+ try:
47
+ return json.loads(payload_text)
48
+ except json.JSONDecodeError:
49
+ return default
50
+
51
+
52
  def get_config():
53
  """Provide frontend with instruments and keyboard layout"""
54
  return {
 
61
  ],
62
  }
63
 
64
+ def process_with_engine(
65
+ engine_id: str,
66
+ events: list,
67
+ options: dict | None = None,
68
+ request: "gr.Request | None" = None,
69
+ device: str = "auto",
70
+ ):
71
  """Process MIDI events through selected engine"""
72
  if not engine_id or not events:
73
  return {"error": "Missing engine_id or events"}
74
 
75
+ x_ip_token = (
76
+ request.headers.get("x-ip-token")
77
+ if request is not None and hasattr(request, "headers")
78
+ else None
79
+ )
80
+ print(
81
+ "process_engine auth:",
82
+ {
83
+ "engine_id": engine_id,
84
+ "has_x_ip_token": bool(x_ip_token),
85
+ },
86
+ )
87
+
88
  try:
89
  engine = EngineRegistry.get_engine(engine_id)
90
+ processed = engine.process(
91
+ events,
92
+ options=options,
93
+ request=request,
94
+ device=device,
95
+ )
96
  return {"success": True, "events": processed}
97
  except ValueError as e:
98
+ traceback.print_exc()
99
  return {"error": str(e)}
100
  except Exception as e:
101
+ traceback.print_exc()
102
  return {"error": f"Processing error: {str(e)}"}
103
 
104
 
105
+ def process_engine_payload(
106
+ payload: dict,
107
+ request: "gr.Request | None" = None,
108
+ device: str = "auto",
109
+ ):
110
+ if not isinstance(payload, dict):
111
+ return {"error": "Invalid payload"}
112
+ return process_with_engine(
113
+ payload.get("engine_id"),
114
+ payload.get("events", []),
115
+ options=payload.get("options"),
116
+ request=request,
117
+ device=device,
118
+ )
119
+
120
+
121
+ def config_event_bridge(_payload_text: str) -> str:
122
+ return json.dumps(get_config())
123
+
124
+
125
+ def save_midi_event_bridge(payload_text: str) -> str:
126
+ events = _parse_json_payload(payload_text, [])
127
+ result = save_midi_api(events)
128
+ return json.dumps(result)
129
+
130
+
131
+ def process_engine_event_bridge_cpu(
132
+ payload_text: str,
133
+ request: "gr.Request | None" = None,
134
+ ) -> str:
135
+ payload = _parse_json_payload(payload_text, {})
136
+ result = process_engine_payload(payload, request=request, device="cpu")
137
+ return json.dumps(result)
138
+
139
+
140
+ @spaces.GPU(duration=120)
141
+ def process_engine_event_bridge_gpu(
142
+ payload_text: str,
143
+ request: "gr.Request | None" = None,
144
+ ) -> str:
145
+ payload = _parse_json_payload(payload_text, {})
146
+ result = process_engine_payload(payload, request=request, device="cuda")
147
+ return json.dumps(result)
148
+
149
+
150
+ def start_background_preload() -> None:
151
+ def _run() -> None:
152
+ try:
153
+ checkpoint_path = preload_godzilla_assets()
154
+ print(f"Godzilla assets preloaded: {checkpoint_path}")
155
+ model_info = preload_godzilla_model(device="cpu")
156
+ print(f"Godzilla model warmed in memory: {model_info}")
157
+ except Exception:
158
+ print("Godzilla preload failed:")
159
+ traceback.print_exc()
160
+
161
+ Thread(target=_run, daemon=True).start()
162
+
163
+
164
+ def login_huggingface_from_env() -> None:
165
+ """Authenticate with Hugging Face if HF_TOKEN is available."""
166
+ token = os.environ.get("HF_TOKEN")
167
+ if not token:
168
+ print("HF_TOKEN not set; skipping huggingface_hub.login()")
169
+ return
170
+
171
+ try:
172
+ login(token=token, add_to_git_credential=False)
173
+ print("Authenticated with Hugging Face using HF_TOKEN")
174
+ except Exception:
175
+ print("huggingface_hub login failed:")
176
+ traceback.print_exc()
177
+
178
+
179
  # =============================================================================
180
  # GRADIO UI
181
  # =============================================================================
182
 
183
+ login_huggingface_from_env()
184
+ start_background_preload()
185
+
186
  # Load HTML and CSS
187
  with open("keyboard.html", "r", encoding="utf-8") as f:
188
  html_content = f.read()
 
193
  with open("static/keyboard.js", "r", encoding="utf-8") as f:
194
  js_content = f.read()
195
 
196
+ body_match = re.search(r"<body[^>]*>(.*)</body>", html_content, flags=re.IGNORECASE | re.DOTALL)
197
+ keyboard_markup = body_match.group(1) if body_match else html_content
198
+ keyboard_markup = re.sub(r"<script\b[^>]*>.*?</script>", "", keyboard_markup, flags=re.IGNORECASE | re.DOTALL)
199
+ keyboard_markup = re.sub(r"<link\b[^>]*>", "", keyboard_markup, flags=re.IGNORECASE)
 
 
 
200
 
201
+ # Make logo rendering robust by embedding local repo logo bytes directly.
202
+ logo_path = "synthia_logo.png"
203
+ if os.path.exists(logo_path):
204
+ try:
205
+ with open(logo_path, "rb") as logo_file:
206
+ logo_b64 = base64.b64encode(logo_file.read()).decode("ascii")
207
+ keyboard_markup = keyboard_markup.replace(
208
+ 'src="/file=synthia_logo.png"',
209
+ f'src="data:image/png;base64,{logo_b64}"',
210
+ )
211
+ except Exception:
212
+ print("Failed to embed synthia_logo.png; keeping original src path.")
213
+ traceback.print_exc()
214
+ else:
215
+ print("synthia_logo.png not found; logo image may not render.")
216
+
217
+ hidden_bridge_css = "\n.vk-hidden { display: none !important; }\n"
218
+ head_html = "\n".join(
219
+ [
220
+ '<script src="https://unpkg.com/tone@next/build/Tone.js"></script>',
221
+ f"<script>{js_content}</script>",
222
+ ]
223
  )
224
 
225
  # Create Gradio app
226
+ with gr.Blocks(title="Virtual MIDI Keyboard", css=css_content + hidden_bridge_css, head=head_html) as demo:
227
+ gr.HTML(keyboard_markup)
228
+
229
+ # Hidden bridges for direct Gradio event calls from frontend JS
230
+ with gr.Group(elem_classes=["vk-hidden"]):
231
+ config_input = gr.Textbox(value="{}", elem_id="vk_config_input", show_label=False)
232
+ config_output = gr.Textbox(elem_id="vk_config_output", show_label=False)
233
+ config_btn = gr.Button("get_config", elem_id="vk_config_btn")
234
  config_btn.click(
235
+ fn=config_event_bridge,
236
  inputs=config_input,
237
  outputs=config_output,
 
238
  )
239
 
240
+ midi_input = gr.Textbox(value="[]", elem_id="vk_save_input", show_label=False)
241
+ midi_output = gr.Textbox(elem_id="vk_save_output", show_label=False)
242
+ midi_btn = gr.Button("save_midi", elem_id="vk_save_btn")
 
243
  midi_btn.click(
244
+ fn=save_midi_event_bridge,
245
  inputs=midi_input,
246
  outputs=midi_output,
 
247
  )
248
 
249
+ engine_input = gr.Textbox(value="{}", elem_id="vk_engine_input", show_label=False)
250
+
251
+ engine_cpu_output = gr.Textbox(elem_id="vk_engine_cpu_output", show_label=False)
252
+ engine_cpu_btn = gr.Button("process_engine_cpu", elem_id="vk_engine_cpu_btn")
253
+ engine_cpu_btn.click(
254
+ fn=process_engine_event_bridge_cpu,
255
+ inputs=engine_input,
256
+ outputs=engine_cpu_output,
257
+ )
258
+
259
+ engine_gpu_output = gr.Textbox(elem_id="vk_engine_gpu_output", show_label=False)
260
+ engine_gpu_btn = gr.Button("process_engine_gpu", elem_id="vk_engine_gpu_btn")
261
+ engine_gpu_btn.click(
262
+ fn=process_engine_event_bridge_gpu,
263
  inputs=engine_input,
264
+ outputs=engine_gpu_output,
 
265
  )
266
 
267
 
engines.py CHANGED
@@ -8,7 +8,7 @@ from typing import List, Dict, Any
8
 
9
  from midi_model import (
10
  count_out_of_range_events,
11
- filter_events_to_keyboard_range,
12
  get_model,
13
  )
14
 
@@ -28,7 +28,13 @@ class ParrotEngine:
28
  def __init__(self):
29
  self.name = "Parrot"
30
 
31
- def process(self, events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
 
 
 
 
 
 
32
  """Return events unchanged"""
33
  if not events:
34
  return []
@@ -60,7 +66,13 @@ class ReverseParrotEngine:
60
  def __init__(self):
61
  self.name = "Reverse Parrot"
62
 
63
- def process(self, events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
 
 
 
 
 
 
64
  """Reverse the sequence of note numbers while keeping timing and event types"""
65
  if not events:
66
  return []
@@ -125,20 +137,53 @@ class GodzillaContinuationEngine:
125
  self.name = "Godzilla"
126
  self.generate_tokens = generate_tokens
127
 
128
- def process(self, events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
 
 
 
 
 
 
129
  if not events:
130
  return []
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  model = get_model("godzilla")
133
  new_events = model.generate_continuation(
134
  events,
135
- tokens=self.generate_tokens,
136
- seed=None,
 
 
 
 
 
137
  )
138
  out_of_range = count_out_of_range_events(new_events)
139
  if out_of_range:
140
- print(f"Godzilla: dropped {out_of_range} out-of-range events")
141
- return filter_events_to_keyboard_range(new_events)
142
 
143
 
144
  # =============================================================================
 
8
 
9
  from midi_model import (
10
  count_out_of_range_events,
11
+ fold_events_to_keyboard_range,
12
  get_model,
13
  )
14
 
 
28
  def __init__(self):
29
  self.name = "Parrot"
30
 
31
+ def process(
32
+ self,
33
+ events: List[Dict[str, Any]],
34
+ options: Dict[str, Any] | None = None,
35
+ request: Any | None = None,
36
+ device: str = "auto",
37
+ ) -> List[Dict[str, Any]]:
38
  """Return events unchanged"""
39
  if not events:
40
  return []
 
66
  def __init__(self):
67
  self.name = "Reverse Parrot"
68
 
69
+ def process(
70
+ self,
71
+ events: List[Dict[str, Any]],
72
+ options: Dict[str, Any] | None = None,
73
+ request: Any | None = None,
74
+ device: str = "auto",
75
+ ) -> List[Dict[str, Any]]:
76
  """Reverse the sequence of note numbers while keeping timing and event types"""
77
  if not events:
78
  return []
 
137
  self.name = "Godzilla"
138
  self.generate_tokens = generate_tokens
139
 
140
+ def process(
141
+ self,
142
+ events: List[Dict[str, Any]],
143
+ options: Dict[str, Any] | None = None,
144
+ request: Any | None = None,
145
+ device: str = "auto",
146
+ ) -> List[Dict[str, Any]]:
147
  if not events:
148
  return []
149
 
150
+ generate_tokens = self.generate_tokens
151
+ seed = None
152
+ temperature = 0.9
153
+ top_p = 0.95
154
+ num_candidates = 3
155
+ if isinstance(options, dict):
156
+ requested_tokens = options.get("generate_tokens")
157
+ if isinstance(requested_tokens, int):
158
+ generate_tokens = max(8, min(256, requested_tokens))
159
+ requested_seed = options.get("seed")
160
+ if isinstance(requested_seed, int):
161
+ seed = requested_seed
162
+ requested_temperature = options.get("temperature")
163
+ if isinstance(requested_temperature, (int, float)):
164
+ temperature = max(0.2, min(1.5, float(requested_temperature)))
165
+ requested_top_p = options.get("top_p")
166
+ if isinstance(requested_top_p, (int, float)):
167
+ top_p = max(0.5, min(0.99, float(requested_top_p)))
168
+ requested_candidates = options.get("num_candidates")
169
+ if isinstance(requested_candidates, int):
170
+ num_candidates = max(1, min(6, requested_candidates))
171
+
172
  model = get_model("godzilla")
173
  new_events = model.generate_continuation(
174
  events,
175
+ tokens=generate_tokens,
176
+ seed=seed,
177
+ temperature=temperature,
178
+ top_p=top_p,
179
+ num_candidates=num_candidates,
180
+ request=request,
181
+ device=device,
182
  )
183
  out_of_range = count_out_of_range_events(new_events)
184
  if out_of_range:
185
+ print(f"Godzilla: remapped {out_of_range} out-of-range events by octave folding")
186
+ return fold_events_to_keyboard_range(new_events)
187
 
188
 
189
  # =============================================================================
keyboard.html CHANGED
@@ -7,60 +7,121 @@
7
  <link rel="stylesheet" href="/file=static/styles.css" />
8
  </head>
9
  <body>
10
- <!-- Welcome Header -->
11
- <div class="welcome-header">
12
- <h1 class="neon-text">SYNTH<i>IA</i></h1>
13
- </div>
14
-
15
- <div id="mainContainer">
16
- <!-- Keyboard Section -->
17
- <div class="keyboard-section">
18
- <div id="keyboard"></div>
19
-
20
- <div class="controls">
21
- <label>
22
- Instrument:
23
- <select id="instrumentSelect">
24
- <option value="synth">Synth</option>
25
- <option value="piano">Piano</option>
26
- <option value="organ">Organ</option>
27
- <option value="bass">Bass</option>
28
- <option value="pluck">Pluck</option>
29
- <option value="fm">FM Synth</option>
30
- </select>
31
- </label>
32
-
33
- <label>
34
- Engine:
35
- <select id="engineSelect">
36
- <option value="parrot">Parrot</option>
37
- <option value="reverse_parrot">Reverse Parrot</option>
38
- <option value="godzilla_continue">Godzilla</option>
39
- </select>
40
- </label>
41
-
42
- <button id="recordBtn">Record</button>
43
- <button id="stopBtn" disabled>Stop</button>
44
- <button id="playbackBtn" disabled>Playback</button>
45
- <button id="saveBtn" disabled>Save MIDI</button>
46
- <button id="panicBtn" style="background: #ff4444; color: white;">Panic 🚨</button>
47
-
48
- <label>
49
- <input type="checkbox" id="keyboardToggle">
50
- Enable Keyboard Input
51
- </label>
52
-
53
- <span id="status">Idle</span>
54
  </div>
55
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- <!-- MIDI Monitor Section -->
58
- <div class="monitor-section">
59
- <div class="terminal-header">
60
- <h4>MIDI MONITOR</h4>
61
- <button id="clearTerminal">Clear</button>
 
62
  </div>
63
- <div id="terminal"></div>
64
  </div>
65
  </div>
66
 
 
7
  <link rel="stylesheet" href="/file=static/styles.css" />
8
  </head>
9
  <body>
10
+ <div class="app-shell">
11
+ <div class="welcome-header">
12
+ <div class="logo-wrap">
13
+ <img
14
+ src="/file=synthia_logo.png"
15
+ alt="SYNTHIA"
16
+ class="logo-image"
17
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  </div>
19
  </div>
20
+
21
+ <div id="mainContainer">
22
+ <div class="keyboard-section card">
23
+ <div id="keyboard"></div>
24
+ <div class="keyboard-toggle-row">
25
+ <label class="keyboard-toggle-pill">
26
+ <input type="checkbox" id="keyboardToggle">
27
+ <span class="toggle-track"></span>
28
+ <span class="toggle-text">Enable Keyboard Input</span>
29
+ </label>
30
+ </div>
31
+
32
+ <div class="controls card">
33
+ <div class="control-grid">
34
+ <label class="control-item">
35
+ Instrument
36
+ <select id="instrumentSelect">
37
+ <option value="synth">Synth</option>
38
+ <option value="piano">Piano</option>
39
+ <option value="organ">Organ</option>
40
+ <option value="bass">Bass</option>
41
+ <option value="pluck">Pluck</option>
42
+ <option value="fm">FM Synth</option>
43
+ </select>
44
+ </label>
45
+
46
+ <label class="control-item">
47
+ AI Voice
48
+ <select id="aiInstrumentSelect">
49
+ <option value="synth">Synth</option>
50
+ <option value="piano">Piano</option>
51
+ <option value="organ">Organ</option>
52
+ <option value="bass">Bass</option>
53
+ <option value="pluck">Pluck</option>
54
+ <option value="fm" selected>FM Synth</option>
55
+ </select>
56
+ </label>
57
+
58
+ <label class="control-item">
59
+ Engine
60
+ <select id="engineSelect">
61
+ <option value="parrot">Parrot</option>
62
+ <option value="reverse_parrot">Reverse Parrot</option>
63
+ <option value="godzilla_continue">Godzilla</option>
64
+ </select>
65
+ </label>
66
+
67
+ <label class="control-item">
68
+ Runtime
69
+ <select id="runtimeSelect">
70
+ <option value="cpu">CPU</option>
71
+ <option value="gpu">ZeroGPU</option>
72
+ <option value="auto" selected>Auto (GPU then CPU)</option>
73
+ </select>
74
+ </label>
75
+
76
+ <label class="control-item">
77
+ AI Style
78
+ <select id="responseStyleSelect">
79
+ <option value="melodic">Melodic</option>
80
+ <option value="motif_echo">Motif Echo</option>
81
+ <option value="playful">Playful</option>
82
+ </select>
83
+ </label>
84
+
85
+ <label class="control-item">
86
+ Response Mode
87
+ <select id="responseModeSelect">
88
+ <option value="raw_godzilla" selected>Raw Godzilla</option>
89
+ <option value="current_pipeline">Current Pipeline</option>
90
+ <option value="musical_polish">Musical Polish</option>
91
+ </select>
92
+ </label>
93
+
94
+ <label class="control-item">
95
+ Response Length
96
+ <select id="responseLengthSelect">
97
+ <option value="short" selected>Short</option>
98
+ <option value="medium">Medium</option>
99
+ <option value="long">Long</option>
100
+ <option value="extended">Extended</option>
101
+ </select>
102
+ </label>
103
+ </div>
104
+
105
+ <div class="action-row">
106
+ <button id="recordBtn" class="btn btn-primary">Record</button>
107
+ <button id="stopBtn" class="btn btn-secondary" disabled>Stop</button>
108
+ <button id="playbackBtn" class="btn btn-secondary" disabled>Playback</button>
109
+ <button id="gameStartBtn" class="btn btn-game">Start Game</button>
110
+ <button id="gameStopBtn" class="btn btn-secondary" disabled>Stop Game</button>
111
+ <button id="saveBtn" class="btn btn-secondary" disabled>Save MIDI</button>
112
+ <button id="panicBtn" class="btn btn-danger">Panic</button>
113
+ <span id="status">Idle</span>
114
+ </div>
115
+ </div>
116
+ </div>
117
 
118
+ <div class="monitor-section card">
119
+ <div class="terminal-header">
120
+ <h4>MIDI MONITOR</h4>
121
+ <button id="clearTerminal" class="btn btn-secondary">Clear</button>
122
+ </div>
123
+ <div id="terminal"></div>
124
  </div>
 
125
  </div>
126
  </div>
127
 
midi_model.py CHANGED
@@ -3,20 +3,37 @@ from __future__ import annotations
3
 
4
  import subprocess
5
  import sys
 
 
 
6
  from dataclasses import dataclass
7
  from pathlib import Path
8
- from typing import Iterable, Optional
9
 
10
  from config import MIDI_DEFAULTS, KEYBOARD_BASE_MIDI, KEYBOARD_OCTAVES
11
 
12
-
13
  DEFAULT_REPO = "asigalov61/Godzilla-Piano-Transformer"
14
  DEFAULT_FILENAME = (
15
- "Godzilla_Piano_Chords_Texturing_Transformer_Trained_Model_22708_steps_"
16
- "0.7515_loss_0.7853_acc.pth"
17
  )
 
 
 
 
 
 
18
 
19
  _MODEL_CACHE: dict[str, object] = {}
 
 
 
 
 
 
 
 
 
20
 
21
 
22
  @dataclass(frozen=True)
@@ -30,6 +47,11 @@ class MidiModel:
30
  *,
31
  tokens: int = 32,
32
  seed: Optional[int] = None,
 
 
 
 
 
33
  ) -> list[dict]:
34
  raise NotImplementedError
35
 
@@ -59,6 +81,99 @@ def ensure_tegridy_tools(base_dir: Path) -> tuple[Path, Path]:
59
  return tools_dir, x_transformer_dir
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def add_sys_path(*paths: Path) -> None:
63
  for path in paths:
64
  path_str = str(path.resolve())
@@ -66,16 +181,23 @@ def add_sys_path(*paths: Path) -> None:
66
  sys.path.insert(0, path_str)
67
 
68
 
69
- def build_model(seq_len: int, pad_idx: int):
 
 
 
 
 
 
 
70
  from x_transformer_2_3_1 import AutoregressiveWrapper, Decoder, TransformerWrapper
71
 
72
  model = TransformerWrapper(
73
  num_tokens=pad_idx + 1,
74
  max_seq_len=seq_len,
75
  attn_layers=Decoder(
76
- dim=2048,
77
- depth=8,
78
- heads=32,
79
  rotary_pos_emb=True,
80
  attn_flash=True,
81
  ),
@@ -91,18 +213,193 @@ def resolve_device(requested: str) -> str:
91
  return requested
92
 
93
 
94
- def load_checkpoint(model, checkpoint_path: Path, device: str) -> None:
95
  import torch
96
 
97
  state = torch.load(checkpoint_path, map_location=device)
98
- model.load_state_dict(state)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
 
101
  def events_to_score_tokens(events: list[dict]) -> list[int]:
102
  if not events:
103
  return []
104
 
105
- active: dict[int, float] = {}
106
  notes: list[tuple[float, float, int]] = []
107
  sorted_events = sorted(events, key=lambda e: e.get("time", 0.0))
108
 
@@ -113,11 +410,11 @@ def events_to_score_tokens(events: list[dict]) -> list[int]:
113
  time_sec = float(event.get("time", 0.0))
114
 
115
  if ev_type == "note_on" and velocity > 0:
116
- active[note] = time_sec
117
  elif ev_type in {"note_off", "note_on"}:
118
- if note in active:
119
- start = active.pop(note)
120
- duration = max(0.0, time_sec - start)
121
  notes.append((start, duration, note))
122
 
123
  if not notes:
@@ -212,14 +509,139 @@ def filter_events_to_keyboard_range(events: list[dict]) -> list[dict]:
212
  ]
213
 
214
 
215
- def build_prime_tokens(score_tokens: list[int], seq_len: int) -> list[int]:
216
- prime = [705, 384, 706]
217
- if score_tokens:
218
- max_score = max(0, seq_len - len(prime))
219
- prime.extend(score_tokens[-max_score:])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  else:
221
- prime.extend([0, 129, 316])
222
- return prime
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
 
225
  def load_model_cached(
@@ -231,28 +653,16 @@ def load_model_cached(
231
  seq_len: int,
232
  pad_idx: int,
233
  device: str,
234
- ) -> tuple[object, str, Path]:
235
- from huggingface_hub import hf_hub_download
236
  import torch
237
 
238
  cache_dir.mkdir(parents=True, exist_ok=True)
239
  resolved_device = resolve_device(device)
240
- cache_key = f"{repo}:{filename}:{seq_len}:{pad_idx}:{resolved_device}"
241
 
242
- if _MODEL_CACHE.get("key") == cache_key:
243
- return (
244
- _MODEL_CACHE["model"],
245
- _MODEL_CACHE["device"],
246
- _MODEL_CACHE["tools_dir"],
247
- )
248
-
249
- checkpoint_path = Path(
250
- hf_hub_download(
251
- repo_id=repo,
252
- filename=filename,
253
- local_dir=str(cache_dir),
254
- repo_type="model",
255
- )
256
  )
257
 
258
  tools_dir, x_transformer_dir = ensure_tegridy_tools(tegridy_dir)
@@ -263,18 +673,91 @@ def load_model_cached(
263
  torch.backends.cuda.matmul.allow_tf32 = True
264
  torch.backends.cudnn.allow_tf32 = True
265
 
266
- model = build_model(seq_len, pad_idx)
267
- load_checkpoint(model, checkpoint_path, resolved_device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  model.to(resolved_device)
269
  model.eval()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
  _MODEL_CACHE["key"] = cache_key
272
  _MODEL_CACHE["model"] = model
273
  _MODEL_CACHE["device"] = resolved_device
274
  _MODEL_CACHE["tools_dir"] = tools_dir
275
  _MODEL_CACHE["checkpoint_path"] = checkpoint_path
 
 
276
 
277
- return model, resolved_device, tools_dir
278
 
279
 
280
  def generate_from_events(
@@ -282,6 +765,9 @@ def generate_from_events(
282
  *,
283
  generate_tokens: int,
284
  seed: int | None,
 
 
 
285
  repo: str,
286
  filename: str,
287
  cache_dir: Path,
@@ -292,7 +778,7 @@ def generate_from_events(
292
  ) -> tuple[list[dict], list[int]]:
293
  import torch
294
 
295
- model, resolved_device, _ = load_model_cached(
296
  repo=repo,
297
  filename=filename,
298
  cache_dir=cache_dir,
@@ -302,31 +788,79 @@ def generate_from_events(
302
  device=device,
303
  )
304
 
305
- if seed is not None:
306
- torch.manual_seed(seed)
307
- if resolved_device == "cuda":
308
- torch.cuda.manual_seed_all(seed)
309
-
310
- score_tokens = events_to_score_tokens(events)
311
- prime = build_prime_tokens(score_tokens, seq_len)
312
  prime_tensor = torch.tensor(prime, dtype=torch.long, device=resolved_device)
313
-
314
- out = model.generate(
315
- prime_tensor,
316
- generate_tokens,
317
- return_prime=True,
318
- eos_token=707,
319
- )
320
-
321
- tokens = out.detach().cpu().tolist()
322
- new_tokens = tokens[len(prime) :]
323
 
324
  last_time_ms = 0.0
325
  if events:
326
  last_time_ms = max(float(e.get("time", 0.0)) for e in events) * 1000.0
327
 
328
- new_events = tokens_to_events(new_tokens, offset_ms=last_time_ms)
329
- return new_events, new_tokens
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
 
332
  def generate_godzilla_continuation(
@@ -334,12 +868,19 @@ def generate_godzilla_continuation(
334
  *,
335
  generate_tokens: int = 32,
336
  seed: int | None = None,
 
 
 
337
  device: str = "auto",
 
338
  ) -> tuple[list[dict], list[int]]:
339
  return generate_from_events(
340
  events,
341
  generate_tokens=generate_tokens,
342
  seed=seed,
 
 
 
343
  repo=DEFAULT_REPO,
344
  filename=DEFAULT_FILENAME,
345
  cache_dir=Path(".cache/godzilla"),
@@ -349,7 +890,6 @@ def generate_godzilla_continuation(
349
  device=device,
350
  )
351
 
352
-
353
  class GodzillaMidiModel(MidiModel):
354
  def __init__(self) -> None:
355
  super().__init__(model_id="godzilla", name="Godzilla")
@@ -360,12 +900,21 @@ class GodzillaMidiModel(MidiModel):
360
  *,
361
  tokens: int = 32,
362
  seed: Optional[int] = None,
 
 
 
 
 
363
  ) -> list[dict]:
364
  new_events, _ = generate_godzilla_continuation(
365
  events,
366
  generate_tokens=tokens,
367
  seed=seed,
368
- device="auto",
 
 
 
 
369
  )
370
  return new_events
371
 
 
3
 
4
  import subprocess
5
  import sys
6
+ import inspect
7
+ import re
8
+ import time
9
  from dataclasses import dataclass
10
  from pathlib import Path
11
+ from typing import Any, Iterable, Optional
12
 
13
  from config import MIDI_DEFAULTS, KEYBOARD_BASE_MIDI, KEYBOARD_OCTAVES
14
 
 
15
  DEFAULT_REPO = "asigalov61/Godzilla-Piano-Transformer"
16
  DEFAULT_FILENAME = (
17
+ "Godzilla_Piano_Transformer_No_Velocity_Trained_Model_21113_steps_"
18
+ "0.3454_loss_0.895_acc.pth"
19
  )
20
+ KNOWN_GODZILLA_CHECKPOINTS = [
21
+ "Godzilla_Piano_Transformer_No_Velocity_Trained_Model_21113_steps_0.3454_loss_0.895_acc.pth",
22
+ "Godzilla_Piano_Transformer_No_Velocity_Trained_Model_14903_steps_0.4874_loss_0.8571_acc.pth",
23
+ "Godzilla_Piano_Transformer_No_Velocity_Trained_Model_32503_steps_0.6553_loss_0.8065_acc.pth",
24
+ "Godzilla_Piano_Chords_Texturing_Transformer_Trained_Model_22708_steps_0.7515_loss_0.7853_acc.pth",
25
+ ]
26
 
27
  _MODEL_CACHE: dict[str, object] = {}
28
+ PROMPT_MAX_SECONDS = 18.0
29
+ PROMPT_PHRASE_GAP_SEC = 0.6
30
+ PROMPT_TARGET_CENTER_MIDI = 67
31
+ PROMPT_MAX_TRANSPOSE = 12
32
+ DEFAULT_MODEL_DIM = 2048
33
+ DEFAULT_MODEL_DEPTH = 8
34
+ DEFAULT_MODEL_HEADS = 32
35
+ DEFAULT_SEQ_LEN = 1536
36
+ DEFAULT_PAD_IDX = 708
37
 
38
 
39
  @dataclass(frozen=True)
 
47
  *,
48
  tokens: int = 32,
49
  seed: Optional[int] = None,
50
+ temperature: float = 0.9,
51
+ top_p: float = 0.95,
52
+ num_candidates: int = 1,
53
+ request: Any | None = None,
54
+ device: str = "auto",
55
  ) -> list[dict]:
56
  raise NotImplementedError
57
 
 
81
  return tools_dir, x_transformer_dir
82
 
83
 
84
+ def preload_godzilla_assets(
85
+ *,
86
+ repo: str = DEFAULT_REPO,
87
+ filename: str = DEFAULT_FILENAME,
88
+ cache_dir: Path = Path(".cache/godzilla"),
89
+ tegridy_dir: Path = Path("external"),
90
+ ) -> Path:
91
+ """
92
+ Download model checkpoint and Tegridy tools during app startup.
93
+ """
94
+ from huggingface_hub import hf_hub_download
95
+
96
+ cache_dir.mkdir(parents=True, exist_ok=True)
97
+ checkpoint_path = download_checkpoint_with_fallback(
98
+ repo=repo,
99
+ filename=filename,
100
+ cache_dir=cache_dir,
101
+ )
102
+ ensure_tegridy_tools(tegridy_dir)
103
+ return checkpoint_path
104
+
105
+
106
+ def preload_godzilla_model(
107
+ *,
108
+ repo: str = DEFAULT_REPO,
109
+ filename: str = DEFAULT_FILENAME,
110
+ cache_dir: Path = Path(".cache/godzilla"),
111
+ tegridy_dir: Path = Path("external"),
112
+ seq_len: int = DEFAULT_SEQ_LEN,
113
+ pad_idx: int = DEFAULT_PAD_IDX,
114
+ device: str = "cpu",
115
+ ) -> dict[str, Any]:
116
+ model, resolved_device, _, resolved_seq_len, resolved_pad_idx = load_model_cached(
117
+ repo=repo,
118
+ filename=filename,
119
+ cache_dir=cache_dir,
120
+ tegridy_dir=tegridy_dir,
121
+ seq_len=seq_len,
122
+ pad_idx=pad_idx,
123
+ device=device,
124
+ )
125
+ return {
126
+ "model_loaded": model is not None,
127
+ "device": resolved_device,
128
+ "seq_len": resolved_seq_len,
129
+ "pad_idx": resolved_pad_idx,
130
+ }
131
+
132
+
133
+ def candidate_checkpoint_filenames(primary: str) -> list[str]:
134
+ ordered = [primary]
135
+ for checkpoint in KNOWN_GODZILLA_CHECKPOINTS:
136
+ if checkpoint not in ordered:
137
+ ordered.append(checkpoint)
138
+ return ordered
139
+
140
+
141
+ def download_checkpoint_with_fallback(
142
+ *,
143
+ repo: str,
144
+ filename: str,
145
+ cache_dir: Path,
146
+ ) -> Path:
147
+ from huggingface_hub import hf_hub_download
148
+
149
+ cache_dir.mkdir(parents=True, exist_ok=True)
150
+ attempted: list[str] = []
151
+ last_error: Exception | None = None
152
+
153
+ for candidate in candidate_checkpoint_filenames(filename):
154
+ try:
155
+ return Path(
156
+ hf_hub_download(
157
+ repo_id=repo,
158
+ filename=candidate,
159
+ local_dir=str(cache_dir),
160
+ repo_type="model",
161
+ )
162
+ )
163
+ except Exception as exc:
164
+ attempted.append(candidate)
165
+ last_error = exc
166
+ message = str(exc)
167
+ if "Entry Not Found" in message or "404" in message:
168
+ continue
169
+ raise
170
+
171
+ raise RuntimeError(
172
+ f"Could not download any Godzilla checkpoint. Tried: {attempted}. "
173
+ f"Last error: {last_error}"
174
+ )
175
+
176
+
177
  def add_sys_path(*paths: Path) -> None:
178
  for path in paths:
179
  path_str = str(path.resolve())
 
181
  sys.path.insert(0, path_str)
182
 
183
 
184
+ def build_model(
185
+ seq_len: int,
186
+ pad_idx: int,
187
+ *,
188
+ dim: int = DEFAULT_MODEL_DIM,
189
+ depth: int = DEFAULT_MODEL_DEPTH,
190
+ heads: int = DEFAULT_MODEL_HEADS,
191
+ ):
192
  from x_transformer_2_3_1 import AutoregressiveWrapper, Decoder, TransformerWrapper
193
 
194
  model = TransformerWrapper(
195
  num_tokens=pad_idx + 1,
196
  max_seq_len=seq_len,
197
  attn_layers=Decoder(
198
+ dim=dim,
199
+ depth=depth,
200
+ heads=heads,
201
  rotary_pos_emb=True,
202
  attn_flash=True,
203
  ),
 
213
  return requested
214
 
215
 
216
+ def load_checkpoint_state(checkpoint_path: Path, device: str) -> dict[str, Any]:
217
  import torch
218
 
219
  state = torch.load(checkpoint_path, map_location=device)
220
+ if isinstance(state, dict) and "state_dict" in state and isinstance(state["state_dict"], dict):
221
+ state = state["state_dict"]
222
+ if not isinstance(state, dict):
223
+ raise RuntimeError(f"Unexpected checkpoint format at {checkpoint_path}")
224
+ return state
225
+
226
+
227
+ def infer_layer_entries_from_state(state: dict[str, Any]) -> int:
228
+ layer_ids: set[int] = set()
229
+ pattern = re.compile(r"^net\.attn_layers\.layers\.(\d+)\.")
230
+ for key in state.keys():
231
+ match = pattern.match(key)
232
+ if match:
233
+ layer_ids.add(int(match.group(1)))
234
+ if not layer_ids:
235
+ return DEFAULT_MODEL_DEPTH * 2
236
+ return max(layer_ids) + 1
237
+
238
+
239
+ def infer_model_shape_from_state(
240
+ state: dict[str, Any],
241
+ *,
242
+ fallback_seq_len: int,
243
+ fallback_pad_idx: int,
244
+ ) -> tuple[int, int, int, list[int]]:
245
+ emb = state.get("net.token_emb.emb.weight")
246
+ if emb is not None and hasattr(emb, "shape") and len(emb.shape) == 2:
247
+ num_tokens = int(emb.shape[0])
248
+ dim = int(emb.shape[1])
249
+ else:
250
+ num_tokens = fallback_pad_idx + 1
251
+ dim = DEFAULT_MODEL_DIM
252
+
253
+ pad_idx = max(0, num_tokens - 1)
254
+ seq_len = 4096 if num_tokens <= 385 else fallback_seq_len
255
+
256
+ layer_entries = infer_layer_entries_from_state(state)
257
+ depth_candidates: list[int] = []
258
+ for candidate in [max(1, layer_entries // 2), layer_entries]:
259
+ if candidate not in depth_candidates:
260
+ depth_candidates.append(candidate)
261
+
262
+ return seq_len, pad_idx, dim, depth_candidates
263
+
264
+
265
+ def adapt_state_for_model(
266
+ raw_state: dict[str, Any],
267
+ model_state: dict[str, Any],
268
+ ) -> dict[str, Any]:
269
+ adapted: dict[str, Any] = {}
270
+ model_keys = set(model_state.keys())
271
+
272
+ for key, value in raw_state.items():
273
+ if key in model_keys:
274
+ adapted[key] = value
275
+ continue
276
+ if key.endswith(".weight"):
277
+ gamma_key = key[:-7] + ".gamma"
278
+ if gamma_key in model_keys:
279
+ adapted[gamma_key] = value
280
+ continue
281
+ if key.endswith(".gamma"):
282
+ weight_key = key[:-6] + ".weight"
283
+ if weight_key in model_keys:
284
+ adapted[weight_key] = value
285
+ continue
286
+
287
+ # Fill optional affine params when norm conventions differ.
288
+ for key, tensor in model_state.items():
289
+ if key in adapted:
290
+ continue
291
+ if key.endswith(".bias"):
292
+ adapted[key] = tensor.new_zeros(tensor.shape)
293
+ elif key.endswith(".gamma"):
294
+ adapted[key] = tensor.new_ones(tensor.shape)
295
+
296
+ return adapted
297
+
298
+
299
+ def sanitize_events(events: list[dict]) -> list[dict]:
300
+ cleaned: list[dict] = []
301
+ for event in events:
302
+ if not isinstance(event, dict):
303
+ continue
304
+ ev_type = event.get("type")
305
+ if ev_type not in {"note_on", "note_off"}:
306
+ continue
307
+ note = max(0, min(127, int(event.get("note", 0))))
308
+ velocity = max(0, min(127, int(event.get("velocity", 0))))
309
+ time_sec = max(0.0, float(event.get("time", 0.0)))
310
+ cleaned.append(
311
+ {
312
+ "type": ev_type,
313
+ "note": note,
314
+ "velocity": velocity,
315
+ "time": time_sec,
316
+ "channel": int(event.get("channel", 0)),
317
+ }
318
+ )
319
+ cleaned.sort(key=lambda e: float(e.get("time", 0.0)))
320
+ return cleaned
321
+
322
+
323
+ def extract_prompt_window(
324
+ events: list[dict],
325
+ *,
326
+ max_seconds: float = PROMPT_MAX_SECONDS,
327
+ phrase_gap_sec: float = PROMPT_PHRASE_GAP_SEC,
328
+ ) -> list[dict]:
329
+ cleaned = sanitize_events(events)
330
+ if not cleaned:
331
+ return []
332
+
333
+ last_time = max(float(e.get("time", 0.0)) for e in cleaned)
334
+ recent_cut = max(0.0, last_time - max_seconds)
335
+
336
+ note_on_times = [
337
+ float(e.get("time", 0.0))
338
+ for e in cleaned
339
+ if e.get("type") == "note_on" and int(e.get("velocity", 0)) > 0
340
+ ]
341
+ if len(note_on_times) < 2:
342
+ return [e for e in cleaned if float(e.get("time", 0.0)) >= recent_cut]
343
+
344
+ phrase_starts = [note_on_times[0]]
345
+ for i in range(1, len(note_on_times)):
346
+ if note_on_times[i] - note_on_times[i - 1] >= phrase_gap_sec:
347
+ phrase_starts.append(note_on_times[i])
348
+
349
+ # Keep the last 1-2 phrases for coherent continuation.
350
+ phrase_cut = phrase_starts[-2] if len(phrase_starts) >= 2 else phrase_starts[-1]
351
+ cut = max(recent_cut, phrase_cut)
352
+ return [e for e in cleaned if float(e.get("time", 0.0)) >= cut]
353
+
354
+
355
+ def estimate_input_velocity(events: list[dict], default: int = 100) -> int:
356
+ velocities = [
357
+ int(e.get("velocity", 0))
358
+ for e in events
359
+ if e.get("type") == "note_on" and int(e.get("velocity", 0)) > 0
360
+ ]
361
+ if not velocities:
362
+ return default
363
+ avg = round(sum(velocities) / len(velocities))
364
+ return max(40, min(120, avg))
365
+
366
+
367
+ def compute_transpose_shift(
368
+ events: list[dict],
369
+ *,
370
+ target_center_midi: int = PROMPT_TARGET_CENTER_MIDI,
371
+ max_transpose: int = PROMPT_MAX_TRANSPOSE,
372
+ ) -> int:
373
+ pitches = [
374
+ int(e.get("note", 0))
375
+ for e in events
376
+ if e.get("type") == "note_on" and int(e.get("velocity", 0)) > 0
377
+ ]
378
+ if not pitches:
379
+ return 0
380
+ pitches.sort()
381
+ median_pitch = pitches[len(pitches) // 2]
382
+ shift = int(round(target_center_midi - median_pitch))
383
+ return max(-max_transpose, min(max_transpose, shift))
384
+
385
+
386
+ def transpose_events(events: list[dict], semitones: int) -> list[dict]:
387
+ if semitones == 0:
388
+ return [dict(event) for event in events]
389
+ out: list[dict] = []
390
+ for event in events:
391
+ copied = dict(event)
392
+ if copied.get("type") in {"note_on", "note_off"}:
393
+ copied["note"] = max(0, min(127, int(copied.get("note", 0)) + semitones))
394
+ out.append(copied)
395
+ return out
396
 
397
 
398
  def events_to_score_tokens(events: list[dict]) -> list[int]:
399
  if not events:
400
  return []
401
 
402
+ active: dict[int, list[tuple[float, int]]] = {}
403
  notes: list[tuple[float, float, int]] = []
404
  sorted_events = sorted(events, key=lambda e: e.get("time", 0.0))
405
 
 
410
  time_sec = float(event.get("time", 0.0))
411
 
412
  if ev_type == "note_on" and velocity > 0:
413
+ active.setdefault(note, []).append((time_sec, velocity))
414
  elif ev_type in {"note_off", "note_on"}:
415
+ if note in active and active[note]:
416
+ start, _ = active[note].pop(0)
417
+ duration = max(0.05, time_sec - start)
418
  notes.append((start, duration, note))
419
 
420
  if not notes:
 
509
  ]
510
 
511
 
512
+ def fold_note_to_keyboard_range(note: int) -> int:
513
+ min_note, max_note = keyboard_note_range()
514
+ folded = int(note)
515
+ while folded < min_note:
516
+ folded += 12
517
+ while folded > max_note:
518
+ folded -= 12
519
+ return max(min_note, min(max_note, folded))
520
+
521
+
522
+ def fold_events_to_keyboard_range(events: list[dict]) -> list[dict]:
523
+ out: list[dict] = []
524
+ for event in events:
525
+ copied = dict(event)
526
+ if copied.get("type") in {"note_on", "note_off"}:
527
+ copied["note"] = fold_note_to_keyboard_range(int(copied.get("note", 0)))
528
+ out.append(copied)
529
+ return out
530
+
531
+
532
+ def resolve_eos_token(pad_idx: int) -> int:
533
+ # Legacy Godzilla checkpoints use 707 as EOS with 708 pad.
534
+ if pad_idx >= 708:
535
+ return 707
536
+ return pad_idx
537
+
538
+
539
+ def build_prime_tokens(score_tokens: list[int], seq_len: int, pad_idx: int) -> list[int]:
540
+ num_tokens = pad_idx + 1
541
+ if pad_idx >= 708:
542
+ prime = [705, 384, 706]
543
+ if score_tokens:
544
+ max_score = max(0, seq_len - len(prime))
545
+ prime.extend(score_tokens[-max_score:])
546
+ else:
547
+ prime.extend([0, 129, 316])
548
  else:
549
+ if score_tokens:
550
+ prime = score_tokens[-max(1, seq_len) :]
551
+ else:
552
+ prime = [0, 129, 316]
553
+
554
+ # Keep prime tokens valid for current vocabulary.
555
+ return [max(0, min(num_tokens - 1, int(tok))) for tok in prime]
556
+
557
+
558
+ def build_generate_kwargs(
559
+ model,
560
+ temperature: float,
561
+ top_p: float,
562
+ eos_token: int,
563
+ ) -> dict[str, Any]:
564
+ kwargs: dict[str, Any] = {
565
+ "return_prime": True,
566
+ "eos_token": eos_token,
567
+ }
568
+ try:
569
+ signature = inspect.signature(model.generate)
570
+ except (TypeError, ValueError):
571
+ return kwargs
572
+
573
+ params = signature.parameters
574
+ safe_temperature = max(0.2, min(1.5, float(temperature)))
575
+ safe_top_p = max(0.5, min(0.99, float(top_p)))
576
+
577
+ if "temperature" in params:
578
+ kwargs["temperature"] = safe_temperature
579
+ if "top_p" in params:
580
+ kwargs["top_p"] = safe_top_p
581
+ elif "filter_thres" in params:
582
+ kwargs["filter_thres"] = safe_top_p
583
+ return kwargs
584
+
585
+
586
+ def generate_tokens_sample(
587
+ model,
588
+ prime_tensor,
589
+ generate_tokens: int,
590
+ *,
591
+ temperature: float,
592
+ top_p: float,
593
+ eos_token: int,
594
+ ) -> list[int]:
595
+ kwargs = build_generate_kwargs(model, temperature, top_p, eos_token)
596
+ try:
597
+ out = model.generate(
598
+ prime_tensor,
599
+ generate_tokens,
600
+ **kwargs,
601
+ )
602
+ except TypeError:
603
+ out = model.generate(
604
+ prime_tensor,
605
+ generate_tokens,
606
+ return_prime=True,
607
+ eos_token=eos_token,
608
+ )
609
+ return out.detach().cpu().tolist()
610
+
611
+
612
+ def score_candidate_events(events: list[dict], prompt_events: list[dict]) -> float:
613
+ notes = [
614
+ int(e.get("note", 0))
615
+ for e in events
616
+ if e.get("type") == "note_on" and int(e.get("velocity", 0)) > 0
617
+ ]
618
+ if not notes:
619
+ return -1e6
620
+
621
+ prompt_notes = [
622
+ int(e.get("note", 0))
623
+ for e in prompt_events
624
+ if e.get("type") == "note_on" and int(e.get("velocity", 0)) > 0
625
+ ]
626
+
627
+ min_note, max_note = keyboard_note_range()
628
+ out_of_range = sum(1 for note in notes if note < min_note or note > max_note)
629
+ repeats = sum(1 for i in range(1, len(notes)) if notes[i] == notes[i - 1])
630
+ big_leaps = sum(max(0, abs(notes[i] - notes[i - 1]) - 7) for i in range(1, len(notes)))
631
+
632
+ score = 0.0
633
+ score += min(len(notes), 24) * 0.25
634
+ score -= out_of_range * 3.5
635
+ score -= repeats * 0.2
636
+ score -= big_leaps * 0.08
637
+
638
+ if prompt_notes:
639
+ prompt_center = sum(prompt_notes) / len(prompt_notes)
640
+ notes_center = sum(notes) / len(notes)
641
+ score -= abs(notes_center - prompt_center) * 0.04
642
+ score -= abs(notes[0] - prompt_notes[-1]) * 0.03
643
+
644
+ return score
645
 
646
 
647
  def load_model_cached(
 
653
  seq_len: int,
654
  pad_idx: int,
655
  device: str,
656
+ ) -> tuple[object, str, Path, int, int]:
 
657
  import torch
658
 
659
  cache_dir.mkdir(parents=True, exist_ok=True)
660
  resolved_device = resolve_device(device)
 
661
 
662
+ checkpoint_path = download_checkpoint_with_fallback(
663
+ repo=repo,
664
+ filename=filename,
665
+ cache_dir=cache_dir,
 
 
 
 
 
 
 
 
 
 
666
  )
667
 
668
  tools_dir, x_transformer_dir = ensure_tegridy_tools(tegridy_dir)
 
673
  torch.backends.cuda.matmul.allow_tf32 = True
674
  torch.backends.cudnn.allow_tf32 = True
675
 
676
+ raw_state = load_checkpoint_state(checkpoint_path, "cpu")
677
+ inferred_seq_len, inferred_pad_idx, inferred_dim, depth_candidates = infer_model_shape_from_state(
678
+ raw_state,
679
+ fallback_seq_len=seq_len,
680
+ fallback_pad_idx=pad_idx,
681
+ )
682
+
683
+ cache_key = (
684
+ f"{repo}:{filename}:{resolved_device}:{inferred_seq_len}:{inferred_pad_idx}:"
685
+ f"{inferred_dim}:{'-'.join(str(d) for d in depth_candidates)}"
686
+ )
687
+ if _MODEL_CACHE.get("key") == cache_key:
688
+ return (
689
+ _MODEL_CACHE["model"],
690
+ _MODEL_CACHE["device"],
691
+ _MODEL_CACHE["tools_dir"],
692
+ _MODEL_CACHE["seq_len"],
693
+ _MODEL_CACHE["pad_idx"],
694
+ )
695
+
696
+ selected_model = None
697
+ selected_depth = None
698
+ last_error: RuntimeError | None = None
699
+ for depth in depth_candidates:
700
+ try:
701
+ model = build_model(
702
+ inferred_seq_len,
703
+ inferred_pad_idx,
704
+ dim=inferred_dim,
705
+ depth=depth,
706
+ heads=DEFAULT_MODEL_HEADS,
707
+ )
708
+ adapted_state = adapt_state_for_model(raw_state, model.state_dict())
709
+ missing, _ = model.load_state_dict(adapted_state, strict=False)
710
+ critical_missing = [
711
+ key
712
+ for key in missing
713
+ if not (key.endswith(".bias") or key.endswith(".gamma"))
714
+ ]
715
+ if critical_missing:
716
+ raise RuntimeError(
717
+ f"critical missing keys for depth={depth}: {critical_missing[:5]}"
718
+ )
719
+ selected_model = model
720
+ selected_depth = depth
721
+ break
722
+ except RuntimeError as exc:
723
+ last_error = exc
724
+
725
+ if selected_model is None:
726
+ raise RuntimeError(
727
+ f"Could not load checkpoint {filename} with inferred configs. "
728
+ f"Tried depths={depth_candidates}. Last error: {last_error}"
729
+ )
730
+
731
+ model = selected_model
732
  model.to(resolved_device)
733
  model.eval()
734
+ if resolved_device == "cuda":
735
+ first_param = next(model.parameters(), None)
736
+ if first_param is None or not first_param.is_cuda:
737
+ raise RuntimeError("Godzilla model failed to move to CUDA device")
738
+
739
+ print(
740
+ "Loaded Godzilla checkpoint config:",
741
+ {
742
+ "filename": filename,
743
+ "seq_len": inferred_seq_len,
744
+ "pad_idx": inferred_pad_idx,
745
+ "dim": inferred_dim,
746
+ "depth": selected_depth,
747
+ "device": resolved_device,
748
+ "cuda_available": torch.cuda.is_available(),
749
+ },
750
+ )
751
 
752
  _MODEL_CACHE["key"] = cache_key
753
  _MODEL_CACHE["model"] = model
754
  _MODEL_CACHE["device"] = resolved_device
755
  _MODEL_CACHE["tools_dir"] = tools_dir
756
  _MODEL_CACHE["checkpoint_path"] = checkpoint_path
757
+ _MODEL_CACHE["seq_len"] = inferred_seq_len
758
+ _MODEL_CACHE["pad_idx"] = inferred_pad_idx
759
 
760
+ return model, resolved_device, tools_dir, inferred_seq_len, inferred_pad_idx
761
 
762
 
763
  def generate_from_events(
 
765
  *,
766
  generate_tokens: int,
767
  seed: int | None,
768
+ temperature: float,
769
+ top_p: float,
770
+ num_candidates: int,
771
  repo: str,
772
  filename: str,
773
  cache_dir: Path,
 
778
  ) -> tuple[list[dict], list[int]]:
779
  import torch
780
 
781
+ model, resolved_device, _, resolved_seq_len, resolved_pad_idx = load_model_cached(
782
  repo=repo,
783
  filename=filename,
784
  cache_dir=cache_dir,
 
788
  device=device,
789
  )
790
 
791
+ prompt_events = extract_prompt_window(events)
792
+ transpose_shift = compute_transpose_shift(prompt_events)
793
+ transposed_prompt_events = transpose_events(prompt_events, transpose_shift)
794
+ score_tokens = events_to_score_tokens(transposed_prompt_events)
795
+ prime = build_prime_tokens(score_tokens, resolved_seq_len, resolved_pad_idx)
 
 
796
  prime_tensor = torch.tensor(prime, dtype=torch.long, device=resolved_device)
797
+ eos_token = resolve_eos_token(resolved_pad_idx)
 
 
 
 
 
 
 
 
 
798
 
799
  last_time_ms = 0.0
800
  if events:
801
  last_time_ms = max(float(e.get("time", 0.0)) for e in events) * 1000.0
802
 
803
+ input_velocity = estimate_input_velocity(prompt_events)
804
+ best_events: list[dict] = []
805
+ best_tokens: list[int] = []
806
+ best_score = -1e9
807
+
808
+ candidate_count = max(1, min(6, int(num_candidates)))
809
+ generation_started = time.perf_counter()
810
+ candidate_timings_ms: list[float] = []
811
+ for idx in range(candidate_count):
812
+ if seed is not None:
813
+ sample_seed = int(seed) + idx
814
+ torch.manual_seed(sample_seed)
815
+ if resolved_device == "cuda":
816
+ torch.cuda.manual_seed_all(sample_seed)
817
+
818
+ candidate_started = time.perf_counter()
819
+ tokens = generate_tokens_sample(
820
+ model,
821
+ prime_tensor,
822
+ generate_tokens,
823
+ temperature=temperature,
824
+ top_p=top_p,
825
+ eos_token=eos_token,
826
+ )
827
+ if resolved_device == "cuda":
828
+ torch.cuda.synchronize()
829
+ candidate_timings_ms.append((time.perf_counter() - candidate_started) * 1000.0)
830
+ new_tokens = tokens[len(prime) :]
831
+
832
+ candidate_events = tokens_to_events(
833
+ new_tokens,
834
+ offset_ms=last_time_ms,
835
+ velocity=input_velocity,
836
+ )
837
+ candidate_events = transpose_events(candidate_events, -transpose_shift)
838
+ candidate_score = score_candidate_events(candidate_events, prompt_events)
839
+
840
+ if candidate_score > best_score or idx == 0:
841
+ best_score = candidate_score
842
+ best_events = candidate_events
843
+ best_tokens = new_tokens
844
+
845
+ generation_total_ms = (time.perf_counter() - generation_started) * 1000.0
846
+ avg_candidate_ms = (
847
+ sum(candidate_timings_ms) / len(candidate_timings_ms)
848
+ if candidate_timings_ms
849
+ else 0.0
850
+ )
851
+ print(
852
+ "Godzilla generation timing (model load excluded):",
853
+ {
854
+ "device": resolved_device,
855
+ "generate_tokens": generate_tokens,
856
+ "candidates": candidate_count,
857
+ "candidate_ms": [round(ms, 2) for ms in candidate_timings_ms],
858
+ "avg_candidate_ms": round(avg_candidate_ms, 2),
859
+ "total_generation_ms": round(generation_total_ms, 2),
860
+ },
861
+ )
862
+
863
+ return best_events, best_tokens
864
 
865
 
866
  def generate_godzilla_continuation(
 
868
  *,
869
  generate_tokens: int = 32,
870
  seed: int | None = None,
871
+ temperature: float = 0.9,
872
+ top_p: float = 0.95,
873
+ num_candidates: int = 1,
874
  device: str = "auto",
875
+ request: Any | None = None,
876
  ) -> tuple[list[dict], list[int]]:
877
  return generate_from_events(
878
  events,
879
  generate_tokens=generate_tokens,
880
  seed=seed,
881
+ temperature=temperature,
882
+ top_p=top_p,
883
+ num_candidates=num_candidates,
884
  repo=DEFAULT_REPO,
885
  filename=DEFAULT_FILENAME,
886
  cache_dir=Path(".cache/godzilla"),
 
890
  device=device,
891
  )
892
 
 
893
  class GodzillaMidiModel(MidiModel):
894
  def __init__(self) -> None:
895
  super().__init__(model_id="godzilla", name="Godzilla")
 
900
  *,
901
  tokens: int = 32,
902
  seed: Optional[int] = None,
903
+ temperature: float = 0.9,
904
+ top_p: float = 0.95,
905
+ num_candidates: int = 1,
906
+ request: Any | None = None,
907
+ device: str = "auto",
908
  ) -> list[dict]:
909
  new_events, _ = generate_godzilla_continuation(
910
  events,
911
  generate_tokens=tokens,
912
  seed=seed,
913
+ temperature=temperature,
914
+ top_p=top_p,
915
+ num_candidates=num_candidates,
916
+ device=device,
917
+ request=request,
918
  )
919
  return new_events
920
 
pyproject.toml CHANGED
@@ -5,6 +5,6 @@ description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.10"
7
  dependencies = [
8
- "gradio>=6.5.1",
9
  "mido>=1.3.3",
10
  ]
 
5
  readme = "README.md"
6
  requires-python = ">=3.10"
7
  dependencies = [
8
+ "gradio==5.49.1",
9
  "mido>=1.3.3",
10
  ]
requirements.txt CHANGED
@@ -3,4 +3,5 @@ mido
3
  torch
4
  huggingface_hub
5
  einops>=0.6
6
- einx
 
 
3
  torch
4
  huggingface_hub
5
  einops>=0.6
6
+ einx
7
+ spaces
static/keyboard.js CHANGED
@@ -65,30 +65,105 @@ const keyShortcuts = {
65
  // DOM ELEMENTS
66
  // =============================================================================
67
 
68
- const keyboardEl = document.getElementById('keyboard');
69
- const statusEl = document.getElementById('status');
70
- const recordBtn = document.getElementById('recordBtn');
71
- const stopBtn = document.getElementById('stopBtn');
72
- const playbackBtn = document.getElementById('playbackBtn');
73
- const saveBtn = document.getElementById('saveBtn');
74
- const panicBtn = document.getElementById('panicBtn');
75
- const keyboardToggle = document.getElementById('keyboardToggle');
76
- const instrumentSelect = document.getElementById('instrumentSelect');
77
- const engineSelect = document.getElementById('engineSelect');
78
- const terminal = document.getElementById('terminal');
79
- const clearTerminal = document.getElementById('clearTerminal');
 
 
 
 
 
 
 
80
 
81
  // =============================================================================
82
  // STATE
83
  // =============================================================================
84
 
85
  let synth = null;
 
86
  let recording = false;
87
  let startTime = 0;
88
  let events = [];
89
  const pressedKeys = new Set();
90
  let selectedEngine = 'parrot'; // Default engine
91
  let serverConfig = null; // Will hold instruments and keyboard config from server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  // =============================================================================
94
  // INSTRUMENT FACTORY
@@ -137,7 +212,8 @@ function populateEngineSelect(engines) {
137
  });
138
 
139
  if (engines.length > 0) {
140
- selectedEngine = engines[0].id;
 
141
  engineSelect.value = selectedEngine;
142
  }
143
  }
@@ -151,10 +227,10 @@ async function initializeFromConfig() {
151
  * Fetch configuration from Python server and initialize UI
152
  */
153
  try {
154
- const response = await fetch('/gradio_api/api/config');
155
- if (!response.ok) throw new Error(`Config fetch failed: ${response.status}`);
156
-
157
- serverConfig = await response.json();
158
 
159
  // Build instruments from config
160
  instruments = buildInstruments(serverConfig.instruments);
@@ -203,6 +279,14 @@ function loadInstrument(type) {
203
  synth = instruments[type]();
204
  }
205
 
 
 
 
 
 
 
 
 
206
  // =============================================================================
207
  // KEYBOARD RENDERING
208
  // =============================================================================
@@ -247,6 +331,560 @@ function nowSec() {
247
  return performance.now() / 1000;
248
  }
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  // =============================================================================
251
  // TERMINAL LOGGING
252
  // =============================================================================
@@ -362,37 +1000,39 @@ function getKeyElement(midiNote) {
362
  return keyboardEl.querySelector(`.key[data-midi="${midiNote}"]`);
363
  }
364
 
365
- document.addEventListener('keydown', async (ev) => {
366
- if (!keyboardToggle.checked) return;
367
- const key = ev.key.toLowerCase();
368
- const keyMap = window.keyMapFromServer || keyMap; // Use server config if available, fallback to hardcoded
369
- if (!keyMap[key] || pressedKeys.has(key)) return;
370
-
371
- ev.preventDefault();
372
- pressedKeys.add(key);
373
-
374
- await Tone.start();
375
-
376
- const midiNote = keyMap[key];
377
- const keyEl = getKeyElement(midiNote);
378
- if (keyEl) keyEl.style.filter = 'brightness(0.85)';
379
- noteOn(midiNote, 100);
380
- });
381
-
382
- document.addEventListener('keyup', (ev) => {
383
- if (!keyboardToggle.checked) return;
384
- const key = ev.key.toLowerCase();
385
- const keyMap = window.keyMapFromServer || keyMap; // Use server config if available, fallback to hardcoded
386
- if (!keyMap[key] || !pressedKeys.has(key)) return;
387
-
388
- ev.preventDefault();
389
- pressedKeys.delete(key);
390
-
391
- const midiNote = keyMap[key];
392
- const keyEl = getKeyElement(midiNote);
393
- if (keyEl) keyEl.style.filter = '';
394
- noteOff(midiNote);
395
- });
 
 
396
 
397
  // =============================================================================
398
  // MOUSE/TOUCH INPUT
@@ -451,36 +1091,7 @@ async function saveMIDI() {
451
  saveBtn.disabled = true;
452
 
453
  try {
454
- const startResp = await fetch('/gradio_api/call/save_midi', {
455
- method: 'POST',
456
- headers: {'Content-Type':'application/json'},
457
- body: JSON.stringify({data: [events]})
458
- });
459
-
460
- if (!startResp.ok) {
461
- const txt = await startResp.text();
462
- throw new Error('Server error: ' + txt);
463
- }
464
-
465
- const startJson = await startResp.json();
466
- if (!startJson || !startJson.event_id) {
467
- throw new Error('Invalid API response');
468
- }
469
-
470
- const resultResp = await fetch(`/gradio_api/call/save_midi/${startJson.event_id}`);
471
- if (!resultResp.ok) {
472
- const txt = await resultResp.text();
473
- throw new Error('Server error: ' + txt);
474
- }
475
-
476
- const resultText = await resultResp.text();
477
- const dataLine = resultText.split('\n').find(line => line.startsWith('data:'));
478
- if (!dataLine) {
479
- throw new Error('Invalid API response');
480
- }
481
-
482
- const payloadList = JSON.parse(dataLine.replace('data:', '').trim());
483
- const payload = Array.isArray(payloadList) ? payloadList[0] : null;
484
  if (!payload || payload.error || !payload.midi_base64) {
485
  throw new Error(payload && payload.error ? payload.error : 'Invalid API response');
486
  }
@@ -507,214 +1118,513 @@ async function saveMIDI() {
507
  }
508
  }
509
 
510
- // =============================================================================
511
- // EVENT LISTENERS
512
- // =============================================================================
 
 
 
 
 
 
 
513
 
514
- instrumentSelect.addEventListener('change', () => {
515
- loadInstrument(instrumentSelect.value);
516
- });
 
 
517
 
518
- keyboardToggle.addEventListener('change', () => {
519
- if (keyboardToggle.checked) {
520
- // Show keyboard shortcuts
521
- keyboardEl.classList.add('shortcuts-visible');
522
- } else {
523
- // Hide keyboard shortcuts
524
- keyboardEl.classList.remove('shortcuts-visible');
525
- // Release all currently pressed keyboard keys
526
- pressedKeys.forEach(key => {
527
- const midiNote = keyMap[key];
528
- const keyEl = getKeyElement(midiNote);
529
- if (keyEl) keyEl.style.filter = '';
530
- noteOff(midiNote);
531
- });
532
- pressedKeys.clear();
533
  }
534
- });
535
 
536
- clearTerminal.addEventListener('click', () => {
537
- terminal.innerHTML = '';
538
- logToTerminal('╔═══════════════════════════════════════════════════════╗', 'timestamp');
539
- logToTerminal('║ 🎹 MIDI MONITOR INITIALIZED 🎹 ║', 'timestamp');
540
- logToTerminal('╚═══════════════════════════════════════════════════════╝', 'timestamp');
541
- logToTerminal('Ready to capture MIDI events...', 'timestamp');
542
- logToTerminal('', '');
543
- });
544
 
545
- recordBtn.addEventListener('click', async () => {
546
- await Tone.start();
547
- beginRecord();
548
- });
 
549
 
550
- stopBtn.addEventListener('click', () => stopRecord());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
- engineSelect.addEventListener('change', (e) => {
553
- selectedEngine = e.target.value;
554
- logToTerminal(`Engine switched to: ${selectedEngine}`, 'timestamp');
555
- });
 
 
 
 
 
 
556
 
557
- playbackBtn.addEventListener('click', async () => {
558
- if (events.length === 0) return alert('No recording to play back');
559
-
560
- // Ensure all notes are off before starting playback
561
- if (synth) {
562
- synth.releaseAll();
 
 
563
  }
564
- keyboardEl.querySelectorAll('.key').forEach(k => {
565
- k.style.filter = '';
566
- });
567
-
568
- statusEl.textContent = 'Playing back...';
569
- playbackBtn.disabled = true;
570
- recordBtn.disabled = true;
571
-
572
- logToTerminal('', '');
573
- logToTerminal('♫♫♫ PLAYBACK STARTED ♫♫♫', 'timestamp');
574
- logToTerminal('', '');
575
-
576
- try {
577
- // Process events through the selected engine
578
- let processedEvents = events;
579
- const selectedEngine = engineSelect.value;
580
-
581
- if (selectedEngine && selectedEngine !== 'parrot') {
582
- // Step 1: Start the engine processing call
583
- const startResp = await fetch('/gradio_api/call/process_engine', {
584
- method: 'POST',
585
- headers: { 'Content-Type': 'application/json' },
586
- body: JSON.stringify({
587
- data: [{
588
- engine_id: selectedEngine,
589
- events: events
590
- }]
591
- })
592
- });
593
-
594
- if (!startResp.ok) {
595
- console.error('Engine API start failed:', startResp.status);
596
- } else {
597
- const startJson = await startResp.json();
598
 
599
-
600
- // Step 2: Poll for the result
601
- if (startJson && startJson.event_id) {
602
- const resultResp = await fetch(`/gradio_api/call/process_engine/${startJson.event_id}`);
603
- if (resultResp.ok) {
604
- const resultText = await resultResp.text();
605
- const dataLine = resultText.split('\n').find(line => line.startsWith('data:'));
606
- if (dataLine) {
607
- const payloadList = JSON.parse(dataLine.replace('data:', '').trim());
608
- const result = Array.isArray(payloadList) ? payloadList[0] : null;
609
-
610
- if (result && result.events) {
611
- processedEvents = result.events;
612
- }
613
- }
614
- }
615
- }
616
- }
617
  }
618
-
619
- // Play back the recorded events
620
- statusEl.textContent = 'Playing back...';
621
  let eventIndex = 0;
622
-
623
  const playEvent = () => {
624
- if (eventIndex >= processedEvents.length) {
625
- // Playback complete - ensure all notes are off
626
- if (synth) {
627
- synth.releaseAll();
628
- }
629
-
630
- // Clear all key highlights
631
  keyboardEl.querySelectorAll('.key').forEach(k => {
632
  k.style.filter = '';
633
  });
634
-
635
- statusEl.textContent = 'Playback complete';
636
- playbackBtn.disabled = false;
637
- recordBtn.disabled = false;
638
-
639
- logToTerminal('', '');
640
- logToTerminal('♫♫♫ PLAYBACK FINISHED ♫♫♫', 'timestamp');
641
- logToTerminal('', '');
642
  return;
643
  }
644
-
645
- const event = processedEvents[eventIndex];
646
- const nextTime = eventIndex + 1 < processedEvents.length
647
- ? processedEvents[eventIndex + 1].time
648
  : event.time;
649
-
650
  if (event.type === 'note_on') {
651
  const freq = Tone.Frequency(event.note, "midi").toFrequency();
652
- synth.triggerAttack(freq, undefined, event.velocity / 127);
653
-
654
- const noteName = midiToNoteName(event.note);
655
- logToTerminal(
656
- `[${event.time.toFixed(3)}s] ► ${noteName} (${event.note})`,
657
- 'note-on'
658
- );
659
-
660
- // Highlight the key being played
 
661
  const keyEl = getKeyElement(event.note);
662
  if (keyEl) keyEl.style.filter = 'brightness(0.7)';
663
  } else if (event.type === 'note_off') {
664
  const freq = Tone.Frequency(event.note, "midi").toFrequency();
665
- synth.triggerRelease(freq);
666
-
667
- const noteName = midiToNoteName(event.note);
668
- logToTerminal(
669
- `[${event.time.toFixed(3)}s] ◄ ${noteName}`,
670
- 'note-off'
671
- );
672
-
673
- // Remove key highlight
 
674
  const keyEl = getKeyElement(event.note);
675
  if (keyEl) keyEl.style.filter = '';
676
  }
677
-
678
  eventIndex++;
679
  const deltaTime = Math.max(0, nextTime - event.time);
680
  setTimeout(playEvent, deltaTime * 1000);
681
  };
682
-
683
  playEvent();
684
- } catch (err) {
685
- console.error('Playback error:', err);
686
- statusEl.textContent = 'Playback error: ' + err.message;
687
- playbackBtn.disabled = false;
688
- recordBtn.disabled = false;
689
-
690
- // Ensure all notes are off on error
691
- if (synth) {
692
- synth.releaseAll();
693
- }
694
- keyboardEl.querySelectorAll('.key').forEach(k => {
695
- k.style.filter = '';
696
- });
697
  }
698
- });
699
 
700
- saveBtn.addEventListener('click', () => saveMIDI());
 
 
 
 
 
 
 
 
701
 
702
- panicBtn.addEventListener('click', () => {
703
- // Stop all notes immediately
704
- if (synth) {
705
- synth.releaseAll();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
  }
707
-
708
- // Clear all pressed keys
709
- pressedKeys.clear();
710
-
711
- // Reset all visual key highlights
 
 
 
 
 
712
  keyboardEl.querySelectorAll('.key').forEach(k => {
713
  k.style.filter = '';
714
  });
715
-
716
- logToTerminal('🚨 PANIC - All notes stopped', 'timestamp');
717
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
 
719
  // =============================================================================
720
  // =============================================================================
@@ -722,22 +1632,54 @@ panicBtn.addEventListener('click', () => {
722
  // =============================================================================
723
 
724
  async function init() {
 
 
 
 
 
725
  // First, load configuration from server
726
  await initializeFromConfig();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
 
728
  // Then load default instrument (synth)
729
  loadInstrument('synth');
 
730
 
731
  // Setup keyboard event listeners and UI
732
  attachPointerEvents();
733
  initTerminal();
734
-
 
 
735
  // Set initial button states
736
  recordBtn.disabled = false;
737
  stopBtn.disabled = true;
738
  saveBtn.disabled = true;
739
  playbackBtn.disabled = true;
 
 
740
  }
741
 
742
  // Start the application when DOM is ready
743
- document.addEventListener('DOMContentLoaded', init);
 
 
 
 
 
 
 
65
  // DOM ELEMENTS
66
  // =============================================================================
67
 
68
+ let keyboardEl = null;
69
+ let statusEl = null;
70
+ let recordBtn = null;
71
+ let stopBtn = null;
72
+ let playbackBtn = null;
73
+ let gameStartBtn = null;
74
+ let gameStopBtn = null;
75
+ let saveBtn = null;
76
+ let panicBtn = null;
77
+ let keyboardToggle = null;
78
+ let instrumentSelect = null;
79
+ let aiInstrumentSelect = null;
80
+ let engineSelect = null;
81
+ let runtimeSelect = null;
82
+ let responseStyleSelect = null;
83
+ let responseModeSelect = null;
84
+ let responseLengthSelect = null;
85
+ let terminal = null;
86
+ let clearTerminal = null;
87
 
88
  // =============================================================================
89
  // STATE
90
  // =============================================================================
91
 
92
  let synth = null;
93
+ let aiSynth = null;
94
  let recording = false;
95
  let startTime = 0;
96
  let events = [];
97
  const pressedKeys = new Set();
98
  let selectedEngine = 'parrot'; // Default engine
99
  let serverConfig = null; // Will hold instruments and keyboard config from server
100
+ let gameActive = false;
101
+ let gameTurn = 0;
102
+ let gameTurnTimerId = null;
103
+ let gameTurnTimeoutId = null;
104
+
105
+ const USER_TURN_LIMIT_SEC = 6;
106
+ const GAME_NEXT_TURN_DELAY_MS = 800;
107
+
108
+ const RESPONSE_MODES = {
109
+ raw_godzilla: { label: 'Raw Godzilla' },
110
+ current_pipeline: { label: 'Current Pipeline' },
111
+ musical_polish: { label: 'Musical Polish' }
112
+ };
113
+
114
+ const RESPONSE_LENGTH_PRESETS = {
115
+ short: {
116
+ label: 'Short',
117
+ generateTokens: 32,
118
+ maxNotes: 8,
119
+ maxDurationSec: 4.0
120
+ },
121
+ medium: {
122
+ label: 'Medium',
123
+ generateTokens: 64,
124
+ maxNotes: 14,
125
+ maxDurationSec: 6.0
126
+ },
127
+ long: {
128
+ label: 'Long',
129
+ generateTokens: 96,
130
+ maxNotes: 20,
131
+ maxDurationSec: 8.0
132
+ },
133
+ extended: {
134
+ label: 'Extended',
135
+ generateTokens: 128,
136
+ maxNotes: 28,
137
+ maxDurationSec: 11.0
138
+ }
139
+ };
140
+
141
+ const RESPONSE_STYLE_PRESETS = {
142
+ melodic: {
143
+ label: 'Melodic',
144
+ maxNotes: 8,
145
+ maxDurationSec: 4.0,
146
+ smoothLeaps: true,
147
+ addMotifEcho: false,
148
+ playfulShift: false
149
+ },
150
+ motif_echo: {
151
+ label: 'Motif Echo',
152
+ maxNotes: 10,
153
+ maxDurationSec: 4.3,
154
+ smoothLeaps: true,
155
+ addMotifEcho: true,
156
+ playfulShift: false
157
+ },
158
+ playful: {
159
+ label: 'Playful',
160
+ maxNotes: 9,
161
+ maxDurationSec: 3.8,
162
+ smoothLeaps: true,
163
+ addMotifEcho: false,
164
+ playfulShift: true
165
+ }
166
+ };
167
 
168
  // =============================================================================
169
  // INSTRUMENT FACTORY
 
212
  });
213
 
214
  if (engines.length > 0) {
215
+ const hasGodzilla = engines.some(engine => engine.id === 'godzilla_continue');
216
+ selectedEngine = hasGodzilla ? 'godzilla_continue' : engines[0].id;
217
  engineSelect.value = selectedEngine;
218
  }
219
  }
 
227
  * Fetch configuration from Python server and initialize UI
228
  */
229
  try {
230
+ serverConfig = await callGradioBridge('config', {});
231
+ if (!serverConfig || typeof serverConfig !== 'object') {
232
+ throw new Error('Invalid config payload');
233
+ }
234
 
235
  // Build instruments from config
236
  instruments = buildInstruments(serverConfig.instruments);
 
279
  synth = instruments[type]();
280
  }
281
 
282
+ function loadAIInstrument(type) {
283
+ if (aiSynth) {
284
+ aiSynth.releaseAll();
285
+ aiSynth.dispose();
286
+ }
287
+ aiSynth = instruments[type]();
288
+ }
289
+
290
  // =============================================================================
291
  // KEYBOARD RENDERING
292
  // =============================================================================
 
331
  return performance.now() / 1000;
332
  }
333
 
334
+ function getBridgeButton(buttonId) {
335
+ return document.getElementById(buttonId) || document.querySelector(`#${buttonId} button`);
336
+ }
337
+
338
+ function getBridgeField(fieldId) {
339
+ const root = document.getElementById(fieldId);
340
+ if (!root) return null;
341
+ if (root instanceof HTMLTextAreaElement || root instanceof HTMLInputElement) {
342
+ return root;
343
+ }
344
+ return root.querySelector('textarea, input');
345
+ }
346
+
347
+ function setFieldValue(field, value) {
348
+ const setter = field instanceof HTMLTextAreaElement
349
+ ? Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
350
+ : Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
351
+ if (setter) {
352
+ setter.call(field, value);
353
+ } else {
354
+ field.value = value;
355
+ }
356
+ field.dispatchEvent(new Event('input', { bubbles: true }));
357
+ field.dispatchEvent(new Event('change', { bubbles: true }));
358
+ }
359
+
360
+ function waitForFieldUpdate(field, previousValue, timeoutMs = 120000) {
361
+ return new Promise((resolve, reject) => {
362
+ const deadline = Date.now() + timeoutMs;
363
+
364
+ const check = () => {
365
+ const nextValue = field.value || '';
366
+ if (nextValue !== previousValue && nextValue !== '') {
367
+ resolve(nextValue);
368
+ return;
369
+ }
370
+ if (Date.now() > deadline) {
371
+ reject(new Error('Timed out waiting for Gradio response'));
372
+ return;
373
+ }
374
+ setTimeout(check, 80);
375
+ };
376
+
377
+ check();
378
+ });
379
+ }
380
+
381
+ async function waitForBridgeElements(timeoutMs = 20000) {
382
+ const required = [
383
+ { kind: 'field', id: 'vk_config_input' },
384
+ { kind: 'field', id: 'vk_config_output' },
385
+ { kind: 'button', id: 'vk_config_btn' },
386
+ { kind: 'field', id: 'vk_save_input' },
387
+ { kind: 'field', id: 'vk_save_output' },
388
+ { kind: 'button', id: 'vk_save_btn' },
389
+ { kind: 'field', id: 'vk_engine_input' },
390
+ { kind: 'field', id: 'vk_engine_cpu_output' },
391
+ { kind: 'button', id: 'vk_engine_cpu_btn' },
392
+ { kind: 'field', id: 'vk_engine_gpu_output' },
393
+ { kind: 'button', id: 'vk_engine_gpu_btn' }
394
+ ];
395
+
396
+ const started = Date.now();
397
+ while (Date.now() - started < timeoutMs) {
398
+ const allReady = required.every(item => (
399
+ item.kind === 'button'
400
+ ? Boolean(getBridgeButton(item.id))
401
+ : Boolean(getBridgeField(item.id))
402
+ ));
403
+ if (allReady) return;
404
+ await new Promise(resolve => setTimeout(resolve, 100));
405
+ }
406
+ throw new Error('Gradio bridge elements were not ready in time');
407
+ }
408
+
409
+ function cacheUIElements() {
410
+ keyboardEl = document.getElementById('keyboard');
411
+ statusEl = document.getElementById('status');
412
+ recordBtn = document.getElementById('recordBtn');
413
+ stopBtn = document.getElementById('stopBtn');
414
+ playbackBtn = document.getElementById('playbackBtn');
415
+ gameStartBtn = document.getElementById('gameStartBtn');
416
+ gameStopBtn = document.getElementById('gameStopBtn');
417
+ saveBtn = document.getElementById('saveBtn');
418
+ panicBtn = document.getElementById('panicBtn');
419
+ keyboardToggle = document.getElementById('keyboardToggle');
420
+ instrumentSelect = document.getElementById('instrumentSelect');
421
+ aiInstrumentSelect = document.getElementById('aiInstrumentSelect');
422
+ engineSelect = document.getElementById('engineSelect');
423
+ runtimeSelect = document.getElementById('runtimeSelect');
424
+ responseStyleSelect = document.getElementById('responseStyleSelect');
425
+ responseModeSelect = document.getElementById('responseModeSelect');
426
+ responseLengthSelect = document.getElementById('responseLengthSelect');
427
+ terminal = document.getElementById('terminal');
428
+ clearTerminal = document.getElementById('clearTerminal');
429
+ }
430
+
431
+ async function waitForKeyboardUIElements(timeoutMs = 20000) {
432
+ const requiredIds = [
433
+ 'keyboard',
434
+ 'status',
435
+ 'recordBtn',
436
+ 'stopBtn',
437
+ 'playbackBtn',
438
+ 'gameStartBtn',
439
+ 'gameStopBtn',
440
+ 'saveBtn',
441
+ 'panicBtn',
442
+ 'keyboardToggle',
443
+ 'instrumentSelect',
444
+ 'engineSelect',
445
+ 'runtimeSelect',
446
+ 'terminal',
447
+ 'clearTerminal'
448
+ ];
449
+
450
+ const started = Date.now();
451
+ while (Date.now() - started < timeoutMs) {
452
+ const allReady = requiredIds.every(id => Boolean(document.getElementById(id)));
453
+ if (allReady) return;
454
+ await new Promise(resolve => setTimeout(resolve, 100));
455
+ }
456
+ throw new Error('Keyboard UI elements were not ready in time');
457
+ }
458
+
459
+ const BRIDGE_ACTIONS = {
460
+ config: {
461
+ inputId: 'vk_config_input',
462
+ outputId: 'vk_config_output',
463
+ buttonId: 'vk_config_btn'
464
+ },
465
+ save_midi: {
466
+ inputId: 'vk_save_input',
467
+ outputId: 'vk_save_output',
468
+ buttonId: 'vk_save_btn'
469
+ },
470
+ process_engine_cpu: {
471
+ inputId: 'vk_engine_input',
472
+ outputId: 'vk_engine_cpu_output',
473
+ buttonId: 'vk_engine_cpu_btn'
474
+ },
475
+ process_engine_gpu: {
476
+ inputId: 'vk_engine_input',
477
+ outputId: 'vk_engine_gpu_output',
478
+ buttonId: 'vk_engine_gpu_btn'
479
+ }
480
+ };
481
+
482
+ async function callGradioBridge(action, payload) {
483
+ const bridge = BRIDGE_ACTIONS[action];
484
+ if (!bridge) {
485
+ throw new Error(`Unknown bridge action: ${action}`);
486
+ }
487
+
488
+ const inputField = getBridgeField(bridge.inputId);
489
+ const outputField = getBridgeField(bridge.outputId);
490
+ const button = getBridgeButton(bridge.buttonId);
491
+ if (!inputField || !outputField || !button) {
492
+ throw new Error(`Bridge controls missing for action: ${action}`);
493
+ }
494
+
495
+ const requestPayload = payload === undefined ? {} : payload;
496
+ setFieldValue(inputField, JSON.stringify(requestPayload));
497
+
498
+ const previousOutput = outputField.value || '';
499
+ setFieldValue(outputField, '');
500
+ button.click();
501
+
502
+ const outputText = await waitForFieldUpdate(outputField, previousOutput);
503
+ try {
504
+ return JSON.parse(outputText);
505
+ } catch (err) {
506
+ throw new Error(`Invalid bridge JSON for ${action}: ${outputText}`);
507
+ }
508
+ }
509
+
510
+ function sortEventsChronologically(eventsToSort) {
511
+ return [...eventsToSort].sort((a, b) => {
512
+ const ta = Number(a.time) || 0;
513
+ const tb = Number(b.time) || 0;
514
+ if (ta !== tb) return ta - tb;
515
+ if (a.type === b.type) return 0;
516
+ if (a.type === 'note_off') return -1;
517
+ if (b.type === 'note_off') return 1;
518
+ return 0;
519
+ });
520
+ }
521
+
522
+ function normalizeEventsToZero(rawEvents) {
523
+ if (!Array.isArray(rawEvents) || rawEvents.length === 0) {
524
+ return [];
525
+ }
526
+
527
+ const cleaned = rawEvents
528
+ .filter(e => e && (e.type === 'note_on' || e.type === 'note_off'))
529
+ .map(e => ({
530
+ type: e.type,
531
+ note: Number(e.note) || 0,
532
+ velocity: Number(e.velocity) || 0,
533
+ time: Number(e.time) || 0,
534
+ channel: Number(e.channel) || 0
535
+ }));
536
+
537
+ if (cleaned.length === 0) {
538
+ return [];
539
+ }
540
+
541
+ const minTime = Math.min(...cleaned.map(e => e.time));
542
+ return sortEventsChronologically(
543
+ cleaned.map(e => ({
544
+ ...e,
545
+ time: Math.max(0, e.time - minTime)
546
+ }))
547
+ );
548
+ }
549
+
550
+ function clampMidiNote(note) {
551
+ const minNote = baseMidi;
552
+ const maxNote = baseMidi + (numOctaves * 12) - 1;
553
+ return Math.max(minNote, Math.min(maxNote, note));
554
+ }
555
+
556
+ function eventsToNotePairs(rawEvents) {
557
+ const pairs = [];
558
+ const activeByNote = new Map();
559
+ const sorted = sortEventsChronologically(rawEvents);
560
+
561
+ sorted.forEach(event => {
562
+ const note = Number(event.note) || 0;
563
+ const time = Number(event.time) || 0;
564
+ const velocity = Number(event.velocity) || 100;
565
+
566
+ if (event.type === 'note_on' && velocity > 0) {
567
+ if (!activeByNote.has(note)) activeByNote.set(note, []);
568
+ activeByNote.get(note).push({ start: time, velocity });
569
+ return;
570
+ }
571
+
572
+ if (event.type === 'note_off' || (event.type === 'note_on' && velocity <= 0)) {
573
+ const stack = activeByNote.get(note);
574
+ if (!stack || stack.length === 0) return;
575
+ const active = stack.shift();
576
+ const end = Math.max(active.start + 0.05, time);
577
+ pairs.push({
578
+ note: clampMidiNote(note),
579
+ start: active.start,
580
+ end,
581
+ velocity: Math.max(1, Math.min(127, active.velocity))
582
+ });
583
+ }
584
+ });
585
+
586
+ return pairs.sort((a, b) => a.start - b.start);
587
+ }
588
+
589
+ function notePairsToEvents(pairs) {
590
+ const eventsOut = [];
591
+ pairs.forEach(pair => {
592
+ const note = clampMidiNote(Math.round(pair.note));
593
+ const start = Math.max(0, Number(pair.start) || 0);
594
+ const end = Math.max(start + 0.05, Number(pair.end) || start + 0.2);
595
+ const velocity = Math.max(1, Math.min(127, Math.round(Number(pair.velocity) || 100)));
596
+
597
+ eventsOut.push({
598
+ type: 'note_on',
599
+ note,
600
+ velocity,
601
+ time: start,
602
+ channel: 0
603
+ });
604
+ eventsOut.push({
605
+ type: 'note_off',
606
+ note,
607
+ velocity: 0,
608
+ time: end,
609
+ channel: 0
610
+ });
611
+ });
612
+ return sortEventsChronologically(eventsOut);
613
+ }
614
+
615
+ function trimNotePairs(pairs, maxNotes, maxDurationSec) {
616
+ const out = [];
617
+ for (let i = 0; i < pairs.length; i++) {
618
+ if (out.length >= maxNotes) break;
619
+ if (pairs[i].start > maxDurationSec) break;
620
+ const boundedEnd = Math.min(pairs[i].end, maxDurationSec);
621
+ out.push({
622
+ ...pairs[i],
623
+ end: Math.max(pairs[i].start + 0.05, boundedEnd)
624
+ });
625
+ }
626
+ return out;
627
+ }
628
+
629
+ function smoothPairLeaps(pairs, maxLeapSemitones = 7) {
630
+ if (pairs.length <= 1) return pairs;
631
+ const smoothed = [{ ...pairs[0], note: clampMidiNote(pairs[0].note) }];
632
+ for (let i = 1; i < pairs.length; i++) {
633
+ const prev = smoothed[i - 1].note;
634
+ let current = pairs[i].note;
635
+ while (Math.abs(current - prev) > maxLeapSemitones) {
636
+ current += current > prev ? -12 : 12;
637
+ }
638
+ smoothed.push({
639
+ ...pairs[i],
640
+ note: clampMidiNote(current)
641
+ });
642
+ }
643
+ return smoothed;
644
+ }
645
+
646
+ function appendMotifEcho(pairs, callEvents, maxDurationSec) {
647
+ const callPitches = normalizeEventsToZero(callEvents)
648
+ .filter(e => e.type === 'note_on' && e.velocity > 0)
649
+ .map(e => clampMidiNote(Number(e.note) || 0))
650
+ .slice(0, 2);
651
+
652
+ if (callPitches.length === 0) return pairs;
653
+
654
+ let nextStart = pairs.length > 0 ? pairs[pairs.length - 1].end + 0.1 : 0.2;
655
+ const out = [...pairs];
656
+ callPitches.forEach((pitch, idx) => {
657
+ const start = nextStart + (idx * 0.28);
658
+ if (start >= maxDurationSec) return;
659
+ out.push({
660
+ note: pitch,
661
+ start,
662
+ end: Math.min(maxDurationSec, start + 0.22),
663
+ velocity: 96
664
+ });
665
+ });
666
+ return out;
667
+ }
668
+
669
+ function applyPlayfulShift(pairs) {
670
+ return pairs.map((pair, idx) => {
671
+ if (idx % 2 === 0) return pair;
672
+ const direction = idx % 4 === 1 ? 2 : -2;
673
+ return {
674
+ ...pair,
675
+ note: clampMidiNote(pair.note + direction)
676
+ };
677
+ });
678
+ }
679
+
680
+ function getSelectedStylePreset() {
681
+ const styleId = responseStyleSelect ? responseStyleSelect.value : 'melodic';
682
+ return {
683
+ styleId,
684
+ ...(RESPONSE_STYLE_PRESETS[styleId] || RESPONSE_STYLE_PRESETS.melodic)
685
+ };
686
+ }
687
+
688
+ function getSelectedResponseMode() {
689
+ const modeId = responseModeSelect ? responseModeSelect.value : 'raw_godzilla';
690
+ return {
691
+ modeId,
692
+ ...(RESPONSE_MODES[modeId] || RESPONSE_MODES.raw_godzilla)
693
+ };
694
+ }
695
+
696
+ function getSelectedResponseLengthPreset() {
697
+ const lengthId = responseLengthSelect ? responseLengthSelect.value : 'short';
698
+ return {
699
+ lengthId,
700
+ ...(RESPONSE_LENGTH_PRESETS[lengthId] || RESPONSE_LENGTH_PRESETS.short)
701
+ };
702
+ }
703
+
704
+ function getDecodingOptionsForMode(modeId) {
705
+ if (modeId === 'raw_godzilla') {
706
+ return { temperature: 1.0, top_p: 0.98, num_candidates: 1 };
707
+ }
708
+ if (modeId === 'musical_polish') {
709
+ return { temperature: 0.85, top_p: 0.93, num_candidates: 4 };
710
+ }
711
+ return { temperature: 0.9, top_p: 0.95, num_candidates: 3 };
712
+ }
713
+
714
+ function getSelectedDecodingOptions() {
715
+ const mode = getSelectedResponseMode();
716
+ return getDecodingOptionsForMode(mode.modeId);
717
+ }
718
+
719
+ function getSelectedRuntime() {
720
+ if (!runtimeSelect || !runtimeSelect.value) return 'auto';
721
+ return runtimeSelect.value;
722
+ }
723
+
724
+ function quantizeToStep(value, step) {
725
+ if (!Number.isFinite(value) || !Number.isFinite(step) || step <= 0) {
726
+ return value;
727
+ }
728
+ return Math.round(value / step) * step;
729
+ }
730
+
731
+ function moveByOctaveTowardTarget(note, target) {
732
+ let candidate = note;
733
+ while (candidate + 12 <= target) {
734
+ candidate += 12;
735
+ }
736
+ while (candidate - 12 >= target) {
737
+ candidate -= 12;
738
+ }
739
+ const up = clampMidiNote(candidate + 12);
740
+ const down = clampMidiNote(candidate - 12);
741
+ const current = clampMidiNote(candidate);
742
+ const best = [current, up, down].reduce((winner, value) => {
743
+ return Math.abs(value - target) < Math.abs(winner - target) ? value : winner;
744
+ }, current);
745
+ return clampMidiNote(best);
746
+ }
747
+
748
+ function getCallProfile(callEvents) {
749
+ const normalizedCall = normalizeEventsToZero(callEvents);
750
+ const pitches = normalizedCall
751
+ .filter(e => e.type === 'note_on' && e.velocity > 0)
752
+ .map(e => clampMidiNote(Number(e.note) || baseMidi));
753
+ const velocities = normalizedCall
754
+ .filter(e => e.type === 'note_on' && e.velocity > 0)
755
+ .map(e => Math.max(1, Math.min(127, Number(e.velocity) || 100)));
756
+
757
+ const keyboardCenter = baseMidi + Math.floor((numOctaves * 12) / 2);
758
+ const center = pitches.length > 0
759
+ ? pitches.reduce((sum, value) => sum + value, 0) / pitches.length
760
+ : keyboardCenter;
761
+ const finalPitch = pitches.length > 0 ? pitches[pitches.length - 1] : keyboardCenter;
762
+ const avgVelocity = velocities.length > 0
763
+ ? velocities.reduce((sum, value) => sum + value, 0) / velocities.length
764
+ : 100;
765
+
766
+ return { pitches, center, finalPitch, avgVelocity };
767
+ }
768
+
769
+ function applyResponseStyle(rawResponseEvents, callEvents, lengthPreset) {
770
+ const preset = getSelectedStylePreset();
771
+ const targetMaxNotes = Math.max(preset.maxNotes, lengthPreset.maxNotes);
772
+ const targetMaxDuration = Math.max(preset.maxDurationSec, lengthPreset.maxDurationSec);
773
+ let notePairs = eventsToNotePairs(normalizeEventsToZero(rawResponseEvents));
774
+ notePairs = trimNotePairs(notePairs, targetMaxNotes, targetMaxDuration);
775
+ if (preset.playfulShift) {
776
+ notePairs = applyPlayfulShift(notePairs);
777
+ }
778
+ if (preset.smoothLeaps) {
779
+ notePairs = smoothPairLeaps(notePairs);
780
+ }
781
+ if (preset.addMotifEcho) {
782
+ notePairs = appendMotifEcho(notePairs, callEvents, targetMaxDuration);
783
+ notePairs = trimNotePairs(notePairs, targetMaxNotes, targetMaxDuration);
784
+ }
785
+ return {
786
+ styleLabel: preset.label,
787
+ events: notePairsToEvents(notePairs)
788
+ };
789
+ }
790
+
791
+ function applyMusicalPolish(rawResponseEvents, callEvents, lengthPreset) {
792
+ const stylePreset = getSelectedStylePreset();
793
+ const callProfile = getCallProfile(callEvents);
794
+ let notePairs = eventsToNotePairs(normalizeEventsToZero(rawResponseEvents));
795
+
796
+ if (notePairs.length === 0) {
797
+ const fallbackPitches = callProfile.pitches.slice(0, 4);
798
+ if (fallbackPitches.length === 0) {
799
+ return [];
800
+ }
801
+ notePairs = fallbackPitches.map((pitch, idx) => {
802
+ const start = idx * 0.28;
803
+ return {
804
+ note: clampMidiNote(pitch),
805
+ start,
806
+ end: start + 0.24,
807
+ velocity: Math.round(callProfile.avgVelocity)
808
+ };
809
+ });
810
+ }
811
+
812
+ const polished = [];
813
+ let previousStart = -1;
814
+ for (let i = 0; i < notePairs.length; i++) {
815
+ const source = notePairs[i];
816
+ let note = moveByOctaveTowardTarget(source.note, callProfile.center);
817
+ if (polished.length > 0) {
818
+ const prev = polished[polished.length - 1].note;
819
+ while (Math.abs(note - prev) > 7) {
820
+ note += note > prev ? -12 : 12;
821
+ }
822
+ note = clampMidiNote(note);
823
+ }
824
+
825
+ const quantizedStart = Math.max(0, quantizeToStep(source.start, 0.125));
826
+ const start = Math.max(quantizedStart, previousStart + 0.06);
827
+ previousStart = start;
828
+
829
+ const rawDur = Math.max(0.1, source.end - source.start);
830
+ const duration = Math.max(0.12, Math.min(0.9, quantizeToStep(rawDur, 0.0625)));
831
+ const velocity = Math.round(
832
+ (Math.max(1, Math.min(127, source.velocity)) * 0.6)
833
+ + (callProfile.avgVelocity * 0.4)
834
+ );
835
+
836
+ polished.push({
837
+ note,
838
+ start,
839
+ end: start + duration,
840
+ velocity: Math.max(1, Math.min(127, velocity))
841
+ });
842
+ }
843
+
844
+ if (polished.length > 0) {
845
+ polished[polished.length - 1].note = moveByOctaveTowardTarget(
846
+ polished[polished.length - 1].note,
847
+ callProfile.finalPitch
848
+ );
849
+ }
850
+
851
+ let out = trimNotePairs(polished, lengthPreset.maxNotes, lengthPreset.maxDurationSec);
852
+ if (stylePreset.addMotifEcho) {
853
+ out = appendMotifEcho(out, callEvents, lengthPreset.maxDurationSec);
854
+ }
855
+ if (stylePreset.playfulShift) {
856
+ out = applyPlayfulShift(out);
857
+ }
858
+ out = smoothPairLeaps(out, 6);
859
+ out = trimNotePairs(out, lengthPreset.maxNotes, lengthPreset.maxDurationSec);
860
+ return out;
861
+ }
862
+
863
+ function buildProcessedAIResponse(rawResponseEvents, callEvents) {
864
+ const mode = getSelectedResponseMode();
865
+ const lengthPreset = getSelectedResponseLengthPreset();
866
+
867
+ if (mode.modeId === 'raw_godzilla') {
868
+ return {
869
+ label: `${mode.label} (${lengthPreset.label})`,
870
+ events: normalizeEventsToZero(rawResponseEvents || [])
871
+ };
872
+ }
873
+
874
+ if (mode.modeId === 'musical_polish') {
875
+ return {
876
+ label: `${mode.label} (${lengthPreset.label})`,
877
+ events: notePairsToEvents(applyMusicalPolish(rawResponseEvents || [], callEvents, lengthPreset))
878
+ };
879
+ }
880
+
881
+ const styled = applyResponseStyle(rawResponseEvents || [], callEvents, lengthPreset);
882
+ return {
883
+ label: `${mode.label} / ${styled.styleLabel} (${lengthPreset.label})`,
884
+ events: styled.events
885
+ };
886
+ }
887
+
888
  // =============================================================================
889
  // TERMINAL LOGGING
890
  // =============================================================================
 
1000
  return keyboardEl.querySelector(`.key[data-midi="${midiNote}"]`);
1001
  }
1002
 
1003
+ function bindGlobalKeyboardHandlers() {
1004
+ document.addEventListener('keydown', async (ev) => {
1005
+ if (!keyboardToggle || !keyboardToggle.checked) return;
1006
+ const key = ev.key.toLowerCase();
1007
+ const activeKeyMap = window.keyMapFromServer || keyMap; // Use server config if available, fallback to hardcoded
1008
+ if (!activeKeyMap[key] || pressedKeys.has(key)) return;
1009
+
1010
+ ev.preventDefault();
1011
+ pressedKeys.add(key);
1012
+
1013
+ await Tone.start();
1014
+
1015
+ const midiNote = activeKeyMap[key];
1016
+ const keyEl = getKeyElement(midiNote);
1017
+ if (keyEl) keyEl.style.filter = 'brightness(0.85)';
1018
+ noteOn(midiNote, 100);
1019
+ });
1020
+
1021
+ document.addEventListener('keyup', (ev) => {
1022
+ if (!keyboardToggle || !keyboardToggle.checked) return;
1023
+ const key = ev.key.toLowerCase();
1024
+ const activeKeyMap = window.keyMapFromServer || keyMap; // Use server config if available, fallback to hardcoded
1025
+ if (!activeKeyMap[key] || !pressedKeys.has(key)) return;
1026
+
1027
+ ev.preventDefault();
1028
+ pressedKeys.delete(key);
1029
+
1030
+ const midiNote = activeKeyMap[key];
1031
+ const keyEl = getKeyElement(midiNote);
1032
+ if (keyEl) keyEl.style.filter = '';
1033
+ noteOff(midiNote);
1034
+ });
1035
+ }
1036
 
1037
  // =============================================================================
1038
  // MOUSE/TOUCH INPUT
 
1091
  saveBtn.disabled = true;
1092
 
1093
  try {
1094
+ const payload = await callGradioBridge('save_midi', events);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1095
  if (!payload || payload.error || !payload.midi_base64) {
1096
  throw new Error(payload && payload.error ? payload.error : 'Invalid API response');
1097
  }
 
1118
  }
1119
  }
1120
 
1121
+ function clearGameTimers() {
1122
+ if (gameTurnTimerId !== null) {
1123
+ clearInterval(gameTurnTimerId);
1124
+ gameTurnTimerId = null;
1125
+ }
1126
+ if (gameTurnTimeoutId !== null) {
1127
+ clearTimeout(gameTurnTimeoutId);
1128
+ gameTurnTimeoutId = null;
1129
+ }
1130
+ }
1131
 
1132
+ async function processEventsThroughEngine(inputEvents, options = {}) {
1133
+ const selectedEngineId = engineSelect.value;
1134
+ if (!selectedEngineId || selectedEngineId === 'parrot') {
1135
+ return { events: inputEvents };
1136
+ }
1137
 
1138
+ const requestOptions = { ...options };
1139
+ const runtimeMode = getSelectedRuntime();
1140
+ if (
1141
+ selectedEngineId === 'godzilla_continue'
1142
+ && typeof requestOptions.generate_tokens !== 'number'
1143
+ ) {
1144
+ requestOptions.generate_tokens = RESPONSE_LENGTH_PRESETS.medium.generateTokens;
 
 
 
 
 
 
 
 
1145
  }
 
1146
 
1147
+ let bridgeAction = 'process_engine_cpu';
1148
+ if (selectedEngineId === 'godzilla_continue') {
1149
+ if (runtimeMode === 'gpu' || runtimeMode === 'auto') {
1150
+ bridgeAction = 'process_engine_gpu';
1151
+ }
1152
+ }
 
 
1153
 
1154
+ const requestPayload = {
1155
+ engine_id: selectedEngineId,
1156
+ events: inputEvents,
1157
+ options: requestOptions
1158
+ };
1159
 
1160
+ let result;
1161
+ try {
1162
+ result = await callGradioBridge(bridgeAction, requestPayload);
1163
+ } catch (err) {
1164
+ if (
1165
+ selectedEngineId === 'godzilla_continue'
1166
+ && runtimeMode === 'auto'
1167
+ && bridgeAction === 'process_engine_gpu'
1168
+ ) {
1169
+ logToTerminal('Runtime auto: ZeroGPU failed, retrying on CPU.', 'timestamp');
1170
+ result = await callGradioBridge('process_engine_cpu', requestPayload);
1171
+ } else {
1172
+ throw err;
1173
+ }
1174
+ }
1175
 
1176
+ if (
1177
+ result
1178
+ && result.error
1179
+ && selectedEngineId === 'godzilla_continue'
1180
+ && runtimeMode === 'auto'
1181
+ && bridgeAction === 'process_engine_gpu'
1182
+ ) {
1183
+ logToTerminal(`Runtime auto: ZeroGPU error (${result.error}), retrying on CPU.`, 'timestamp');
1184
+ result = await callGradioBridge('process_engine_cpu', requestPayload);
1185
+ }
1186
 
1187
+ if (result && result.error) {
1188
+ throw new Error(result.error);
1189
+ }
1190
+ if (!result || !Array.isArray(result.events)) {
1191
+ throw new Error('Engine returned no events');
1192
+ }
1193
+ if (result.warning) {
1194
+ logToTerminal(`ENGINE WARNING: ${result.warning}`, 'timestamp');
1195
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1196
 
1197
+ return result;
1198
+ }
1199
+
1200
+ function playEvents(eventsToPlay, { logSymbols = true, useAISynth = false } = {}) {
1201
+ return new Promise((resolve) => {
1202
+ if (!Array.isArray(eventsToPlay) || eventsToPlay.length === 0) {
1203
+ resolve();
1204
+ return;
 
 
 
 
 
 
 
 
 
 
1205
  }
1206
+
1207
+ const playbackSynth = useAISynth && aiSynth ? aiSynth : synth;
 
1208
  let eventIndex = 0;
1209
+
1210
  const playEvent = () => {
1211
+ if (eventIndex >= eventsToPlay.length) {
1212
+ if (playbackSynth) playbackSynth.releaseAll();
 
 
 
 
 
1213
  keyboardEl.querySelectorAll('.key').forEach(k => {
1214
  k.style.filter = '';
1215
  });
1216
+ resolve();
 
 
 
 
 
 
 
1217
  return;
1218
  }
1219
+
1220
+ const event = eventsToPlay[eventIndex];
1221
+ const nextTime = eventIndex + 1 < eventsToPlay.length
1222
+ ? eventsToPlay[eventIndex + 1].time
1223
  : event.time;
1224
+
1225
  if (event.type === 'note_on') {
1226
  const freq = Tone.Frequency(event.note, "midi").toFrequency();
1227
+ if (playbackSynth) {
1228
+ playbackSynth.triggerAttack(freq, undefined, event.velocity / 127);
1229
+ }
1230
+ if (logSymbols) {
1231
+ const noteName = midiToNoteName(event.note);
1232
+ logToTerminal(
1233
+ `[${event.time.toFixed(3)}s] ► ${noteName} (${event.note})`,
1234
+ 'note-on'
1235
+ );
1236
+ }
1237
  const keyEl = getKeyElement(event.note);
1238
  if (keyEl) keyEl.style.filter = 'brightness(0.7)';
1239
  } else if (event.type === 'note_off') {
1240
  const freq = Tone.Frequency(event.note, "midi").toFrequency();
1241
+ if (playbackSynth) {
1242
+ playbackSynth.triggerRelease(freq);
1243
+ }
1244
+ if (logSymbols) {
1245
+ const noteName = midiToNoteName(event.note);
1246
+ logToTerminal(
1247
+ `[${event.time.toFixed(3)}s] ◄ ${noteName}`,
1248
+ 'note-off'
1249
+ );
1250
+ }
1251
  const keyEl = getKeyElement(event.note);
1252
  if (keyEl) keyEl.style.filter = '';
1253
  }
1254
+
1255
  eventIndex++;
1256
  const deltaTime = Math.max(0, nextTime - event.time);
1257
  setTimeout(playEvent, deltaTime * 1000);
1258
  };
1259
+
1260
  playEvent();
1261
+ });
1262
+ }
1263
+
1264
+ async function startGameLoop() {
1265
+ if (gameActive) return;
1266
+ await Tone.start();
1267
+
1268
+ if (engineSelect.querySelector('option[value="godzilla_continue"]')) {
1269
+ engineSelect.value = 'godzilla_continue';
1270
+ selectedEngine = 'godzilla_continue';
 
 
 
1271
  }
 
1272
 
1273
+ gameActive = true;
1274
+ gameTurn = 0;
1275
+ gameStartBtn.disabled = true;
1276
+ gameStopBtn.disabled = false;
1277
+ recordBtn.disabled = true;
1278
+ stopBtn.disabled = true;
1279
+ playbackBtn.disabled = true;
1280
+ saveBtn.disabled = true;
1281
+ statusEl.textContent = 'Game started';
1282
 
1283
+ logToTerminal('', '');
1284
+ logToTerminal('🎮 CALL & RESPONSE GAME STARTED', 'timestamp');
1285
+ logToTerminal(
1286
+ `Flow: ${USER_TURN_LIMIT_SEC}s call, AI response, repeat until you stop.`,
1287
+ 'timestamp'
1288
+ );
1289
+ const stylePreset = getSelectedStylePreset();
1290
+ const modePreset = getSelectedResponseMode();
1291
+ const lengthPreset = getSelectedResponseLengthPreset();
1292
+ const decodingPreset = getSelectedDecodingOptions();
1293
+ logToTerminal(
1294
+ `AI mode: ${modePreset.label} | length: ${lengthPreset.label} | style: ${stylePreset.label}`,
1295
+ 'timestamp'
1296
+ );
1297
+ logToTerminal(
1298
+ `Decoding: temp=${decodingPreset.temperature} top_p=${decodingPreset.top_p} candidates=${decodingPreset.num_candidates}`,
1299
+ 'timestamp'
1300
+ );
1301
+ logToTerminal('', '');
1302
+
1303
+ await startUserTurn();
1304
+ }
1305
+
1306
+ function stopGameLoop(reason = 'Game stopped') {
1307
+ clearGameTimers();
1308
+ if (recording) {
1309
+ stopRecord();
1310
  }
1311
+ gameActive = false;
1312
+ gameStartBtn.disabled = false;
1313
+ gameStopBtn.disabled = true;
1314
+ recordBtn.disabled = false;
1315
+ stopBtn.disabled = true;
1316
+ playbackBtn.disabled = events.length === 0;
1317
+ saveBtn.disabled = events.length === 0;
1318
+ statusEl.textContent = reason;
1319
+ if (synth) synth.releaseAll();
1320
+ if (aiSynth) aiSynth.releaseAll();
1321
  keyboardEl.querySelectorAll('.key').forEach(k => {
1322
  k.style.filter = '';
1323
  });
1324
+ logToTerminal(`🎮 ${reason}`, 'timestamp');
1325
+ }
1326
+
1327
+ async function startUserTurn() {
1328
+ if (!gameActive) return;
1329
+ clearGameTimers();
1330
+
1331
+ gameTurn += 1;
1332
+ beginRecord();
1333
+ gameStartBtn.disabled = true;
1334
+ gameStopBtn.disabled = false;
1335
+ recordBtn.disabled = true;
1336
+ stopBtn.disabled = true;
1337
+ playbackBtn.disabled = true;
1338
+ saveBtn.disabled = true;
1339
+
1340
+ let remaining = USER_TURN_LIMIT_SEC;
1341
+ statusEl.textContent = `Turn ${gameTurn}: your call (${remaining}s)`;
1342
+ logToTerminal(`Turn ${gameTurn}: your call starts now`, 'timestamp');
1343
+
1344
+ gameTurnTimerId = setInterval(() => {
1345
+ if (!gameActive) return;
1346
+ remaining -= 1;
1347
+ if (remaining > 0) {
1348
+ statusEl.textContent = `Turn ${gameTurn}: your call (${remaining}s)`;
1349
+ }
1350
+ }, 1000);
1351
+
1352
+ gameTurnTimeoutId = setTimeout(() => {
1353
+ void finishUserTurn();
1354
+ }, USER_TURN_LIMIT_SEC * 1000);
1355
+ }
1356
+
1357
+ async function finishUserTurn() {
1358
+ if (!gameActive) return;
1359
+ clearGameTimers();
1360
+ if (recording) stopRecord();
1361
+ recordBtn.disabled = true;
1362
+ stopBtn.disabled = true;
1363
+ playbackBtn.disabled = true;
1364
+ saveBtn.disabled = true;
1365
+
1366
+ const callEvents = [...events];
1367
+ if (callEvents.length === 0) {
1368
+ statusEl.textContent = `Turn ${gameTurn}: no notes, try again`;
1369
+ logToTerminal('No notes captured, restarting your turn...', 'timestamp');
1370
+ setTimeout(() => {
1371
+ void startUserTurn();
1372
+ }, GAME_NEXT_TURN_DELAY_MS);
1373
+ return;
1374
+ }
1375
+
1376
+ try {
1377
+ statusEl.textContent = `Turn ${gameTurn}: AI thinking...`;
1378
+ logToTerminal(`Turn ${gameTurn}: AI is thinking...`, 'timestamp');
1379
+
1380
+ const lengthPreset = getSelectedResponseLengthPreset();
1381
+ const promptEvents = normalizeEventsToZero(callEvents);
1382
+ const decodingOptions = getSelectedDecodingOptions();
1383
+ const result = await processEventsThroughEngine(promptEvents, {
1384
+ generate_tokens: lengthPreset.generateTokens,
1385
+ ...decodingOptions
1386
+ });
1387
+ const processedResponse = buildProcessedAIResponse(result.events || [], callEvents);
1388
+ const aiEvents = processedResponse.events;
1389
+
1390
+ if (!gameActive) return;
1391
+
1392
+ statusEl.textContent = `Turn ${gameTurn}: AI responds`;
1393
+ logToTerminal(
1394
+ `Turn ${gameTurn}: AI response (${processedResponse.label})`,
1395
+ 'timestamp'
1396
+ );
1397
+ await playEvents(aiEvents, { useAISynth: true });
1398
+
1399
+ if (!gameActive) return;
1400
+ setTimeout(() => {
1401
+ void startUserTurn();
1402
+ }, GAME_NEXT_TURN_DELAY_MS);
1403
+ } catch (err) {
1404
+ console.error('Game turn error:', err);
1405
+ logToTerminal(`ENGINE ERROR: ${err.message}`, 'timestamp');
1406
+ stopGameLoop(`Game stopped: ${err.message}`);
1407
+ }
1408
+ }
1409
+
1410
+ // =============================================================================
1411
+ // EVENT LISTENERS
1412
+ // =============================================================================
1413
+
1414
+ let listenersBound = false;
1415
+
1416
+ function bindUIEventListeners() {
1417
+ if (listenersBound) return;
1418
+ listenersBound = true;
1419
+
1420
+ bindGlobalKeyboardHandlers();
1421
+
1422
+ if (instrumentSelect) {
1423
+ instrumentSelect.addEventListener('change', () => {
1424
+ loadInstrument(instrumentSelect.value);
1425
+ });
1426
+ }
1427
+
1428
+ if (aiInstrumentSelect) {
1429
+ aiInstrumentSelect.addEventListener('change', () => {
1430
+ loadAIInstrument(aiInstrumentSelect.value);
1431
+ logToTerminal(`AI voice switched to: ${aiInstrumentSelect.value}`, 'timestamp');
1432
+ });
1433
+ }
1434
+
1435
+ if (keyboardToggle) {
1436
+ keyboardToggle.addEventListener('change', () => {
1437
+ if (keyboardToggle.checked) {
1438
+ // Show keyboard shortcuts
1439
+ keyboardEl.classList.add('shortcuts-visible');
1440
+ } else {
1441
+ // Hide keyboard shortcuts
1442
+ keyboardEl.classList.remove('shortcuts-visible');
1443
+ // Release all currently pressed keyboard keys
1444
+ pressedKeys.forEach(key => {
1445
+ const activeKeyMap = window.keyMapFromServer || keyMap;
1446
+ const midiNote = activeKeyMap[key];
1447
+ const keyEl = getKeyElement(midiNote);
1448
+ if (keyEl) keyEl.style.filter = '';
1449
+ noteOff(midiNote);
1450
+ });
1451
+ pressedKeys.clear();
1452
+ }
1453
+ });
1454
+ }
1455
+
1456
+ if (clearTerminal) {
1457
+ clearTerminal.addEventListener('click', () => {
1458
+ terminal.innerHTML = '';
1459
+ logToTerminal('╔═══════════════════════════════════════════════════════╗', 'timestamp');
1460
+ logToTerminal('║ 🎹 MIDI MONITOR INITIALIZED 🎹 ║', 'timestamp');
1461
+ logToTerminal('╚═══════════════════════════════════════════════════════╝', 'timestamp');
1462
+ logToTerminal('Ready to capture MIDI events...', 'timestamp');
1463
+ logToTerminal('', '');
1464
+ });
1465
+ }
1466
+
1467
+ if (recordBtn) {
1468
+ recordBtn.addEventListener('click', async () => {
1469
+ if (gameActive) return;
1470
+ await Tone.start();
1471
+ beginRecord();
1472
+ });
1473
+ }
1474
+
1475
+ if (stopBtn) {
1476
+ stopBtn.addEventListener('click', () => {
1477
+ if (gameActive) return;
1478
+ stopRecord();
1479
+ });
1480
+ }
1481
+
1482
+ if (engineSelect) {
1483
+ engineSelect.addEventListener('change', (e) => {
1484
+ selectedEngine = e.target.value;
1485
+ logToTerminal(`Engine switched to: ${selectedEngine}`, 'timestamp');
1486
+ });
1487
+ }
1488
+
1489
+ if (runtimeSelect) {
1490
+ runtimeSelect.addEventListener('change', () => {
1491
+ const mode = getSelectedRuntime();
1492
+ const runtimeLabel = mode === 'gpu' ? 'ZeroGPU' : (mode === 'auto' ? 'Auto (GPU->CPU)' : 'CPU');
1493
+ logToTerminal(`Runtime switched to: ${runtimeLabel}`, 'timestamp');
1494
+ });
1495
+ }
1496
+
1497
+ if (responseStyleSelect) {
1498
+ responseStyleSelect.addEventListener('change', () => {
1499
+ const preset = getSelectedStylePreset();
1500
+ logToTerminal(`AI style switched to: ${preset.label}`, 'timestamp');
1501
+ });
1502
+ }
1503
+
1504
+ if (responseModeSelect) {
1505
+ responseModeSelect.addEventListener('change', () => {
1506
+ const mode = getSelectedResponseMode();
1507
+ const decode = getSelectedDecodingOptions();
1508
+ logToTerminal(
1509
+ `Response mode switched to: ${mode.label} (temp=${decode.temperature}, top_p=${decode.top_p}, candidates=${decode.num_candidates})`,
1510
+ 'timestamp'
1511
+ );
1512
+ });
1513
+ }
1514
+
1515
+ if (responseLengthSelect) {
1516
+ responseLengthSelect.addEventListener('change', () => {
1517
+ const lengthPreset = getSelectedResponseLengthPreset();
1518
+ logToTerminal(
1519
+ `Response length switched to: ${lengthPreset.label} (${lengthPreset.generateTokens} tokens)`,
1520
+ 'timestamp'
1521
+ );
1522
+ });
1523
+ }
1524
+
1525
+ if (gameStartBtn) {
1526
+ gameStartBtn.addEventListener('click', () => {
1527
+ void startGameLoop();
1528
+ });
1529
+ }
1530
+
1531
+ if (gameStopBtn) {
1532
+ gameStopBtn.addEventListener('click', () => {
1533
+ stopGameLoop('Game stopped');
1534
+ });
1535
+ }
1536
+
1537
+ if (playbackBtn) {
1538
+ playbackBtn.addEventListener('click', async () => {
1539
+ if (gameActive) return alert('Stop the game first.');
1540
+ if (events.length === 0) return alert('No recording to play back');
1541
+
1542
+ // Ensure all notes are off before starting playback
1543
+ if (synth) {
1544
+ synth.releaseAll();
1545
+ }
1546
+ keyboardEl.querySelectorAll('.key').forEach(k => {
1547
+ k.style.filter = '';
1548
+ });
1549
+
1550
+ statusEl.textContent = 'Playing back...';
1551
+ playbackBtn.disabled = true;
1552
+ recordBtn.disabled = true;
1553
+
1554
+ logToTerminal('', '');
1555
+ logToTerminal('♫♫♫ PLAYBACK STARTED ♫♫♫', 'timestamp');
1556
+ logToTerminal('', '');
1557
+
1558
+ try {
1559
+ let engineOptions = {};
1560
+ if (engineSelect.value === 'godzilla_continue') {
1561
+ const lengthPreset = getSelectedResponseLengthPreset();
1562
+ engineOptions = {
1563
+ generate_tokens: lengthPreset.generateTokens,
1564
+ ...getSelectedDecodingOptions()
1565
+ };
1566
+ }
1567
+ const result = await processEventsThroughEngine(events, engineOptions);
1568
+ let processedEvents = result.events || [];
1569
+ if (engineSelect.value === 'godzilla_continue') {
1570
+ const processedResponse = buildProcessedAIResponse(processedEvents, events);
1571
+ processedEvents = processedResponse.events;
1572
+ logToTerminal(`Playback response mode: ${processedResponse.label}`, 'timestamp');
1573
+ }
1574
+ await playEvents(processedEvents, {
1575
+ useAISynth: engineSelect.value !== 'parrot'
1576
+ });
1577
+
1578
+ statusEl.textContent = 'Playback complete';
1579
+ playbackBtn.disabled = false;
1580
+ recordBtn.disabled = false;
1581
+ logToTerminal('', '');
1582
+ logToTerminal('♫♫♫ PLAYBACK FINISHED ♫♫♫', 'timestamp');
1583
+ logToTerminal('', '');
1584
+ } catch (err) {
1585
+ console.error('Playback error:', err);
1586
+ statusEl.textContent = 'Playback error: ' + err.message;
1587
+ logToTerminal(`ENGINE ERROR: ${err.message}`, 'timestamp');
1588
+ playbackBtn.disabled = false;
1589
+ recordBtn.disabled = false;
1590
+
1591
+ // Ensure all notes are off on error
1592
+ if (synth) {
1593
+ synth.releaseAll();
1594
+ }
1595
+ keyboardEl.querySelectorAll('.key').forEach(k => {
1596
+ k.style.filter = '';
1597
+ });
1598
+ }
1599
+ });
1600
+ }
1601
+
1602
+ if (saveBtn) {
1603
+ saveBtn.addEventListener('click', () => saveMIDI());
1604
+ }
1605
+
1606
+ if (panicBtn) {
1607
+ panicBtn.addEventListener('click', () => {
1608
+ // Stop all notes immediately
1609
+ if (synth) {
1610
+ synth.releaseAll();
1611
+ }
1612
+ if (aiSynth) {
1613
+ aiSynth.releaseAll();
1614
+ }
1615
+
1616
+ // Clear all pressed keys
1617
+ pressedKeys.clear();
1618
+
1619
+ // Reset all visual key highlights
1620
+ keyboardEl.querySelectorAll('.key').forEach(k => {
1621
+ k.style.filter = '';
1622
+ });
1623
+
1624
+ logToTerminal('🚨 PANIC - All notes stopped', 'timestamp');
1625
+ });
1626
+ }
1627
+ }
1628
 
1629
  // =============================================================================
1630
  // =============================================================================
 
1632
  // =============================================================================
1633
 
1634
  async function init() {
1635
+ await waitForKeyboardUIElements();
1636
+ await waitForBridgeElements();
1637
+ cacheUIElements();
1638
+ bindUIEventListeners();
1639
+
1640
  // First, load configuration from server
1641
  await initializeFromConfig();
1642
+
1643
+ if (responseStyleSelect && !responseStyleSelect.value) {
1644
+ responseStyleSelect.value = 'melodic';
1645
+ }
1646
+ if (responseModeSelect && !responseModeSelect.value) {
1647
+ responseModeSelect.value = 'raw_godzilla';
1648
+ }
1649
+ if (responseLengthSelect && !responseLengthSelect.value) {
1650
+ responseLengthSelect.value = 'short';
1651
+ }
1652
+ if (runtimeSelect && !runtimeSelect.value) {
1653
+ runtimeSelect.value = 'auto';
1654
+ }
1655
+ if (aiInstrumentSelect && !aiInstrumentSelect.value) {
1656
+ aiInstrumentSelect.value = 'fm';
1657
+ }
1658
 
1659
  // Then load default instrument (synth)
1660
  loadInstrument('synth');
1661
+ loadAIInstrument(aiInstrumentSelect ? aiInstrumentSelect.value : 'fm');
1662
 
1663
  // Setup keyboard event listeners and UI
1664
  attachPointerEvents();
1665
  initTerminal();
1666
+ const runtimeMode = getSelectedRuntime();
1667
+ const runtimeLabel = runtimeMode === 'gpu' ? 'ZeroGPU' : (runtimeMode === 'auto' ? 'Auto (GPU->CPU)' : 'CPU');
1668
+ logToTerminal(`Runtime mode: ${runtimeLabel}`, 'timestamp');
1669
  // Set initial button states
1670
  recordBtn.disabled = false;
1671
  stopBtn.disabled = true;
1672
  saveBtn.disabled = true;
1673
  playbackBtn.disabled = true;
1674
+ gameStartBtn.disabled = false;
1675
+ gameStopBtn.disabled = true;
1676
  }
1677
 
1678
  // Start the application when DOM is ready
1679
+ if (document.readyState === 'loading') {
1680
+ document.addEventListener('DOMContentLoaded', () => {
1681
+ void init();
1682
+ });
1683
+ } else {
1684
+ void init();
1685
+ }
static/styles.css CHANGED
@@ -1,418 +1,722 @@
1
- /* Virtual MIDI Keyboard - Retro Synth Theme */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- /* Layout */
4
- body {
5
- font-family: 'Courier New', 'Consolas', monospace;
6
- padding: 0;
7
  margin: 0;
8
- background: linear-gradient(180deg, #0a0a0f 0%, #1a0a1f 100%);
9
- color: #e0e0ff;
10
  min-height: 100vh;
11
- overflow-x: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
 
14
- /* Welcome Header */
15
  .welcome-header {
16
  text-align: center;
17
- padding: 30px 20px 20px;
18
- background: linear-gradient(180deg, rgba(138, 43, 226, 0.1) 0%, transparent 100%);
19
- border-bottom: 2px solid rgba(138, 43, 226, 0.3);
20
- }
21
-
22
- .neon-text {
23
- font-size: 2.5em;
24
- font-weight: bold;
25
- margin: 0 0 10px;
26
- color: #ff00ff;
27
- text-shadow:
28
- 0 0 10px #ff00ff,
29
- 0 0 20px #ff00ff,
30
- 0 0 30px #ff00ff,
31
- 0 0 40px #8b00ff,
32
- 0 0 70px #8b00ff;
33
- letter-spacing: 2px;
34
- }
35
-
36
- #mainContainer {
37
- display: flex;
38
- flex-direction: column;
39
- align-items: center;
40
- gap: 30px;
41
- padding: 20px;
42
- max-width: 1400px;
43
- margin: 0 auto;
44
  }
45
 
46
- .keyboard-section {
47
- width: 100%;
48
- display: flex;
49
- flex-direction: column;
50
- align-items: center;
51
- gap: 15px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
 
54
- .monitor-section {
 
55
  width: 100%;
56
- max-width: 1200px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
 
59
- /* Keyboard */
60
- #keyboard {
61
- display: flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  user-select: none;
63
  touch-action: none;
64
- padding: 20px;
65
- background: rgba(0, 0, 0, 0.4);
66
- border-radius: 15px;
67
- border: 2px solid rgba(138, 43, 226, 0.5);
68
- box-shadow:
69
- 0 0 20px rgba(138, 43, 226, 0.3),
70
- inset 0 0 30px rgba(0, 0, 0, 0.5);
71
- }
72
-
73
- .key {
74
- width: 42px;
75
- height: 180px;
76
- border: 2px solid #ff00ff;
77
- margin: 0 1px;
78
- background: linear-gradient(180deg, #fff 0%, #f0f0ff 100%);
79
- position: relative;
80
- display: flex;
81
- align-items: flex-end;
82
- justify-content: center;
83
  cursor: pointer;
84
- touch-action: none;
85
- transition: all 0.1s ease;
86
- box-shadow:
87
- 0 4px 8px rgba(0, 0, 0, 0.3),
88
- inset 0 -2px 5px rgba(0, 0, 0, 0.1);
89
- color: #2a0050;
90
- font-weight: bold;
91
  }
92
 
93
- .key .shortcut-hint {
94
- font-size: 10px;
95
- opacity: 0;
96
- color: #5a00cc;
97
- font-weight: bold;
98
- text-shadow: 0 0 2px rgba(90, 0, 204, 0.3);
99
- transition: opacity 0.2s ease;
100
  }
101
 
102
  .key.black {
103
- color: #00ffff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
105
 
106
  .key.black .shortcut-hint {
107
- color: #00ffff;
108
- text-shadow: 0 0 3px rgba(0, 255, 255, 0.5);
109
  }
110
 
111
  .shortcuts-visible .key .shortcut-hint {
112
  opacity: 1;
113
  }
114
 
115
- .key:hover {
116
- box-shadow:
117
- 0 0 15px rgba(255, 0, 255, 0.5),
118
- inset 0 -2px 5px rgba(0, 0, 0, 0.1);
119
- }
120
-
121
- .key.black {
122
- width: 30px;
123
- height: 110px;
124
- background: linear-gradient(180deg, #1a0a2e 0%, #0a0a1f 100%);
125
- border: 2px solid #8b00ff;
126
- color: #00ffff;
127
- margin-left: -15px;
128
- margin-right: -15px;
129
- z-index: 2;
130
  position: relative;
131
- box-shadow:
132
- 0 4px 8px rgba(0, 0, 0, 0.5),
133
- inset 0 -2px 5px rgba(138, 43, 226, 0.3);
 
 
 
 
 
 
 
 
 
134
  }
135
 
136
- .key.black:hover {
137
- box-shadow:
138
- 0 0 15px rgba(0, 255, 255, 0.5),
139
- inset 0 -2px 5px rgba(138, 43, 226, 0.3);
 
 
140
  }
141
 
142
- /* Controls */
143
- .controls {
144
- display: flex;
145
- gap: 10px;
146
- align-items: center;
147
- flex-wrap: wrap;
148
- justify-content: center;
149
- padding: 15px;
150
- background: rgba(0, 0, 0, 0.3);
151
- border-radius: 10px;
152
- border: 1px solid rgba(138, 43, 226, 0.3);
 
 
 
 
 
 
 
 
 
 
 
153
  }
154
 
155
- .controls label {
156
- color: #00ffff;
157
- font-weight: bold;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  display: flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  align-items: center;
160
- gap: 5px;
 
 
161
  }
162
 
163
  select,
164
- button {
165
- padding: 8px 16px;
166
- background: linear-gradient(180deg, #8b00ff 0%, #5a00cc 100%);
167
- color: #00ffff;
168
- border: 2px solid #ff00ff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  border-radius: 5px;
170
- font-family: 'Courier New', monospace;
171
- font-weight: bold;
 
172
  cursor: pointer;
173
- transition: all 0.2s ease;
174
- box-shadow: 0 0 10px rgba(138, 43, 226, 0.3);
175
  }
176
 
177
- select:hover,
178
- button:hover:not(:disabled) {
179
- background: linear-gradient(180deg, #a000ff 0%, #7000ee 100%);
180
- box-shadow:
181
- 0 0 20px rgba(255, 0, 255, 0.6),
182
- 0 0 30px rgba(138, 43, 226, 0.4);
183
- transform: translateY(-1px);
 
 
 
 
 
184
  }
185
 
186
- button:active:not(:disabled) {
187
- transform: translateY(0);
188
- box-shadow: 0 0 10px rgba(138, 43, 226, 0.3);
 
 
189
  }
190
 
191
- button:disabled {
192
- opacity: 0.3;
193
- cursor: not-allowed;
194
  }
195
 
196
- input[type="checkbox"] {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  cursor: pointer;
198
- width: 18px;
199
- height: 18px;
200
- accent-color: #ff00ff;
201
  }
202
 
203
- #status {
204
- color: #00ff00;
205
- font-weight: bold;
206
- text-shadow: 0 0 5px #00ff00;
207
- padding: 5px 10px;
208
- background: rgba(0, 0, 0, 0.5);
209
- border-radius: 5px;
 
 
 
 
 
 
 
 
 
 
 
210
  }
211
 
212
- /* MIDI Terminal */
213
- .terminal-header {
214
- display: flex;
215
- justify-content: space-between;
216
- align-items: center;
217
- padding: 10px 15px;
218
- background: linear-gradient(90deg, rgba(138, 43, 226, 0.2) 0%, rgba(0, 255, 255, 0.1) 100%);
219
- border: 2px solid rgba(138, 43, 226, 0.5);
220
- border-bottom: none;
221
- border-radius: 10px 10px 0 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
223
 
224
  .terminal-header h4 {
225
  margin: 0;
226
- color: #ff00ff;
227
- font-size: 1.1em;
228
- letter-spacing: 2px;
229
- text-shadow: 0 0 10px #ff00ff;
230
- }
231
-
232
- .terminal-header button {
233
- padding: 6px 12px;
234
- font-size: 0.85em;
235
- }
236
-
237
- #terminal {
238
- background: #0a0a0f;
239
- color: #00ff00;
240
- font-family: 'Courier New', monospace;
241
- font-size: 13px;
242
- padding: 15px;
243
- height: 250px;
244
- overflow-y: auto;
245
- border: 2px solid rgba(138, 43, 226, 0.5);
246
- border-top: 1px solid rgba(138, 43, 226, 0.3);
247
- border-radius: 0 0 10px 10px;
248
  white-space: pre-wrap;
249
  word-wrap: break-word;
250
- box-shadow:
251
- inset 0 0 30px rgba(0, 0, 0, 0.8),
252
- 0 0 20px rgba(138, 43, 226, 0.2);
 
 
 
 
253
  }
254
 
255
- #terminal .note-on {
256
- color: #00ff00;
257
- text-shadow: 0 0 5px #00ff00;
258
  }
259
 
260
- #terminal .note-off {
261
- color: #ff00ff;
262
- text-shadow: 0 0 5px #ff00ff;
263
  }
264
 
265
- #terminal .timestamp {
266
- color: #00ffff;
267
- text-shadow: 0 0 3px #00ffff;
268
  }
269
 
270
- /* Scrollbar styling */
271
  #terminal::-webkit-scrollbar {
272
  width: 10px;
273
  }
274
 
275
  #terminal::-webkit-scrollbar-track {
276
- background: rgba(0, 0, 0, 0.5);
277
  }
278
 
279
  #terminal::-webkit-scrollbar-thumb {
280
- background: linear-gradient(180deg, #ff00ff 0%, #8b00ff 100%);
281
- border-radius: 5px;
282
  }
283
 
284
- #terminal::-webkit-scrollbar-thumb:hover {
285
- background: linear-gradient(180deg, #ff33ff 0%, #aa00ff 100%);
 
 
 
 
 
 
 
 
286
  }
287
- /* =============================================================================
288
- RESPONSIVE DESIGN - MOBILE & TABLET
289
- ============================================================================= */
290
 
291
- /* Tablet (landscape) */
292
- @media (max-width: 1024px) {
293
- .welcome-header h1 {
294
- font-size: 2em;
295
  }
296
-
297
- .key {
298
- width: 35px;
299
- height: 150px;
300
  }
301
-
302
- .key.black {
303
- width: 25px;
304
- height: 95px;
305
- margin-left: -12px;
306
- margin-right: -12px;
307
  }
308
  }
309
 
310
- /* Mobile (portrait and small screens) */
311
- @media (max-width: 768px) {
312
- body {
313
- padding: 10px;
314
  }
315
-
316
- .welcome-header {
317
- padding: 15px;
318
- margin-bottom: 15px;
319
  }
320
-
321
- .welcome-header h1 {
322
- font-size: 1.8em;
323
- letter-spacing: 3px;
 
324
  }
325
-
326
- #mainContainer {
327
- gap: 10px;
328
  }
329
-
330
- #keyboard {
331
- padding: 10px;
332
- overflow-x: auto;
333
- overflow-y: hidden;
334
- -webkit-overflow-scrolling: touch;
 
 
 
 
 
 
 
335
  }
336
-
337
  .key {
338
- width: 28px;
339
- height: 120px;
340
  font-size: 9px;
341
  }
342
-
343
  .key.black {
344
- width: 20px;
345
- height: 75px;
346
- margin-left: -10px;
347
- margin-right: -10px;
348
  }
349
-
350
- .key .shortcut-hint {
351
- font-size: 8px;
352
  }
353
-
354
- .controls {
355
- gap: 8px;
356
- padding: 10px;
357
- font-size: 14px;
358
  }
359
-
360
- button, select {
361
- font-size: 12px;
362
- padding: 6px 10px;
363
  }
364
-
365
- #status {
366
- font-size: 12px;
 
 
 
 
 
 
 
 
 
367
  }
368
-
369
- .terminal-header h4 {
370
- font-size: 14px;
 
 
371
  }
372
-
373
  #terminal {
374
- height: 150px;
375
  font-size: 11px;
376
  }
377
  }
378
 
379
- /* Very small mobile (portrait) */
380
  @media (max-width: 480px) {
381
- .welcome-header h1 {
382
- font-size: 1.5em;
383
- letter-spacing: 2px;
 
 
 
 
 
 
 
 
384
  }
385
-
386
  .key {
387
- width: 22px;
388
- height: 100px;
389
  font-size: 8px;
390
  }
391
-
392
  .key.black {
393
- width: 16px;
394
- height: 65px;
395
- margin-left: -8px;
396
- margin-right: -8px;
397
- }
398
-
399
- .key .shortcut-hint {
400
- font-size: 7px;
401
- }
402
-
403
- .controls {
404
- gap: 6px;
405
- padding: 8px;
406
- font-size: 12px;
407
  }
408
-
409
- button, select {
410
- font-size: 11px;
411
- padding: 5px 8px;
412
- }
413
-
414
- #terminal {
415
- height: 120px;
416
- font-size: 10px;
417
- }
418
- }
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Space+Mono:wght@400;700&display=swap');
2
+
3
+ :root {
4
+ --bg-night: #090511;
5
+ --bg-deep: #150b2e;
6
+ --panel: rgba(18, 10, 38, 0.75);
7
+ --panel-border: rgba(255, 67, 176, 0.4);
8
+ --panel-glow: 0 0 42px rgba(255, 67, 176, 0.24);
9
+ --text-main: #f4deff;
10
+ --text-soft: #c89cff;
11
+ --neon-pink: #ff3fb0;
12
+ --neon-cyan: #3ef4ff;
13
+ --neon-blue: #55a4ff;
14
+ --neon-violet: #8b40ff;
15
+ --neon-green: #8affc7;
16
+ --danger: #ff5f7a;
17
+ --keyboard-white-top: #fef0ff;
18
+ --keyboard-white-bottom: #f1d6ff;
19
+ --keyboard-black-top: #321355;
20
+ --keyboard-black-bottom: #120727;
21
+ --radius-l: 20px;
22
+ --radius-m: 14px;
23
+ --radius-s: 10px;
24
+ }
25
+
26
+ * {
27
+ box-sizing: border-box;
28
+ }
29
+
30
+ html,
31
+ body {
32
+ width: 100%;
33
+ max-width: 100%;
34
+ overflow-x: clip;
35
+ }
36
 
37
+ body {
 
 
 
38
  margin: 0;
 
 
39
  min-height: 100vh;
40
+ position: relative;
41
+ overflow-x: clip;
42
+ color: var(--text-main);
43
+ font-family: 'Space Mono', monospace;
44
+ background:
45
+ radial-gradient(1000px 500px at 10% -5%, rgba(139, 64, 255, 0.35), transparent 60%),
46
+ radial-gradient(800px 480px at 100% 0%, rgba(62, 244, 255, 0.2), transparent 60%),
47
+ linear-gradient(175deg, var(--bg-night), var(--bg-deep));
48
+ }
49
+
50
+ body::before,
51
+ body::after {
52
+ content: '';
53
+ position: fixed;
54
+ inset: 0;
55
+ pointer-events: none;
56
+ z-index: 0;
57
+ }
58
+
59
+ body::before {
60
+ background:
61
+ radial-gradient(circle at 50% 12%, rgba(255, 112, 190, 0.22) 0 18%, transparent 42%),
62
+ repeating-linear-gradient(
63
+ 90deg,
64
+ rgba(62, 244, 255, 0.06) 0 2px,
65
+ transparent 2px 90px
66
+ );
67
+ opacity: 0.9;
68
+ animation: skyPulse 7s ease-in-out infinite alternate;
69
+ }
70
+
71
+ body::after {
72
+ top: 52%;
73
+ height: 62%;
74
+ background:
75
+ linear-gradient(180deg, rgba(139, 64, 255, 0.08), rgba(9, 5, 17, 0.8)),
76
+ repeating-linear-gradient(
77
+ 0deg,
78
+ rgba(62, 244, 255, 0.15) 0 2px,
79
+ transparent 2px 28px
80
+ ),
81
+ repeating-linear-gradient(
82
+ 90deg,
83
+ rgba(255, 63, 176, 0.18) 0 1px,
84
+ transparent 1px 62px
85
+ );
86
+ transform-origin: top center;
87
+ transform: perspective(650px) rotateX(62deg);
88
+ animation: gridRush 9s linear infinite;
89
+ }
90
+
91
+ .app-shell {
92
+ max-width: 1360px;
93
+ margin: 0 auto;
94
+ padding: 20px 18px 30px;
95
+ position: relative;
96
+ z-index: 1;
97
+ overflow-x: clip;
98
+ }
99
+
100
+ .app-shell::before {
101
+ content: '';
102
+ position: absolute;
103
+ left: 0;
104
+ right: 0;
105
+ top: 118px;
106
+ height: 120px;
107
+ background:
108
+ radial-gradient(closest-side, rgba(62, 244, 255, 0.42), transparent 72%),
109
+ radial-gradient(closest-side, rgba(255, 63, 176, 0.36), transparent 76%);
110
+ filter: blur(22px);
111
+ opacity: 0.6;
112
+ animation: waveDrift 6.5s ease-in-out infinite;
113
+ transform: scaleX(1.15);
114
+ pointer-events: none;
115
  }
116
 
 
117
  .welcome-header {
118
  text-align: center;
119
+ margin-bottom: 18px;
120
+ position: relative;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
 
123
+ .welcome-header::after {
124
+ content: '';
125
+ position: absolute;
126
+ left: 50%;
127
+ bottom: -14px;
128
+ width: min(640px, 88%);
129
+ height: 10px;
130
+ transform: translateX(-50%);
131
+ background:
132
+ radial-gradient(circle at 10% 50%, rgba(62, 244, 255, 0.7), transparent 35%),
133
+ radial-gradient(circle at 50% 50%, rgba(255, 63, 176, 0.65), transparent 34%),
134
+ radial-gradient(circle at 90% 50%, rgba(139, 64, 255, 0.7), transparent 35%);
135
+ filter: blur(8px);
136
+ border-radius: 999px;
137
+ animation: waveDrift 5.8s ease-in-out infinite;
138
+ }
139
+
140
+ .eyebrow {
141
+ margin: 0 0 8px;
142
+ text-transform: uppercase;
143
+ letter-spacing: 0.24em;
144
+ font-size: 11px;
145
+ color: var(--neon-cyan);
146
+ text-shadow: 0 0 12px rgba(62, 244, 255, 0.7);
147
+ }
148
+
149
+ .logo-wrap {
150
+ width: min(700px, 96%);
151
+ margin: 0 auto 2px;
152
+ padding: 0;
153
+ border: 0;
154
+ background: none;
155
+ box-shadow: none;
156
  }
157
 
158
+ .logo-image {
159
+ display: block;
160
  width: 100%;
161
+ max-height: 186px;
162
+ object-fit: contain;
163
+ filter:
164
+ drop-shadow(0 0 8px rgba(255, 63, 176, 0.72))
165
+ drop-shadow(0 0 26px rgba(62, 244, 255, 0.52))
166
+ drop-shadow(0 0 50px rgba(139, 64, 255, 0.42));
167
+ animation: logoFloat 4.2s ease-in-out infinite;
168
+ }
169
+
170
+ .subtitle {
171
+ margin: 4px 0 0;
172
+ color: var(--text-soft);
173
+ font-size: clamp(0.92rem, 2.2vw, 1.04rem);
174
+ }
175
+
176
+ #mainContainer {
177
+ display: grid;
178
+ grid-template-columns: minmax(0, 1fr);
179
+ gap: 20px;
180
  }
181
 
182
+ .card {
183
+ background: var(--panel);
184
+ border: 1px solid var(--panel-border);
185
+ border-radius: var(--radius-l);
186
+ box-shadow:
187
+ var(--panel-glow),
188
+ inset 0 0 20px rgba(139, 64, 255, 0.17),
189
+ 0 0 0 1px rgba(62, 244, 255, 0.12);
190
+ backdrop-filter: blur(6px);
191
+ }
192
+
193
+ .keyboard-section {
194
+ padding: 18px;
195
+ }
196
+
197
+ #keyboard {
198
+ display: flex;
199
+ justify-content: center;
200
+ width: 100%;
201
+ padding: 20px 14px;
202
+ border-radius: 16px;
203
+ border: 1px solid rgba(62, 244, 255, 0.38);
204
+ background:
205
+ linear-gradient(180deg, rgba(21, 10, 43, 0.92), rgba(10, 4, 23, 0.92)),
206
+ radial-gradient(circle at 50% 0, rgba(62, 244, 255, 0.14), transparent 60%);
207
+ box-shadow:
208
+ inset 0 0 20px rgba(62, 244, 255, 0.16),
209
+ 0 0 22px rgba(62, 244, 255, 0.2),
210
+ 0 0 34px rgba(255, 63, 176, 0.18);
211
  user-select: none;
212
  touch-action: none;
213
+ overflow-x: auto;
214
+ }
215
+
216
+ .key {
217
+ width: 44px;
218
+ height: 190px;
219
+ margin: 0 1px;
220
+ border: 1px solid rgba(255, 63, 176, 0.45);
221
+ border-bottom-width: 4px;
222
+ border-radius: 0 0 11px 11px;
223
+ background: linear-gradient(180deg, var(--keyboard-white-top), var(--keyboard-white-bottom));
224
+ position: relative;
225
+ display: flex;
226
+ align-items: flex-end;
227
+ justify-content: center;
 
 
 
 
228
  cursor: pointer;
229
+ color: #51176e;
230
+ font-size: 11px;
231
+ font-weight: 700;
232
+ transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
 
 
 
233
  }
234
 
235
+ .key:hover {
236
+ transform: translateY(1px);
237
+ box-shadow: inset 0 -5px 12px rgba(255, 63, 176, 0.16);
 
 
 
 
238
  }
239
 
240
  .key.black {
241
+ width: 30px;
242
+ height: 118px;
243
+ margin-left: -15px;
244
+ margin-right: -15px;
245
+ z-index: 3;
246
+ border-radius: 0 0 9px 9px;
247
+ border-color: rgba(62, 244, 255, 0.55);
248
+ border-bottom-width: 3px;
249
+ background: linear-gradient(180deg, var(--keyboard-black-top), var(--keyboard-black-bottom));
250
+ color: #bdf6ff;
251
+ box-shadow: 0 6px 10px rgba(0, 0, 0, 0.55);
252
+ }
253
+
254
+ .key.black:hover {
255
+ box-shadow: 0 0 16px rgba(62, 244, 255, 0.4);
256
+ }
257
+
258
+ .key .shortcut-hint {
259
+ display: block;
260
+ opacity: 0;
261
+ font-size: 10px;
262
+ color: var(--neon-violet);
263
+ text-shadow: 0 0 9px rgba(139, 64, 255, 0.8);
264
+ transition: opacity 160ms ease;
265
  }
266
 
267
  .key.black .shortcut-hint {
268
+ color: var(--neon-cyan);
269
+ text-shadow: 0 0 9px rgba(62, 244, 255, 0.95);
270
  }
271
 
272
  .shortcuts-visible .key .shortcut-hint {
273
  opacity: 1;
274
  }
275
 
276
+ .keyboard-toggle-row {
277
+ display: flex;
278
+ justify-content: flex-end;
279
+ margin-top: 10px;
280
+ margin-bottom: 2px;
281
+ }
282
+
283
+ .keyboard-toggle-pill {
 
 
 
 
 
 
 
284
  position: relative;
285
+ display: inline-flex;
286
+ align-items: center;
287
+ gap: 10px;
288
+ padding: 8px 12px;
289
+ border-radius: 999px;
290
+ border: 1px solid rgba(62, 244, 255, 0.36);
291
+ background: linear-gradient(160deg, rgba(22, 10, 46, 0.9), rgba(10, 6, 26, 0.92));
292
+ box-shadow:
293
+ 0 0 16px rgba(62, 244, 255, 0.16),
294
+ inset 0 0 12px rgba(255, 63, 176, 0.12);
295
+ cursor: pointer;
296
+ user-select: none;
297
  }
298
 
299
+ .keyboard-toggle-pill input[type='checkbox'] {
300
+ position: absolute;
301
+ opacity: 0;
302
+ width: 0;
303
+ height: 0;
304
+ margin: 0;
305
  }
306
 
307
+ .toggle-track {
308
+ position: relative;
309
+ width: 44px;
310
+ height: 24px;
311
+ border-radius: 999px;
312
+ border: 1px solid rgba(255, 63, 176, 0.5);
313
+ background: rgba(10, 8, 25, 0.95);
314
+ box-shadow: inset 0 0 8px rgba(255, 63, 176, 0.22);
315
+ transition: background 140ms ease, border-color 140ms ease, box-shadow 140ms ease;
316
+ }
317
+
318
+ .toggle-track::before {
319
+ content: '';
320
+ position: absolute;
321
+ top: 2px;
322
+ left: 2px;
323
+ width: 18px;
324
+ height: 18px;
325
+ border-radius: 50%;
326
+ background: linear-gradient(180deg, #ff77d0, #ff3fb0);
327
+ box-shadow: 0 0 10px rgba(255, 63, 176, 0.6);
328
+ transition: transform 140ms ease, background 140ms ease, box-shadow 140ms ease;
329
  }
330
 
331
+ .keyboard-toggle-pill input[type='checkbox']:checked + .toggle-track {
332
+ border-color: rgba(62, 244, 255, 0.8);
333
+ background: linear-gradient(90deg, rgba(255, 63, 176, 0.32), rgba(62, 244, 255, 0.38));
334
+ box-shadow:
335
+ inset 0 0 10px rgba(62, 244, 255, 0.3),
336
+ 0 0 14px rgba(62, 244, 255, 0.25);
337
+ }
338
+
339
+ .keyboard-toggle-pill input[type='checkbox']:checked + .toggle-track::before {
340
+ transform: translateX(20px);
341
+ background: linear-gradient(180deg, #9ffcff, #3ef4ff);
342
+ box-shadow: 0 0 11px rgba(62, 244, 255, 0.8);
343
+ }
344
+
345
+ .keyboard-toggle-pill input[type='checkbox']:focus-visible + .toggle-track {
346
+ outline: 2px solid rgba(62, 244, 255, 0.75);
347
+ outline-offset: 2px;
348
+ }
349
+
350
+ .toggle-text {
351
+ color: #d9e3ff;
352
+ font-size: 12px;
353
+ font-weight: 700;
354
+ letter-spacing: 0.02em;
355
+ text-shadow: 0 0 8px rgba(62, 244, 255, 0.25);
356
+ }
357
+
358
+ .controls {
359
+ margin-top: 16px;
360
+ padding: 16px;
361
+ border-radius: var(--radius-m);
362
+ border: 1px solid rgba(62, 244, 255, 0.35);
363
+ background: rgba(11, 6, 24, 0.72);
364
+ }
365
+
366
+ .control-grid {
367
+ display: grid;
368
+ grid-template-columns: repeat(auto-fit, minmax(165px, 1fr));
369
+ gap: 10px;
370
+ }
371
+
372
+ .control-item {
373
  display: flex;
374
+ flex-direction: column;
375
+ gap: 7px;
376
+ padding: 10px;
377
+ border-radius: var(--radius-s);
378
+ border: 1px solid rgba(255, 63, 176, 0.32);
379
+ background: linear-gradient(160deg, rgba(31, 15, 62, 0.88), rgba(16, 8, 34, 0.9));
380
+ color: #f5d4ff;
381
+ font-size: 12px;
382
+ font-weight: 700;
383
+ letter-spacing: 0.01em;
384
+ }
385
+
386
+ .control-item-toggle {
387
+ flex-direction: row;
388
  align-items: center;
389
+ justify-content: flex-start;
390
+ gap: 10px;
391
+ font-size: 13px;
392
  }
393
 
394
  select,
395
+ button {
396
+ font-family: 'Space Mono', monospace;
397
+ }
398
+
399
+ select {
400
+ width: 100%;
401
+ padding: 8px 10px;
402
+ border: 1px solid rgba(62, 244, 255, 0.42);
403
+ border-radius: 9px;
404
+ background: rgba(10, 6, 22, 0.92);
405
+ color: var(--neon-cyan);
406
+ font-size: 13px;
407
+ }
408
+
409
+ select:focus,
410
+ button:focus,
411
+ input[type='checkbox']:focus {
412
+ outline: 2px solid rgba(62, 244, 255, 0.6);
413
+ outline-offset: 1px;
414
+ }
415
+
416
+ input[type='checkbox'] {
417
+ -webkit-appearance: none;
418
+ appearance: none;
419
+ width: 20px;
420
+ height: 20px;
421
  border-radius: 5px;
422
+ border: 2px solid var(--neon-pink);
423
+ background: rgba(11, 6, 24, 0.95);
424
+ position: relative;
425
  cursor: pointer;
426
+ box-shadow: inset 0 0 8px rgba(255, 63, 176, 0.2);
 
427
  }
428
 
429
+ input[type='checkbox']::after {
430
+ content: '';
431
+ position: absolute;
432
+ left: 5px;
433
+ top: 1px;
434
+ width: 5px;
435
+ height: 10px;
436
+ border: solid var(--neon-cyan);
437
+ border-width: 0 2px 2px 0;
438
+ transform: rotate(45deg) scale(0);
439
+ transition: transform 90ms ease;
440
+ filter: drop-shadow(0 0 5px rgba(62, 244, 255, 0.8));
441
  }
442
 
443
+ input[type='checkbox']:checked {
444
+ background: rgba(255, 63, 176, 0.26);
445
+ box-shadow:
446
+ inset 0 0 10px rgba(255, 63, 176, 0.6),
447
+ 0 0 10px rgba(255, 63, 176, 0.45);
448
  }
449
 
450
+ input[type='checkbox']:checked::after {
451
+ transform: rotate(45deg) scale(1);
 
452
  }
453
 
454
+ .action-row {
455
+ margin-top: 14px;
456
+ display: flex;
457
+ flex-wrap: wrap;
458
+ align-items: center;
459
+ gap: 8px;
460
+ }
461
+
462
+ .btn {
463
+ border: 0;
464
+ border-radius: 10px;
465
+ padding: 9px 14px;
466
+ font-size: 13px;
467
+ font-weight: 700;
468
+ color: #fff;
469
  cursor: pointer;
470
+ transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
471
+ text-shadow: 0 0 8px rgba(0, 0, 0, 0.45);
 
472
  }
473
 
474
+ .btn:hover:not(:disabled) {
475
+ transform: translateY(-1px);
476
+ filter: saturate(1.1) brightness(1.08);
477
+ }
478
+
479
+ .btn:disabled {
480
+ opacity: 0.42;
481
+ cursor: not-allowed;
482
+ }
483
+
484
+ .btn-primary {
485
+ background: linear-gradient(180deg, #ff4fbd, #d42f8e);
486
+ box-shadow: 0 0 18px rgba(255, 79, 189, 0.45);
487
+ }
488
+
489
+ .btn-secondary {
490
+ background: linear-gradient(180deg, #5464ff, #383fbc);
491
+ box-shadow: 0 0 15px rgba(84, 100, 255, 0.38);
492
  }
493
 
494
+ .btn-game {
495
+ background: linear-gradient(180deg, #43d3ff, #2f9bff);
496
+ color: #041322;
497
+ text-shadow: none;
498
+ box-shadow: 0 0 18px rgba(67, 211, 255, 0.45);
499
+ }
500
+
501
+ .btn-danger {
502
+ background: linear-gradient(180deg, #ff6688, #e9385f);
503
+ box-shadow: 0 0 18px rgba(255, 102, 136, 0.42);
504
+ }
505
+
506
+ #status {
507
+ margin-left: auto;
508
+ padding: 8px 12px;
509
+ border-radius: 999px;
510
+ border: 1px solid rgba(62, 244, 255, 0.6);
511
+ background: rgba(10, 9, 28, 0.8);
512
+ color: var(--neon-green);
513
+ text-shadow: 0 0 8px rgba(138, 255, 199, 0.75);
514
+ font-size: 12px;
515
+ font-weight: 700;
516
+ }
517
+
518
+ .monitor-section {
519
+ overflow: hidden;
520
+ }
521
+
522
+ .terminal-header {
523
+ display: flex;
524
+ justify-content: space-between;
525
+ align-items: center;
526
+ gap: 12px;
527
+ padding: 12px 14px;
528
+ border-bottom: 1px solid rgba(62, 244, 255, 0.28);
529
+ background: linear-gradient(180deg, rgba(33, 16, 70, 0.85), rgba(20, 9, 40, 0.75));
530
  }
531
 
532
  .terminal-header h4 {
533
  margin: 0;
534
+ font-family: 'Orbitron', sans-serif;
535
+ font-size: 14px;
536
+ letter-spacing: 0.08em;
537
+ color: var(--neon-pink);
538
+ text-shadow: 0 0 12px rgba(255, 63, 176, 0.8);
539
+ }
540
+
541
+ #terminal {
542
+ margin: 0;
543
+ min-height: 220px;
544
+ max-height: 320px;
545
+ overflow-y: auto;
546
+ padding: 14px;
 
 
 
 
 
 
 
 
 
547
  white-space: pre-wrap;
548
  word-wrap: break-word;
549
+ background:
550
+ radial-gradient(700px 220px at 100% 0, rgba(62, 244, 255, 0.18), transparent 62%),
551
+ #07040f;
552
+ color: #a5ffd2;
553
+ font-family: 'Space Mono', monospace;
554
+ font-size: 12px;
555
+ line-height: 1.35;
556
  }
557
 
558
+ #terminal .note-on {
559
+ color: #8affc7;
560
+ text-shadow: 0 0 8px rgba(138, 255, 199, 0.75);
561
  }
562
 
563
+ #terminal .note-off {
564
+ color: #8dc5ff;
565
+ text-shadow: 0 0 8px rgba(141, 197, 255, 0.7);
566
  }
567
 
568
+ #terminal .timestamp {
569
+ color: #ffdd84;
570
+ text-shadow: 0 0 8px rgba(255, 221, 132, 0.7);
571
  }
572
 
 
573
  #terminal::-webkit-scrollbar {
574
  width: 10px;
575
  }
576
 
577
  #terminal::-webkit-scrollbar-track {
578
+ background: rgba(8, 5, 18, 0.7);
579
  }
580
 
581
  #terminal::-webkit-scrollbar-thumb {
582
+ border-radius: 10px;
583
+ background: linear-gradient(180deg, #ff3fb0, #8b40ff);
584
  }
585
 
586
+ @keyframes logoFloat {
587
+ 0% {
588
+ transform: translateY(0) scale(1);
589
+ }
590
+ 50% {
591
+ transform: translateY(-4px) scale(1.01);
592
+ }
593
+ 100% {
594
+ transform: translateY(0) scale(1);
595
+ }
596
  }
 
 
 
597
 
598
+ @keyframes waveDrift {
599
+ 0% {
600
+ transform: translateX(-2%) translateY(0);
 
601
  }
602
+ 50% {
603
+ transform: translateX(2%) translateY(6px);
 
 
604
  }
605
+ 100% {
606
+ transform: translateX(-2%) translateY(0);
 
 
 
 
607
  }
608
  }
609
 
610
+ @keyframes skyPulse {
611
+ 0% {
612
+ opacity: 0.82;
613
+ filter: hue-rotate(0deg);
614
  }
615
+ 100% {
616
+ opacity: 1;
617
+ filter: hue-rotate(12deg);
 
618
  }
619
+ }
620
+
621
+ @keyframes gridRush {
622
+ 0% {
623
+ background-position: 0 0, 0 0, 0 0;
624
  }
625
+ 100% {
626
+ background-position: 0 0, 0 220px, 62px 0;
 
627
  }
628
+ }
629
+
630
+ @media (max-width: 980px) {
631
+ .app-shell {
632
+ padding: 16px 12px 24px;
633
+ }
634
+
635
+ .keyboard-section {
636
+ padding: 12px;
637
+ }
638
+
639
+ .controls {
640
+ padding: 12px;
641
  }
642
+
643
  .key {
644
+ width: 35px;
645
+ height: 158px;
646
  font-size: 9px;
647
  }
648
+
649
  .key.black {
650
+ width: 24px;
651
+ height: 98px;
652
+ margin-left: -12px;
653
+ margin-right: -12px;
654
  }
655
+
656
+ .control-grid {
657
+ grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
658
  }
659
+ }
660
+
661
+ @media (max-width: 640px) {
662
+ .welcome-header {
663
+ margin-bottom: 12px;
664
  }
665
+
666
+ .logo-wrap {
667
+ width: min(560px, 98%);
668
+ padding: 0;
669
  }
670
+
671
+ .logo-image {
672
+ max-height: 132px;
673
+ }
674
+
675
+ .control-grid {
676
+ grid-template-columns: 1fr 1fr;
677
+ }
678
+
679
+ .keyboard-toggle-row {
680
+ justify-content: center;
681
+ margin-top: 12px;
682
  }
683
+
684
+ #status {
685
+ margin-left: 0;
686
+ width: 100%;
687
+ text-align: center;
688
  }
689
+
690
  #terminal {
691
+ min-height: 180px;
692
  font-size: 11px;
693
  }
694
  }
695
 
 
696
  @media (max-width: 480px) {
697
+ .app-shell::before {
698
+ top: 90px;
699
+ height: 80px;
700
+ }
701
+
702
+ .control-grid {
703
+ grid-template-columns: 1fr;
704
+ }
705
+
706
+ #keyboard {
707
+ justify-content: flex-start;
708
  }
709
+
710
  .key {
711
+ width: 27px;
712
+ height: 126px;
713
  font-size: 8px;
714
  }
715
+
716
  .key.black {
717
+ width: 18px;
718
+ height: 76px;
719
+ margin-left: -9px;
720
+ margin-right: -9px;
 
 
 
 
 
 
 
 
 
 
721
  }
722
+ }
 
 
 
 
 
 
 
 
 
 
synthia_logo.png ADDED

Git LFS Details

  • SHA256: e7a0370a37e39bc83962443be323824a1ef23d4a35f4e947a23a80f9c405717f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.3 MB