bruAristimunha commited on
Commit
03ad70c
·
verified ·
1 Parent(s): ea1577d

Redesign: IBM Plex typography + Okabe-Ito palette + restructured layout

Browse files
Files changed (1) hide show
  1. app.py +444 -102
app.py CHANGED
@@ -1,15 +1,15 @@
1
  """Braindecode Model Explorer — interactive architecture browser.
2
 
3
- This Hugging Face Space lets users browse all 57 EEG model architectures
4
- in braindecode, read the rendered docstring (parameters, references,
5
- architecture figure), and instantiate any model with custom signal
6
- shape to inspect its parameter count and layer summary.
7
 
8
- No pretrained weights are loaded — this is a pure architecture explorer.
9
 
10
- Run locally:
11
- pip install -r requirements.txt
12
- python app.py
 
13
  """
14
 
15
  from __future__ import annotations
@@ -51,11 +51,18 @@ def _discover_models() -> dict[str, type]:
51
 
52
  MODELS: dict[str, type] = _discover_models()
53
  MODEL_NAMES: list[str] = sorted(MODELS.keys())
 
 
 
 
 
 
 
54
 
55
  # ---------------------------------------------------------------------------
56
  # Heuristic defaults for the signal-shape form. Different model families
57
  # expect very different inputs (sleep stagers want 30 s @ 100 Hz; motor-
58
- # imagery models want ~4 s @ 250 Hz). Pick a reasonable default per family.
59
  # ---------------------------------------------------------------------------
60
 
61
  DEFAULTS = {
@@ -81,41 +88,402 @@ def _defaults_for(name: str) -> dict[str, Any]:
81
 
82
 
83
  # ---------------------------------------------------------------------------
84
- # Rendering helpers
 
85
  # ---------------------------------------------------------------------------
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  def _info_card(name: str) -> str:
 
88
  cls = MODELS[name]
89
- sig = get_signature_str(cls)
90
- link = get_source_link(cls)
91
- link_html = (
92
- f'<a href="{link}" target="_blank" '
93
- f'style="color:#0072B2;text-decoration:none;">View source on GitHub →</a>'
94
- if link
95
- else ""
 
 
 
 
 
 
 
 
 
 
96
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  return (
98
- f"<div style='background:#f6f8fa;padding:12px 16px;border-radius:8px;"
99
- f"border:1px solid #d0d7de;margin-bottom:12px;'>"
100
- f"<div style='font-size:1.3em;font-weight:600;color:#0072B2;"
101
- f"margin-bottom:4px;'>{name}</div>"
102
- f"<div style='font-family:Menlo,Consolas,monospace;font-size:0.82em;"
103
- f"color:#475569;margin-bottom:6px;word-break:break-all;'>{sig}</div>"
104
- f"<div style='font-size:0.9em;'>{link_html}</div>"
105
- f"</div>"
106
  )
107
 
108
 
109
- def show_model(name: str) -> tuple[str, str, dict, dict, dict, dict]:
110
- """Update info card, rendered docstring, and form defaults."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  if name not in MODELS:
112
- return "", "", {}, {}, {}, {}
113
  info = _info_card(name)
114
  doc_html = render_docstring_html(MODELS[name].__doc__)
115
  d = _defaults_for(name)
116
  return (
117
  info,
118
  doc_html,
 
119
  gr.update(value=d["n_chans"]),
120
  gr.update(value=d["sfreq"]),
121
  gr.update(value=d["input_window_seconds"]),
@@ -123,16 +491,10 @@ def show_model(name: str) -> tuple[str, str, dict, dict, dict, dict]:
123
  )
124
 
125
 
126
- def instantiate(
127
- name: str,
128
- n_chans: int,
129
- sfreq: float,
130
- window_s: float,
131
- n_outputs: int,
132
- ) -> tuple[str, str]:
133
- """Instantiate the selected model and run a dummy forward pass."""
134
  if name not in MODELS:
135
- return "Pick a model first.", ""
136
 
137
  cls = MODELS[name]
138
  n_times = int(round(window_s * sfreq))
@@ -143,19 +505,16 @@ def instantiate(
143
  input_window_seconds=float(window_s),
144
  n_outputs=int(n_outputs),
145
  )
146
-
147
- # Drop kwargs the class does not accept (some models do not take all
148
- # of these in __init__; the mixin infers what it can).
149
  sig_params = set(inspect.signature(cls.__init__).parameters)
150
  kwargs = {k: v for k, v in kwargs.items() if k in sig_params}
151
 
152
  try:
153
  model = cls(**kwargs)
154
- except Exception as exc: # noqa: BLE001 — surface any constructor error
155
- return f"❌ **Failed to instantiate `{name}`** with `{kwargs}`:\n```\n{exc}\n```", ""
 
156
 
157
  n_params = sum(p.numel() for p in model.parameters())
158
- n_train = sum(p.numel() for p in model.parameters() if p.requires_grad)
159
 
160
  try:
161
  info = summary(
@@ -169,6 +528,7 @@ def instantiate(
169
  except Exception as exc: # noqa: BLE001
170
  summary_str = f"(torchinfo summary unavailable: {exc})"
171
 
 
172
  try:
173
  x = torch.randn(2, int(n_chans), n_times)
174
  with torch.no_grad():
@@ -177,92 +537,74 @@ def instantiate(
177
  except Exception as exc: # noqa: BLE001
178
  out_shape = f"forward failed: {exc}"
179
 
180
- header = (
181
- f"### `{name}` instantiated\n\n"
182
- f"| metric | value |\n|---|---|\n"
183
- f"| total parameters | **{n_params:,}** |\n"
184
- f"| trainable parameters | {n_train:,} |\n"
185
- f"| input shape | `(batch, {n_chans}, {n_times})` |\n"
186
- f"| output shape | `{out_shape}` |\n"
187
- f"| input window | {window_s} s @ {sfreq} Hz |\n"
188
  )
189
- return header, f"```\n{summary_str}\n```"
190
 
191
 
192
  # ---------------------------------------------------------------------------
193
  # UI
194
  # ---------------------------------------------------------------------------
195
 
196
- INTRO = """
197
- # Braindecode Model Explorer
198
-
199
- Browse **57 EEG / biosignal model architectures** from
200
- [braindecode](https://braindecode.org). Read the rendered docstring,
201
- configure signal shape, and instantiate any model live to inspect its
202
- parameter count and layer summary.
203
-
204
- > No pretrained weights are loaded — this is a pure architecture browser.
205
- > For weights, see [`huggingface.co/braindecode`](https://huggingface.co/braindecode).
206
- """
207
-
208
-
209
  def build_app() -> gr.Blocks:
 
 
 
 
 
210
  with gr.Blocks(
211
  title="Braindecode Model Explorer",
212
- theme=gr.themes.Soft(primary_hue="blue"),
 
213
  ) as app:
214
- gr.Markdown(INTRO)
215
 
216
- with gr.Row():
217
- with gr.Column(scale=1):
 
218
  model_dd = gr.Dropdown(
219
  choices=MODEL_NAMES,
220
- value="EEGNetv4",
221
  label="Architecture",
222
  interactive=True,
 
223
  )
224
- info_html = gr.HTML(_info_card("EEGNetv4"))
225
-
226
- gr.Markdown("### Signal configuration")
227
  with gr.Group():
228
  n_chans = gr.Number(value=22, label="n_chans", precision=0)
229
- sfreq = gr.Number(value=250, label="sfreq (Hz)")
230
- window_s = gr.Number(
231
- value=4.0, label="input_window_seconds"
232
- )
233
- n_outputs = gr.Number(
234
- value=4, label="n_outputs", precision=0
 
 
 
 
 
 
 
 
 
 
 
 
235
  )
236
- run_btn = gr.Button("Instantiate model", variant="primary")
237
-
238
- with gr.Column(scale=2):
239
- with gr.Tabs():
240
- with gr.TabItem("Documentation"):
241
- doc_html = gr.HTML(
242
- render_docstring_html(MODELS["EEGNetv4"].__doc__)
243
- )
244
- with gr.TabItem("Live instance"):
245
- result_md = gr.Markdown(
246
- "_Press **Instantiate model** to build the network._"
247
- )
248
- summary_md = gr.Markdown()
249
-
250
- # wiring
251
  model_dd.change(
252
  show_model,
253
  inputs=model_dd,
254
- outputs=[info_html, doc_html, n_chans, sfreq, window_s, n_outputs],
255
  )
256
  run_btn.click(
257
  instantiate,
258
  inputs=[model_dd, n_chans, sfreq, window_s, n_outputs],
259
- outputs=[result_md, summary_md],
260
- )
261
-
262
- gr.Markdown(
263
- "---\nMade with [braindecode](https://braindecode.org) · "
264
- "Source: [github.com/braindecode/braindecode]"
265
- "(https://github.com/braindecode/braindecode)"
266
  )
267
 
268
  return app
 
1
  """Braindecode Model Explorer — interactive architecture browser.
2
 
3
+ Hugging Face Space that browses every EEG architecture in braindecode.
4
+ For each model: rendered docstring (figure, references, parameter list)
5
+ plus live instantiation to inspect param count and layer summary.
 
6
 
7
+ No pretrained weights are loaded — this is a pure architecture browser.
8
 
9
+ Aesthetic: editorial scientific instrument. IBM Plex (Sans / Serif /
10
+ Mono), Okabe-Ito colorblind-safe palette, warm-paper background. All
11
+ visual styling lives in GLOBAL_CSS below; the docstring renderer emits
12
+ structural HTML only.
13
  """
14
 
15
  from __future__ import annotations
 
51
 
52
  MODELS: dict[str, type] = _discover_models()
53
  MODEL_NAMES: list[str] = sorted(MODELS.keys())
54
+ DEFAULT_MODEL = "EEGNetv4" if "EEGNetv4" in MODELS else MODEL_NAMES[0]
55
+
56
+ try:
57
+ import braindecode as _bd
58
+ BD_VERSION = getattr(_bd, "__version__", "unknown")
59
+ except Exception:
60
+ BD_VERSION = "unknown"
61
 
62
  # ---------------------------------------------------------------------------
63
  # Heuristic defaults for the signal-shape form. Different model families
64
  # expect very different inputs (sleep stagers want 30 s @ 100 Hz; motor-
65
+ # imagery models want ~4 s @ 250 Hz).
66
  # ---------------------------------------------------------------------------
67
 
68
  DEFAULTS = {
 
88
 
89
 
90
  # ---------------------------------------------------------------------------
91
+ # Global stylesheet — IBM Plex + Okabe-Ito + spatial system. Injected
92
+ # once via gr.Blocks(css=...).
93
  # ---------------------------------------------------------------------------
94
 
95
+ GLOBAL_CSS = """
96
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Serif:wght@500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
97
+
98
+ :root {
99
+ --bd-blue: #0072B2;
100
+ --bd-green: #009E73;
101
+ --bd-orange: #D55E00;
102
+ --bd-pink: #CC79A7;
103
+ --bd-yellow: #E69F00;
104
+ --bd-skyblue: #56B4E9;
105
+ --bd-paper: #FAFAF7;
106
+ --bd-paper-deep: #F1EFE8;
107
+ --bd-rule: #E5E2D9;
108
+ --bd-ink: #1a1a1a;
109
+ --bd-meta: #6b6b6b;
110
+ }
111
+
112
+ /* Container & background ---------------------------------------------- */
113
+ body, .gradio-container {
114
+ background: var(--bd-paper) !important;
115
+ font-family: 'IBM Plex Sans', system-ui, sans-serif !important;
116
+ color: var(--bd-ink);
117
+ }
118
+ .gradio-container { max-width: 1320px !important; padding: 0 24px !important; }
119
+ .gradio-container * { font-family: inherit; }
120
+
121
+ /* Header band --------------------------------------------------------- */
122
+ .bd-header {
123
+ display: flex;
124
+ align-items: baseline;
125
+ justify-content: space-between;
126
+ padding: 22px 0 18px 0;
127
+ border-bottom: 1px solid var(--bd-rule);
128
+ margin-bottom: 28px;
129
+ flex-wrap: wrap;
130
+ gap: 12px;
131
+ }
132
+ .bd-header-title {
133
+ font-family: 'IBM Plex Serif', serif;
134
+ font-size: 26px;
135
+ font-weight: 600;
136
+ color: var(--bd-ink);
137
+ letter-spacing: -0.015em;
138
+ }
139
+ .bd-header-title .bd-mark {
140
+ color: var(--bd-blue);
141
+ font-weight: 500;
142
+ font-style: italic;
143
+ }
144
+ .bd-header-meta {
145
+ font-family: 'IBM Plex Mono', monospace;
146
+ font-size: 12px;
147
+ color: var(--bd-meta);
148
+ letter-spacing: 0.04em;
149
+ text-transform: uppercase;
150
+ }
151
+ .bd-header-meta .bd-dot { color: var(--bd-blue); margin: 0 8px; }
152
+
153
+ /* Info card (model display) ------------------------------------------- */
154
+ .bd-info {
155
+ margin: 0 0 24px 0;
156
+ padding-bottom: 18px;
157
+ border-bottom: 2px solid var(--bd-blue);
158
+ }
159
+ .bd-display {
160
+ font-family: 'IBM Plex Serif', serif;
161
+ font-size: 36px;
162
+ font-weight: 600;
163
+ color: var(--bd-blue);
164
+ letter-spacing: -0.02em;
165
+ line-height: 1.1;
166
+ margin: 0 0 6px 0;
167
+ }
168
+ .bd-tagline {
169
+ font-family: 'IBM Plex Sans', sans-serif;
170
+ font-size: 14px;
171
+ color: var(--bd-meta);
172
+ margin-bottom: 14px;
173
+ font-style: italic;
174
+ }
175
+ .bd-sig {
176
+ font-family: 'IBM Plex Mono', monospace;
177
+ font-size: 13px;
178
+ line-height: 1.55;
179
+ white-space: pre;
180
+ overflow-x: auto;
181
+ padding: 10px 14px;
182
+ background: var(--bd-paper-deep);
183
+ border-left: 2px solid var(--bd-blue);
184
+ color: #2a2a2a;
185
+ margin: 0 0 10px 0;
186
+ }
187
+ .bd-sig::-webkit-scrollbar { height: 6px; }
188
+ .bd-sig::-webkit-scrollbar-thumb { background: var(--bd-rule); border-radius: 3px; }
189
+ .bd-sig::-webkit-scrollbar-thumb:hover { background: var(--bd-blue); }
190
+ .bd-source {
191
+ display: inline-block;
192
+ color: var(--bd-meta);
193
+ font-size: 13px;
194
+ text-decoration: none;
195
+ border-bottom: 1px solid transparent;
196
+ transition: all 0.15s ease;
197
+ }
198
+ .bd-source:hover { color: var(--bd-blue); border-bottom-color: var(--bd-blue); }
199
+
200
+ /* Stat tile (live param count) ---------------------------------------- */
201
+ .bd-stat-card {
202
+ background: var(--bd-paper-deep);
203
+ border: 1px solid var(--bd-rule);
204
+ border-radius: 4px;
205
+ padding: 14px 16px;
206
+ margin-top: 12px;
207
+ }
208
+ .bd-meta-label {
209
+ font-family: 'IBM Plex Sans', sans-serif;
210
+ font-size: 11px;
211
+ font-weight: 600;
212
+ letter-spacing: 0.1em;
213
+ text-transform: uppercase;
214
+ color: var(--bd-meta);
215
+ margin: 0 0 4px 0;
216
+ }
217
+ .bd-stat {
218
+ font-family: 'IBM Plex Mono', monospace;
219
+ font-size: 28px;
220
+ font-weight: 600;
221
+ font-variant-numeric: tabular-nums;
222
+ color: var(--bd-blue);
223
+ line-height: 1;
224
+ }
225
+ .bd-stat-sub {
226
+ font-family: 'IBM Plex Mono', monospace;
227
+ font-size: 11px;
228
+ color: var(--bd-meta);
229
+ margin-top: 6px;
230
+ letter-spacing: 0.02em;
231
+ }
232
+
233
+ /* Section heading separator ------------------------------------------- */
234
+ .bd-section-rule {
235
+ display: flex; align-items: center;
236
+ gap: 12px;
237
+ margin: 28px 0 14px 0;
238
+ }
239
+ .bd-section-rule::before, .bd-section-rule::after {
240
+ content: ""; flex: 1;
241
+ height: 1px; background: var(--bd-rule);
242
+ }
243
+ .bd-section-rule span {
244
+ font-family: 'IBM Plex Sans', sans-serif;
245
+ font-size: 11px;
246
+ font-weight: 600;
247
+ letter-spacing: 0.14em;
248
+ text-transform: uppercase;
249
+ color: var(--bd-meta);
250
+ }
251
+
252
+ /* Docstring rendering (consumed by render_docstring_html) ------------- */
253
+ .bd-doc {
254
+ font-family: 'IBM Plex Sans', sans-serif;
255
+ font-size: 16px;
256
+ line-height: 1.65;
257
+ color: var(--bd-ink);
258
+ }
259
+ .bd-doc p, .bd-doc li { font-size: 16px; margin: 8px 0; }
260
+ .bd-doc h1, .bd-doc h2, .bd-doc h3 {
261
+ font-family: 'IBM Plex Serif', serif;
262
+ color: var(--bd-blue);
263
+ margin-top: 1.4em;
264
+ margin-bottom: 0.45em;
265
+ letter-spacing: -0.01em;
266
+ }
267
+ .bd-doc h1 { font-size: 24px; font-weight: 600; }
268
+ .bd-doc h2 { font-size: 20px; font-weight: 600; }
269
+ .bd-doc h3 { font-size: 17px; font-weight: 600;
270
+ font-family: 'IBM Plex Sans', sans-serif; }
271
+ .bd-doc pre {
272
+ background: var(--bd-paper-deep);
273
+ padding: 12px 14px;
274
+ border-radius: 4px;
275
+ font-family: 'IBM Plex Mono', monospace;
276
+ font-size: 13px;
277
+ line-height: 1.55;
278
+ overflow-x: auto;
279
+ border-left: 2px solid var(--bd-blue);
280
+ }
281
+ .bd-doc code {
282
+ background: rgba(0, 114, 178, 0.08);
283
+ padding: 1px 6px;
284
+ border-radius: 3px;
285
+ font-family: 'IBM Plex Mono', monospace;
286
+ font-size: 14px;
287
+ color: #0a4d77;
288
+ }
289
+ .bd-doc pre code { background: transparent; padding: 0; color: inherit; font-size: inherit; }
290
+ .bd-doc img {
291
+ max-width: 100%;
292
+ display: block;
293
+ margin: 18px auto;
294
+ border-radius: 4px;
295
+ box-shadow: 0 2px 14px rgba(0, 114, 178, 0.10);
296
+ }
297
+ .bd-doc table {
298
+ border-collapse: collapse;
299
+ margin: 14px 0;
300
+ font-size: 14px;
301
+ font-variant-numeric: tabular-nums;
302
+ width: 100%;
303
+ }
304
+ .bd-doc th, .bd-doc td {
305
+ border: 1px solid var(--bd-rule);
306
+ padding: 7px 12px;
307
+ text-align: left;
308
+ vertical-align: top;
309
+ }
310
+ .bd-doc th { background: var(--bd-paper-deep); font-weight: 600; color: var(--bd-meta); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
311
+ .bd-doc .admonition {
312
+ border-left: 3px solid var(--bd-blue);
313
+ background: rgba(0, 114, 178, 0.05);
314
+ padding: 10px 16px;
315
+ margin: 16px 0;
316
+ border-radius: 0 4px 4px 0;
317
+ font-size: 15px;
318
+ }
319
+ .bd-doc .admonition.important { border-color: var(--bd-orange); background: rgba(213, 94, 0, 0.05); }
320
+ .bd-doc .admonition.note { border-color: var(--bd-green); background: rgba(0, 158, 115, 0.05); }
321
+ .bd-doc .admonition-title { font-weight: 600; margin-bottom: 4px; }
322
+ .bd-doc dl.field-list {
323
+ display: grid; grid-template-columns: max-content auto;
324
+ gap: 6px 16px; font-size: 15px; margin: 12px 0;
325
+ }
326
+ .bd-doc dl.field-list dt { font-weight: 600; color: var(--bd-meta); font-size: 13px; letter-spacing: 0.03em; text-transform: uppercase; padding-top: 2px; }
327
+ .bd-doc a { color: var(--bd-blue); text-decoration: none; border-bottom: 1px solid rgba(0, 114, 178, 0.3); }
328
+ .bd-doc a:hover { border-bottom-color: var(--bd-blue); }
329
+
330
+ /* Inline badge produced by docstring_renderer ------------------------- */
331
+ .bd-badge {
332
+ display: inline-block;
333
+ padding: 3px 10px;
334
+ border-radius: 3px;
335
+ color: white;
336
+ font-family: 'IBM Plex Sans', sans-serif;
337
+ font-size: 12px;
338
+ font-weight: 600;
339
+ letter-spacing: 0.02em;
340
+ margin: 0 4px 4px 0;
341
+ }
342
+
343
+ /* Form labels --------------------------------------------------------- */
344
+ label > span, .gradio-container .label-wrap span {
345
+ font-family: 'IBM Plex Sans', sans-serif;
346
+ font-size: 12px !important;
347
+ font-weight: 600 !important;
348
+ letter-spacing: 0.06em !important;
349
+ text-transform: uppercase !important;
350
+ color: var(--bd-meta) !important;
351
+ }
352
+ input[type="number"], textarea, select {
353
+ font-family: 'IBM Plex Mono', monospace !important;
354
+ font-size: 14px !important;
355
+ }
356
+
357
+ /* Primary button ------------------------------------------------------ */
358
+ button.primary, button[variant="primary"], .bd-cta {
359
+ background: var(--bd-blue) !important;
360
+ color: white !important;
361
+ font-family: 'IBM Plex Sans', sans-serif !important;
362
+ font-size: 13px !important;
363
+ font-weight: 600 !important;
364
+ letter-spacing: 0.06em !important;
365
+ text-transform: uppercase !important;
366
+ border-radius: 4px !important;
367
+ padding: 11px 18px !important;
368
+ border: none !important;
369
+ transition: background 0.15s ease;
370
+ }
371
+ button.primary:hover, button[variant="primary"]:hover, .bd-cta:hover {
372
+ background: #005a8c !important;
373
+ }
374
+
375
+ /* Footer -------------------------------------------------------------- */
376
+ .bd-footer {
377
+ margin: 40px 0 20px 0;
378
+ padding-top: 18px;
379
+ border-top: 1px solid var(--bd-rule);
380
+ font-family: 'IBM Plex Sans', sans-serif;
381
+ font-size: 12px;
382
+ color: var(--bd-meta);
383
+ letter-spacing: 0.04em;
384
+ display: flex;
385
+ justify-content: space-between;
386
+ flex-wrap: wrap;
387
+ gap: 8px;
388
+ }
389
+ .bd-footer a { color: var(--bd-blue); text-decoration: none; }
390
+ .bd-footer a:hover { text-decoration: underline; }
391
+ """
392
+
393
+
394
+ # ---------------------------------------------------------------------------
395
+ # HTML fragments
396
+ # ---------------------------------------------------------------------------
397
+
398
+ import html as _html
399
+
400
+
401
  def _info_card(name: str) -> str:
402
+ """Primary visual anchor — display name + scrollable signature + source link."""
403
  cls = MODELS[name]
404
+ sig = _html.escape(get_signature_str(cls))
405
+ link = get_source_link(cls) or "#"
406
+ docstring_first = (cls.__doc__ or "").strip().splitlines()
407
+ tagline = ""
408
+ if docstring_first:
409
+ first = docstring_first[0].strip()
410
+ # Strip rST cite markers like [Foo2023]_
411
+ import re as _re
412
+ first = _re.sub(r"\[\w+\]_", "", first).strip()
413
+ tagline = _html.escape(first[:200])
414
+ return (
415
+ f'<div class="bd-info">'
416
+ f' <div class="bd-display">{_html.escape(name)}</div>'
417
+ f' {f"<div class=\"bd-tagline\">{tagline}</div>" if tagline else ""}'
418
+ f' <pre class="bd-sig">{sig}</pre>'
419
+ f' <a class="bd-source" href="{link}" target="_blank">↗ Source on GitHub</a>'
420
+ f'</div>'
421
  )
422
+
423
+
424
+ def _stat_tile(params: int | None = None, *, n_chans: int | None = None,
425
+ n_times: int | None = None, out_shape=None) -> str:
426
+ """Live parameter count + input/output shapes."""
427
+ if params is None:
428
+ return (
429
+ '<div class="bd-stat-card">'
430
+ '<div class="bd-meta-label">Parameters</div>'
431
+ '<div class="bd-stat" style="color: var(--bd-meta);">—</div>'
432
+ '<div class="bd-stat-sub">press build to instantiate</div>'
433
+ '</div>'
434
+ )
435
+ pretty_out = (
436
+ f"({', '.join(str(d) for d in out_shape)})"
437
+ if isinstance(out_shape, tuple)
438
+ else str(out_shape)
439
+ )
440
+ return (
441
+ '<div class="bd-stat-card">'
442
+ '<div class="bd-meta-label">Parameters</div>'
443
+ f'<div class="bd-stat">{params:,}</div>'
444
+ f'<div class="bd-stat-sub">in (b, {n_chans}, {n_times}) → {pretty_out}</div>'
445
+ '</div>'
446
+ )
447
+
448
+
449
+ def _header_band() -> str:
450
  return (
451
+ '<div class="bd-header">'
452
+ '<div class="bd-header-title">braindecode <span class="bd-mark">model explorer</span></div>'
453
+ f'<div class="bd-header-meta">v{BD_VERSION}<span class="bd-dot">•</span>{len(MODELS)} architectures<span class="bd-dot">•</span>no weights</div>'
454
+ '</div>'
 
 
 
 
455
  )
456
 
457
 
458
+ def _section_rule(label: str) -> str:
459
+ return f'<div class="bd-section-rule"><span>{label}</span></div>'
460
+
461
+
462
+ def _footer() -> str:
463
+ return (
464
+ '<div class="bd-footer">'
465
+ '<div>An architecture browser for <a href="https://braindecode.org">braindecode</a>. '
466
+ 'No pretrained weights served here — see '
467
+ '<a href="https://huggingface.co/braindecode">huggingface.co/braindecode</a>.</div>'
468
+ '<div><a href="https://github.com/braindecode/braindecode">github.com/braindecode/braindecode</a></div>'
469
+ '</div>'
470
+ )
471
+
472
+
473
+ # ---------------------------------------------------------------------------
474
+ # Event handlers
475
+ # ---------------------------------------------------------------------------
476
+
477
+ def show_model(name: str):
478
  if name not in MODELS:
479
+ return "", "", _stat_tile(), {}, {}, {}, {}
480
  info = _info_card(name)
481
  doc_html = render_docstring_html(MODELS[name].__doc__)
482
  d = _defaults_for(name)
483
  return (
484
  info,
485
  doc_html,
486
+ _stat_tile(), # reset stat tile when switching models
487
  gr.update(value=d["n_chans"]),
488
  gr.update(value=d["sfreq"]),
489
  gr.update(value=d["input_window_seconds"]),
 
491
  )
492
 
493
 
494
+ def instantiate(name, n_chans, sfreq, window_s, n_outputs):
495
+ """Build the model and return (stat_html, layer_summary_md)."""
 
 
 
 
 
 
496
  if name not in MODELS:
497
+ return _stat_tile(), "Pick a model first."
498
 
499
  cls = MODELS[name]
500
  n_times = int(round(window_s * sfreq))
 
505
  input_window_seconds=float(window_s),
506
  n_outputs=int(n_outputs),
507
  )
 
 
 
508
  sig_params = set(inspect.signature(cls.__init__).parameters)
509
  kwargs = {k: v for k, v in kwargs.items() if k in sig_params}
510
 
511
  try:
512
  model = cls(**kwargs)
513
+ except Exception as exc: # noqa: BLE001
514
+ err = f"❌ **Failed to instantiate `{name}`** with `{kwargs}`:\n```\n{exc}\n```"
515
+ return _stat_tile(), err
516
 
517
  n_params = sum(p.numel() for p in model.parameters())
 
518
 
519
  try:
520
  info = summary(
 
528
  except Exception as exc: # noqa: BLE001
529
  summary_str = f"(torchinfo summary unavailable: {exc})"
530
 
531
+ out_shape: Any = "?"
532
  try:
533
  x = torch.randn(2, int(n_chans), n_times)
534
  with torch.no_grad():
 
537
  except Exception as exc: # noqa: BLE001
538
  out_shape = f"forward failed: {exc}"
539
 
540
+ stat = _stat_tile(
541
+ params=n_params, n_chans=int(n_chans), n_times=n_times, out_shape=out_shape
 
 
 
 
 
 
542
  )
543
+ return stat, f"```\n{summary_str}\n```"
544
 
545
 
546
  # ---------------------------------------------------------------------------
547
  # UI
548
  # ---------------------------------------------------------------------------
549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  def build_app() -> gr.Blocks:
551
+ theme = gr.themes.Soft(
552
+ primary_hue=gr.themes.colors.blue,
553
+ font=[gr.themes.GoogleFont("IBM Plex Sans"), "system-ui", "sans-serif"],
554
+ font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"],
555
+ )
556
  with gr.Blocks(
557
  title="Braindecode Model Explorer",
558
+ theme=theme,
559
+ css=GLOBAL_CSS,
560
  ) as app:
561
+ gr.HTML(_header_band())
562
 
563
+ with gr.Row(equal_height=False):
564
+ # ---------- LEFT: controls + stat tile ----------
565
+ with gr.Column(scale=1, min_width=280):
566
  model_dd = gr.Dropdown(
567
  choices=MODEL_NAMES,
568
+ value=DEFAULT_MODEL,
569
  label="Architecture",
570
  interactive=True,
571
+ filterable=True,
572
  )
573
+ gr.HTML(_section_rule("Signal configuration"))
 
 
574
  with gr.Group():
575
  n_chans = gr.Number(value=22, label="n_chans", precision=0)
576
+ sfreq = gr.Number(value=250, label="sfreq · Hz")
577
+ window_s = gr.Number(value=4.0, label="window · seconds")
578
+ n_outputs = gr.Number(value=4, label="n_outputs", precision=0)
579
+ run_btn = gr.Button(
580
+ "Build network", variant="primary", elem_classes="bd-cta"
581
+ )
582
+ stat_html = gr.HTML(_stat_tile())
583
+
584
+ # ---------- RIGHT: model info + docstring ----------
585
+ with gr.Column(scale=3):
586
+ info_html = gr.HTML(_info_card(DEFAULT_MODEL))
587
+ gr.HTML(_section_rule("Architecture documentation"))
588
+ doc_html = gr.HTML(
589
+ render_docstring_html(MODELS[DEFAULT_MODEL].__doc__)
590
+ )
591
+ with gr.Accordion("Layer summary (after build)", open=False):
592
+ summary_md = gr.Markdown(
593
+ "_Press **Build network** to populate the summary._"
594
  )
595
+
596
+ gr.HTML(_footer())
597
+
598
+ # ---------- wiring ----------
 
 
 
 
 
 
 
 
 
 
 
599
  model_dd.change(
600
  show_model,
601
  inputs=model_dd,
602
+ outputs=[info_html, doc_html, stat_html, n_chans, sfreq, window_s, n_outputs],
603
  )
604
  run_btn.click(
605
  instantiate,
606
  inputs=[model_dd, n_chans, sfreq, window_s, n_outputs],
607
+ outputs=[stat_html, summary_md],
 
 
 
 
 
 
608
  )
609
 
610
  return app