Spaces:
Sleeping
Sleeping
Commit ·
5dbb9fb
1
Parent(s): 0fe228e
Variant card layout — title above audio; bars on 3 lines, v-name bottom-right; anchor matches
Browse files
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
|
| 819 |
-
"""
|
| 820 |
-
|
| 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
|
| 825 |
-
return gr.update(value=None, visible=True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
|
| 827 |
def _emit(status: str):
|
| 828 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1054 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
|
| 1056 |
-
|
| 1057 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1058 |
if not breakdown:
|
| 1059 |
-
return
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
|
|
|
|
|
|
| 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.
|
| 1457 |
-
#
|
| 1458 |
-
#
|
|
|
|
|
|
|
| 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 |
-
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
| 1469 |
-
|
| 1470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
| 1792 |
path = lr.get("audio_path") or None
|
| 1793 |
if not path:
|
| 1794 |
-
return gr.update(
|
| 1795 |
-
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 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 /
|