"""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"""
{label}
""" MODEL_PADS_HTML = "" # unused — model uses full Gradio dropdown again FORMAT_PADS_HTML = _pad_field("Export format", "pads-format") DURATION_BAY_HTML = """
03:00
DURATION · EXP
03:0060:00
""" GENRE_PADS_HTML = _pad_field("Genre", "pads-genre") STYLE_PADS_HTML = _pad_field("Style", "pads-style") + """
About this style

""" 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 = """
""" AUTO_OPTS_HTML = """
After generate
""" INSTRUMENTS_BROWSER_HTML = """
Instruments / textures
None selected — open a group below
""" KNOB_HTML = """
118 BPM
TEMPO · soft guide (ACE ~30–300)
""" MOOD_BOARD_HTML = """
MOOD
KEY
METER
""" BRAND_HTML = """
INSPIRE
ACE-Step · Want a faster & more stable experience? Try acemusic.ai
DECK READY
""" PRESET_HTML = """
PRESETS
""" # 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 = '×'; 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 = ''; 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 { 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 = `
${name}
`; 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
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)) )