kalamishere commited on
Commit
5dbb9fb
·
1 Parent(s): 0fe228e

Variant card layout — title above audio; bars on 3 lines, v-name bottom-right; anchor matches

Browse files
Files changed (2) hide show
  1. app.py +99 -43
  2. theme.py +54 -0
app.py CHANGED
@@ -815,17 +815,28 @@ def regenerate_variants(prompt_text: str, last_run_state, duration: int = 15, ap
815
  paths: list[str | None] = [None, None, None, None, None]
816
  matches: list[dict | None] = [None, None, None, None, None]
817
 
818
- def _slot(idx: int):
819
- """Build the gr.update for slot idx. Label encodes the BPM / Harm /
820
- Rhyt bar breakdown once we've scored the variant against the anchor;
821
- plain `v3` before scoring or on failure."""
822
- label = _match_label(idx, matches[idx])
823
  if paths[idx]:
824
- return gr.update(value=paths[idx], visible=True, label=label)
825
- return gr.update(value=None, visible=True, label=label)
 
 
 
 
 
826
 
827
  def _emit(status: str):
828
- return (*(_slot(i) for i in range(5)), status)
 
 
 
 
 
 
 
 
829
 
830
  if not (prompt_text or "").strip():
831
  yield _emit("_❌ Enter a prompt to regenerate from._")
@@ -1050,26 +1061,39 @@ def _quick_match(variant_path: str, anchor_bpm: float | None,
1050
  return None
1051
 
1052
 
1053
- def _match_label(idx: int, breakdown: dict | None) -> str:
1054
- """Render the variant Audio's label as `V3 · BPM ▮▮▮▯ · Harm ▮▮▮▮ · Rhyt ▮▮▯▯`.
 
 
 
 
 
 
 
1055
 
1056
- Each axis maps 0–100 0–4 filled blocks (▮) padded to 4 with ▯.
1057
- Pre-regen / failure: returns just `v3`."""
 
 
 
 
1058
  if not breakdown:
1059
- return f"v{idx + 1}"
1060
- def bars(v: int) -> str:
1061
- # Quartile thresholds 12 / 38 / 63 / 88. Tightening these later
1062
- # is cheap if the spread feels off in practice.
1063
- if v >= 88: n = 4
1064
- elif v >= 63: n = 3
1065
- elif v >= 38: n = 2
1066
- elif v >= 12: n = 1
1067
- else: n = 0
1068
- return "" * n + "▯" * (4 - n)
1069
- return (f"V{idx + 1} · "
1070
- f"BPM {bars(breakdown['bpm'])} · "
1071
- f"Harm {bars(breakdown['harm'])} · "
1072
- f"Rhyt {bars(breakdown['rhyt'])}")
 
 
1073
 
1074
 
1075
  def _section_seek_html(a) -> str:
@@ -1453,21 +1477,47 @@ def build_ui() -> gr.Blocks:
1453
  gr.HTML(cost_pip_html(SA3_COST_PER_CALL * 5, suffix="5 variants · flat"))
1454
  regen_status = gr.Markdown("")
1455
  # Anchor + 5 variants on one row so users can A/B-test
1456
- # each variant against the original. The anchor's audio
1457
- # path is populated from last_run on analysis-complete;
1458
- # hidden until then so the slot doesn't sit empty.
 
 
1459
  gr.HTML('<div class="dc-label" style="margin-top:14px;">'
1460
  'ANCHOR + 5 VARIANTS</div>')
1461
  with gr.Row(elem_classes=["dc-variant-row"]):
1462
- anchor_audio = gr.Audio(label="★ ANCHOR", interactive=False,
1463
- type="filepath", scale=1,
1464
- visible=False,
1465
- elem_classes=["dc-anchor-audio"])
1466
- var1 = gr.Audio(label="v1", interactive=False, type="filepath", scale=1)
1467
- var2 = gr.Audio(label="v2", interactive=False, type="filepath", scale=1)
1468
- var3 = gr.Audio(label="v3", interactive=False, type="filepath", scale=1)
1469
- var4 = gr.Audio(label="v4", interactive=False, type="filepath", scale=1)
1470
- var5 = gr.Audio(label="v5", interactive=False, type="filepath", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1471
 
1472
  # Downstream outputs — three copy-paste artefacts a producer
1473
  # leaves with after the brief. Collapsed by default; expand
@@ -1786,17 +1836,21 @@ def build_ui() -> gr.Blocks:
1786
 
1787
  # Populate the anchor audio slot from last_run.audio_path so the
1788
  # user can A/B against the original next to V1–V5 in one row.
 
1789
  def _set_anchor(lr):
1790
  if not isinstance(lr, dict):
1791
- return gr.update(value=None, visible=False)
 
1792
  path = lr.get("audio_path") or None
1793
  if not path:
1794
- return gr.update(value=None, visible=False)
1795
- return gr.update(value=path, visible=True)
 
 
1796
  last_run.change(
1797
  fn=_set_anchor,
1798
  inputs=[last_run],
1799
- outputs=[anchor_audio],
1800
  )
1801
 
1802
  # Auto-snap variant duration to the closest preset for the anchor.
@@ -1825,7 +1879,9 @@ def build_ui() -> gr.Blocks:
1825
  regen_chain = regen_btn.click(
1826
  fn=regenerate_variants,
1827
  inputs=[sa3_var, last_run, regen_duration, api_key_state],
1828
- outputs=[var1, var2, var3, var4, var5, regen_status],
 
 
1829
  api_name="regenerate_variants",
1830
  ).then(
1831
  # After all 5 land, refresh the crate dropdown so the new tiles
 
815
  paths: list[str | None] = [None, None, None, None, None]
816
  matches: list[dict | None] = [None, None, None, None, None]
817
 
818
+ def _audio_slot(idx: int):
819
+ """gr.update for the Audio component at slot idx. Label is empty
820
+ because the visible title is now the gr.HTML above it."""
 
 
821
  if paths[idx]:
822
+ return gr.update(value=paths[idx], visible=True)
823
+ return gr.update(value=None, visible=True)
824
+
825
+ def _title_slot(idx: int):
826
+ """gr.update for the gr.HTML title above slot idx. Carries the
827
+ BPM/Harm/Rhyt bar breakdown once we've scored the variant."""
828
+ return gr.update(value=_variant_title_html(idx, matches[idx]))
829
 
830
  def _emit(status: str):
831
+ # Output order matches the regen_btn.click outputs binding:
832
+ # (var1, var2, var3, var4, var5,
833
+ # var1_title, var2_title, var3_title, var4_title, var5_title,
834
+ # regen_status)
835
+ return (
836
+ *(_audio_slot(i) for i in range(5)),
837
+ *(_title_slot(i) for i in range(5)),
838
+ status,
839
+ )
840
 
841
  if not (prompt_text or "").strip():
842
  yield _emit("_❌ Enter a prompt to regenerate from._")
 
1061
  return None
1062
 
1063
 
1064
+ def _bars(v: int) -> str:
1065
+ """Map a 0–100 score to a 4-block bar string. Quartiles at 12/38/63/88."""
1066
+ if v >= 88: n = 4
1067
+ elif v >= 63: n = 3
1068
+ elif v >= 38: n = 2
1069
+ elif v >= 12: n = 1
1070
+ else: n = 0
1071
+ return "▮" * n + "▯" * (4 - n)
1072
+
1073
 
1074
+ def _variant_title_html(idx: int, breakdown: dict | None) -> str:
1075
+ """Render a per-variant title block to sit above the Audio component.
1076
+
1077
+ Layout: BPM/Harm/Rhyt as three left-aligned lines (`Harm ▮▮▮▮`), name
1078
+ (`v1`) bottom-right. Pre-regen / failure: just the name."""
1079
+ name = f"v{idx + 1}"
1080
  if not breakdown:
1081
+ return (
1082
+ '<div class="dc-variant-title">'
1083
+ f'<div class="bars placeholder">BPM ▯▯▯▯<br>Harm ▯▯▯▯<br>Rhyt ▯▯▯▯</div>'
1084
+ f'<div class="name">{name}</div>'
1085
+ '</div>'
1086
+ )
1087
+ return (
1088
+ '<div class="dc-variant-title">'
1089
+ '<div class="bars">'
1090
+ f'BPM {_bars(breakdown["bpm"])}<br>'
1091
+ f'Harm {_bars(breakdown["harm"])}<br>'
1092
+ f'Rhyt {_bars(breakdown["rhyt"])}'
1093
+ '</div>'
1094
+ f'<div class="name">{name}</div>'
1095
+ '</div>'
1096
+ )
1097
 
1098
 
1099
  def _section_seek_html(a) -> str:
 
1477
  gr.HTML(cost_pip_html(SA3_COST_PER_CALL * 5, suffix="5 variants · flat"))
1478
  regen_status = gr.Markdown("")
1479
  # Anchor + 5 variants on one row so users can A/B-test
1480
+ # each variant against the original. Each card is a Column
1481
+ # with a custom HTML header (so the BPM/Harm/Rhyt bars can
1482
+ # render left-aligned on three lines) + an Audio component
1483
+ # with show_label=False (since the header replaces the label).
1484
+ # Same column wrapping for the anchor so all 6 cards line up.
1485
  gr.HTML('<div class="dc-label" style="margin-top:14px;">'
1486
  'ANCHOR + 5 VARIANTS</div>')
1487
  with gr.Row(elem_classes=["dc-variant-row"]):
1488
+ with gr.Column(scale=1, elem_classes=["dc-variant-cell"]):
1489
+ anchor_title = gr.HTML(
1490
+ '<div class="dc-variant-title">'
1491
+ '<div class="bars anchor-label">★ ANCHOR</div>'
1492
+ '<div class="name">orig</div>'
1493
+ '</div>',
1494
+ visible=False,
1495
+ )
1496
+ anchor_audio = gr.Audio(label="", show_label=False,
1497
+ interactive=False,
1498
+ type="filepath", scale=1,
1499
+ visible=False,
1500
+ elem_classes=["dc-anchor-audio"])
1501
+ with gr.Column(scale=1, elem_classes=["dc-variant-cell"]):
1502
+ var1_title = gr.HTML(_variant_title_html(0, None))
1503
+ var1 = gr.Audio(label="", show_label=False,
1504
+ interactive=False, type="filepath", scale=1)
1505
+ with gr.Column(scale=1, elem_classes=["dc-variant-cell"]):
1506
+ var2_title = gr.HTML(_variant_title_html(1, None))
1507
+ var2 = gr.Audio(label="", show_label=False,
1508
+ interactive=False, type="filepath", scale=1)
1509
+ with gr.Column(scale=1, elem_classes=["dc-variant-cell"]):
1510
+ var3_title = gr.HTML(_variant_title_html(2, None))
1511
+ var3 = gr.Audio(label="", show_label=False,
1512
+ interactive=False, type="filepath", scale=1)
1513
+ with gr.Column(scale=1, elem_classes=["dc-variant-cell"]):
1514
+ var4_title = gr.HTML(_variant_title_html(3, None))
1515
+ var4 = gr.Audio(label="", show_label=False,
1516
+ interactive=False, type="filepath", scale=1)
1517
+ with gr.Column(scale=1, elem_classes=["dc-variant-cell"]):
1518
+ var5_title = gr.HTML(_variant_title_html(4, None))
1519
+ var5 = gr.Audio(label="", show_label=False,
1520
+ interactive=False, type="filepath", scale=1)
1521
 
1522
  # Downstream outputs — three copy-paste artefacts a producer
1523
  # leaves with after the brief. Collapsed by default; expand
 
1836
 
1837
  # Populate the anchor audio slot from last_run.audio_path so the
1838
  # user can A/B against the original next to V1–V5 in one row.
1839
+ # Anchor title + audio flip visibility together.
1840
  def _set_anchor(lr):
1841
  if not isinstance(lr, dict):
1842
+ return (gr.update(visible=False),
1843
+ gr.update(value=None, visible=False))
1844
  path = lr.get("audio_path") or None
1845
  if not path:
1846
+ return (gr.update(visible=False),
1847
+ gr.update(value=None, visible=False))
1848
+ return (gr.update(visible=True),
1849
+ gr.update(value=path, visible=True))
1850
  last_run.change(
1851
  fn=_set_anchor,
1852
  inputs=[last_run],
1853
+ outputs=[anchor_title, anchor_audio],
1854
  )
1855
 
1856
  # Auto-snap variant duration to the closest preset for the anchor.
 
1879
  regen_chain = regen_btn.click(
1880
  fn=regenerate_variants,
1881
  inputs=[sa3_var, last_run, regen_duration, api_key_state],
1882
+ outputs=[var1, var2, var3, var4, var5,
1883
+ var1_title, var2_title, var3_title, var4_title, var5_title,
1884
+ regen_status],
1885
  api_name="regenerate_variants",
1886
  ).then(
1887
  # After all 5 land, refresh the crate dropdown so the new tiles
theme.py CHANGED
@@ -335,6 +335,60 @@ h3 { font-size: 13px; color: #C9CDD4; }
335
  background: #14171D;
336
  }
337
  .dc-var-hold .chips { display: flex; gap: 6px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  /* ---- Section seek strip — clickable chips beneath the analysis
339
  waveform. Each section's chip carries a 2px top-border in its
340
  semantic colour (mint-deep Intro / mint Build / coral Core /
 
335
  background: #14171D;
336
  }
337
  .dc-var-hold .chips { display: flex; gap: 6px; }
338
+ /* ---- Variant cards (anchor + 5 variants in one row) — each is a
339
+ Column containing a custom HTML title + an Audio component, so
340
+ they all line up at the same height regardless of how the audio
341
+ widget grows. Title carries the BPM/Harm/Rhyt bar breakdown on
342
+ three left-aligned lines + the v-name in the bottom-right. */
343
+ .dc-variant-cell {
344
+ min-width: 0; /* let columns shrink in a tight row */
345
+ }
346
+ .dc-variant-title {
347
+ background: #14171D;
348
+ border: 1px solid #232831;
349
+ border-radius: 8px 8px 0 0;
350
+ border-bottom: none;
351
+ padding: 8px 10px;
352
+ min-height: 76px;
353
+ display: flex;
354
+ justify-content: space-between;
355
+ align-items: flex-end;
356
+ gap: 6px;
357
+ }
358
+ .dc-variant-title .bars {
359
+ font-family: 'JetBrains Mono', monospace;
360
+ font-size: 10px;
361
+ line-height: 1.5;
362
+ color: #C9CDD4;
363
+ text-align: left;
364
+ flex: 1;
365
+ }
366
+ .dc-variant-title .bars.placeholder {
367
+ color: #3A3F47; /* faded — pre-regen state */
368
+ }
369
+ .dc-variant-title .bars.anchor-label {
370
+ font-size: 12px;
371
+ color: #FFC24B; /* amber for the anchor */
372
+ font-weight: 600;
373
+ letter-spacing: 0.05em;
374
+ }
375
+ .dc-variant-title .name {
376
+ font-family: 'JetBrains Mono', monospace;
377
+ font-size: 11px;
378
+ color: #5E6671;
379
+ font-weight: 600;
380
+ text-transform: uppercase;
381
+ letter-spacing: 0.08em;
382
+ }
383
+ /* The Audio sits directly under the title — kill its rounded top so
384
+ they read as one card. */
385
+ .dc-variant-cell .audio-container,
386
+ .dc-variant-cell [class*="audio"] > div:first-child {
387
+ border-top-left-radius: 0 !important;
388
+ border-top-right-radius: 0 !important;
389
+ border-top: none !important;
390
+ }
391
+
392
  /* ---- Section seek strip — clickable chips beneath the analysis
393
  waveform. Each section's chip carries a 2px top-border in its
394
  semantic colour (mint-deep Intro / mint Build / coral Core /