techfreakworm commited on
Commit
59b9fee
·
unverified ·
1 Parent(s): 0ea6ca2

fix(ui): replace gr.Tabs with sidebar nav per wireframes

Browse files

The previous M0 implementation used gr.Tabs which renders top-positioned
horizontal tabs — contradicting the approved wireframes at
docs/superpowers/specs/mockups/ which show a LEFT sidebar with 5 mode
pills + History section.

This rewrite uses a gr.Radio (re-skinned via CSS) as the sidebar nav,
with 5 gr.Group panes in the content column whose visibility is
toggled by the radio's change event. The radio's native :checked
pseudo-class supplies the active-item highlight (white text + white
left border + dark bg).

theme.py:
- New .ams-body row layout (sidebar + content gap)
- .ams-sidebar fixed-width column (190-210px)
- .ams-side-radio re-skin: vertical, full-width pills, no native circle
- .ams-side-radio label:has(input:checked) — active state
- .ams-history block below the radio
- .ams-content right column with min-height
- Responsive: tablet shrinks sidebar to icon rail, mobile flips to top tab strip

app.py:
- Build header status with detected device suffix (READY · MPS / CUDA / CPU)
- Sidebar (gr.Column min_width=190) holds radio + history HTML
- Content column holds 5 gr.Group panes, generate visible by default
- mode.change(_switch_pane, ...) returns visibility updates for all panes

DO NOT switch back to gr.Tabs — the wireframes are the source of truth.

Files changed (2) hide show
  1. app.py +99 -16
  2. theme.py +170 -23
app.py CHANGED
@@ -1,8 +1,36 @@
1
  """ACE Music Studio — Gradio entrypoint.
2
 
3
- On HF Spaces, `_bootstrap()` runs once on import to mirror the read-only
4
- preload cache into a writable tree. On Mac/Linux locally, it's a no-op.
5
- The backend singleton is lazy-loaded on first generation request.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
8
  from __future__ import annotations
@@ -17,6 +45,7 @@ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
17
 
18
  import gradio as gr
19
 
 
20
  import theme
21
 
22
  HEADER_HTML = """
@@ -24,10 +53,23 @@ HEADER_HTML = """
24
  <div>
25
  <div class="ams-brand">ACE Music Studio<span class="ams-brand-period">.</span></div>
26
  </div>
27
- <div class="ams-status">ready</div>
28
  </div>
29
  """.strip()
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  CTA_HTML = """
32
  <div class="ams-cta">
33
  Built with <span class="ams-cta-heart">♥</span>.
@@ -39,6 +81,23 @@ CTA_HTML = """
39
  """.strip()
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def _bootstrap() -> None:
43
  """HF Spaces: mirror read-only preload cache into a writable tree.
44
 
@@ -48,21 +107,45 @@ def _bootstrap() -> None:
48
 
49
 
50
  def build_app() -> gr.Blocks:
 
 
51
  with gr.Blocks(theme=theme.build_theme(), css=theme.CSS, title="ACE Music Studio") as demo:
52
- gr.HTML(HEADER_HTML)
53
  gr.HTML(CTA_HTML)
54
 
55
- with gr.Tabs():
56
- with gr.Tab("🎵 Generate"):
57
- gr.Markdown("Generate tab placeholder — implemented in M1.")
58
- with gr.Tab("🎤 Cover"):
59
- gr.Markdown("Cover tab placeholder — implemented in M3.")
60
- with gr.Tab("⏩ Extend"):
61
- gr.Markdown("Extend tab placeholder — implemented in M3.")
62
- with gr.Tab("✏️ Edit"):
63
- gr.Markdown("Edit tab placeholder — implemented in M3.")
64
- with gr.Tab("✍️ Lyrics"):
65
- gr.Markdown("Lyrics tab placeholder — implemented in M4.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  return demo
68
 
 
1
  """ACE Music Studio — Gradio entrypoint.
2
 
3
+ UI ARCHITECTURE (locked read this before editing):
4
+
5
+ The five "modes" (Generate / Cover / Extend / Edit / Lyrics) are NOT
6
+ implemented via ``gr.Tabs``. The wireframes at
7
+ ``docs/superpowers/specs/mockups/`` show a LEFT sidebar with mode pills +
8
+ a session History section, and a single content column on the right.
9
+
10
+ The implementation pattern is:
11
+
12
+ gr.Row(elem_classes=["ams-body"])
13
+ ├── gr.Column(min_width=190, elem_classes=["ams-sidebar"])
14
+ │ ├── gr.Radio(label=None, elem_classes=["ams-side-radio"]) ← 5 mode choices
15
+ │ └── gr.HTML(... "History · session" ...)
16
+ └── gr.Column(elem_classes=["ams-content"])
17
+ ├── gr.Group(visible=True) ← pane_generate
18
+ ├── gr.Group(visible=False) ← pane_cover
19
+ ├── gr.Group(visible=False) ← pane_extend
20
+ ├── gr.Group(visible=False) ← pane_edit
21
+ └── gr.Group(visible=False) ← pane_lyrics
22
+
23
+ The Radio's ``change`` event fires ``_switch_pane(mode)`` which returns
24
+ visibility updates for the five Groups. The Radio's native ``:checked``
25
+ state gives us the sidebar "active item" highlight for free via CSS
26
+ (see ``theme.CSS`` for ``.ams-side-radio`` selectors).
27
+
28
+ DO NOT switch this back to ``gr.Tabs`` — that produces top-positioned
29
+ horizontal tabs which contradicts the wireframes.
30
+
31
+ On HF Spaces, ``_bootstrap()`` runs once on import to mirror the
32
+ read-only preload cache into a writable tree. On Mac/Linux locally,
33
+ it's a no-op until M7.
34
  """
35
 
36
  from __future__ import annotations
 
45
 
46
  import gradio as gr
47
 
48
+ import ace_pipeline
49
  import theme
50
 
51
  HEADER_HTML = """
 
53
  <div>
54
  <div class="ams-brand">ACE Music Studio<span class="ams-brand-period">.</span></div>
55
  </div>
56
+ <div class="ams-status" id="ams-status">ready</div>
57
  </div>
58
  """.strip()
59
 
60
+
61
+ def _status_html(device: str) -> str:
62
+ """Right-aligned status indicator in the header. Updated at boot only."""
63
+ return f"""
64
+ <div class="ams-header">
65
+ <div>
66
+ <div class="ams-brand">ACE Music Studio<span class="ams-brand-period">.</span></div>
67
+ </div>
68
+ <div class="ams-status">ready · {device.upper()}</div>
69
+ </div>
70
+ """.strip()
71
+
72
+
73
  CTA_HTML = """
74
  <div class="ams-cta">
75
  Built with <span class="ams-cta-heart">♥</span>.
 
81
  """.strip()
82
 
83
 
84
+ HISTORY_HTML = """
85
+ <div class="ams-history">
86
+ <div class="ams-history-title">History · session</div>
87
+ <div class="ams-history-empty">No generations yet</div>
88
+ </div>
89
+ """.strip()
90
+
91
+
92
+ MODE_CHOICES = [
93
+ ("🎵 Generate", "generate"),
94
+ ("🎤 Cover", "cover"),
95
+ ("⏩ Extend", "extend"),
96
+ ("✏️ Edit", "edit"),
97
+ ("✍️ Lyrics", "lyrics"),
98
+ ]
99
+
100
+
101
  def _bootstrap() -> None:
102
  """HF Spaces: mirror read-only preload cache into a writable tree.
103
 
 
107
 
108
 
109
  def build_app() -> gr.Blocks:
110
+ device = ace_pipeline.detect_device()
111
+
112
  with gr.Blocks(theme=theme.build_theme(), css=theme.CSS, title="ACE Music Studio") as demo:
113
+ gr.HTML(_status_html(device))
114
  gr.HTML(CTA_HTML)
115
 
116
+ with gr.Row(elem_classes=["ams-body"]):
117
+ # --- Sidebar ----------------------------------------------------
118
+ with gr.Column(scale=0, min_width=190, elem_classes=["ams-sidebar"]):
119
+ mode = gr.Radio(
120
+ choices=MODE_CHOICES,
121
+ value="generate",
122
+ label=None,
123
+ show_label=False,
124
+ container=False,
125
+ elem_classes=["ams-side-radio"],
126
+ )
127
+ gr.HTML(HISTORY_HTML)
128
+
129
+ # --- Content ----------------------------------------------------
130
+ with gr.Column(scale=10, elem_classes=["ams-content"]):
131
+ with gr.Group(visible=True, elem_classes=["ams-tab-pane"]) as pane_generate:
132
+ gr.Markdown("### 🎵 Generate\n\nPlaceholder — implemented in M1.")
133
+ with gr.Group(visible=False, elem_classes=["ams-tab-pane"]) as pane_cover:
134
+ gr.Markdown("### 🎤 Cover\n\nPlaceholder — implemented in M3.")
135
+ with gr.Group(visible=False, elem_classes=["ams-tab-pane"]) as pane_extend:
136
+ gr.Markdown("### ⏩ Extend\n\nPlaceholder — implemented in M3.")
137
+ with gr.Group(visible=False, elem_classes=["ams-tab-pane"]) as pane_edit:
138
+ gr.Markdown("### ✏️ Edit\n\nPlaceholder — implemented in M3.")
139
+ with gr.Group(visible=False, elem_classes=["ams-tab-pane"]) as pane_lyrics:
140
+ gr.Markdown("### ✍️ Lyrics\n\nPlaceholder — implemented in M4.")
141
+
142
+ panes = [pane_generate, pane_cover, pane_extend, pane_edit, pane_lyrics]
143
+
144
+ def _switch_pane(selected: str):
145
+ order = ["generate", "cover", "extend", "edit", "lyrics"]
146
+ return tuple(gr.Group(visible=(selected == name)) for name in order)
147
+
148
+ mode.change(fn=_switch_pane, inputs=mode, outputs=panes)
149
 
150
  return demo
151
 
theme.py CHANGED
@@ -2,6 +2,19 @@
2
 
3
  Palette tokens are the source of truth; CSS pulls from them. The audio
4
  waveform is the only optionally-colored element (rendered white in v1).
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  """
6
 
7
  from __future__ import annotations
@@ -18,6 +31,7 @@ INK = "#E5E5E5"
18
  INK_MUTED = "#6B6B6B"
19
  PRIMARY = "#FFFFFF"
20
  ERROR_BG = "#1A1A1A"
 
21
  RADIUS = "6px"
22
  FONT_STACK = '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'
23
 
@@ -48,38 +62,129 @@ def build_theme() -> gr.themes.Base:
48
 
49
 
50
  CSS = f"""
51
- /* --- Sole brand bits --------------------------------------------------- */
52
  .ams-header {{
53
  display:flex; justify-content:space-between; align-items:baseline;
54
- padding:8px 0 4px 0;
55
  }}
56
  .ams-brand {{
57
- font-size:16px; font-weight:600; letter-spacing:-0.01em; color:{INK};
58
  }}
59
  .ams-brand-period {{ color:{PRIMARY}; }}
60
- .ams-status {{ font-size:11px; color:{INK_MUTED}; letter-spacing:0.02em; }}
 
 
 
61
 
62
- /* --- CTA banner -------------------------------------------------------- */
63
  .ams-cta {{
64
- font-size:13px; color:{INK_MUTED}; margin:2px 0 12px 0; padding-bottom:10px;
 
65
  border-bottom:1px solid {BORDER};
66
  }}
67
  .ams-cta strong {{ color:{INK}; }}
68
  .ams-cta-heart {{ color:{PRIMARY}; }}
69
  .ams-cta a {{ color:{INK}; text-decoration:underline; }}
70
 
71
- /* --- Sidebar nav (desktop >= 1024) ------------------------------------ */
72
- .ams-sidebar {{ background:{SURFACE_STRONG}; padding:14px 10px; border-radius:{RADIUS}; min-width:170px; }}
73
- .ams-side-item {{
74
- display:block; padding:8px 10px; border-radius:4px; margin-bottom:3px;
75
- font-size:13px; color:{INK_MUTED}; cursor:pointer; text-decoration:none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  }}
77
- .ams-side-item.active {{
78
- background:#1A1A1A; color:{PRIMARY};
79
- border-left:2px solid {PRIMARY}; padding-left:8px;
 
80
  }}
81
 
82
- /* --- LoRA chip pill --------------------------------------------------- */
83
  .ams-chip {{
84
  display:inline-block; padding:5px 10px; border-radius:14px;
85
  font-size:11px; margin:0 5px 5px 0; background:{SURFACE_STRONG};
@@ -88,22 +193,64 @@ CSS = f"""
88
  .ams-chip.on {{ border-color:{PRIMARY}; color:{PRIMARY}; }}
89
  .ams-chip.upload {{ border-style:dashed; color:{PRIMARY}; }}
90
 
91
- /* --- LoRA file drop zone (tighten Gradio default ~400px height) ------ */
92
  .ams-lora-file .upload-container {{ min-height:56px !important; }}
93
 
94
- /* --- Hide Gradio footer ----------------------------------------------- */
95
  footer {{ display:none !important; }}
96
 
97
- /* --- Responsive: tablet 640-1024 px ----------------------------------- */
98
  @media (max-width: 1024px) {{
99
- .ams-sidebar {{ min-width:34px; padding:6px 4px; }}
100
- .ams-side-item {{ font-size:0; padding:6px; }}
101
- .ams-side-item::first-letter {{ font-size:16px; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }}
103
 
104
- /* --- Responsive: mobile < 640 px -------------------------------------- */
105
  @media (max-width: 640px) {{
106
- .ams-sidebar {{ display:none; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  .ams-cta {{ font-size:11px; }}
108
  }}
109
  """
 
2
 
3
  Palette tokens are the source of truth; CSS pulls from them. The audio
4
  waveform is the only optionally-colored element (rendered white in v1).
5
+
6
+ UI architecture (locked):
7
+ - Sidebar layout (NOT ``gr.Tabs``) per wireframes at
8
+ ``docs/superpowers/specs/mockups/``.
9
+ - ``.ams-sidebar`` is a flex column at desktop, fixed-width 170-190 px.
10
+ - ``.ams-side-radio`` is the mode-nav: a ``gr.Radio`` re-skinned via CSS
11
+ so each option renders as a full-width sidebar pill. The native
12
+ ``:checked`` pseudo-class supplies the "active" highlight.
13
+ - ``.ams-content`` is the right column containing 5 ``.ams-tab-pane``
14
+ groups; one is visible at a time.
15
+ - Media queries: at ``<= 1024 px`` the sidebar shrinks to an icon rail.
16
+ At ``<= 640 px`` the sidebar is replaced by a horizontal scroll strip
17
+ at the top.
18
  """
19
 
20
  from __future__ import annotations
 
31
  INK_MUTED = "#6B6B6B"
32
  PRIMARY = "#FFFFFF"
33
  ERROR_BG = "#1A1A1A"
34
+ HOVER_BG = "#1A1A1A"
35
  RADIUS = "6px"
36
  FONT_STACK = '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'
37
 
 
62
 
63
 
64
  CSS = f"""
65
+ /* === Body chrome ======================================================= */
66
  .ams-header {{
67
  display:flex; justify-content:space-between; align-items:baseline;
68
+ padding:10px 4px 6px 4px;
69
  }}
70
  .ams-brand {{
71
+ font-size:18px; font-weight:600; letter-spacing:-0.01em; color:{INK};
72
  }}
73
  .ams-brand-period {{ color:{PRIMARY}; }}
74
+ .ams-status {{
75
+ font-size:11px; color:{INK_MUTED};
76
+ letter-spacing:0.06em; text-transform:uppercase;
77
+ }}
78
 
 
79
  .ams-cta {{
80
+ font-size:13px; color:{INK_MUTED};
81
+ margin:2px 4px 14px 4px; padding-bottom:12px;
82
  border-bottom:1px solid {BORDER};
83
  }}
84
  .ams-cta strong {{ color:{INK}; }}
85
  .ams-cta-heart {{ color:{PRIMARY}; }}
86
  .ams-cta a {{ color:{INK}; text-decoration:underline; }}
87
 
88
+ /* === Body row (sidebar + content) ====================================== */
89
+ .ams-body {{
90
+ gap:16px !important;
91
+ align-items:stretch !important;
92
+ }}
93
+
94
+ /* === Sidebar (desktop >= 1024px) ======================================= */
95
+ .ams-sidebar {{
96
+ background:{SURFACE_STRONG} !important;
97
+ padding:14px 8px !important;
98
+ border-radius:{RADIUS} !important;
99
+ border:1px solid {BORDER} !important;
100
+ min-width:190px;
101
+ max-width:210px;
102
+ }}
103
+
104
+ /* --- Mode radio (re-skin gr.Radio as a vertical sidebar nav) ----------- */
105
+ .ams-side-radio {{
106
+ background:transparent !important;
107
+ border:none !important;
108
+ padding:0 !important;
109
+ width:100%;
110
+ }}
111
+ .ams-side-radio .wrap {{
112
+ display:flex !important;
113
+ flex-direction:column !important;
114
+ gap:2px !important;
115
+ background:transparent !important;
116
+ border:none !important;
117
+ }}
118
+ /* Each radio option becomes a sidebar pill */
119
+ .ams-side-radio label {{
120
+ display:flex !important;
121
+ align-items:center !important;
122
+ padding:9px 12px !important;
123
+ margin:0 !important;
124
+ border-radius:4px !important;
125
+ border:none !important;
126
+ border-left:2px solid transparent !important;
127
+ background:transparent !important;
128
+ color:{INK_MUTED} !important;
129
+ font-size:13px !important;
130
+ font-weight:500 !important;
131
+ cursor:pointer !important;
132
+ transition:background 80ms ease, color 80ms ease, border-color 80ms ease;
133
+ min-height:0 !important;
134
+ width:100%;
135
+ box-sizing:border-box;
136
+ }}
137
+ .ams-side-radio label:hover {{
138
+ background:{HOVER_BG} !important;
139
+ color:{INK} !important;
140
+ }}
141
+ /* Hide the native radio circle */
142
+ .ams-side-radio label input[type="radio"] {{
143
+ display:none !important;
144
+ }}
145
+ /* Active state: white text + white left border + dark bg */
146
+ .ams-side-radio label.selected,
147
+ .ams-side-radio label:has(input[type="radio"]:checked) {{
148
+ background:{HOVER_BG} !important;
149
+ color:{PRIMARY} !important;
150
+ border-left-color:{PRIMARY} !important;
151
+ font-weight:600 !important;
152
+ }}
153
+ /* Hide the (now-empty) form-element-info row that gr.Radio injects */
154
+ .ams-side-radio + div:empty {{ display:none !important; }}
155
+
156
+ /* --- Session history block (below the mode radio) --------------------- */
157
+ .ams-history {{
158
+ margin-top:14px;
159
+ padding-top:10px;
160
+ border-top:1px solid {BORDER};
161
+ }}
162
+ .ams-history-title {{
163
+ font-size:10px; color:{INK_MUTED};
164
+ letter-spacing:0.1em; text-transform:uppercase;
165
+ padding:0 4px 6px 4px;
166
+ }}
167
+ .ams-history-empty {{
168
+ font-size:11px; color:#3F3F3F;
169
+ font-style:italic;
170
+ padding:6px 4px;
171
+ }}
172
+
173
+ /* === Content area ====================================================== */
174
+ .ams-content {{
175
+ background:{SURFACE} !important;
176
+ border:1px solid {BORDER} !important;
177
+ border-radius:{RADIUS} !important;
178
+ padding:16px !important;
179
+ min-height:540px;
180
  }}
181
+ .ams-tab-pane {{
182
+ background:transparent !important;
183
+ border:none !important;
184
+ padding:0 !important;
185
  }}
186
 
187
+ /* === LoRA chip pill (used in M2+) ====================================== */
188
  .ams-chip {{
189
  display:inline-block; padding:5px 10px; border-radius:14px;
190
  font-size:11px; margin:0 5px 5px 0; background:{SURFACE_STRONG};
 
193
  .ams-chip.on {{ border-color:{PRIMARY}; color:{PRIMARY}; }}
194
  .ams-chip.upload {{ border-style:dashed; color:{PRIMARY}; }}
195
 
196
+ /* === LoRA file drop zone (tighten Gradio default ~400px height) ======== */
197
  .ams-lora-file .upload-container {{ min-height:56px !important; }}
198
 
199
+ /* === Hide Gradio footer ================================================ */
200
  footer {{ display:none !important; }}
201
 
202
+ /* === Responsive: tablet 640-1024 px ==================================== */
203
  @media (max-width: 1024px) {{
204
+ .ams-sidebar {{
205
+ min-width:48px !important;
206
+ max-width:48px !important;
207
+ padding:8px 4px !important;
208
+ }}
209
+ /* Hide labels, keep only the leading emoji */
210
+ .ams-side-radio label {{
211
+ font-size:0 !important;
212
+ padding:8px 0 !important;
213
+ justify-content:center !important;
214
+ }}
215
+ .ams-side-radio label::first-letter {{
216
+ font-size:16px !important;
217
+ }}
218
+ /* Hide history in tablet rail mode */
219
+ .ams-history {{ display:none !important; }}
220
  }}
221
 
222
+ /* === Responsive: mobile < 640 px ======================================= */
223
  @media (max-width: 640px) {{
224
+ .ams-body {{
225
+ flex-direction:column !important;
226
+ }}
227
+ .ams-sidebar {{
228
+ min-width:100% !important;
229
+ max-width:100% !important;
230
+ padding:6px !important;
231
+ }}
232
+ /* Mobile: switch sidebar to horizontal scroll strip */
233
+ .ams-side-radio .wrap {{
234
+ flex-direction:row !important;
235
+ overflow-x:auto !important;
236
+ gap:4px !important;
237
+ }}
238
+ .ams-side-radio label {{
239
+ font-size:11px !important;
240
+ white-space:nowrap !important;
241
+ border-left:none !important;
242
+ border-bottom:2px solid transparent !important;
243
+ padding:8px 10px !important;
244
+ justify-content:flex-start !important;
245
+ }}
246
+ .ams-side-radio label::first-letter {{
247
+ font-size:13px !important;
248
+ }}
249
+ .ams-side-radio label:has(input[type="radio"]:checked) {{
250
+ border-left-color:transparent !important;
251
+ border-bottom-color:{PRIMARY} !important;
252
+ }}
253
+ .ams-history {{ display:none !important; }}
254
  .ams-cta {{ font-size:11px; }}
255
  }}
256
  """