Spaces:
Sleeping
Sleeping
Dylan Deshler commited on
Commit ·
fea940a
1
Parent(s): 021839a
UI refactor
Browse files
app.py
CHANGED
|
@@ -78,6 +78,36 @@ ALL_SIGNAL_LABELS = {
|
|
| 78 |
}
|
| 79 |
ALL_SIGNAL_CHOICES = [(ALL_SIGNAL_LABELS[s], s) for s in ALL_SIGNALS]
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
# The third tab is a lab bench rather than a feature: which model stage, the two
|
| 82 |
# history knobs whose right value is still an open question, and the A/B between
|
| 83 |
# the two guidance compositions. Off on a public Space, on with SHOW_TESTING=1.
|
|
@@ -988,9 +1018,10 @@ def _simple_status(msg: str) -> str:
|
|
| 988 |
# edit report from controls Generate does not have; the latter is bookkeeping
|
| 989 |
# about a track the listener can already hear, so it says nothing.
|
| 990 |
return out("" if head.startswith(
|
| 991 |
-
("Continued", "Fast-forwarded", "Regenerated", "
|
| 992 |
-
"
|
| 993 |
-
"
|
|
|
|
| 994 |
|
| 995 |
|
| 996 |
def _coverage_table(track: Track) -> str:
|
|
@@ -1018,9 +1049,9 @@ def _coverage_table(track: Track) -> str:
|
|
| 1018 |
for s in sigs:
|
| 1019 |
n_on = int(np.count_nonzero(np.asarray(c.active[s], dtype=bool)))
|
| 1020 |
if n_on == T:
|
| 1021 |
-
state = "🟩 **on**
|
| 1022 |
elif n_on == 0:
|
| 1023 |
-
state = "⬜ off
|
| 1024 |
else:
|
| 1025 |
state = f"🟨 partial — on for {n_on} of {T}"
|
| 1026 |
rows.append(f"| {ALL_SIGNAL_LABELS[s]} | {state} |")
|
|
@@ -1133,7 +1164,6 @@ def _preview_figure(track, signal, start, end, shape, mode, wave, *pvals):
|
|
| 1133 |
def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
| 1134 |
backend = backend or MockBackend()
|
| 1135 |
presets = backend.list_presets()
|
| 1136 |
-
signal_choices = [(SIGNAL_LABELS[s], s) for s in EDITABLE_SIGNALS]
|
| 1137 |
|
| 1138 |
with gr.Blocks(title="Conditioning Composer") as demo:
|
| 1139 |
gr.HTML(_PLOT_CSS, elem_classes="style-inject")
|
|
@@ -1227,14 +1257,15 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 1227 |
# GENERATE
|
| 1228 |
# =================================================================
|
| 1229 |
with gr.Tab("Generate"):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
gr.Markdown(
|
| 1231 |
-
"Pick something to start from,
|
| 1232 |
-
"
|
| 1233 |
-
"and take 24 measures of it apart — the track carries "
|
| 1234 |
-
"across.")
|
| 1235 |
with gr.Row():
|
| 1236 |
with gr.Column(scale=1):
|
| 1237 |
-
gr.Markdown("### 1 · Start from")
|
| 1238 |
s_preset = gr.Dropdown(presets, value=presets[0],
|
| 1239 |
label="A real song")
|
| 1240 |
s_preset_btn = gr.Button("Start from this song",
|
|
@@ -1252,12 +1283,23 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 1252 |
"it.")
|
| 1253 |
s_text_btn = gr.Button("Start from this description")
|
| 1254 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1255 |
# ---- tempo ------------------------------------------
|
| 1256 |
-
#
|
| 1257 |
-
#
|
| 1258 |
-
#
|
| 1259 |
-
#
|
| 1260 |
-
#
|
| 1261 |
# paragraph making it. The preview is live and works
|
| 1262 |
# with no track loaded -- dial the curve in BEFORE
|
| 1263 |
# generating, which is the whole reason it is up here.
|
|
@@ -1286,18 +1328,18 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 1286 |
"decode time, so it is never ignored, even with "
|
| 1287 |
"every signal off.</sub>")
|
| 1288 |
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
| 1301 |
s_audio = gr.Audio(label="Current track",
|
| 1302 |
type="filepath", interactive=False)
|
| 1303 |
# The radio's player lives HERE, in the same slot as the
|
|
@@ -1322,13 +1364,6 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 1322 |
# hear.
|
| 1323 |
s_summary = gr.Markdown(visible=False)
|
| 1324 |
|
| 1325 |
-
# One section, not two, because the three buttons at the
|
| 1326 |
-
# bottom of it are the same operation at three lengths --
|
| 1327 |
-
# eight measures, a leg, or until you stop it -- and all
|
| 1328 |
-
# of them read the controls above them. Splitting
|
| 1329 |
-
# "continue" from "radio" would put those controls under
|
| 1330 |
-
# one heading and silently apply them to the other.
|
| 1331 |
-
gr.Markdown("### 3 · Keep it going")
|
| 1332 |
# One control for every way the style can move, because
|
| 1333 |
# in the backend they are one choice: the typed steer,
|
| 1334 |
# the two walks and style-off are exclusive branches of
|
|
@@ -1455,7 +1490,8 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 1455 |
scale=1)
|
| 1456 |
s_status = gr.Markdown()
|
| 1457 |
|
| 1458 |
-
|
|
|
|
| 1459 |
# it, and this is the only place that answers whether
|
| 1460 |
# the mode you picked is doing anything. Updated on
|
| 1461 |
# every press and at every leg boundary of a live
|
|
@@ -1633,28 +1669,19 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 1633 |
# Turning a signal off over PART of a song is still
|
| 1634 |
# reachable, and correctly: seat the window there and
|
| 1635 |
# edit it as its own window.
|
| 1636 |
-
gr.Markdown(
|
| 1637 |
-
"### Conditioning coverage\n"
|
| 1638 |
-
f"On or off for **all {N_CHUNKS} measures the model "
|
| 1639 |
-
"is reading**. Where a signal is off the model "
|
| 1640 |
-
"invents it instead of following the curve; "
|
| 1641 |
-
"everything off is unconditional generation.\n\n"
|
| 1642 |
-
"<sub>Whole window only: this checkpoint never saw "
|
| 1643 |
-
"a signal that was on for some measures and off for "
|
| 1644 |
-
"others. To free one over part of a song, move the "
|
| 1645 |
-
"selection there and do it a window at a "
|
| 1646 |
-
"time.</sub>")
|
| 1647 |
coverage_tbl = gr.Markdown()
|
| 1648 |
-
|
| 1649 |
-
|
| 1650 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1651 |
with gr.Row():
|
| 1652 |
-
cov_on = gr.Button("
|
| 1653 |
-
|
| 1654 |
-
cov_off = gr.Button("Turn OFF (free)")
|
| 1655 |
-
with gr.Row():
|
| 1656 |
-
cov_all_off = gr.Button("All OFF (unconditional)")
|
| 1657 |
-
cov_all_on = gr.Button("All ON")
|
| 1658 |
|
| 1659 |
# =================================================================
|
| 1660 |
# EDIT
|
|
@@ -1703,90 +1730,95 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 1703 |
"training's *_cfg*). Higher = stronger "
|
| 1704 |
"adherence to the conditioning, but can "
|
| 1705 |
"distort.")
|
| 1706 |
-
vary_seed = gr.Number(value=2, label="Variation seed",
|
| 1707 |
-
precision=0)
|
| 1708 |
-
vary_btn = gr.Button("↻ Vary (new noise)")
|
| 1709 |
-
gr.Markdown(
|
| 1710 |
-
"<sub>*Vary* rewrites the whole window from the "
|
| 1711 |
-
"same conditioning with different noise — a second "
|
| 1712 |
-
"take, not an edit.</sub>")
|
| 1713 |
|
| 1714 |
-
# ----
|
| 1715 |
-
|
| 1716 |
-
|
| 1717 |
-
|
| 1718 |
-
|
| 1719 |
-
|
| 1720 |
-
|
| 1721 |
-
|
| 1722 |
-
|
| 1723 |
-
|
| 1724 |
-
|
| 1725 |
-
|
| 1726 |
-
|
| 1727 |
-
|
| 1728 |
-
|
| 1729 |
-
|
| 1730 |
-
|
| 1731 |
-
|
| 1732 |
-
|
| 1733 |
-
|
| 1734 |
-
|
| 1735 |
-
|
| 1736 |
-
|
| 1737 |
-
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
| 1744 |
-
|
| 1745 |
-
|
| 1746 |
-
|
| 1747 |
-
|
| 1748 |
-
|
| 1749 |
-
|
| 1750 |
-
|
| 1751 |
-
|
| 1752 |
-
|
| 1753 |
-
|
| 1754 |
-
|
| 1755 |
-
|
| 1756 |
-
|
| 1757 |
-
|
| 1758 |
-
|
| 1759 |
-
|
| 1760 |
-
|
| 1761 |
-
|
| 1762 |
-
|
| 1763 |
-
|
| 1764 |
-
|
| 1765 |
-
|
| 1766 |
-
|
| 1767 |
-
|
| 1768 |
-
|
| 1769 |
-
|
| 1770 |
-
|
| 1771 |
-
|
| 1772 |
-
|
| 1773 |
-
|
| 1774 |
-
|
| 1775 |
-
|
| 1776 |
-
|
| 1777 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1778 |
|
| 1779 |
# ---- regenerate ---------------------------------------------
|
| 1780 |
-
# At the bottom, after the
|
| 1781 |
-
#
|
| 1782 |
-
|
| 1783 |
-
|
| 1784 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1785 |
with gr.Row():
|
| 1786 |
-
# Follows the track (see refresh_window): pressing
|
| 1787 |
-
# Regenerate without touching anything has to be the same
|
| 1788 |
-
# seed the window was written with, or "unchanged" is a
|
| 1789 |
-
# setting the UI makes unreachable.
|
| 1790 |
rg_seed = gr.Number(value=1, label="Seed",
|
| 1791 |
precision=0, scale=1)
|
| 1792 |
regen_btn = gr.Button("♫ Regenerate",
|
|
@@ -2075,8 +2107,7 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 2075 |
return _edit_outputs(tr, f"Switched to stage {int(stage)} model.")
|
| 2076 |
|
| 2077 |
def _stage_controls(stage, cur_sig, cur_cov):
|
| 2078 |
-
"""Restrict the two
|
| 2079 |
-
to the signals the selected model reads.
|
| 2080 |
|
| 2081 |
The plots gate themselves off `track.stage`, but these are inputs,
|
| 2082 |
not views: with no track loaded there is nothing to gate against,
|
|
@@ -2085,10 +2116,19 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 2085 |
the track because stage_rb is what the entry points read for the
|
| 2086 |
NEXT generation — and on_swap pushes the restored track's stage back
|
| 2087 |
into it, so swapping is covered for free. The current selection is
|
| 2088 |
-
kept when it survives the switch.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2089 |
sigs = signals_for_stage(int(stage))
|
| 2090 |
ed = [(SIGNAL_LABELS[s], s) for s in EDITABLE_SIGNALS if s in sigs]
|
| 2091 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2092 |
|
| 2093 |
def keep(v, opts):
|
| 2094 |
return v if any(v == o[1] for o in opts) else opts[0][1]
|
|
@@ -2096,7 +2136,6 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 2096 |
return (
|
| 2097 |
gr.update(choices=ed, value=keep(cur_sig, ed)),
|
| 2098 |
gr.update(choices=cov, value=keep(cur_cov, cov)),
|
| 2099 |
-
gr.update(interactive=("chroma" in sigs)),
|
| 2100 |
)
|
| 2101 |
|
| 2102 |
def _diag_note(track):
|
|
@@ -2849,6 +2888,35 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 2849 |
tr, f"{mode.title()}d {shape} on **{lbl}** over "
|
| 2850 |
f"measures [{int(start)},{int(end)}).")
|
| 2851 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2852 |
# ---- moving the editable window --------------------------------------
|
| 2853 |
# Not a control of its own any more: the selection says which measures
|
| 2854 |
# to rewrite, `_sel_window` says which N_CHUNKS have to be loaded to do
|
|
@@ -2942,25 +3010,22 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 2942 |
f"the window was held frozen.{clamped}" + _splice_note(tr)
|
| 2943 |
+ _diag_note(tr))
|
| 2944 |
|
| 2945 |
-
def on_vary(track, seed, is_draft, guidance):
|
| 2946 |
-
if track is None:
|
| 2947 |
-
return (gr.update(),) * 5 + ("Start a track first.",)
|
| 2948 |
-
tr = backend.set_guidance(track, float(guidance))
|
| 2949 |
-
tr = backend.vary(tr, int(seed), n_steps=_steps(is_draft))
|
| 2950 |
-
a, c, cov, s = _render_outputs(tr)
|
| 2951 |
-
return (tr, a, c, cov, s,
|
| 2952 |
-
f"Variation with seed {int(seed)}." + _splice_note(tr))
|
| 2953 |
-
|
| 2954 |
def _apply_coverage(track, signal, on):
|
| 2955 |
-
"""Toggle a signal on/off across the WHOLE
|
| 2956 |
-
re-render). Not a range: see the comment on
|
|
|
|
|
|
|
| 2957 |
if track is None:
|
| 2958 |
return (gr.update(),) * 5 + ("Start a track first.",)
|
| 2959 |
-
|
| 2960 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2961 |
first = int(track.prefix_measures)
|
| 2962 |
return _edit_outputs(
|
| 2963 |
-
tr, f"Turned
|
| 2964 |
f"{first}–{first + track.cond.T}.")
|
| 2965 |
|
| 2966 |
def on_cov_on(track, signal):
|
|
@@ -2969,22 +3034,6 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 2969 |
def on_cov_off(track, signal):
|
| 2970 |
return _apply_coverage(track, signal, False)
|
| 2971 |
|
| 2972 |
-
def _apply_all(track, on):
|
| 2973 |
-
if track is None:
|
| 2974 |
-
return (gr.update(),) * 5 + ("Start a track first.",)
|
| 2975 |
-
tr = track
|
| 2976 |
-
for sig in ALL_SIGNALS:
|
| 2977 |
-
tr = backend.set_active(tr, sig, 0, tr.cond.T, on)
|
| 2978 |
-
msg = ("All signals off — unconditional." if not on
|
| 2979 |
-
else "All signals on — fully conditioned.")
|
| 2980 |
-
return _edit_outputs(tr, msg)
|
| 2981 |
-
|
| 2982 |
-
def on_cov_all_off(track):
|
| 2983 |
-
return _apply_all(track, False)
|
| 2984 |
-
|
| 2985 |
-
def on_cov_all_on(track):
|
| 2986 |
-
return _apply_all(track, True)
|
| 2987 |
-
|
| 2988 |
# ---- A/B slots ------------------------------------------------------
|
| 2989 |
# A = `cur` (being edited), B = `prev` (parked snapshot). Both hold a
|
| 2990 |
# whole Track, so a swap carries the conditioning arrays, per-measure
|
|
@@ -3127,17 +3176,19 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 3127 |
# `_raw_continue` / `_raw_fast_forward`, which were bound above this
|
| 3128 |
# block precisely so the Generate wrappers get mirrored once rather
|
| 3129 |
# than twice.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3130 |
(on_new_preset, on_new_text, on_new_upload, on_set_bpm,
|
| 3131 |
-
|
| 3132 |
-
|
| 3133 |
-
on_cov_on, on_cov_off, on_cov_all_off, on_cov_all_on, on_swap,
|
| 3134 |
-
on_seat) = [
|
| 3135 |
_mirror(f) for f in (
|
| 3136 |
on_new_preset, on_new_text, on_new_upload,
|
| 3137 |
-
on_set_bpm,
|
| 3138 |
-
on_simple_continue, on_simple_ff,
|
| 3139 |
-
|
| 3140 |
-
on_cov_all_off, on_cov_all_on, on_swap, on_seat)]
|
| 3141 |
on_undo = _mirror(on_undo, off=1)
|
| 3142 |
|
| 3143 |
# ---- wiring ---------------------------------------------------------
|
|
@@ -3155,6 +3206,13 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 3155 |
|
| 3156 |
def refresh_preview(track, signal, start, end, shape, mode, wave,
|
| 3157 |
*pvals):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3158 |
return _preview_figure(track, signal, start, end, shape, mode, wave,
|
| 3159 |
*pvals)
|
| 3160 |
|
|
@@ -3465,31 +3523,26 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 3465 |
.then(on_regen,
|
| 3466 |
[cur, sel_from, sel_to, rg_seed, draft, guidance_sl],
|
| 3467 |
out_all))
|
| 3468 |
-
tail(vary_btn.click(_snapshot, [cur, hist], hist).then(
|
| 3469 |
-
on_vary, [cur, vary_seed, draft, guidance_sl], out_all))
|
| 3470 |
|
| 3471 |
-
# --- the
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3472 |
tail(apply_btn.click(_snapshot, [cur, hist], hist).then(
|
| 3473 |
-
|
| 3474 |
[cur, signal_dd, ed_start, ed_end, shape_dd, mode_rb, wave_dd,
|
|
|
|
| 3475 |
draft, *pslots],
|
| 3476 |
out_all))
|
| 3477 |
-
|
| 3478 |
-
|
| 3479 |
-
tail(style_btn.click(_snapshot, [cur, hist], hist).then(
|
| 3480 |
-
on_morph_style, [cur, style_dd, style_amt, draft], out_all))
|
| 3481 |
-
tail(style_text_btn.click(_snapshot, [cur, hist], hist).then(
|
| 3482 |
-
on_style_from_text, [cur, style_text], out_all))
|
| 3483 |
|
| 3484 |
# --- coverage ---------------------------------------------------------
|
| 3485 |
tail(cov_on.click(_snapshot, [cur, hist], hist).then(
|
| 3486 |
on_cov_on, [cur, cov_signal], out_all))
|
| 3487 |
tail(cov_off.click(_snapshot, [cur, hist], hist).then(
|
| 3488 |
on_cov_off, [cur, cov_signal], out_all))
|
| 3489 |
-
tail(cov_all_off.click(_snapshot, [cur, hist], hist).then(
|
| 3490 |
-
on_cov_all_off, [cur], out_all))
|
| 3491 |
-
tail(cov_all_on.click(_snapshot, [cur, hist], hist).then(
|
| 3492 |
-
on_cov_all_on, [cur], out_all))
|
| 3493 |
|
| 3494 |
# --- undo / A / B -----------------------------------------------------
|
| 3495 |
# Undo restores a whole Track, so it has to rebuild every A-facing view
|
|
@@ -3517,7 +3570,7 @@ def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
|
| 3517 |
# views, this one rebuilds the controls. demo.load runs it once so the
|
| 3518 |
# initial radio position is honoured before anything is generated.
|
| 3519 |
stage_ctl_in = [stage_rb, signal_dd, cov_signal]
|
| 3520 |
-
stage_ctl_out = [signal_dd, cov_signal
|
| 3521 |
stage_rb.change(_stage_controls, stage_ctl_in, stage_ctl_out)
|
| 3522 |
demo.load(_stage_controls, stage_ctl_in, stage_ctl_out)
|
| 3523 |
|
|
|
|
| 78 |
}
|
| 79 |
ALL_SIGNAL_CHOICES = [(ALL_SIGNAL_LABELS[s], s) for s in ALL_SIGNALS]
|
| 80 |
|
| 81 |
+
# A value for the coverage picker that means "every signal", so that turning the
|
| 82 |
+
# lot on or off is the same two buttons as turning one on or off. Sentinel-
|
| 83 |
+
# prefixed because it shares a dropdown with real signal names and is passed to
|
| 84 |
+
# the same handler; nothing in ALL_SIGNALS can ever start with "~".
|
| 85 |
+
COV_ALL = "~all"
|
| 86 |
+
COV_ALL_LABEL = "All signals"
|
| 87 |
+
|
| 88 |
+
# Edit's one picker: the shapeable curves, then the three edits that are not
|
| 89 |
+
# curves. All four write conditioning for the same window and none of them makes
|
| 90 |
+
# a sound — what differs is the controls each needs, so they share a selector
|
| 91 |
+
# that reveals the right ones (see the editor block in build_app). Sentinel-
|
| 92 |
+
# prefixed for the same reason COV_ALL is: they travel through handlers that
|
| 93 |
+
# also take real signal names.
|
| 94 |
+
EDIT_KEY, EDIT_MORPH, EDIT_TEXT = "~key", "~morph", "~text"
|
| 95 |
+
EDIT_SPECIALS = (EDIT_KEY, EDIT_MORPH, EDIT_TEXT)
|
| 96 |
+
EDIT_SPECIAL_CHOICES = [("key", EDIT_KEY),
|
| 97 |
+
("style — morph toward a preset", EDIT_MORPH),
|
| 98 |
+
("style — from text", EDIT_TEXT)]
|
| 99 |
+
EDIT_TARGETS = ([(SIGNAL_LABELS[s], s) for s in EDITABLE_SIGNALS]
|
| 100 |
+
+ EDIT_SPECIAL_CHOICES)
|
| 101 |
+
_EDIT_ACTIONS = {EDIT_KEY: "Set key",
|
| 102 |
+
EDIT_MORPH: "Morph style",
|
| 103 |
+
EDIT_TEXT: "Set style from text"}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _edit_action(target: str) -> str:
|
| 107 |
+
"""The Apply button's label for a target. Every curve shares one label
|
| 108 |
+
because for a curve the shape dropdown is the verb, not the signal."""
|
| 109 |
+
return _EDIT_ACTIONS.get(target, "Apply shape")
|
| 110 |
+
|
| 111 |
# The third tab is a lab bench rather than a feature: which model stage, the two
|
| 112 |
# history knobs whose right value is still an open question, and the A/B between
|
| 113 |
# the two guidance compositions. Off on a public Space, on with SHOW_TESTING=1.
|
|
|
|
| 1018 |
# edit report from controls Generate does not have; the latter is bookkeeping
|
| 1019 |
# about a track the listener can already hear, so it says nothing.
|
| 1020 |
return out("" if head.startswith(
|
| 1021 |
+
("Continued", "Fast-forwarded", "Regenerated", "Rewrote", "Rendered",
|
| 1022 |
+
"Applied", "Replaced", "Added", "Set ", "Style set", "Morphed",
|
| 1023 |
+
"Turned", "Coverage", "Undo", "Seeded", "Extracted",
|
| 1024 |
+
"Unconditional")) else head)
|
| 1025 |
|
| 1026 |
|
| 1027 |
def _coverage_table(track: Track) -> str:
|
|
|
|
| 1049 |
for s in sigs:
|
| 1050 |
n_on = int(np.count_nonzero(np.asarray(c.active[s], dtype=bool)))
|
| 1051 |
if n_on == T:
|
| 1052 |
+
state = "🟩 **on**"
|
| 1053 |
elif n_on == 0:
|
| 1054 |
+
state = "⬜ off"
|
| 1055 |
else:
|
| 1056 |
state = f"🟨 partial — on for {n_on} of {T}"
|
| 1057 |
rows.append(f"| {ALL_SIGNAL_LABELS[s]} | {state} |")
|
|
|
|
| 1164 |
def build_app(backend: ModelBackend | None = None) -> gr.Blocks:
|
| 1165 |
backend = backend or MockBackend()
|
| 1166 |
presets = backend.list_presets()
|
|
|
|
| 1167 |
|
| 1168 |
with gr.Blocks(title="Conditioning Composer") as demo:
|
| 1169 |
gr.HTML(_PLOT_CSS, elem_classes="style-inject")
|
|
|
|
| 1257 |
# GENERATE
|
| 1258 |
# =================================================================
|
| 1259 |
with gr.Tab("Generate"):
|
| 1260 |
+
# Starting a track and extending one were two halves of one tab
|
| 1261 |
+
# and are two different sittings: you start once, then spend the
|
| 1262 |
+
# session on **Continue**. Nothing here makes a sound on this
|
| 1263 |
+
# tab -- the player is on Continue, which is where you go next.
|
| 1264 |
gr.Markdown(
|
| 1265 |
+
"Pick something to start from, then hear it and keep it "
|
| 1266 |
+
"going on **Continue**.")
|
|
|
|
|
|
|
| 1267 |
with gr.Row():
|
| 1268 |
with gr.Column(scale=1):
|
|
|
|
| 1269 |
s_preset = gr.Dropdown(presets, value=presets[0],
|
| 1270 |
label="A real song")
|
| 1271 |
s_preset_btn = gr.Button("Start from this song",
|
|
|
|
| 1283 |
"it.")
|
| 1284 |
s_text_btn = gr.Button("Start from this description")
|
| 1285 |
|
| 1286 |
+
# Last, because it applies to all three entry points
|
| 1287 |
+
# above rather than to any one of them, and because it
|
| 1288 |
+
# is the control you change least.
|
| 1289 |
+
s_seed = gr.Number(
|
| 1290 |
+
value=0, label="Seed", precision=0,
|
| 1291 |
+
info="Where the music starts from: the same seed "
|
| 1292 |
+
"and the same settings open the same way. "
|
| 1293 |
+
"Change it for a different take of the same "
|
| 1294 |
+
"idea.")
|
| 1295 |
+
|
| 1296 |
+
with gr.Column(scale=1):
|
| 1297 |
# ---- tempo ------------------------------------------
|
| 1298 |
+
# Beside the entry points, because that is the pairing:
|
| 1299 |
+
# a song and an upload arrive with a measured tempo, a
|
| 1300 |
+
# description says nothing about tempo at all and takes
|
| 1301 |
+
# whatever this block says. Adjacency is the whole
|
| 1302 |
+
# explanation, which is why there is no longer a
|
| 1303 |
# paragraph making it. The preview is live and works
|
| 1304 |
# with no track loaded -- dial the curve in BEFORE
|
| 1305 |
# generating, which is the whole reason it is up here.
|
|
|
|
| 1328 |
"decode time, so it is never ignored, even with "
|
| 1329 |
"every signal off.</sub>")
|
| 1330 |
|
| 1331 |
+
# =================================================================
|
| 1332 |
+
# CONTINUE
|
| 1333 |
+
# =================================================================
|
| 1334 |
+
# Extending a track, and hearing it. One section, not two, because
|
| 1335 |
+
# the three buttons at the bottom are the same operation at three
|
| 1336 |
+
# lengths -- eight measures, a leg, or until you stop it -- and all
|
| 1337 |
+
# of them read the controls above them. Splitting "continue" from
|
| 1338 |
+
# "radio" would put those controls under one heading and silently
|
| 1339 |
+
# apply them to the other.
|
| 1340 |
+
with gr.Tab("Continue"):
|
| 1341 |
+
with gr.Row():
|
| 1342 |
+
with gr.Column(scale=1):
|
| 1343 |
s_audio = gr.Audio(label="Current track",
|
| 1344 |
type="filepath", interactive=False)
|
| 1345 |
# The radio's player lives HERE, in the same slot as the
|
|
|
|
| 1364 |
# hear.
|
| 1365 |
s_summary = gr.Markdown(visible=False)
|
| 1366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1367 |
# One control for every way the style can move, because
|
| 1368 |
# in the backend they are one choice: the typed steer,
|
| 1369 |
# the two walks and style-off are exclusive branches of
|
|
|
|
| 1490 |
scale=1)
|
| 1491 |
s_status = gr.Markdown()
|
| 1492 |
|
| 1493 |
+
with gr.Column(scale=1):
|
| 1494 |
+
# Open by default: "how it moves" is the column beside
|
| 1495 |
# it, and this is the only place that answers whether
|
| 1496 |
# the mode you picked is doing anything. Updated on
|
| 1497 |
# every press and at every leg boundary of a live
|
|
|
|
| 1669 |
# Turning a signal off over PART of a song is still
|
| 1670 |
# reachable, and correctly: seat the window there and
|
| 1671 |
# edit it as its own window.
|
| 1672 |
+
gr.Markdown("### Conditioning coverage")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1673 |
coverage_tbl = gr.Markdown()
|
| 1674 |
+
# "All signals" is an entry in the picker rather than a
|
| 1675 |
+
# second pair of buttons. Four buttons where two would
|
| 1676 |
+
# do, and the two extra ones were the same verb again
|
| 1677 |
+
# with a different scope -- scope is what the dropdown
|
| 1678 |
+
# above them is for.
|
| 1679 |
+
cov_signal = gr.Dropdown(
|
| 1680 |
+
[(COV_ALL_LABEL, COV_ALL)] + ALL_SIGNAL_CHOICES,
|
| 1681 |
+
value=COV_ALL, label="signal")
|
| 1682 |
with gr.Row():
|
| 1683 |
+
cov_on = gr.Button("On", variant="primary")
|
| 1684 |
+
cov_off = gr.Button("Off")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1685 |
|
| 1686 |
# =================================================================
|
| 1687 |
# EDIT
|
|
|
|
| 1730 |
"training's *_cfg*). Higher = stronger "
|
| 1731 |
"adherence to the conditioning, but can "
|
| 1732 |
"distort.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1733 |
|
| 1734 |
+
# ---- the editor ---------------------------------------------
|
| 1735 |
+
# ONE picker for everything that can be changed, and one button
|
| 1736 |
+
# that does it. Key and style used to be a second column with
|
| 1737 |
+
# two more buttons in it, which read as a different kind of
|
| 1738 |
+
# operation; they are not -- all four write conditioning for the
|
| 1739 |
+
# same window and none of them makes a sound. What differs is
|
| 1740 |
+
# the controls each one needs, so the picker reveals those and
|
| 1741 |
+
# hides the rest. EDIT_TARGETS is the whole mapping.
|
| 1742 |
+
gr.Markdown("### Edit the conditioning")
|
| 1743 |
+
signal_dd = gr.Dropdown(EDIT_TARGETS, value=EDITABLE_SIGNALS[0],
|
| 1744 |
+
label="what to edit")
|
| 1745 |
+
|
| 1746 |
+
# Groups, not individual components, because `_param_updates`
|
| 1747 |
+
# already owns the visibility of `pslots` and `wave_dd` (per
|
| 1748 |
+
# shape) -- two handlers writing the same `visible` flag from
|
| 1749 |
+
# different triggers is a race with no winner. A container's
|
| 1750 |
+
# visibility is a flag nothing else touches.
|
| 1751 |
+
with gr.Column(visible=True) as curve_grp:
|
| 1752 |
+
with gr.Row():
|
| 1753 |
+
ed_start = gr.Slider(0, N_CHUNKS - 1, value=0, step=1,
|
| 1754 |
+
label="from measure (in window)")
|
| 1755 |
+
ed_end = gr.Slider(1, N_CHUNKS, value=N_CHUNKS, step=1,
|
| 1756 |
+
label="to measure (in window)")
|
| 1757 |
+
with gr.Row():
|
| 1758 |
+
with gr.Column():
|
| 1759 |
+
shape_dd = gr.Dropdown(SHAPES, value=SHAPES[0],
|
| 1760 |
+
label="shape")
|
| 1761 |
+
shape_help = gr.Markdown(
|
| 1762 |
+
f"*{SHAPE_HELP[SHAPES[0]]}*")
|
| 1763 |
+
# generic parameter slots (relabelled per shape)
|
| 1764 |
+
pslots = [gr.Number(value=0.0, label=f"p{i}",
|
| 1765 |
+
visible=False)
|
| 1766 |
+
for i in range(MAX_PARAMS)]
|
| 1767 |
+
wave_dd = gr.Dropdown(["sine", "triangle", "saw"],
|
| 1768 |
+
value="sine", label="wave",
|
| 1769 |
+
visible=False)
|
| 1770 |
+
mode_rb = gr.Radio(["replace", "add"],
|
| 1771 |
+
value="replace", label="mode")
|
| 1772 |
+
with gr.Column():
|
| 1773 |
+
preview_plot = gr.Plot(
|
| 1774 |
+
label="Preview (live — not yet applied)",
|
| 1775 |
+
elem_classes="plot-card")
|
| 1776 |
+
|
| 1777 |
+
with gr.Column(visible=False) as key_grp:
|
| 1778 |
+
with gr.Row():
|
| 1779 |
+
root_dd = gr.Dropdown(
|
| 1780 |
+
[("C", 0), ("C#", 1), ("D", 2), ("D#", 3),
|
| 1781 |
+
("E", 4), ("F", 5), ("F#", 6), ("G", 7),
|
| 1782 |
+
("G#", 8), ("A", 9), ("A#", 10), ("B", 11)],
|
| 1783 |
+
value=0, label="root")
|
| 1784 |
+
minor_cb = gr.Checkbox(value=False, label="minor")
|
| 1785 |
+
|
| 1786 |
+
with gr.Column(visible=False) as morph_grp:
|
| 1787 |
+
with gr.Row():
|
| 1788 |
+
style_dd = gr.Dropdown(presets, value=presets[0],
|
| 1789 |
+
label="morph style toward")
|
| 1790 |
+
style_amt = gr.Slider(0.0, 1.0, value=0.5, step=0.05,
|
| 1791 |
+
label="amount")
|
| 1792 |
+
|
| 1793 |
+
with gr.Column(visible=False) as text_grp:
|
| 1794 |
+
# Whole window, like the two above it and like coverage. It
|
| 1795 |
+
# used to take a range, which was the same untrained
|
| 1796 |
+
# configuration the coverage panel now refuses to make:
|
| 1797 |
+
# this writes `active["style"] = True` over the range it is
|
| 1798 |
+
# given, so a range on a track whose style was off produced
|
| 1799 |
+
# a window with style on for part of it.
|
| 1800 |
+
style_text = gr.Textbox(
|
| 1801 |
+
label="style prompt",
|
| 1802 |
+
placeholder="e.g. smoky late-night piano trio")
|
| 1803 |
+
|
| 1804 |
+
apply_btn = gr.Button(_edit_action(EDITABLE_SIGNALS[0]),
|
| 1805 |
+
variant="primary")
|
| 1806 |
|
| 1807 |
# ---- regenerate ---------------------------------------------
|
| 1808 |
+
# At the bottom, after the editor, because that is the order the
|
| 1809 |
+
# work happens in: nothing above this point makes a sound.
|
| 1810 |
+
#
|
| 1811 |
+
# THE seed for this tab, and the only one. There used to be a
|
| 1812 |
+
# second (a "variation seed" beside a Vary button), which was
|
| 1813 |
+
# the same operation -- rewrite the window from the same
|
| 1814 |
+
# conditioning with different noise -- reached by a different
|
| 1815 |
+
# control and remembered in a different box, so the two could
|
| 1816 |
+
# disagree about which take you were listening to. Regenerate
|
| 1817 |
+
# with this changed IS Vary; Regenerate with it untouched is the
|
| 1818 |
+
# take you already have, because it follows the track (see
|
| 1819 |
+
# refresh_window) and is always the seed the window was written
|
| 1820 |
+
# with.
|
| 1821 |
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1822 |
rg_seed = gr.Number(value=1, label="Seed",
|
| 1823 |
precision=0, scale=1)
|
| 1824 |
regen_btn = gr.Button("♫ Regenerate",
|
|
|
|
| 2107 |
return _edit_outputs(tr, f"Switched to stage {int(stage)} model.")
|
| 2108 |
|
| 2109 |
def _stage_controls(stage, cur_sig, cur_cov):
|
| 2110 |
+
"""Restrict the two pickers to the signals the selected model reads.
|
|
|
|
| 2111 |
|
| 2112 |
The plots gate themselves off `track.stage`, but these are inputs,
|
| 2113 |
not views: with no track loaded there is nothing to gate against,
|
|
|
|
| 2116 |
the track because stage_rb is what the entry points read for the
|
| 2117 |
NEXT generation — and on_swap pushes the restored track's stage back
|
| 2118 |
into it, so swapping is covered for free. The current selection is
|
| 2119 |
+
kept when it survives the switch.
|
| 2120 |
+
|
| 2121 |
+
Key and style are filtered by the channel they actually write --
|
| 2122 |
+
key is chroma, both style edits are style -- which is how the old
|
| 2123 |
+
greyed-out key button's job gets done now that there is no button to
|
| 2124 |
+
grey: an edit the stage cannot hear is not in the list."""
|
| 2125 |
sigs = signals_for_stage(int(stage))
|
| 2126 |
ed = [(SIGNAL_LABELS[s], s) for s in EDITABLE_SIGNALS if s in sigs]
|
| 2127 |
+
ed += [(lbl, t) for lbl, t in EDIT_SPECIAL_CHOICES
|
| 2128 |
+
if ("chroma" if t == EDIT_KEY else "style") in sigs]
|
| 2129 |
+
cov = ([(COV_ALL_LABEL, COV_ALL)]
|
| 2130 |
+
+ [(ALL_SIGNAL_LABELS[s], s) for s in ALL_SIGNALS
|
| 2131 |
+
if s in sigs])
|
| 2132 |
|
| 2133 |
def keep(v, opts):
|
| 2134 |
return v if any(v == o[1] for o in opts) else opts[0][1]
|
|
|
|
| 2136 |
return (
|
| 2137 |
gr.update(choices=ed, value=keep(cur_sig, ed)),
|
| 2138 |
gr.update(choices=cov, value=keep(cur_cov, cov)),
|
|
|
|
| 2139 |
)
|
| 2140 |
|
| 2141 |
def _diag_note(track):
|
|
|
|
| 2888 |
tr, f"{mode.title()}d {shape} on **{lbl}** over "
|
| 2889 |
f"measures [{int(start)},{int(end)}).")
|
| 2890 |
|
| 2891 |
+
def _target_controls(target):
|
| 2892 |
+
"""Show the controls the picked target needs, hide the rest, and
|
| 2893 |
+
say on the button what pressing it will do.
|
| 2894 |
+
|
| 2895 |
+
Containers, not components: `_param_updates` owns `pslots` and
|
| 2896 |
+
`wave_dd` per shape, and two handlers writing one `visible` flag
|
| 2897 |
+
from different triggers is a race."""
|
| 2898 |
+
return (gr.update(visible=target not in EDIT_SPECIALS),
|
| 2899 |
+
gr.update(visible=target == EDIT_KEY),
|
| 2900 |
+
gr.update(visible=target == EDIT_MORPH),
|
| 2901 |
+
gr.update(visible=target == EDIT_TEXT),
|
| 2902 |
+
gr.update(value=_edit_action(target)))
|
| 2903 |
+
|
| 2904 |
+
def on_edit_apply(track, target, start, end, shape, mode, wave,
|
| 2905 |
+
root, minor, style, amount, prompt, is_draft,
|
| 2906 |
+
*pvals):
|
| 2907 |
+
"""The one Apply button. Four edits, one press, chosen by the
|
| 2908 |
+
picker -- the handlers below are unchanged, this only routes to
|
| 2909 |
+
them. They all return `_edit_outputs`, so the wiring downstream
|
| 2910 |
+
cannot tell which one ran."""
|
| 2911 |
+
if target == EDIT_KEY:
|
| 2912 |
+
return on_set_key(track, root, minor, is_draft)
|
| 2913 |
+
if target == EDIT_MORPH:
|
| 2914 |
+
return on_morph_style(track, style, amount, is_draft)
|
| 2915 |
+
if target == EDIT_TEXT:
|
| 2916 |
+
return on_style_from_text(track, prompt)
|
| 2917 |
+
return on_apply(track, target, start, end, shape, mode, wave,
|
| 2918 |
+
is_draft, *pvals)
|
| 2919 |
+
|
| 2920 |
# ---- moving the editable window --------------------------------------
|
| 2921 |
# Not a control of its own any more: the selection says which measures
|
| 2922 |
# to rewrite, `_sel_window` says which N_CHUNKS have to be loaded to do
|
|
|
|
| 3010 |
f"the window was held frozen.{clamped}" + _splice_note(tr)
|
| 3011 |
+ _diag_note(tr))
|
| 3012 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3013 |
def _apply_coverage(track, signal, on):
|
| 3014 |
+
"""Toggle a signal — or all of them — on/off across the WHOLE
|
| 3015 |
+
window (edit only, no re-render). Not a range: see the comment on
|
| 3016 |
+
the coverage panel. COV_ALL is handled here rather than by its own
|
| 3017 |
+
pair of buttons, because "which signals" is the dropdown's job."""
|
| 3018 |
if track is None:
|
| 3019 |
return (gr.update(),) * 5 + ("Start a track first.",)
|
| 3020 |
+
sigs = ALL_SIGNALS if signal == COV_ALL else [signal]
|
| 3021 |
+
tr = track
|
| 3022 |
+
for s in sigs:
|
| 3023 |
+
tr = backend.set_active(tr, s, 0, tr.cond.T, bool(on))
|
| 3024 |
+
lbl = (COV_ALL_LABEL.lower() if signal == COV_ALL
|
| 3025 |
+
else f"**{ALL_SIGNAL_LABELS.get(signal, signal)}**")
|
| 3026 |
first = int(track.prefix_measures)
|
| 3027 |
return _edit_outputs(
|
| 3028 |
+
tr, f"Turned {lbl} {'on' if on else 'off'} for measures "
|
| 3029 |
f"{first}–{first + track.cond.T}.")
|
| 3030 |
|
| 3031 |
def on_cov_on(track, signal):
|
|
|
|
| 3034 |
def on_cov_off(track, signal):
|
| 3035 |
return _apply_coverage(track, signal, False)
|
| 3036 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3037 |
# ---- A/B slots ------------------------------------------------------
|
| 3038 |
# A = `cur` (being edited), B = `prev` (parked snapshot). Both hold a
|
| 3039 |
# whole Track, so a swap carries the conditioning arrays, per-measure
|
|
|
|
| 3176 |
# `_raw_continue` / `_raw_fast_forward`, which were bound above this
|
| 3177 |
# block precisely so the Generate wrappers get mirrored once rather
|
| 3178 |
# than twice.
|
| 3179 |
+
#
|
| 3180 |
+
# `on_apply`, `on_set_key`, `on_morph_style` and `on_style_from_text`
|
| 3181 |
+
# are absent for the same reason: `on_edit_apply` is the only thing
|
| 3182 |
+
# wired to them, and mirroring both it and them would append the four
|
| 3183 |
+
# mirror outputs twice.
|
| 3184 |
(on_new_preset, on_new_text, on_new_upload, on_set_bpm,
|
| 3185 |
+
on_set_stage, on_simple_continue, on_simple_ff, on_edit_apply,
|
| 3186 |
+
on_regen, on_cov_on, on_cov_off, on_swap, on_seat) = [
|
|
|
|
|
|
|
| 3187 |
_mirror(f) for f in (
|
| 3188 |
on_new_preset, on_new_text, on_new_upload,
|
| 3189 |
+
on_set_bpm, on_set_stage,
|
| 3190 |
+
on_simple_continue, on_simple_ff, on_edit_apply,
|
| 3191 |
+
on_regen, on_cov_on, on_cov_off, on_swap, on_seat)]
|
|
|
|
| 3192 |
on_undo = _mirror(on_undo, off=1)
|
| 3193 |
|
| 3194 |
# ---- wiring ---------------------------------------------------------
|
|
|
|
| 3206 |
|
| 3207 |
def refresh_preview(track, signal, start, end, shape, mode, wave,
|
| 3208 |
*pvals):
|
| 3209 |
+
"""The live shape preview, or nothing when the picker is not on a
|
| 3210 |
+
curve. `_preview_figure` reads `cond.<signal>`, and the three
|
| 3211 |
+
special targets are not channels -- the plot is hidden with the
|
| 3212 |
+
rest of the curve controls in that case, so there is nothing to
|
| 3213 |
+
draw either."""
|
| 3214 |
+
if signal in EDIT_SPECIALS:
|
| 3215 |
+
return gr.update()
|
| 3216 |
return _preview_figure(track, signal, start, end, shape, mode, wave,
|
| 3217 |
*pvals)
|
| 3218 |
|
|
|
|
| 3523 |
.then(on_regen,
|
| 3524 |
[cur, sel_from, sel_to, rg_seed, draft, guidance_sl],
|
| 3525 |
out_all))
|
|
|
|
|
|
|
| 3526 |
|
| 3527 |
+
# --- the editor -------------------------------------------------------
|
| 3528 |
+
# One button, every edit. The inputs are the union of what the four
|
| 3529 |
+
# branches need, because gradio binds an event's inputs once and the
|
| 3530 |
+
# picker chooses the branch at press time -- the controls the branch
|
| 3531 |
+
# does not read are hidden, not absent, and their values are ignored.
|
| 3532 |
tail(apply_btn.click(_snapshot, [cur, hist], hist).then(
|
| 3533 |
+
on_edit_apply,
|
| 3534 |
[cur, signal_dd, ed_start, ed_end, shape_dd, mode_rb, wave_dd,
|
| 3535 |
+
root_dd, minor_cb, style_dd, style_amt, style_text,
|
| 3536 |
draft, *pslots],
|
| 3537 |
out_all))
|
| 3538 |
+
signal_dd.change(_target_controls, [signal_dd],
|
| 3539 |
+
[curve_grp, key_grp, morph_grp, text_grp, apply_btn])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3540 |
|
| 3541 |
# --- coverage ---------------------------------------------------------
|
| 3542 |
tail(cov_on.click(_snapshot, [cur, hist], hist).then(
|
| 3543 |
on_cov_on, [cur, cov_signal], out_all))
|
| 3544 |
tail(cov_off.click(_snapshot, [cur, hist], hist).then(
|
| 3545 |
on_cov_off, [cur, cov_signal], out_all))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3546 |
|
| 3547 |
# --- undo / A / B -----------------------------------------------------
|
| 3548 |
# Undo restores a whole Track, so it has to rebuild every A-facing view
|
|
|
|
| 3570 |
# views, this one rebuilds the controls. demo.load runs it once so the
|
| 3571 |
# initial radio position is honoured before anything is generated.
|
| 3572 |
stage_ctl_in = [stage_rb, signal_dd, cov_signal]
|
| 3573 |
+
stage_ctl_out = [signal_dd, cov_signal]
|
| 3574 |
stage_rb.change(_stage_controls, stage_ctl_in, stage_ctl_out)
|
| 3575 |
demo.load(_stage_controls, stage_ctl_in, stage_ctl_out)
|
| 3576 |
|