techfreakworm commited on
Commit
3a8ec98
·
unverified ·
1 Parent(s): 0ef5d8d

feat(ui): m6 polish — in-memory history sidebar + _safe_call error wrapper

Browse files

H2 + H3 from the M6 plan, in one commit because the app.py hunks are
interleaved.

- _HISTORY list + _history_render + _history_push helpers, capped at 12
newest-first entries. Each on_*_click handler now returns a third
output (history HTML) wired into the dynamic gr.HTML in the sidebar.
- _safe_call wrapper centralises LoRAValidationError / ValueError /
FileNotFoundError / RuntimeError -> gr.Error translation so each
click handler stays a single call into modes.*. MPS RuntimeErrors get
the PYTORCH_ENABLE_MPS_FALLBACK hint.
- theme.CSS gains compact .ams-history-row styling (mono mode token +
ellipsis sans label) and hides the history block at the 640 px
mobile breakpoint.

Files changed (2) hide show
  1. app.py +175 -110
  2. theme.py +47 -0
app.py CHANGED
@@ -67,6 +67,33 @@ def get_backend() -> be.ACEStepStudioBackend:
67
  return _BACKEND
68
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  def _sha256(path: str) -> str:
71
  """Stream a file through SHA-256 in 64 KB chunks.
72
 
@@ -172,24 +199,23 @@ def on_generate_click(
172
  progress=gr.Progress(track_tqdm=True), # noqa: B008
173
  ):
174
  loras = [lora_state] if lora_state else []
175
- try:
176
- out_path, meta = modes.generate(
177
- get_backend(),
178
- params={
179
- "prompt": prompt,
180
- "lyrics": lyrics,
181
- "duration_s": int(duration_s),
182
- "instrumental": instrumental_label == "Instrumental",
183
- "seed": random.randint(1, 2_147_483_647),
184
- "loras": loras,
185
- "advanced": {},
186
- "lm": {},
187
- "dcw": {},
188
- },
189
- )
190
- except ValueError as e:
191
- raise gr.Error(str(e)) from e
192
- return out_path, meta
193
 
194
 
195
  def on_cover_click(
@@ -203,24 +229,24 @@ def on_cover_click(
203
  ):
204
  """Cover-mode click. ref_audio is a filepath from gr.Audio(type='filepath')."""
205
  loras = [lora_state] if lora_state else []
206
- try:
207
- return modes.cover(
208
- get_backend(),
209
- params={
210
- "ref_audio": ref_audio,
211
- "prompt": prompt,
212
- "lyrics": lyrics,
213
- "duration_s": int(duration_s),
214
- "audio_cover_strength": float(audio_cover_strength),
215
- "seed": random.randint(1, 2_147_483_647),
216
- "loras": loras,
217
- "advanced": {},
218
- "lm": {},
219
- "dcw": {},
220
- },
221
- )
222
- except ValueError as e:
223
- raise gr.Error(str(e)) from e
224
 
225
 
226
  def on_extend_click(
@@ -238,28 +264,28 @@ def on_extend_click(
238
  ):
239
  """Extend-mode click. seed_audio is a filepath from gr.Audio(type='filepath')."""
240
  loras = [lora_state] if lora_state else []
241
- try:
242
- return modes.extend(
243
- get_backend(),
244
- params={
245
- "seed_audio": seed_audio,
246
- "extra_prompt": extra_prompt,
247
- "extension_lyrics": extension_lyrics,
248
- "extra_duration_s": int(extra_duration_s),
249
- "wav_crossfade_s": float(wav_crossfade_s),
250
- "repaint_mode": repaint_mode,
251
- "repaint_strength": float(repaint_strength),
252
- "latent_crossfade_frames": int(latent_crossfade_frames),
253
- "chunk_mask_mode": chunk_mask_mode,
254
- "seed": random.randint(1, 2_147_483_647),
255
- "loras": loras,
256
- "advanced": {},
257
- "lm": {},
258
- "dcw": {},
259
- },
260
- )
261
- except ValueError as e:
262
- raise gr.Error(str(e)) from e
263
 
264
 
265
  def on_draft_lyrics(
@@ -283,27 +309,27 @@ def on_draft_lyrics(
283
  ``lyrics_lm``; the first click triggers a ~4 GB MLX download (cached
284
  afterwards) and ~30 s warm-up before the draft appears.
285
  """
286
- try:
287
- return modes.lyrics(
288
- get_backend(),
289
- params={
290
- "brief": brief,
291
- "structure": structure,
292
- "language": language,
293
- "tone": tone,
294
- "verse_lines": int(verse_lines),
295
- "chorus_lines": int(chorus_lines),
296
- "bridge_lines": int(bridge_lines),
297
- "rhyme": rhyme,
298
- "temperature": float(temperature),
299
- "top_p": float(top_p),
300
- "top_k": int(top_k),
301
- "max_new_tokens": int(max_new_tokens),
302
- "seed": int(seed) if seed is not None else None,
303
- },
304
- )
305
- except ValueError as e:
306
- raise gr.Error(str(e)) from e
307
 
308
 
309
  def on_separate_stems(audio_path):
@@ -357,31 +383,31 @@ def on_edit_click(
357
  ):
358
  """Edit-mode click. source_audio is a filepath from gr.Audio(type='filepath')."""
359
  loras = [lora_state] if lora_state else []
360
- try:
361
- return modes.edit(
362
- get_backend(),
363
- params={
364
- "source_audio": source_audio,
365
- "sub_mode": sub_mode,
366
- "source_lyrics": source_lyrics,
367
- "target_lyrics": target_lyrics,
368
- "segment_start_s": float(segment_start_s),
369
- "segment_end_s": float(segment_end_s),
370
- "repaint_strength": float(repaint_strength),
371
- "repaint_mode": repaint_mode,
372
- "flow_source_caption": flow_source_caption,
373
- "flow_n_min": float(flow_n_min),
374
- "flow_n_max": float(flow_n_max),
375
- "flow_n_avg": int(flow_n_avg),
376
- "seed": random.randint(1, 2_147_483_647),
377
- "loras": loras,
378
- "advanced": {},
379
- "lm": {},
380
- "dcw": {},
381
- },
382
- )
383
- except ValueError as e:
384
- raise gr.Error(str(e)) from e
385
 
386
 
387
  HEADER_HTML = """
@@ -425,6 +451,41 @@ HISTORY_HTML = """
425
  """.strip()
426
 
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  MODE_CHOICES = [
429
  ("🎵 Generate", "generate"),
430
  ("🎤 Cover", "cover"),
@@ -460,7 +521,11 @@ def build_app() -> gr.Blocks:
460
  container=False,
461
  elem_classes=["ams-side-radio"],
462
  )
463
- gr.HTML(HISTORY_HTML)
 
 
 
 
464
 
465
  # --- Content ----------------------------------------------------
466
  with gr.Column(scale=10, elem_classes=["ams-content"]):
@@ -490,7 +555,7 @@ def build_app() -> gr.Blocks:
490
  g["instrumental"],
491
  g["lora_state"],
492
  ],
493
- outputs=[g["output_audio"], g["output_meta"]],
494
  )
495
  # Post-processing actions (M5/G2)
496
  g["separate_stems_btn"].click(
@@ -535,7 +600,7 @@ def build_app() -> gr.Blocks:
535
  c["audio_cover_strength"],
536
  c["lora_state"],
537
  ],
538
- outputs=[c["output_audio"], c["output_meta"]],
539
  )
540
  # Post-processing actions (M5/G2)
541
  c["separate_stems_btn"].click(
@@ -584,7 +649,7 @@ def build_app() -> gr.Blocks:
584
  x["chunk_mask_mode"],
585
  x["lora_state"],
586
  ],
587
- outputs=[x["output_audio"], x["output_meta"]],
588
  )
589
  # Post-processing actions (M5/G2)
590
  x["separate_stems_btn"].click(
@@ -636,7 +701,7 @@ def build_app() -> gr.Blocks:
636
  e["flow_n_avg"],
637
  e["lora_state"],
638
  ],
639
- outputs=[e["output_audio"], e["output_meta"]],
640
  )
641
  # Post-processing actions (M5/G2)
642
  e["separate_stems_btn"].click(
@@ -673,7 +738,7 @@ def build_app() -> gr.Blocks:
673
  lyr["max_new_tokens"],
674
  lyr["seed"],
675
  ],
676
- outputs=[lyr["lyrics_output"], lyr["meta_output"]],
677
  )
678
  # Cross-tab "Use these in Generate" — pipes the drafted
679
  # text straight into the Generate tab's lyrics textbox.
 
67
  return _BACKEND
68
 
69
 
70
+ def _safe_call(fn, *args, **kwargs):
71
+ """Wrap a mode handler so all known exceptions become friendly gr.Error toasts.
72
+
73
+ Centralising this here lets every on_*_click handler stay a single-line
74
+ call into modes.* without each one repeating the try/except mosaic. The
75
+ error classes mirror what each mode handler can actually raise:
76
+
77
+ - ``lora_stack.LoRAValidationError`` — uploaded LoRA isn't compatible
78
+ - ``ValueError`` — mode-handler param validation (missing prompt, etc.)
79
+ - ``FileNotFoundError`` — user-supplied ref_audio path doesn't exist
80
+ - ``RuntimeError`` — pipeline crash, including MPS op-fallback failures
81
+ """
82
+ try:
83
+ return fn(*args, **kwargs)
84
+ except lora_stack.LoRAValidationError as e:
85
+ raise gr.Error(str(e)) from e
86
+ except ValueError as e:
87
+ raise gr.Error(str(e)) from e
88
+ except FileNotFoundError as e:
89
+ raise gr.Error(f"File not found: {e}") from e
90
+ except RuntimeError as e:
91
+ msg = str(e)
92
+ if "MPS" in msg or "mps" in msg:
93
+ raise gr.Error(f"Apple Silicon op issue: {msg}. PYTORCH_ENABLE_MPS_FALLBACK is enabled.") from e
94
+ raise gr.Error(f"Generation failed: {msg}") from e
95
+
96
+
97
  def _sha256(path: str) -> str:
98
  """Stream a file through SHA-256 in 64 KB chunks.
99
 
 
199
  progress=gr.Progress(track_tqdm=True), # noqa: B008
200
  ):
201
  loras = [lora_state] if lora_state else []
202
+ out_path, meta = _safe_call(
203
+ modes.generate,
204
+ get_backend(),
205
+ params={
206
+ "prompt": prompt,
207
+ "lyrics": lyrics,
208
+ "duration_s": int(duration_s),
209
+ "instrumental": instrumental_label == "Instrumental",
210
+ "seed": random.randint(1, 2_147_483_647),
211
+ "loras": loras,
212
+ "advanced": {},
213
+ "lm": {},
214
+ "dcw": {},
215
+ },
216
+ )
217
+ new_history = _history_push("generate", prompt or "(no prompt)")
218
+ return out_path, meta, new_history
 
219
 
220
 
221
  def on_cover_click(
 
229
  ):
230
  """Cover-mode click. ref_audio is a filepath from gr.Audio(type='filepath')."""
231
  loras = [lora_state] if lora_state else []
232
+ out_path, meta = _safe_call(
233
+ modes.cover,
234
+ get_backend(),
235
+ params={
236
+ "ref_audio": ref_audio,
237
+ "prompt": prompt,
238
+ "lyrics": lyrics,
239
+ "duration_s": int(duration_s),
240
+ "audio_cover_strength": float(audio_cover_strength),
241
+ "seed": random.randint(1, 2_147_483_647),
242
+ "loras": loras,
243
+ "advanced": {},
244
+ "lm": {},
245
+ "dcw": {},
246
+ },
247
+ )
248
+ new_history = _history_push("cover", prompt or "(cover)")
249
+ return out_path, meta, new_history
250
 
251
 
252
  def on_extend_click(
 
264
  ):
265
  """Extend-mode click. seed_audio is a filepath from gr.Audio(type='filepath')."""
266
  loras = [lora_state] if lora_state else []
267
+ out_path, meta = _safe_call(
268
+ modes.extend,
269
+ get_backend(),
270
+ params={
271
+ "seed_audio": seed_audio,
272
+ "extra_prompt": extra_prompt,
273
+ "extension_lyrics": extension_lyrics,
274
+ "extra_duration_s": int(extra_duration_s),
275
+ "wav_crossfade_s": float(wav_crossfade_s),
276
+ "repaint_mode": repaint_mode,
277
+ "repaint_strength": float(repaint_strength),
278
+ "latent_crossfade_frames": int(latent_crossfade_frames),
279
+ "chunk_mask_mode": chunk_mask_mode,
280
+ "seed": random.randint(1, 2_147_483_647),
281
+ "loras": loras,
282
+ "advanced": {},
283
+ "lm": {},
284
+ "dcw": {},
285
+ },
286
+ )
287
+ new_history = _history_push("extend", extra_prompt or "(extend)")
288
+ return out_path, meta, new_history
289
 
290
 
291
  def on_draft_lyrics(
 
309
  ``lyrics_lm``; the first click triggers a ~4 GB MLX download (cached
310
  afterwards) and ~30 s warm-up before the draft appears.
311
  """
312
+ lyrics_text, meta = _safe_call(
313
+ modes.lyrics,
314
+ get_backend(),
315
+ params={
316
+ "brief": brief,
317
+ "structure": structure,
318
+ "language": language,
319
+ "tone": tone,
320
+ "verse_lines": int(verse_lines),
321
+ "chorus_lines": int(chorus_lines),
322
+ "bridge_lines": int(bridge_lines),
323
+ "rhyme": rhyme,
324
+ "temperature": float(temperature),
325
+ "top_p": float(top_p),
326
+ "top_k": int(top_k),
327
+ "max_new_tokens": int(max_new_tokens),
328
+ "seed": int(seed) if seed is not None else None,
329
+ },
330
+ )
331
+ new_history = _history_push("lyrics", brief or "(brief)")
332
+ return lyrics_text, meta, new_history
333
 
334
 
335
  def on_separate_stems(audio_path):
 
383
  ):
384
  """Edit-mode click. source_audio is a filepath from gr.Audio(type='filepath')."""
385
  loras = [lora_state] if lora_state else []
386
+ out_path, meta = _safe_call(
387
+ modes.edit,
388
+ get_backend(),
389
+ params={
390
+ "source_audio": source_audio,
391
+ "sub_mode": sub_mode,
392
+ "source_lyrics": source_lyrics,
393
+ "target_lyrics": target_lyrics,
394
+ "segment_start_s": float(segment_start_s),
395
+ "segment_end_s": float(segment_end_s),
396
+ "repaint_strength": float(repaint_strength),
397
+ "repaint_mode": repaint_mode,
398
+ "flow_source_caption": flow_source_caption,
399
+ "flow_n_min": float(flow_n_min),
400
+ "flow_n_max": float(flow_n_max),
401
+ "flow_n_avg": int(flow_n_avg),
402
+ "seed": random.randint(1, 2_147_483_647),
403
+ "loras": loras,
404
+ "advanced": {},
405
+ "lm": {},
406
+ "dcw": {},
407
+ },
408
+ )
409
+ new_history = _history_push("edit", target_lyrics or sub_mode or "(edit)")
410
+ return out_path, meta, new_history
411
 
412
 
413
  HEADER_HTML = """
 
451
  """.strip()
452
 
453
 
454
+ # --- In-memory history (M6/H2) ----------------------------------------------
455
+ # Per spec §13, persistent history is out of scope for v1. The sidebar block
456
+ # is an in-process list that lives for the lifetime of the Gradio process and
457
+ # resets on reload. Newest entries first; capped at _HISTORY_MAX so the
458
+ # bordered sidebar stays compact at the desktop breakpoint.
459
+ _HISTORY: list[dict] = []
460
+ _HISTORY_MAX = 12
461
+
462
+
463
+ def _history_render() -> str:
464
+ """Render _HISTORY into the sidebar HTML block.
465
+
466
+ Falls back to the empty-state HTML constant when no rows are present so
467
+ the placeholder copy stays exactly aligned with the wireframe.
468
+ """
469
+ if not _HISTORY:
470
+ return HISTORY_HTML
471
+ rows_html = "\n".join(
472
+ f'<div class="ams-history-row" title="{h["label"]}">'
473
+ f'<span class="ams-history-mode">{h["mode"]}</span>'
474
+ f'<span class="ams-history-label">{h["label"]}</span>'
475
+ f"</div>"
476
+ for h in _HISTORY
477
+ )
478
+ return f'<div class="ams-history"><div class="ams-history-title">History · session</div>{rows_html}</div>'
479
+
480
+
481
+ def _history_push(mode: str, label: str) -> str:
482
+ """Push a generation onto the history and return the new HTML."""
483
+ _HISTORY.insert(0, {"mode": mode, "label": (label or "").strip()[:30] or "(untitled)"})
484
+ while len(_HISTORY) > _HISTORY_MAX:
485
+ _HISTORY.pop()
486
+ return _history_render()
487
+
488
+
489
  MODE_CHOICES = [
490
  ("🎵 Generate", "generate"),
491
  ("🎤 Cover", "cover"),
 
521
  container=False,
522
  elem_classes=["ams-side-radio"],
523
  )
524
+ # Dynamic in-memory history (M6/H2). Initial value renders
525
+ # the same "No generations yet" placeholder the static block
526
+ # used to emit; each click handler refreshes the HTML via
527
+ # _history_push().
528
+ history_html = gr.HTML(HISTORY_HTML, elem_classes=["ams-history-wrapper"])
529
 
530
  # --- Content ----------------------------------------------------
531
  with gr.Column(scale=10, elem_classes=["ams-content"]):
 
555
  g["instrumental"],
556
  g["lora_state"],
557
  ],
558
+ outputs=[g["output_audio"], g["output_meta"], history_html],
559
  )
560
  # Post-processing actions (M5/G2)
561
  g["separate_stems_btn"].click(
 
600
  c["audio_cover_strength"],
601
  c["lora_state"],
602
  ],
603
+ outputs=[c["output_audio"], c["output_meta"], history_html],
604
  )
605
  # Post-processing actions (M5/G2)
606
  c["separate_stems_btn"].click(
 
649
  x["chunk_mask_mode"],
650
  x["lora_state"],
651
  ],
652
+ outputs=[x["output_audio"], x["output_meta"], history_html],
653
  )
654
  # Post-processing actions (M5/G2)
655
  x["separate_stems_btn"].click(
 
701
  e["flow_n_avg"],
702
  e["lora_state"],
703
  ],
704
+ outputs=[e["output_audio"], e["output_meta"], history_html],
705
  )
706
  # Post-processing actions (M5/G2)
707
  e["separate_stems_btn"].click(
 
738
  lyr["max_new_tokens"],
739
  lyr["seed"],
740
  ],
741
+ outputs=[lyr["lyrics_output"], lyr["meta_output"], history_html],
742
  )
743
  # Cross-tab "Use these in Generate" — pipes the drafted
744
  # text straight into the Generate tab's lyrics textbox.
theme.py CHANGED
@@ -954,6 +954,53 @@ main, .contain {{
954
  }}
955
  }}
956
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
957
  /* Hide Gradio footer + the floating "Use via API" / settings panel */
958
  footer {{ display:none !important; }}
959
  .show-api {{ display:none !important; }}
 
954
  }}
955
  }}
956
 
957
+ /* ============================================================
958
+ * History rows — clickable-looking compact list (M6/H2)
959
+ * Replaces the static "No generations yet" placeholder with a live
960
+ * in-memory feed of mode + label tuples. The mode segment renders in
961
+ * mono uppercase to mirror the small uppercase labels used throughout
962
+ * the sidebar; the label segment uses the sans body face and truncates
963
+ * with ellipsis at the sidebar's compact 188-210 px width.
964
+ * ============================================================ */
965
+ .ams-content .ams-history-wrapper {{
966
+ margin-top: 4px;
967
+ }}
968
+ .ams-history-row {{
969
+ display: flex;
970
+ gap: 6px;
971
+ align-items: baseline;
972
+ font-family: {FONT_MONO};
973
+ font-size: 10px;
974
+ color: {INK_MUTED};
975
+ padding: 4px 6px;
976
+ border-radius: 3px;
977
+ cursor: default;
978
+ }}
979
+ .ams-history-row:hover {{
980
+ background: {HOVER_BG};
981
+ color: {INK};
982
+ }}
983
+ .ams-history-mode {{
984
+ color: {PRIMARY};
985
+ text-transform: lowercase;
986
+ letter-spacing: 0;
987
+ flex-shrink: 0;
988
+ }}
989
+ .ams-history-label {{
990
+ color: {INK_MUTED};
991
+ overflow: hidden;
992
+ text-overflow: ellipsis;
993
+ white-space: nowrap;
994
+ font-family: {FONT_SANS};
995
+ font-size: 11px;
996
+ }}
997
+ @media (max-width: 640px) {{
998
+ .ams-history,
999
+ .ams-history-wrapper {{
1000
+ display: none !important;
1001
+ }}
1002
+ }}
1003
+
1004
  /* Hide Gradio footer + the floating "Use via API" / settings panel */
1005
  footer {{ display:none !important; }}
1006
  .show-api {{ display:none !important; }}