ACE-Step-Inspire / console_ui.py
41-807's picture
Upload folder using huggingface_hub
6cb9905 verified
Raw
History Blame Contribute Delete
76.7 kB
"""DJ-console visual assets for ACE-Step Inspire (CSS + knob HTML/JS)."""
CONSOLE_CSS = r"""
@import url('https://fonts.googleapis.com/css2?family=Audiowide&family=Rajdhani:wght@500;600;700&family=Share+Tech+Mono&display=swap');
:root {
--bg0: #07080b;
--bg1: #0e1016;
--panel: #141821;
--panel-2: #1a1f2b;
--line: #2a3142;
--metal: #9aa3b2;
--muted: #6b7385;
--amber: #ffb020;
--amber-dim: #c48212;
--led: #3dffb0;
--led-dim: #1a6b4d;
--danger: #ff5c5c;
--text: #e8ecf4;
}
html, body {
background: var(--bg0) !important;
color-scheme: dark !important;
}
.gradio-container {
max-width: 100% !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
background:
radial-gradient(1200px 500px at 50% -10%, rgba(255,176,32,0.08), transparent 60%),
radial-gradient(900px 400px at 100% 20%, rgba(61,255,176,0.05), transparent 50%),
linear-gradient(180deg, var(--bg1), var(--bg0)) !important;
font-family: 'Rajdhani', sans-serif !important;
color: var(--text) !important;
min-height: 100vh;
color-scheme: dark !important;
}
footer, .svelte-1ipelgc { display: none !important; }
/* Kill default card chrome */
.console-shell .block,
.console-shell .form,
.console-shell .panel-wrap {
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
.console-shell label,
.console-shell .label-wrap span {
font-family: 'Share Tech Mono', monospace !important;
font-size: 0.72rem !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
color: var(--muted) !important;
}
/* Force dark fields even when the OS / browser prefers light UI */
.console-shell,
.console-shell * {
color-scheme: dark !important;
}
.console-shell input,
.console-shell textarea,
.console-shell select,
.console-shell .wrap input,
.console-shell .wrap textarea,
.console-shell [data-testid="dropdown"] input,
.console-shell [data-testid="textbox"] textarea,
.console-shell [data-testid="textbox"] input,
.console-shell .secondary-wrap input {
background: #0a0c11 !important;
background-color: #0a0c11 !important;
border: 1px solid var(--line) !important;
border-radius: 4px !important;
color: var(--text) !important;
-webkit-text-fill-color: var(--text) !important;
caret-color: var(--text) !important;
font-family: 'Rajdhani', sans-serif !important;
font-size: 1rem !important;
color-scheme: dark !important;
}
.console-shell input::placeholder,
.console-shell textarea::placeholder {
color: var(--muted) !important;
-webkit-text-fill-color: var(--muted) !important;
opacity: 1 !important;
}
.console-shell select option,
.console-shell option {
background: #0a0c11 !important;
color: var(--text) !important;
}
/* Gradio dropdown option panels (often portaled to body) */
.options,
ul.options,
div.options,
.global-container .options,
body .options,
body ul[role="listbox"],
div[role="listbox"] {
background: #141821 !important;
background-color: #141821 !important;
border: 1px solid var(--line) !important;
color: var(--text) !important;
color-scheme: dark !important;
}
.options li,
.options .item,
ul.options > *,
div[role="listbox"] [role="option"],
body .options li,
body .options .item {
background: #141821 !important;
background-color: #141821 !important;
color: var(--text) !important;
-webkit-text-fill-color: var(--text) !important;
}
.options li:hover,
.options .item:hover,
.options li.selected,
.options .item.selected,
div[role="listbox"] [role="option"][aria-selected="true"],
div[role="listbox"] [role="option"]:hover {
background: #1f2633 !important;
background-color: #1f2633 !important;
color: var(--amber) !important;
-webkit-text-fill-color: var(--amber) !important;
}
.console-shell .secondary-wrap,
.console-shell [data-testid="checkbox-group"] {
gap: 0.45rem !important;
}
/* Equal side racks */
.rack-row > div.rack-panel {
min-width: 0 !important;
}
/* Brand bar */
.brand-bar {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 0.85rem 1rem 0.4rem;
border-bottom: 1px solid var(--line);
box-sizing: border-box;
width: 100%;
}
.brand-bar .booth {
font-family: 'Audiowide', sans-serif;
font-size: 1.55rem;
letter-spacing: 0.18em;
color: var(--amber);
text-shadow: 0 0 18px rgba(255,176,32,0.35);
}
.brand-bar .sub {
font-family: 'Share Tech Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.04em;
color: var(--muted);
margin-top: 0.2rem;
max-width: 42rem;
line-height: 1.35;
}
.brand-bar .sub a {
color: var(--amber);
text-decoration: none;
border-bottom: 1px solid rgba(255, 176, 32, 0.45);
}
.brand-bar .sub a:hover {
color: #ffc95a;
border-bottom-color: var(--amber);
}
.brand-bar .live {
font-family: 'Share Tech Mono', monospace;
font-size: 0.7rem;
color: var(--led);
letter-spacing: 0.16em;
}
.brand-bar .live::before {
content: '';
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
background: var(--led);
margin-right: 8px;
box-shadow: 0 0 10px var(--led);
animation: pulse-led 1.8s ease-in-out infinite;
}
@keyframes pulse-led {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
/* Deck row — same gutter as rack-row; no Group card (transparent shell) */
div.row.deck-shell,
div.row.deck-shell-row,
.deck-shell,
.deck-shell-row {
display: grid !important;
grid-template-columns: minmax(0, 1fr) minmax(0, 7fr) !important;
gap: 0.75rem !important;
align-items: stretch !important;
margin: 0.75rem 0 0.5rem !important;
padding: 0 1rem !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
overflow: hidden !important;
flex-wrap: nowrap !important;
background: transparent !important;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
div.row.deck-shell-row > div,
.deck-shell-row > div {
min-width: 0 !important;
max-width: 100% !important;
align-self: stretch !important;
height: auto !important;
box-sizing: border-box !important;
}
.deck-shell-row > .deck-gen-col,
div.row.deck-shell-row > div.deck-gen-col {
flex: unset !important;
width: auto !important;
max-width: none !important;
}
.deck-shell-row > .deck-panel,
div.row.deck-shell-row > div.deck-panel {
flex: unset !important;
width: auto !important;
max-width: 100% !important;
min-width: 0 !important;
}
.deck-gen-col,
.deck-gen-col > div,
.deck-gen-col .block,
.deck-gen-col .form,
.deck-gen-col button,
#generate-song-btn {
height: 100% !important;
min-height: 100% !important;
box-sizing: border-box !important;
}
#generate-song-btn {
width: 100% !important;
background: linear-gradient(180deg, #ffc14d, #e09100) !important;
color: #1a1200 !important;
border: none !important;
border-radius: 10px !important;
box-shadow: 0 0 24px rgba(255,176,32,0.35);
font-family: 'Audiowide', sans-serif !important;
letter-spacing: 0.06em !important;
font-size: 0.92rem !important;
white-space: pre-line !important;
line-height: 1.2 !important;
padding: 0.85rem 0.45rem !important;
text-align: center !important;
}
#generate-song-btn:hover {
filter: brightness(1.05);
}
/* Original deck panel look */
.deck-panel {
margin: 0;
padding: 0.75rem 1rem 1rem;
background: linear-gradient(180deg, #12151d, #0b0d12);
border: 1px solid var(--line);
border-radius: 10px;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03), 0 10px 40px rgba(0,0,0,0.35);
min-width: 0 !important;
min-height: 233px !important;
overflow: hidden !important;
box-sizing: border-box !important;
max-width: 100% !important;
}
.deck-panel .deck-label {
font-family: 'Share Tech Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.18em;
color: var(--muted);
margin-bottom: 0.35rem;
}
.deck-panel .empty-hint {
font-family: 'Share Tech Mono', monospace;
font-size: 0.75rem;
color: var(--muted);
opacity: 0.85;
margin: 0.25rem 0 0.5rem;
}
#deck-audio,
#deck-audio .audio-container,
#deck-audio audio,
.deck-panel audio {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.deck-panel .audio-container {
background: transparent !important;
border: none !important;
padding: 0 !important;
}
.deck-live #deck-audio {
filter: drop-shadow(0 0 12px rgba(61,255,176,0.15));
}
/* Racks */
/* Racks — same 1rem side gutter as deck-shell */
.rack-row {
padding: 0.5rem 1rem 0.25rem !important;
align-items: stretch !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
}
.rack-panel {
background: linear-gradient(165deg, var(--panel-2), var(--panel));
border: 1px solid var(--line);
border-radius: 10px;
padding: 0.85rem 0.9rem 1rem;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
height: 100%;
min-height: 520px;
}
.rack-title {
font-family: 'Share Tech Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.2em;
color: var(--amber-dim);
margin: 0 0 0.75rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid var(--line);
}
/* Instrument browser — selected chips + category accordion */
.inst-browser {
margin: 0.35rem 0 0.85rem;
}
.inst-browser .pad-field-label {
margin-bottom: 0.45rem;
}
.inst-active {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
min-height: 2rem;
margin-bottom: 0.55rem;
}
.inst-active:empty + .inst-empty {
display: block;
}
.inst-empty {
display: none;
font-family: 'Share Tech Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.08em;
color: var(--muted);
margin: -0.25rem 0 0.55rem;
}
.inst-chip {
appearance: none;
-webkit-appearance: none;
cursor: pointer;
background: linear-gradient(180deg, #222836, #151922);
border: 1px solid var(--amber);
border-radius: 6px;
padding: 0.38rem 0.55rem;
color: var(--amber);
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
font-size: 0.85rem;
letter-spacing: 0.03em;
box-shadow: 0 0 12px rgba(255,176,32,0.2), inset 0 0 8px rgba(255,176,32,0.06);
}
.inst-chip:hover {
border-color: var(--danger);
color: var(--danger);
box-shadow: none;
}
.inst-chip .x {
margin-left: 0.35rem;
opacity: 0.7;
font-family: 'Share Tech Mono', monospace;
font-size: 0.75rem;
}
.inst-groups {
display: flex;
flex-direction: column;
gap: 0.3rem;
border-top: 1px dashed var(--line);
padding-top: 0.45rem;
}
.inst-group {
border: 1px solid var(--line);
border-radius: 6px;
background: #0b0d13;
overflow: hidden;
}
.inst-group-head {
appearance: none;
-webkit-appearance: none;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
cursor: pointer;
background: transparent;
border: none;
padding: 0.45rem 0.6rem;
color: var(--metal);
font-family: 'Share Tech Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
text-align: left;
}
.inst-group-head:hover {
color: var(--text);
}
.inst-group.open .inst-group-head {
color: var(--amber);
border-bottom: 1px solid var(--line);
}
.inst-group-head .count {
color: var(--muted);
letter-spacing: 0.06em;
}
.inst-group.open .inst-group-head .count {
color: var(--led);
}
.inst-group-head .chev {
color: var(--muted);
transition: transform 0.12s ease;
}
.inst-group.open .inst-group-head .chev {
transform: rotate(90deg);
color: var(--amber);
}
.inst-group-body {
display: none;
flex-wrap: wrap;
gap: 0.35rem;
padding: 0.5rem 0.55rem 0.55rem;
max-height: 180px;
overflow-y: auto;
}
.inst-group.open .inst-group-body {
display: flex;
}
.inst-option {
appearance: none;
-webkit-appearance: none;
cursor: pointer;
background: linear-gradient(180deg, #222836, #151922);
border: 1px solid #323a4d;
border-radius: 6px;
padding: 0.38rem 0.55rem;
color: var(--metal);
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
font-size: 0.82rem;
letter-spacing: 0.03em;
box-shadow: 0 2px 0 #0a0c10;
}
.inst-option:hover {
border-color: #4a5568;
color: var(--text);
}
.inst-option.active {
border-color: var(--amber);
color: var(--amber);
box-shadow: 0 0 10px rgba(255,176,32,0.2);
}
/* Gradio instrument checkbox group stays for state; UI is .inst-browser */
#instrument-pads {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(0,0,0,0) !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* Shared exclusive pad / chip grids */
.pad-field {
margin: 0 0 0.75rem;
}
.pad-field-label {
font-family: 'Share Tech Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.14em;
color: var(--muted);
text-transform: uppercase;
margin: 0 0 0.4rem;
}
.style-guide {
margin: -0.35rem 0 0.85rem;
padding: 0.55rem 0.65rem 0.6rem;
border-left: 2px solid var(--amber-dim);
background: linear-gradient(90deg, rgba(255,176,32,0.06), transparent 70%);
}
.style-guide .pad-field-label {
color: var(--amber-dim);
margin-bottom: 0.3rem;
}
.style-guide-body {
margin: 0;
font-family: 'Rajdhani', sans-serif;
font-size: 0.92rem;
font-weight: 500;
line-height: 1.35;
color: var(--metal);
letter-spacing: 0.01em;
}
.pad-grid {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.pad-grid.seg-col {
flex-direction: column;
flex-wrap: nowrap;
}
.pad {
appearance: none;
-webkit-appearance: none;
cursor: pointer;
background: linear-gradient(180deg, #222836, #151922);
border: 1px solid #323a4d;
border-radius: 6px;
padding: 0.45rem 0.65rem;
color: var(--metal);
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
font-size: 0.88rem;
letter-spacing: 0.03em;
line-height: 1.2;
text-align: left;
box-shadow: 0 2px 0 #0a0c10;
transition: transform 0.08s ease, box-shadow 0.15s ease, border-color 0.15s, color 0.15s;
}
.pad:hover {
border-color: #4a5568;
color: var(--text);
}
.pad.active {
border-color: var(--amber);
color: var(--amber);
box-shadow: 0 0 12px rgba(255,176,32,0.25), inset 0 0 10px rgba(255,176,32,0.08);
transform: translateY(1px);
}
.pad.compact {
font-size: 0.8rem;
padding: 0.38rem 0.55rem;
}
.pad.toggle-wide {
width: 100%;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.12em;
font-family: 'Share Tech Mono', monospace;
font-size: 0.72rem;
}
/* Duration dial — layout mirror of the BPM knob (right rack) */
.duration-bay {
display: flex;
flex-direction: column;
align-items: center;
user-select: none;
margin: 0.15rem 0 1rem;
}
.duration-bay .dur-face {
width: 148px;
height: 148px;
border-radius: 50%;
background:
radial-gradient(circle at 35% 30%, #3a4255, #171b24 55%, #0a0c10 80%),
conic-gradient(from 0deg, #2a3140, #4a5366, #2a3140);
border: 3px solid #4a5368;
box-shadow:
inset 0 0 18px rgba(0,0,0,0.65),
0 8px 18px rgba(0,0,0,0.45),
0 0 0 4px #0c0e14;
position: relative;
cursor: grab;
touch-action: none;
}
.duration-bay .dur-face:active { cursor: grabbing; }
.duration-bay .dur-face::before {
content: '';
position: absolute;
inset: 14px;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #2b3240, #12151c);
border: 1px solid #3a4254;
}
.duration-bay .dur-pointer {
position: absolute;
left: 50%;
top: 18px;
width: 4px;
height: 42px;
margin-left: -2px;
border-radius: 2px;
background: linear-gradient(180deg, var(--led), #1a6b4d);
box-shadow: 0 0 10px rgba(61,255,176,0.55);
transform-origin: 50% 56px;
z-index: 2;
}
.duration-bay .dur-mmss {
margin-top: 0.65rem;
font-family: 'Audiowide', sans-serif;
font-size: 1.65rem;
color: var(--led);
text-shadow: 0 0 12px rgba(61,255,176,0.35);
letter-spacing: 0.08em;
}
.duration-bay .dur-caption {
font-family: 'Share Tech Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.18em;
color: var(--muted);
margin-top: 0.15rem;
}
.duration-bay .dur-slider-wrap {
width: 100%;
max-width: 168px;
margin-top: 0.65rem;
}
.duration-bay .dur-slider-wrap input[type=range] {
width: 100%;
accent-color: var(--led);
cursor: pointer;
}
.duration-bay .dur-marks {
display: flex;
justify-content: space-between;
font-family: 'Share Tech Mono', monospace;
font-size: 0.58rem;
letter-spacing: 0.06em;
color: var(--muted);
margin-top: 0.2rem;
}
/* Hide Gradio fields driven by pad UI */
.pad-hidden,
#bpm_number,
#auto_download,
#auto_play {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(0,0,0,0) !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* Action bar */
.action-bar {
margin: 0.6rem 0 1.25rem !important;
padding: 0.75rem 1rem !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
background: #0d1017;
border: none;
border-radius: 0;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
display: flex;
gap: 0.75rem;
align-items: center;
flex-wrap: wrap;
}
.action-bar button {
font-family: 'Audiowide', sans-serif !important;
letter-spacing: 0.08em !important;
}
.ghost-btn button,
#prompt-btn, #info-btn, #surprise-btn, #expert-btn {
background: #171b25 !important;
border: 1px solid var(--line) !important;
color: var(--metal) !important;
}
/* Presets in MACHINE column (localStorage) */
.preset-bar {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
align-items: center;
margin: 0.85rem 0 0.15rem;
padding-top: 0.65rem;
border-top: 1px dashed var(--line);
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.preset-bar .preset-label {
font-family: 'Share Tech Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.14em;
color: var(--muted);
width: 100%;
margin: 0 0 0.15rem;
}
.preset-bar input,
.preset-bar select {
background: #0a0c11 !important;
border: 1px solid var(--line) !important;
border-radius: 4px !important;
color: var(--text) !important;
font-family: 'Rajdhani', sans-serif !important;
font-size: 0.88rem !important;
padding: 0.35rem 0.5rem !important;
min-height: 34px;
flex: 1 1 auto;
min-width: 0;
box-sizing: border-box;
}
.preset-bar input { width: 100%; flex: 1 1 100%; }
.preset-bar select { width: 100%; flex: 1 1 100%; min-width: 0; max-width: none; }
.preset-bar button {
background: #171b25 !important;
border: 1px solid var(--line) !important;
color: var(--metal) !important;
font-family: 'Share Tech Mono', monospace !important;
font-size: 0.68rem !important;
letter-spacing: 0.1em !important;
text-transform: uppercase !important;
padding: 0.4rem 0.55rem !important;
border-radius: 4px !important;
cursor: pointer;
flex: 1 1 auto;
}
.preset-bar button:hover {
border-color: var(--amber) !important;
color: var(--amber) !important;
}
.preset-bar .preset-status {
font-family: 'Share Tech Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.08em;
color: var(--led);
width: 100%;
min-height: 1em;
}
/* After-generate toggle pads (download / play) */
.auto-opts {
margin: 0.65rem 0 0.25rem;
padding: 0.55rem 0.55rem 0.45rem;
background: #0b0d13;
border: 1px solid var(--line);
border-radius: 8px;
}
.auto-opts .pad-field-label {
margin-bottom: 0.45rem;
}
.auto-opts .pad-grid {
gap: 0.35rem;
}
.auto-opts .pad.toggle-auto {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
text-align: left;
text-transform: uppercase;
letter-spacing: 0.1em;
font-family: 'Share Tech Mono', monospace;
font-size: 0.7rem;
padding: 0.5rem 0.65rem;
}
.auto-opts .pad.toggle-auto .auto-led {
width: 8px;
height: 8px;
border-radius: 50%;
background: #2a3142;
box-shadow: inset 0 0 0 1px #1a1f2b;
flex-shrink: 0;
}
.auto-opts .pad.toggle-auto.active {
border-color: var(--led);
color: var(--led);
box-shadow: 0 0 14px rgba(61,255,176,0.18), inset 0 0 10px rgba(61,255,176,0.06);
}
.auto-opts .pad.toggle-auto.active .auto-led {
background: var(--led);
box-shadow: 0 0 10px var(--led);
}
/* Off-screen DownloadButton used for auto-save after generate */
#inspire-autodl {
position: fixed !important;
left: -10000px !important;
top: 0 !important;
width: 1px !important;
height: 1px !important;
opacity: 0.01 !important;
overflow: hidden !important;
z-index: -1 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
min-width: 0 !important;
}
/* Tempo knob */
.knob-wrap {
display: flex;
flex-direction: column;
align-items: center;
user-select: none;
margin: 0.25rem 0 0.75rem;
}
.knob-face {
width: 148px;
height: 148px;
border-radius: 50%;
background:
radial-gradient(circle at 35% 30%, #3a4255, #171b24 55%, #0a0c10 80%),
conic-gradient(from 0deg, #2a3140, #4a5366, #2a3140);
border: 3px solid #4a5368;
box-shadow:
inset 0 0 18px rgba(0,0,0,0.65),
0 8px 18px rgba(0,0,0,0.45),
0 0 0 4px #0c0e14;
position: relative;
cursor: grab;
touch-action: none;
}
.knob-face:active { cursor: grabbing; }
.knob-face::before {
content: '';
position: absolute;
inset: 14px;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #2b3240, #12151c);
border: 1px solid #3a4254;
}
.knob-pointer {
position: absolute;
left: 50%;
top: 18px;
width: 4px;
height: 42px;
margin-left: -2px;
border-radius: 2px;
background: linear-gradient(180deg, var(--amber), #8a5a00);
box-shadow: 0 0 10px rgba(255,176,32,0.55);
transform-origin: 50% 56px;
z-index: 2;
}
.knob-bpm {
margin-top: 0.65rem;
font-family: 'Audiowide', sans-serif;
font-size: 1.65rem;
color: var(--led);
text-shadow: 0 0 12px rgba(61,255,176,0.35);
letter-spacing: 0.06em;
}
.knob-caption {
font-family: 'Share Tech Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.18em;
color: var(--muted);
margin-top: 0.15rem;
}
/* Modal overlay */
.modal-shell {
position: fixed !important;
inset: 0;
z-index: 1000;
background: rgba(4,5,8,0.72);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.modal-card {
width: min(720px, 94vw);
max-height: 80vh;
overflow: auto;
background: #141821;
border: 1px solid var(--line);
border-radius: 12px;
padding: 1rem 1.1rem 1.2rem;
box-shadow: 0 20px 60px rgba(0,0,0,0.55);
}
.modal-card .rack-title { color: var(--amber); }
/* Expert modal — compact control stack */
.expert-modal-card {
width: min(420px, 94vw);
}
.expert-modal-card .block {
margin-bottom: 0.35rem !important;
}
/* Mood board */
.mood-board {
display: grid;
grid-template-columns: 1fr 170px;
gap: 0.75rem;
align-items: stretch;
margin: 0.35rem 0 0.85rem;
padding: 0.65rem;
background: #0b0d13;
border: 1px solid var(--line);
border-radius: 8px;
}
.mood-faders {
display: flex;
justify-content: flex-start;
gap: 0.28rem;
min-height: 168px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 0.35rem;
}
.mood-fader {
flex: 0 0 30px;
width: 30px;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
}
.mood-fader .track {
position: relative;
width: 18px;
flex: 1;
background: linear-gradient(180deg, #1c2230, #0a0c11);
border: 1px solid #2e3648;
border-radius: 4px;
cursor: ns-resize;
touch-action: none;
}
.mood-fader .fill {
position: absolute;
left: 2px; right: 2px; bottom: 2px;
border-radius: 2px;
background: linear-gradient(180deg, var(--amber), #6a3f00);
box-shadow: 0 0 8px rgba(255,176,32,0.25);
height: 50%;
pointer-events: none;
}
.mood-fader .cap {
position: absolute;
left: -3px; right: -3px;
height: 10px;
border-radius: 2px;
background: #d7dde8;
border: 1px solid #8a93a5;
box-shadow: 0 2px 4px rgba(0,0,0,0.4);
transform: translateY(50%);
bottom: 50%;
pointer-events: none;
}
.mood-fader .ax {
font-family: 'Share Tech Mono', monospace;
font-size: 0.52rem;
letter-spacing: 0.02em;
color: var(--muted);
text-transform: lowercase;
writing-mode: vertical-rl;
transform: rotate(180deg);
height: 4.2rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mood-radar-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#mood-radar {
width: 148px;
height: 148px;
}
.mood-derived {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.35rem;
}
.mood-chip {
font-family: 'Share Tech Mono', monospace;
font-size: 0.68rem;
letter-spacing: 0.08em;
padding: 0.35rem 0.55rem;
border-radius: 4px;
border: 1px solid var(--line);
background: #10141c;
color: var(--led);
}
.mood-chip span { color: var(--muted); margin-right: 0.35rem; }
/* Hide native Gradio chrome for derived fields we drive from the board */
#mood_dims, #mood_label_box, #key_box, #meter_box {
position: absolute !important; width: 1px !important; height: 1px !important;
overflow: hidden !important; clip: rect(0,0,0,0) !important; opacity: 0 !important;
}
@media (max-width: 1100px) {
.rack-panel { min-height: unset; }
.mood-board { grid-template-columns: 1fr; }
div.row.deck-shell-row,
.deck-shell-row {
grid-template-columns: 1fr !important;
}
.deck-shell-row > .deck-gen-col,
div.row.deck-shell-row > div.deck-gen-col {
max-width: none !important;
width: 100% !important;
}
#generate-song-btn {
min-height: 52px !important;
height: 52px !important;
}
.preset-bar { width: 100%; }
}
"""
def _pad_field(label: str, grid_id: str, extra_class: str = "") -> str:
cls = f"pad-grid {extra_class}".strip()
return f"""
<div class="pad-field">
<div class="pad-field-label">{label}</div>
<div class="{cls}" id="{grid_id}"></div>
</div>
"""
MODEL_PADS_HTML = "" # unused — model uses full Gradio dropdown again
FORMAT_PADS_HTML = _pad_field("Export format", "pads-format")
DURATION_BAY_HTML = """
<div class="duration-bay" id="duration-bay">
<div class="dur-face" id="dur-face" title="Drag around the dial · scroll to nudge · exponential time">
<div class="dur-pointer" id="dur-pointer"></div>
</div>
<div class="dur-mmss"><span id="dur-mmss">03:00</span></div>
<div class="dur-caption">DURATION · EXP</div>
<div class="dur-slider-wrap">
<input type="range" id="dur-slider" min="0" max="1000" step="1" value="500" aria-label="Duration exponential" />
<div class="dur-marks"><span>0</span><span>3:00</span><span>60:00</span></div>
</div>
</div>
"""
GENRE_PADS_HTML = _pad_field("Genre", "pads-genre")
STYLE_PADS_HTML = _pad_field("Style", "pads-style") + """
<div class="style-guide" id="style-guide">
<div class="pad-field-label">About this style</div>
<p class="style-guide-body" id="style-guide-body"></p>
</div>
"""
USECASE_PADS_HTML = _pad_field("Use case", "pads-usecase")
VOCAL_PADS_HTML = _pad_field("Vocal character", "pads-vocal")
LANGUAGE_PADS_HTML = _pad_field("Language", "pads-language")
STRUCTURE_PADS_HTML = _pad_field("Structure", "pads-structure")
INSTRUMENTAL_PAD_HTML = """
<div class="pad-field">
<button type="button" class="pad toggle-wide" id="pad-instrumental" data-on="0">Instrumental · off</button>
</div>
"""
AUTO_OPTS_HTML = """
<div class="pad-field auto-opts" id="auto-opts">
<div class="pad-field-label">After generate</div>
<div class="pad-grid seg-col">
<button type="button" class="pad toggle-auto" id="pad-auto-download" data-on="0">
<span>Download auto</span><span class="auto-led" aria-hidden="true"></span>
</button>
<button type="button" class="pad toggle-auto" id="pad-auto-play" data-on="0">
<span>Play auto</span><span class="auto-led" aria-hidden="true"></span>
</button>
</div>
</div>
"""
INSTRUMENTS_BROWSER_HTML = """
<div class="inst-browser" id="inst-browser">
<div class="pad-field-label">Instruments / textures</div>
<div class="inst-active" id="inst-active"></div>
<div class="inst-empty" id="inst-empty">None selected — open a group below</div>
<div class="inst-groups" id="inst-groups"></div>
</div>
"""
KNOB_HTML = """
<div class="knob-wrap" id="tempo-knob-root">
<div class="knob-face" id="tempo-knob" title="Rotate around the center · scroll to nudge">
<div class="knob-pointer" id="tempo-pointer"></div>
</div>
<div class="knob-bpm"><span id="tempo-bpm-readout">118</span> BPM</div>
<div class="knob-caption">TEMPO · soft guide (ACE ~30–300)</div>
</div>
"""
MOOD_BOARD_HTML = """
<div class="mood-board" id="mood-board">
<div class="mood-faders" id="mood-faders"></div>
<div class="mood-radar-wrap">
<canvas id="mood-radar" width="148" height="148"></canvas>
</div>
</div>
<div class="mood-derived">
<div class="mood-chip"><span>MOOD</span><b id="mood-chip-label">—</b></div>
<div class="mood-chip"><span>KEY</span><b id="mood-chip-key">—</b></div>
<div class="mood-chip"><span>METER</span><b id="mood-chip-meter">—</b></div>
</div>
"""
BRAND_HTML = """
<div class="brand-bar">
<div>
<div class="booth">INSPIRE</div>
<div class="sub">ACE-Step · Want a faster &amp; more stable experience? Try <a href="https://acemusic.ai" target="_blank" rel="noopener noreferrer">acemusic.ai</a></div>
</div>
<div class="live">DECK READY</div>
</div>
"""
PRESET_HTML = """
<div class="preset-bar" id="preset-bar">
<span class="preset-label">PRESETS</span>
<input type="text" id="preset-name" placeholder="Name" maxlength="48" autocomplete="off" />
<button type="button" id="preset-save">Save</button>
<select id="preset-select" aria-label="Saved presets"></select>
<button type="button" id="preset-load">Load</button>
<button type="button" id="preset-delete">Delete</button>
<span class="preset-status" id="preset-status"></span>
</div>
"""
# Combined client script: rotary tempo + mood board + pad grids
import json
from mood_engine import MOOD_NAMES, resolve_style_bundle
from style_guides import STYLE_GUIDES, style_guide_text
from presets import (
DEFAULT_DURATION,
DEFAULT_GENRE,
DEFAULT_MODEL,
GENRES,
INSTRUMENT_GROUPS,
MODELS,
STRUCTURES,
USE_CASES,
USE_CASE_BPM,
VOCAL_LANGUAGES,
)
_AUDIO_FORMATS = ("MP3", "AAC (M4A)", "FLAC", "WAV")
_DEFAULT_AUDIO_FORMAT = "MP3"
def _model_pad_label(name: str) -> str:
if "non-XL" in name:
return "Turbo"
if "XL Turbo" in name:
return "XL Turbo"
if "XL SFT" in name:
return "XL SFT"
if "XL Base" in name:
return "XL Base"
return name
def _format_pad_label(name: str) -> str:
if name.startswith("AAC"):
return "AAC"
return name
def _style_meta(genre: str, style: str) -> dict:
dims_s, _label, _key, _meter, instruments, bpm = resolve_style_bundle(genre, style)
try:
dims = json.loads(dims_s)
except json.JSONDecodeError:
dims = {}
return {"mood_dims": dims, "instruments": instruments, "bpm": bpm}
_PAD_DATA = {
"models": [{"value": k, "label": _model_pad_label(k)} for k in MODELS],
"formats": [{"value": f, "label": _format_pad_label(f)} for f in _AUDIO_FORMATS],
"genres": {
name: {
"styles": g["styles"],
"vocal": g["vocal"],
"instruments": g["instruments"],
"default_bpm": g["default_bpm"],
"bpm": list(g["bpm"]),
"styles_meta": {st: _style_meta(name, st) for st in g["styles"]},
"style_guides": {
st: STYLE_GUIDES.get(name, {}).get(st) or style_guide_text(name, st)
for st in g["styles"]
},
}
for name, g in GENRES.items()
},
"genre_list": list(GENRES.keys()),
"instrument_groups": [{"name": n, "items": items} for n, items in INSTRUMENT_GROUPS],
"use_cases": list(USE_CASES),
"use_case_bpm": {k: v for k, v in USE_CASE_BPM.items()},
"languages": list(VOCAL_LANGUAGES.keys()),
"structures": list(STRUCTURES.keys()),
"duration_presets": [60, 120, 180, 240, 300, 600],
"defaults": {
"model": DEFAULT_MODEL,
"format": _DEFAULT_AUDIO_FORMAT,
"genre": DEFAULT_GENRE,
"style": GENRES[DEFAULT_GENRE]["styles"][0],
"vocal": GENRES[DEFAULT_GENRE]["vocal"][0],
"use_case": "(none)",
"language": "English",
"structure": list(STRUCTURES.keys())[0],
"duration": DEFAULT_DURATION,
"bpm": GENRES[DEFAULT_GENRE]["default_bpm"],
},
}
_CONSOLE_JS_TMPL = r"""
() => {
const BPM_MIN = 1;
// no upper clamp — ACE soft-guides ~30–300 but we let the knob run free
const MOOD_NAMES = __MOOD_NAMES__;
const PAD_DATA = __PAD_DATA__;
const MAJOR = ['C major','G major','D major','A major','F major','Bb major','Eb major'];
const MINOR = ['A minor','E minor','D minor','C minor','F minor','G minor','B minor'];
const MINOR_BIAS = new Set(['dark','melancholic','mysterious','aggressive','brooding','gritty','heartbroken','bittersweet','introspective','suspenseful']);
const MAJOR_BIAS = new Set(['uplifting','euphoric','playful','romantic','celebratory','sunny','hopeful','confident','festive','joyful','anthemic','catchy']);
const WALTZISH = new Set(['intimate','tender','sensual','wistful','pastoral','smoky']);
const COMPOUND = new Set(['hypnotic','groovy','peak-time','euphoric','festive']);
let bpm = PAD_DATA.defaults.bpm || 118;
let angle = 0;
let draggingKnob = false;
let lastAngle = null;
let dims = Object.fromEntries(MOOD_NAMES.map(m => [m, 0]));
let lastGenreSeen = '';
let padsReady = false;
// While the console UI owns tempo/duration, ignore Gradio DOM echoes (stale state).
let consoleOwnedUntil = 0;
const clamp = (v,a,b) => Math.max(a, Math.min(b, v));
function markConsoleOwned(ms) {
consoleOwnedUntil = Date.now() + (ms == null ? 3000 : ms);
}
function consoleIsOwned() {
return Date.now() < consoleOwnedUntil;
}
function setInput(sel, val) {
const root = document.querySelector(sel);
if (!root) return;
const input = root.querySelector('textarea, input:not([type=checkbox]):not([type=radio]):not([type=range]), select');
if (!input) return;
const s = String(val);
try {
const proto = input.tagName === 'TEXTAREA'
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
if (setter) setter.call(input, s);
else input.value = s;
} catch (err) {
input.value = s;
}
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
try {
input.dispatchEvent(new InputEvent('input', { bubbles: true, data: s, inputType: 'insertFromPaste' }));
} catch (_) {}
}
function fieldRoot(id) {
return document.getElementById(id) || document.querySelector('#' + id);
}
function getTextLike(id) {
const root = fieldRoot(id);
if (!root) return '';
const input = root.querySelector('textarea, input:not([type=checkbox]):not([type=radio]):not([type=range]), select');
return input ? input.value : '';
}
function getNumber(id) {
const v = parseFloat(getTextLike(id));
return Number.isFinite(v) ? v : null;
}
function getCheckbox(id) {
const root = fieldRoot(id);
if (!root) return false;
const input = root.querySelector('input[type=checkbox]');
return !!(input && input.checked);
}
function getDropdown(id) {
return getTextLike(id);
}
function getCheckboxGroup(id) {
const root = fieldRoot(id);
if (!root) return [];
return Array.from(root.querySelectorAll('input[type=checkbox]:checked')).map((el) => {
if (el.value && el.value !== 'on') return el.value;
const lab = el.closest('label');
return (lab ? lab.textContent : '').trim();
}).filter(Boolean);
}
function getSlider(id) {
const root = fieldRoot(id);
if (!root) return null;
const range = root.querySelector('input[type=range]');
if (range) {
const v = parseFloat(range.value);
return Number.isFinite(v) ? v : null;
}
return getNumber(id);
}
function setTextLike(id, val) {
setInput('#' + id, val == null ? '' : val);
}
function setCheckbox(id, checked) {
const root = fieldRoot(id);
if (!root) return;
const input = root.querySelector('input[type=checkbox]');
if (!input) return;
const want = !!checked;
if (input.checked === want) return;
input.click();
}
function setSlider(id, val) {
if (val == null || !Number.isFinite(Number(val))) return;
const root = fieldRoot(id);
if (!root) return;
const range = root.querySelector('input[type=range]');
const num = root.querySelector('input[type=number]');
const target = range || num;
if (!target) {
setTextLike(id, val);
return;
}
const s = String(val);
try {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
if (setter) setter.call(target, s);
else target.value = s;
} catch (_) {
target.value = s;
}
target.dispatchEvent(new Event('input', { bubbles: true }));
target.dispatchEvent(new Event('change', { bubbles: true }));
}
function setCheckboxGroup(id, values) {
const root = fieldRoot(id);
if (!root) return;
const want = new Set((values || []).map(String));
root.querySelectorAll('input[type=checkbox]').forEach((el) => {
let label = el.value && el.value !== 'on' ? el.value : '';
if (!label) {
const lab = el.closest('label');
label = (lab ? lab.textContent : '').trim();
}
const should = want.has(label);
if (el.checked !== should) el.click();
});
if (id === 'instrument-pads') renderInstBrowser();
}
function getSelectedInstruments() {
return getCheckboxGroup('instrument-pads');
}
function toggleInstrument(name) {
const cur = new Set(getSelectedInstruments());
if (cur.has(name)) cur.delete(name);
else cur.add(name);
setCheckboxGroup('instrument-pads', Array.from(cur));
}
function refreshInstGroupBodies() {
const groupsEl = document.getElementById('inst-groups');
if (!groupsEl) return;
const selected = new Set(getSelectedInstruments());
const groups = PAD_DATA.instrument_groups || [];
groupsEl.querySelectorAll('.inst-group').forEach((wrap) => {
const name = wrap.dataset.group;
const g = groups.find((x) => x.name === name);
if (!g) return;
const nSel = g.items.filter((i) => selected.has(i)).length;
const count = wrap.querySelector('.count');
if (count) count.textContent = nSel ? (nSel + '/' + g.items.length) : String(g.items.length);
const body = wrap.querySelector('.inst-group-body');
if (!body) return;
if (!wrap.classList.contains('open')) {
body.innerHTML = '';
return;
}
body.innerHTML = '';
g.items.forEach((item) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'inst-option' + (selected.has(item) ? ' active' : '');
btn.textContent = item;
btn.addEventListener('click', () => toggleInstrument(item));
body.appendChild(btn);
});
});
}
function renderInstBrowser() {
const active = document.getElementById('inst-active');
const empty = document.getElementById('inst-empty');
const groupsEl = document.getElementById('inst-groups');
if (!active || !groupsEl) return;
const selected = getSelectedInstruments();
active.innerHTML = '';
selected.forEach((name) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'inst-chip';
btn.innerHTML = '<span></span><span class="x">×</span>';
btn.querySelector('span').textContent = name;
btn.title = 'Remove ' + name;
btn.addEventListener('click', () => toggleInstrument(name));
active.appendChild(btn);
});
if (empty) empty.style.display = selected.length ? 'none' : 'block';
const groups = PAD_DATA.instrument_groups || [];
if (!groupsEl.dataset.built) {
groupsEl.dataset.built = '1';
groupsEl.innerHTML = '';
groups.forEach((g) => {
const wrap = document.createElement('div');
wrap.className = 'inst-group';
wrap.dataset.group = g.name;
const head = document.createElement('button');
head.type = 'button';
head.className = 'inst-group-head';
head.innerHTML = '<span class="gname"></span><span class="count"></span><span class="chev">›</span>';
head.querySelector('.gname').textContent = g.name;
head.addEventListener('click', () => {
const wasOpen = wrap.classList.contains('open');
groupsEl.querySelectorAll('.inst-group').forEach((el) => el.classList.remove('open'));
if (!wasOpen) wrap.classList.add('open');
refreshInstGroupBodies();
});
const body = document.createElement('div');
body.className = 'inst-group-body';
wrap.appendChild(head);
wrap.appendChild(body);
groupsEl.appendChild(wrap);
});
}
refreshInstGroupBodies();
}
function bindInstBrowser() {
if (!document.getElementById('inst-browser')) return;
renderInstBrowser();
}
function topMoods(n) {
return Object.entries(dims)
.sort((a,b) => b[1]-a[1] || a[0].localeCompare(b[0]))
.filter(([,v]) => v > 0)
.slice(0, n);
}
function moodLabel() {
const tops = topMoods(3);
return tops.length ? tops.map(([k]) => k).join(' ') : 'atmospheric';
}
function deriveKey() {
let minorS = 0, majorS = 0;
for (const [k,v] of Object.entries(dims)) {
if (MINOR_BIAS.has(k)) minorS += v;
if (MAJOR_BIAS.has(k)) majorS += v;
}
const pool = majorS >= minorS ? MAJOR : MINOR;
const warm = (dims.romantic||0) + (dims.sensual||0);
return pool[warm % pool.length];
}
function deriveMeter() {
let w = 0, c = 0;
for (const [k,v] of Object.entries(dims)) {
if (WALTZISH.has(k)) w += v;
if (COMPOUND.has(k)) c += v;
}
if (w >= 90) return '3';
if (c >= 140) return '6';
if ((dims.aggressive||0) >= 70) return '2';
return '4';
}
function pushMoodToGradio() {
const label = moodLabel();
const key = deriveKey();
const meter = deriveMeter();
setInput('#mood_dims', JSON.stringify(dims));
setInput('#mood_label_box', label);
setInput('#key_box', key);
setInput('#meter_box', meter);
const elL = document.getElementById('mood-chip-label');
const elK = document.getElementById('mood-chip-key');
const elM = document.getElementById('mood-chip-meter');
if (elL) elL.textContent = label;
if (elK) elK.textContent = key;
if (elM) elM.textContent = (meter === '6' ? '6/8' : (meter + '/4'));
}
function drawRadar() {
const canvas = document.getElementById('mood-radar');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const cx = W/2, cy = H/2, R = Math.min(W,H)*0.36;
const tops = topMoods(5);
const axes = tops.length ? tops : [['atmospheric', 40]];
ctx.clearRect(0,0,W,H);
ctx.strokeStyle = '#2a3142';
for (let ring = 1; ring <= 4; ring++) {
ctx.beginPath();
for (let i=0;i<axes.length;i++) {
const a = -Math.PI/2 + i * (Math.PI*2/axes.length);
const r = R * (ring/4);
const x = cx + Math.cos(a)*r, y = cy + Math.sin(a)*r;
if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
}
ctx.closePath();
ctx.stroke();
}
ctx.beginPath();
ctx.fillStyle = 'rgba(255,176,32,0.18)';
axes.forEach((ax,i) => {
const a = -Math.PI/2 + i * (Math.PI*2/axes.length);
const r = R * (ax[1]/100);
const x = cx + Math.cos(a)*r, y = cy + Math.sin(a)*r;
if (i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
});
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#ffb020';
ctx.stroke();
ctx.fillStyle = '#9aa3b2';
ctx.font = '8px monospace';
axes.forEach((ax,i) => {
const a = -Math.PI/2 + i * (Math.PI*2/axes.length);
const x = cx + Math.cos(a)*(R+16);
const y = cy + Math.sin(a)*(R+16);
ctx.textAlign = 'center';
const label = ax[0].length > 8 ? ax[0].slice(0,7)+'…' : ax[0];
ctx.fillText(label, x, y+3);
});
}
function paintFader(track, val) {
const fill = track.querySelector('.fill');
const cap = track.querySelector('.cap');
if (fill) fill.style.height = `calc(${val}% - 2px)`;
if (cap) cap.style.bottom = `${val}%`;
}
function renderFaders() {
const host = document.getElementById('mood-faders');
if (!host) return;
if (!host.dataset.ready) {
host.innerHTML = '';
MOOD_NAMES.forEach((name) => {
const col = document.createElement('div');
col.className = 'mood-fader';
col.innerHTML = `<div class="track" data-axis="${name}"><div class="fill"></div><div class="cap"></div></div><div class="ax" title="${name}">${name}</div>`;
host.appendChild(col);
const track = col.querySelector('.track');
const setFromY = (clientY) => {
const rect = track.getBoundingClientRect();
const t = clamp(1 - (clientY - rect.top)/rect.height, 0, 1);
dims[name] = Math.round(t*100);
paintFader(track, dims[name]);
drawRadar();
pushMoodToGradio();
};
track.addEventListener('pointerdown', (e) => {
e.preventDefault();
track.setPointerCapture(e.pointerId);
setFromY(e.clientY);
const move = (ev) => setFromY(ev.clientY);
const up = () => {
track.removeEventListener('pointermove', move);
track.removeEventListener('pointerup', up);
};
track.addEventListener('pointermove', move);
track.addEventListener('pointerup', up);
});
});
host.dataset.ready = '1';
}
host.querySelectorAll('.track').forEach((track) => {
paintFader(track, dims[track.dataset.axis] || 0);
});
}
function syncDimsFromGradio() {
const root = document.querySelector('#mood_dims');
const input = root && root.querySelector('textarea, input');
if (!input || !input.value) return;
try {
const parsed = JSON.parse(input.value);
let changed = false;
MOOD_NAMES.forEach((k) => {
if (typeof parsed[k] === 'number' && parsed[k] !== dims[k]) {
dims[k] = clamp(parsed[k], 0, 100);
changed = true;
}
});
if (changed) {
renderFaders();
drawRadar();
const label = moodLabel();
const key = deriveKey();
const meter = deriveMeter();
const elL = document.getElementById('mood-chip-label');
const elK = document.getElementById('mood-chip-key');
const elM = document.getElementById('mood-chip-meter');
if (elL) elL.textContent = label;
if (elK) elK.textContent = key;
if (elM) elM.textContent = (meter === '6' ? '6/8' : (meter + '/4'));
setInput('#mood_label_box', label);
setInput('#key_box', key);
setInput('#meter_box', meter);
}
} catch (_) {}
}
function applyMoodPreset(rawDims) {
let parsed = rawDims;
if (typeof rawDims === 'string') {
try { parsed = JSON.parse(rawDims); } catch (_) { return; }
}
if (!parsed || typeof parsed !== 'object') return;
MOOD_NAMES.forEach((k) => {
dims[k] = typeof parsed[k] === 'number' ? clamp(parsed[k], 0, 100) : 0;
});
renderFaders();
drawRadar();
pushMoodToGradio();
}
function pointerAngle(face, e) {
const r = face.getBoundingClientRect();
const cx = r.left + r.width/2, cy = r.top + r.height/2;
return Math.atan2(e.clientY - cy, e.clientX - cx);
}
function renderKnob() {
const pointer = document.getElementById('tempo-pointer');
const readout = document.getElementById('tempo-bpm-readout');
if (pointer) pointer.style.transform = `rotate(${angle}deg)`;
if (readout) readout.textContent = String(Math.round(bpm));
}
function setBpm(v, degDelta) {
bpm = Math.max(BPM_MIN, Math.round(v));
if (typeof degDelta === 'number') angle = (angle + degDelta) % 360;
renderKnob();
setInput('#bpm_number', bpm);
markConsoleOwned();
}
function suggestStyleBpm(genre, style) {
const g = PAD_DATA.genres[genre] || PAD_DATA.genres[PAD_DATA.defaults.genre];
if (!g) return PAD_DATA.defaults.bpm || 118;
const lo = Number(g.bpm[0]);
const hi = Number(g.bpm[1]);
let base = Number(g.default_bpm) || 118;
const s = String(style || '').toLowerCase();
const slow = ['ballad','doom','ambient','downtempo','study','intimate','quiet','lovers','chill','lo-fi','lofi','roots','delta','solo piano','chamber','baroque','minimalist','meditation','dub'];
const fast = ['dance','club','trance','techno','house','drill','trap','thrash','metalcore','drum and bass','dnb','peak','party','edm','power metal','punk','reggaeton','salsa','afrobeats','future bass','workout'];
const midFast = ['pop','rock','k-pop','synthwave','cumbia','ska','bebop'];
if (slow.some((k) => s.includes(k))) base -= 22;
else if (fast.some((k) => s.includes(k))) base += 18;
else if (midFast.some((k) => s.includes(k))) base += 6;
return Math.max(lo, Math.min(hi, Math.round(base)));
}
function applyUseCaseBpm(useCase) {
const map = PAD_DATA.use_case_bpm || {};
const v = map[useCase];
if (v == null || v === undefined) return;
setBpm(Number(v));
}
function applyStylePreset(genre, style) {
const g = PAD_DATA.genres[genre] || PAD_DATA.genres[PAD_DATA.defaults.genre];
if (!g) return;
const meta = (g.styles_meta && g.styles_meta[style]) || null;
const bpmVal = meta && meta.bpm != null ? meta.bpm : suggestStyleBpm(genre, style);
setBpm(bpmVal);
if (meta && meta.mood_dims && typeof meta.mood_dims === 'object') {
MOOD_NAMES.forEach((k) => {
const n = Number(meta.mood_dims[k]);
dims[k] = Number.isFinite(n) ? clamp(n, 0, 100) : 0;
});
renderFaders();
drawRadar();
pushMoodToGradio();
}
if (meta && Array.isArray(meta.instruments) && meta.instruments.length) {
setCheckboxGroup('instrument-pads', meta.instruments);
} else if (Array.isArray(g.instruments) && g.instruments.length) {
setCheckboxGroup('instrument-pads', g.instruments.slice(0, 4));
}
}
function applyGenreStyleBpm(genre, style) {
applyStylePreset(genre, style);
}
function bindKnob() {
const face = document.getElementById('tempo-knob');
if (!face || face.dataset.bound === '1') return;
face.dataset.bound = '1';
face.addEventListener('pointerdown', (e) => {
draggingKnob = true;
lastAngle = pointerAngle(face, e);
face.setPointerCapture(e.pointerId);
});
face.addEventListener('pointermove', (e) => {
if (!draggingKnob || lastAngle == null) return;
const a = pointerAngle(face, e);
let delta = a - lastAngle;
while (delta > Math.PI) delta -= Math.PI*2;
while (delta < -Math.PI) delta += Math.PI*2;
lastAngle = a;
const deg = delta * 180 / Math.PI;
setBpm(bpm + deg * 0.45, deg);
});
face.addEventListener('pointerup', () => { draggingKnob = false; lastAngle = null; });
face.addEventListener('pointercancel', () => { draggingKnob = false; lastAngle = null; });
face.addEventListener('wheel', (e) => {
e.preventDefault();
setBpm(bpm - e.deltaY * 0.05, -e.deltaY * 0.2);
}, { passive: false });
renderKnob();
}
function bindBpmGradioObserver() {
const root = document.getElementById('bpm_number');
if (!root || root.dataset.bpmObs === '1') return;
root.dataset.bpmObs = '1';
new MutationObserver(() => syncBpmFromGradio()).observe(root, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
});
const input = root.querySelector('input');
if (input) {
input.addEventListener('input', syncBpmFromGradio);
input.addEventListener('change', syncBpmFromGradio);
}
}
// ── Exclusive pad grids ────────────────────────────────────────────────
function normalizeOpts(options) {
return (options || []).map((o) => {
if (typeof o === 'string' || typeof o === 'number') {
return { value: String(o), label: String(o) };
}
return { value: String(o.value), label: String(o.label != null ? o.label : o.value) };
});
}
function renderExclusivePads(hostId, options, selected, onSelect, compact) {
const host = document.getElementById(hostId);
if (!host) return;
const opts = normalizeOpts(options);
const sel = selected != null ? String(selected) : '';
host.innerHTML = '';
opts.forEach((opt) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'pad' + (compact ? ' compact' : '') + (opt.value === sel ? ' active' : '');
btn.textContent = opt.label;
btn.title = opt.value;
btn.dataset.value = opt.value;
btn.addEventListener('click', () => {
host.querySelectorAll('.pad').forEach((p) => p.classList.remove('active'));
btn.classList.add('active');
onSelect(opt.value);
});
host.appendChild(btn);
});
}
function markPadActive(hostId, value) {
const host = document.getElementById(hostId);
if (!host) return;
const v = String(value);
host.querySelectorAll('.pad').forEach((p) => {
p.classList.toggle('active', p.dataset.value === v);
});
}
function updateStyleGuide(genre, style) {
const el = document.getElementById('style-guide-body');
if (!el) return;
const g = PAD_DATA.genres[genre];
const text = (g && g.style_guides && g.style_guides[style]) || '';
el.textContent = text || (
style
? (style + ': a ' + genre + ' style. Compare nearby pads to see how feel and production shift within the genre.')
: 'Select a style to see how it differs from others in this genre.'
);
}
function rebuildStyleVocalForGenre(genre, preferStyle, preferVocal) {
const g = PAD_DATA.genres[genre];
if (!g) return;
const style = (preferStyle && g.styles.includes(preferStyle)) ? preferStyle : g.styles[0];
const vocal = (preferVocal && g.vocal.includes(preferVocal)) ? preferVocal : g.vocal[0];
renderExclusivePads('pads-style', g.styles, style, (v) => {
setTextLike('style', v);
applyGenreStyleBpm(genre, v);
updateStyleGuide(genre, v);
}, true);
renderExclusivePads('pads-vocal', g.vocal, vocal, (v) => setTextLike('vocal', v), true);
updateStyleGuide(genre, style);
}
function formatMmSs(sec) {
const s = Math.max(0, Math.round(Number(sec) || 0));
const m = Math.floor(s / 60);
const r = s % 60;
return String(m).padStart(2, '0') + ':' + String(r).padStart(2, '0');
}
// Exponential duration: t=0 → 0s, t=0.5 → 180s, t=1 → 3600s
const DUR_MAX = 3600;
const DUR_GAMMA = Math.log2(20); // 0.5^gamma * 3600 = 180
let durT = Math.pow(180 / DUR_MAX, 1 / DUR_GAMMA);
let draggingDur = false;
let lastDurAngle = null;
function tToSec(t) {
t = clamp(t, 0, 1);
if (t <= 0) return 0;
return Math.round(DUR_MAX * Math.pow(t, DUR_GAMMA));
}
function secToT(sec) {
const s = clamp(Number(sec) || 0, 0, DUR_MAX);
if (s <= 0) return 0;
return Math.pow(s / DUR_MAX, 1 / DUR_GAMMA);
}
function durAngleFromT(t) {
// Map t 0..1 onto a 270° pot sweep (-135 .. +135)
return -135 + clamp(t, 0, 1) * 270;
}
function renderDuration() {
const sec = tToSec(durT);
const mmss = document.getElementById('dur-mmss');
const pointer = document.getElementById('dur-pointer');
const slider = document.getElementById('dur-slider');
const bay = document.getElementById('duration-bay');
const pipelineSec = sec <= 0 ? 10 : Math.max(10, sec);
if (bay) bay.dataset.seconds = String(pipelineSec);
if (mmss) mmss.textContent = formatMmSs(sec);
if (pointer) pointer.style.transform = `rotate(${durAngleFromT(durT)}deg)`;
if (slider) {
const v = Math.round(clamp(durT, 0, 1) * 1000);
if (String(slider.value) !== String(v)) slider.value = String(v);
}
}
function setDuration(sec) {
let n = Math.round(Number(sec));
if (!Number.isFinite(n)) n = PAD_DATA.defaults.duration;
n = clamp(n, 0, DUR_MAX);
durT = secToT(n);
// Pipeline minimum is 10s; dial can show 00:00 at the left stop
setTextLike('duration', n <= 0 ? 10 : Math.max(10, n));
markConsoleOwned();
renderDuration();
}
function setDurationFromT(t) {
durT = clamp(t, 0, 1);
const sec = tToSec(durT);
setTextLike('duration', sec <= 0 ? 10 : Math.max(10, sec));
markConsoleOwned();
renderDuration();
}
function bindDurationBay() {
const face = document.getElementById('dur-face');
const slider = document.getElementById('dur-slider');
if (!face || face.dataset.bound === '1') return;
face.dataset.bound = '1';
const initial = getNumber('duration');
setDuration(initial != null ? initial : PAD_DATA.defaults.duration);
face.addEventListener('pointerdown', (e) => {
draggingDur = true;
lastDurAngle = pointerAngle(face, e);
face.setPointerCapture(e.pointerId);
});
face.addEventListener('pointermove', (e) => {
if (!draggingDur || lastDurAngle == null) return;
const a = pointerAngle(face, e);
let delta = a - lastDurAngle;
while (delta > Math.PI) delta -= Math.PI * 2;
while (delta < -Math.PI) delta += Math.PI * 2;
lastDurAngle = a;
// clockwise increases duration; scale so ~half turn covers useful range
setDurationFromT(durT + delta * 0.35);
});
face.addEventListener('pointerup', () => { draggingDur = false; lastDurAngle = null; });
face.addEventListener('pointercancel', () => { draggingDur = false; lastDurAngle = null; });
face.addEventListener('wheel', (e) => {
e.preventDefault();
setDurationFromT(durT - e.deltaY * 0.00045);
}, { passive: false });
if (slider && slider.dataset.bound !== '1') {
slider.dataset.bound = '1';
slider.addEventListener('input', () => {
setDurationFromT(parseInt(slider.value, 10) / 1000);
});
}
}
function syncDurationFromGradio() {
// Duration is console-owned (dial). Gradio's hidden Number often lags and must not yank the dial.
// Presets / boot call setDuration() directly.
return;
}
function syncBpmFromGradio() {
// While the knob/pads own tempo, ignore Gradio DOM echoes of stale state.
if (draggingKnob || consoleIsOwned()) return;
const root = document.querySelector('#bpm_number');
const input = root && root.querySelector('input');
if (!input) return;
const v = parseFloat(input.value);
if (!Number.isFinite(v)) return;
if (Math.abs(v - bpm) >= 0.5) {
bpm = Math.max(BPM_MIN, Math.round(v));
renderKnob();
}
}
function padVal(id, fallback) {
const active = document.querySelector('#' + id + ' .pad.active');
if (active && active.dataset.value) return active.dataset.value;
return fallback;
}
function liveDurationSec() {
const bay = document.getElementById('duration-bay');
if (bay && bay.dataset.seconds) {
const n = parseInt(bay.dataset.seconds, 10);
if (Number.isFinite(n) && n >= 10) return Math.min(3600, n);
}
const mmss = document.getElementById('dur-mmss');
if (mmss) {
const m = String(mmss.textContent || '').trim().match(/^(\d+):(\d{2})$/);
if (m) {
const sec = parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
return Math.max(10, Math.min(3600, sec || 10));
}
}
const fromDial = tToSec(durT);
return Math.max(10, Math.min(3600, fromDial <= 0 ? 10 : fromDial));
}
function liveBpmValue() {
const el = document.getElementById('tempo-bpm-readout');
let live = el ? parseInt(el.textContent, 10) : NaN;
if (!Number.isFinite(live) || live < 1) live = bpm;
return Math.max(1, Math.round(live));
}
function readLiveConsole() {
const instruments = (() => {
const chips = Array.from(document.querySelectorAll('#inst-active .inst-chip span:first-child'))
.map((el) => (el.textContent || '').trim())
.filter(Boolean);
if (chips.length) return chips;
return getCheckboxGroup('instrument-pads');
})();
const instPad = document.getElementById('pad-instrumental');
const autoDl = document.getElementById('pad-auto-download');
const autoPl = document.getElementById('pad-auto-play');
return {
duration: liveDurationSec(),
bpm: liveBpmValue(),
genre: padVal('pads-genre', getDropdown('genre') || PAD_DATA.defaults.genre),
style: padVal('pads-style', getDropdown('style') || PAD_DATA.defaults.style),
vocal: padVal('pads-vocal', getDropdown('vocal') || PAD_DATA.defaults.vocal),
language: padVal('pads-language', getDropdown('language') || PAD_DATA.defaults.language),
structure: padVal('pads-structure', getDropdown('structure') || PAD_DATA.defaults.structure),
use_case: padVal('pads-usecase', getDropdown('use_case') || PAD_DATA.defaults.use_case),
audio_format: padVal('pads-format', getDropdown('audio_format') || PAD_DATA.defaults.format),
instrumental: instPad ? instPad.dataset.on === '1' : getCheckbox('instrumental'),
auto_download: autoDl ? autoDl.dataset.on === '1' : getCheckbox('auto_download'),
auto_play: autoPl ? autoPl.dataset.on === '1' : getCheckbox('auto_play'),
instruments,
mood: getTextLike('mood_label_box') || '',
keyscale: getTextLike('key_box') || '',
timesignature: getTextLike('meter_box') || '',
theme: getTextLike('inspire_theme') || getTextLike('theme') || '',
instructions: getTextLike('inspire_instructions') || '',
};
}
// Gradio button js= preprocessors call this (console UI is source of truth at click time).
window.readLiveConsole = readLiveConsole;
window.releaseConsoleOwned = function releaseConsoleOwned() {
consoleOwnedUntil = 0;
};
window.syncBpmFromGradio = syncBpmFromGradio;
function syncAutoOptPad(padId, checkboxId, onLabel, offLabel) {
const btn = document.getElementById(padId);
if (!btn) return;
const on = getCheckbox(checkboxId);
btn.dataset.on = on ? '1' : '0';
btn.classList.toggle('active', on);
const label = btn.querySelector('span:not(.auto-led)');
if (label) label.textContent = on ? onLabel : offLabel;
}
function syncAutoOptsPads() {
syncAutoOptPad('pad-auto-download', 'auto_download', 'Download auto · on', 'Download auto');
syncAutoOptPad('pad-auto-play', 'auto_play', 'Play auto · on', 'Play auto');
}
function bindAutoOptsPads() {
const pairs = [
['pad-auto-download', 'auto_download'],
['pad-auto-play', 'auto_play'],
];
pairs.forEach(([padId, checkboxId]) => {
const btn = document.getElementById(padId);
if (!btn || btn.dataset.bound === '1') return;
btn.dataset.bound = '1';
btn.addEventListener('click', () => {
const next = btn.dataset.on !== '1';
setCheckbox(checkboxId, next);
syncAutoOptsPads();
});
});
syncAutoOptsPads();
}
function syncInstrumentalPad() {
const btn = document.getElementById('pad-instrumental');
if (!btn) return;
const on = getCheckbox('instrumental');
btn.dataset.on = on ? '1' : '0';
btn.classList.toggle('active', on);
btn.textContent = on ? 'Instrumental · on' : 'Instrumental · off';
}
function bindInstrumentalPad() {
const btn = document.getElementById('pad-instrumental');
if (!btn || btn.dataset.bound === '1') return;
btn.dataset.bound = '1';
btn.addEventListener('click', () => {
const next = btn.dataset.on !== '1';
setCheckbox('instrumental', next);
syncInstrumentalPad();
});
syncInstrumentalPad();
}
function bindAllPads() {
if (!document.getElementById('pads-format')) return;
if (!padsReady) {
renderExclusivePads('pads-format', PAD_DATA.formats, getDropdown('audio_format') || PAD_DATA.defaults.format, (v) => setTextLike('audio_format', v), true);
renderExclusivePads('pads-genre', PAD_DATA.genre_list, getDropdown('genre') || PAD_DATA.defaults.genre, (v) => {
setTextLike('genre', v);
lastGenreSeen = v;
rebuildStyleVocalForGenre(v);
const g = PAD_DATA.genres[v];
const st = g ? g.styles[0] : '';
if (g) setTextLike('style', st);
if (g) setTextLike('vocal', g.vocal[0]);
applyGenreStyleBpm(v, st);
}, true);
renderExclusivePads('pads-usecase', PAD_DATA.use_cases, getDropdown('use_case') || PAD_DATA.defaults.use_case, (v) => {
setTextLike('use_case', v);
applyUseCaseBpm(v);
}, true);
renderExclusivePads('pads-language', PAD_DATA.languages, getDropdown('language') || PAD_DATA.defaults.language, (v) => setTextLike('language', v), true);
renderExclusivePads('pads-structure', PAD_DATA.structures, getDropdown('structure') || PAD_DATA.defaults.structure, (v) => setTextLike('structure', v), true);
const g0 = getDropdown('genre') || PAD_DATA.defaults.genre;
lastGenreSeen = g0;
rebuildStyleVocalForGenre(g0, getDropdown('style'), getDropdown('vocal'));
bindDurationBay();
bindInstrumentalPad();
bindAutoOptsPads();
bindInstBrowser();
padsReady = true;
}
}
function refreshPadsFromGradio() {
if (!padsReady) return;
const genre = getDropdown('genre');
if (genre && genre !== lastGenreSeen) {
lastGenreSeen = genre;
rebuildStyleVocalForGenre(genre, getDropdown('style'), getDropdown('vocal'));
markPadActive('pads-genre', genre);
} else {
markPadActive('pads-genre', genre || PAD_DATA.defaults.genre);
markPadActive('pads-style', getDropdown('style'));
markPadActive('pads-vocal', getDropdown('vocal'));
updateStyleGuide(genre || PAD_DATA.defaults.genre, getDropdown('style') || PAD_DATA.defaults.style);
}
markPadActive('pads-format', getDropdown('audio_format'));
markPadActive('pads-usecase', getDropdown('use_case'));
markPadActive('pads-language', getDropdown('language'));
markPadActive('pads-structure', getDropdown('structure'));
syncDurationFromGradio();
syncInstrumentalPad();
syncAutoOptsPads();
renderInstBrowser();
}
// ── localStorage presets ───────────────────────────────────────────────
const PRESET_KEY = 'ace-inspire-presets-v1';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function collectPreset() {
let moodDimsRaw = getTextLike('mood_dims');
let moodDimsObj = dims;
try {
if (moodDimsRaw) moodDimsObj = JSON.parse(moodDimsRaw);
} catch (_) {}
const liveBpmEl = document.getElementById('tempo-bpm-readout');
const liveBpm = liveBpmEl ? parseInt(liveBpmEl.textContent, 10) : getNumber('bpm_number');
return {
v: 1,
savedAt: new Date().toISOString(),
model: getDropdown('model'),
duration: getNumber('duration'),
steps: getSlider('steps'),
guidance: getSlider('guidance'),
shift: getSlider('shift'),
seed: getNumber('seed'),
random_seed: getCheckbox('random_seed'),
lyric_seed: getNumber('lyric_seed'),
genre: getDropdown('genre'),
style: getDropdown('style'),
use_case: getDropdown('use_case'),
theme: getTextLike('inspire_theme') || getTextLike('theme'),
instructions: getTextLike('inspire_instructions') || '',
vocal: getDropdown('vocal'),
mood_dims: moodDimsObj,
mood: getTextLike('mood_label_box'),
keyscale: getTextLike('key_box'),
timesignature: getTextLike('meter_box'),
instruments: getCheckboxGroup('instrument-pads'),
bpm: Number.isFinite(liveBpm) ? liveBpm : getNumber('bpm_number'),
language: getDropdown('language'),
structure: getDropdown('structure'),
instrumental: getCheckbox('instrumental'),
lyrics: getTextLike('lyrics'),
audio_format: getDropdown('audio_format'),
};
}
function readStore() {
try {
const raw = localStorage.getItem(PRESET_KEY);
const data = raw ? JSON.parse(raw) : {};
return data && typeof data === 'object' ? data : {};
} catch (_) {
return {};
}
}
function writeStore(obj) {
localStorage.setItem(PRESET_KEY, JSON.stringify(obj));
}
function presetStatus(msg, ok) {
const el = document.getElementById('preset-status');
if (!el) return;
el.textContent = msg || '';
el.style.color = ok === false ? 'var(--danger)' : 'var(--led)';
}
function refreshPresetSelect(selected) {
const sel = document.getElementById('preset-select');
if (!sel) return;
const store = readStore();
const names = Object.keys(store).sort((a, b) => a.localeCompare(b));
const cur = selected != null ? selected : sel.value;
sel.innerHTML = '';
const ph = document.createElement('option');
ph.value = '';
ph.textContent = names.length ? '— choose —' : '— none saved —';
sel.appendChild(ph);
names.forEach((name) => {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
sel.appendChild(opt);
});
if (cur && store[cur]) sel.value = cur;
}
async function applyPreset(p) {
if (!p || typeof p !== 'object') return;
if (p.model != null) {
setTextLike('model', p.model);
await sleep(450);
}
if (p.duration != null) setDuration(p.duration);
if (p.audio_format != null) setTextLike('audio_format', p.audio_format);
if (p.steps != null) setSlider('steps', p.steps);
if (p.guidance != null) setSlider('guidance', p.guidance);
if (p.shift != null) setSlider('shift', p.shift);
if (p.seed != null) setTextLike('seed', p.seed);
if (p.lyric_seed != null) setTextLike('lyric_seed', p.lyric_seed);
if (typeof p.random_seed === 'boolean') setCheckbox('random_seed', p.random_seed);
if (p.use_case != null) setTextLike('use_case', p.use_case);
if (p.theme != null) {
setTextLike('inspire_theme', p.theme);
setTextLike('theme', p.theme);
}
if (p.instructions != null) setTextLike('inspire_instructions', p.instructions);
if (p.language != null) setTextLike('language', p.language);
if (p.structure != null) setTextLike('structure', p.structure);
if (typeof p.instrumental === 'boolean') setCheckbox('instrumental', p.instrumental);
if (p.lyrics != null) setTextLike('lyrics', p.lyrics);
if (p.genre != null) {
setTextLike('genre', p.genre);
lastGenreSeen = p.genre;
rebuildStyleVocalForGenre(p.genre, p.style, p.vocal);
await sleep(450);
}
if (p.style != null) setTextLike('style', p.style);
if (p.vocal != null) setTextLike('vocal', p.vocal);
if (Array.isArray(p.instruments)) {
await sleep(120);
setCheckboxGroup('instrument-pads', p.instruments);
}
if (p.bpm != null) setBpm(Number(p.bpm));
if (p.mood_dims) applyMoodPreset(p.mood_dims);
if (p.mood) setTextLike('mood_label_box', p.mood);
if (p.keyscale) setTextLike('key_box', p.keyscale);
if (p.timesignature) setTextLike('meter_box', p.timesignature);
if (p.keyscale) {
const elK = document.getElementById('mood-chip-key');
if (elK) elK.textContent = p.keyscale;
}
if (p.timesignature) {
const elM = document.getElementById('mood-chip-meter');
const m = String(p.timesignature);
if (elM) elM.textContent = (m === '6' ? '6/8' : (m + '/4'));
}
if (p.mood) {
const elL = document.getElementById('mood-chip-label');
if (elL) elL.textContent = p.mood;
}
refreshPadsFromGradio();
}
function bindPresets() {
const bar = document.getElementById('preset-bar');
if (!bar || bar.dataset.bound === '1') return;
bar.dataset.bound = '1';
refreshPresetSelect();
document.getElementById('preset-save')?.addEventListener('click', () => {
const nameEl = document.getElementById('preset-name');
const name = (nameEl?.value || document.getElementById('preset-select')?.value || '').trim();
if (!name) {
presetStatus('name required', false);
return;
}
const store = readStore();
store[name] = collectPreset();
writeStore(store);
if (nameEl) nameEl.value = name;
refreshPresetSelect(name);
presetStatus('saved', true);
});
document.getElementById('preset-load')?.addEventListener('click', async () => {
const name = document.getElementById('preset-select')?.value;
if (!name) {
presetStatus('pick one', false);
return;
}
const store = readStore();
const p = store[name];
if (!p) {
presetStatus('missing', false);
return;
}
presetStatus('loading…', true);
try {
await applyPreset(p);
const nameEl = document.getElementById('preset-name');
if (nameEl) nameEl.value = name;
presetStatus('loaded', true);
} catch (err) {
console.error(err);
presetStatus('load failed', false);
}
});
document.getElementById('preset-delete')?.addEventListener('click', () => {
const name = document.getElementById('preset-select')?.value;
if (!name) {
presetStatus('pick one', false);
return;
}
const store = readStore();
if (!store[name]) {
presetStatus('missing', false);
return;
}
delete store[name];
writeStore(store);
refreshPresetSelect('');
const nameEl = document.getElementById('preset-name');
if (nameEl && nameEl.value.trim() === name) nameEl.value = '';
presetStatus('deleted', true);
});
}
function syncGenerateHeight() {
const row = document.querySelector('.deck-shell-row');
const deck = document.querySelector('.deck-shell-row .deck-panel, #deck-panel');
const gen = document.querySelector('.deck-shell-row .deck-gen-col');
const btn = document.querySelector('#generate-song-btn');
if (!row || !deck || !gen || !btn) return;
if (!btn.dataset.stacked) {
btn.dataset.stacked = '1';
btn.innerHTML = 'Generate<br>song';
}
const h = Math.ceil(deck.getBoundingClientRect().height);
if (h > 0) {
gen.style.height = h + 'px';
btn.style.height = '100%';
}
}
function boot() {
bindKnob();
bindBpmGradioObserver();
bindPresets();
bindAllPads();
syncGenerateHeight();
const deck = document.querySelector('.deck-shell-row .deck-panel, #deck-panel');
if (deck && !deck.dataset.heightObs) {
deck.dataset.heightObs = '1';
if (window.ResizeObserver) {
new ResizeObserver(() => syncGenerateHeight()).observe(deck);
}
}
const root = document.querySelector('#mood_dims');
const input = root && root.querySelector('textarea, input');
if (input && input.value) {
try {
const parsed = JSON.parse(input.value);
MOOD_NAMES.forEach((k) => {
if (typeof parsed[k] === 'number') dims[k] = clamp(parsed[k], 0, 100);
});
} catch (_) {}
}
renderFaders();
drawRadar();
pushMoodToGradio();
}
setInterval(() => {
bindKnob();
bindBpmGradioObserver();
bindPresets();
bindAllPads();
refreshPadsFromGradio();
syncGenerateHeight();
syncBpmFromGradio();
syncDimsFromGradio();
if (!document.getElementById('mood-faders')?.dataset.ready) {
renderFaders();
drawRadar();
}
}, 400);
boot();
}
"""
CONSOLE_JS = (
_CONSOLE_JS_TMPL
.replace("__MOOD_NAMES__", json.dumps(list(MOOD_NAMES)))
.replace("__PAD_DATA__", json.dumps(_PAD_DATA))
)