| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ACE Studio — say the song, hear it back</title> |
| <meta name="description" content="Describe a song in plain English. ACE Studio writes the lyrics and produces the track — powered by our own fast ACE-Step turbo model."> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet"> |
| <style> |
| :root{ |
| --bg-0:#150f1e; |
| --bg-1:#1c1428; |
| --surface: rgba(255,255,255,0.045); |
| --surface-2: rgba(255,255,255,0.075); |
| --line: rgba(246,241,234,0.10); |
| --line-strong: rgba(246,241,234,0.20); |
| --ink:#f6f1ea; |
| --ink-dim:#a89bc2; |
| --ink-faint:#6f6386; |
| --coral:#ff6b4a; |
| --coral-dim:#8a3a29; |
| --gold:#ffd166; |
| --gold-dim:#8a6f2f; |
| --radius:16px; |
| } |
| *{box-sizing:border-box;} |
| html{scroll-behavior:smooth;} |
| body{ |
| margin:0; min-height:100vh; |
| background: |
| radial-gradient(ellipse 65% 45% at 18% -8%, rgba(255,107,74,0.14), transparent 60%), |
| radial-gradient(ellipse 55% 40% at 88% 6%, rgba(255,209,102,0.08), transparent 60%), |
| linear-gradient(180deg, var(--bg-0) 0%, var(--bg-1) 100%); |
| color:var(--ink); |
| font-family:'Inter', system-ui, sans-serif; |
| -webkit-font-smoothing:antialiased; |
| } |
| a{color:inherit;} |
| ::selection{background:var(--coral); color:#1a1006;} |
| ::-webkit-scrollbar{width:10px;} |
| ::-webkit-scrollbar-thumb{background:linear-gradient(180deg,var(--coral),var(--gold)); border-radius:6px;} |
| |
| .wrap{max-width:760px; margin:0 auto; padding:0 24px;} |
| |
| |
| .topbar{display:flex; align-items:center; justify-content:space-between; padding:22px 0;} |
| .wordmark{font-family:'Fraunces', serif; font-weight:600; font-size:18px; letter-spacing:0.2px;} |
| .wordmark .dot{color:var(--coral);} |
| .topbar nav{display:flex; gap:22px; font-size:13px; color:var(--ink-dim);} |
| .topbar nav a{text-decoration:none; transition:color .2s ease;} |
| .topbar nav a:hover{color:var(--gold);} |
| |
| |
| .hero{padding:48px 0 8px 0; text-align:center;} |
| .eyebrow{ |
| display:inline-block; font-family:'IBM Plex Mono', monospace; font-size:12px; |
| letter-spacing:1.4px; text-transform:uppercase; color:var(--gold); |
| background:rgba(255,209,102,0.08); border:1px solid rgba(255,209,102,0.25); |
| padding:6px 14px; border-radius:999px; margin-bottom:26px; |
| } |
| h1.headline{ |
| font-family:'Fraunces', serif; font-weight:600; font-style:normal; |
| font-size:clamp(34px, 5.6vw, 56px); line-height:1.12; margin:0 0 16px 0; letter-spacing:-0.3px; |
| } |
| h1.headline em{font-style:italic; color:var(--coral); font-weight:500;} |
| p.sub{font-size:16.5px; color:var(--ink-dim); max-width:480px; margin:0 auto 40px auto; line-height:1.6;} |
| |
| |
| .composer{ |
| background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); |
| padding:6px 6px 18px 6px; text-align:left; backdrop-filter:blur(8px); |
| } |
| .composer textarea{ |
| width:100%; background:transparent; border:none; outline:none; resize:none; |
| color:var(--ink); font-family:'Fraunces', serif; font-weight:500; font-size:19px; |
| line-height:1.5; padding:20px 18px 8px 18px; height:108px; |
| } |
| .composer textarea::placeholder{color:var(--ink-faint); font-style:italic;} |
| .composer-row{display:flex; align-items:center; justify-content:space-between; padding:6px 12px 4px 18px; gap:12px; flex-wrap:wrap;} |
| .duration-pill{ |
| display:inline-flex; align-items:center; gap:8px; font-family:'IBM Plex Mono', monospace; font-size:12.5px; color:var(--ink-dim); |
| } |
| .duration-pill select{ |
| background:var(--surface-2); border:1px solid var(--line); color:var(--ink); |
| border-radius:8px; padding:6px 10px; font-family:'IBM Plex Mono', monospace; font-size:12.5px; |
| } |
| .generate-btn{ |
| font-family:'Inter', sans-serif; font-weight:700; font-size:15px; |
| background:linear-gradient(120deg, var(--coral), #ff8c6b); color:#210c06; |
| border:none; border-radius:11px; padding:13px 26px; cursor:pointer; |
| box-shadow:0 10px 28px rgba(255,107,74,0.25); transition:transform .15s ease, box-shadow .15s ease; |
| } |
| .generate-btn:hover{transform:translateY(-2px); box-shadow:0 14px 34px rgba(255,107,74,0.35);} |
| .generate-btn:disabled{opacity:0.55; cursor:not-allowed; transform:none;} |
| .hint{margin:14px 4px 0 4px; font-size:12px; color:var(--ink-faint); font-family:'IBM Plex Mono', monospace;} |
| |
| |
| .status-row{display:flex; align-items:center; gap:14px; margin:26px 0 0 0; min-height:34px;} |
| .reel{width:34px; height:34px; flex-shrink:0; display:none;} |
| .reel.spin{display:block; animation:reel-spin 2.2s linear infinite;} |
| @keyframes reel-spin{ to{ transform:rotate(360deg); } } |
| .status-text{font-family:'IBM Plex Mono', monospace; font-size:12.5px; color:var(--ink-dim);} |
| .status-text.error{color:#ff8b7a;} |
| |
| |
| .result{ |
| display:none; margin-top:34px; background:var(--surface); border:1px solid var(--line); |
| border-radius:var(--radius); padding:26px 26px 22px 26px; |
| } |
| .result.show{display:block;} |
| .result-head{display:flex; align-items:center; gap:16px; margin-bottom:18px;} |
| .reel-lg{width:56px; height:56px; flex-shrink:0;} |
| .reel-lg.spin{animation:reel-spin 3s linear infinite;} |
| .result-title{font-family:'Fraunces', serif; font-weight:600; font-size:22px; margin:0 0 6px 0;} |
| .result-tags{display:flex; gap:6px; flex-wrap:wrap;} |
| .tag-pill{ |
| font-family:'IBM Plex Mono', monospace; font-size:11px; color:var(--gold); |
| background:rgba(255,209,102,0.08); border:1px solid rgba(255,209,102,0.22); |
| padding:3px 9px; border-radius:999px; |
| } |
| audio{width:100%; margin:6px 0 16px 0; border-radius:10px;} |
| .lyrics-toggle{ |
| background:none; border:none; color:var(--ink-dim); font-family:'IBM Plex Mono', monospace; |
| font-size:12px; cursor:pointer; padding:0; text-decoration:underline dotted var(--ink-faint); |
| } |
| .lyrics-toggle:hover{color:var(--gold);} |
| .lyrics-body{ |
| display:none; margin-top:14px; padding:16px 18px; background:rgba(0,0,0,0.18); |
| border-radius:10px; border:1px solid var(--line); |
| font-family:'Fraunces', serif; font-size:15px; line-height:1.75; white-space:pre-wrap; color:var(--ink); |
| } |
| .lyrics-body.show{display:block;} |
| .download-row{margin-top:14px;} |
| .download-link{ |
| font-family:'IBM Plex Mono', monospace; font-size:12px; color:var(--ink-dim); |
| text-decoration:none; border-bottom:1px dashed var(--ink-faint); |
| } |
| .download-link:hover{color:var(--gold); border-color:var(--gold-dim);} |
| |
| |
| footer{ |
| margin-top:60px; padding:24px 0 44px 0; border-top:1px solid var(--line); |
| display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; |
| font-size:12px; color:var(--ink-faint); font-family:'IBM Plex Mono', monospace; |
| } |
| footer a{text-decoration:none; color:var(--ink-faint); transition:color .2s ease;} |
| footer a:hover{color:var(--coral);} |
| footer .links{display:flex; gap:16px; flex-wrap:wrap;} |
| |
| @media (max-width:560px){ |
| .topbar nav{display:none;} |
| .composer-row{align-items:flex-start;} |
| } |
| @media (prefers-reduced-motion: reduce){ |
| .reel.spin, .reel-lg.spin{animation:none;} |
| } |
| </style> |
| </head> |
| <body> |
| <div class="wrap"> |
|
|
| <div class="topbar"> |
| <div class="wordmark">ACE <span class="dot">Studio</span></div> |
| <nav> |
| <a href="https://github.com/ACE-Step/ACE-Step-1.5" target="_blank">GitHub</a> |
| <a href="https://huggingface.co/collections/ACE-Step/ace-step-15" target="_blank">Model</a> |
| </nav> |
| </div> |
|
|
| <section class="hero"> |
| <span class="eyebrow">Fast turbo model · ZeroGPU</span> |
| <h1 class="headline">Say the song.<br>Hear it back <em>in seconds</em>.</h1> |
| <p class="sub">Describe a mood, a story, a genre — anything. We write the lyrics and produce the track for you.</p> |
| </section> |
|
|
| <div class="composer"> |
| <textarea id="desc" placeholder="a bittersweet indie-folk song about leaving your hometown, gentle acoustic guitar, warm female vocals..."></textarea> |
| <div class="composer-row"> |
| <label class="duration-pill"> |
| Length |
| <select id="duration"> |
| <option value="30">30s</option> |
| <option value="60" selected>60s</option> |
| <option value="90">90s</option> |
| <option value="120">120s</option> |
| </select> |
| </label> |
| <button class="generate-btn" id="generate-btn">Write & Produce →</button> |
| </div> |
| </div> |
| <p class="hint">⌘/Ctrl + Enter to generate · first run may take a little longer while a GPU is allocated</p> |
|
|
| <div class="status-row" id="status-row"> |
| <svg class="reel" id="reel-sm" viewBox="0 0 40 40" aria-hidden="true"> |
| <circle cx="20" cy="20" r="17" fill="none" stroke="#8a3a29" stroke-width="2.5"/> |
| <circle cx="20" cy="20" r="17" fill="none" stroke="#ff6b4a" stroke-width="2.5" stroke-dasharray="20 87" stroke-linecap="round"/> |
| <circle cx="20" cy="20" r="5" fill="#ffd166"/> |
| <circle cx="20" cy="9" r="2.2" fill="#1c1428"/> |
| <circle cx="9" cy="26" r="2.2" fill="#1c1428"/> |
| <circle cx="31" cy="26" r="2.2" fill="#1c1428"/> |
| </svg> |
| <span class="status-text" id="status-text"></span> |
| </div> |
|
|
| <div class="result" id="result"> |
| <div class="result-head"> |
| <svg class="reel-lg" id="reel-lg" viewBox="0 0 40 40" aria-hidden="true"> |
| <circle cx="20" cy="20" r="17" fill="none" stroke="#8a3a29" stroke-width="2.5"/> |
| <circle cx="20" cy="20" r="17" fill="none" stroke="#ff6b4a" stroke-width="2.5" stroke-dasharray="20 87" stroke-linecap="round"/> |
| <circle cx="20" cy="20" r="5" fill="#ffd166"/> |
| <circle cx="20" cy="9" r="2.2" fill="#1c1428"/> |
| <circle cx="9" cy="26" r="2.2" fill="#1c1428"/> |
| <circle cx="31" cy="26" r="2.2" fill="#1c1428"/> |
| </svg> |
| <div> |
| <h3 class="result-title" id="result-title">—</h3> |
| <div class="result-tags" id="result-tags"></div> |
| </div> |
| </div> |
| <audio id="result-audio" controls></audio> |
| <div> |
| <button class="lyrics-toggle" id="lyrics-toggle">Show lyrics</button> |
| <div class="lyrics-body" id="lyrics-body"></div> |
| </div> |
| <div class="download-row"> |
| <a class="download-link" id="download-link" download="song.wav">Download WAV ↓</a> |
| </div> |
| </div> |
|
|
| <footer> |
| <span>ACE Studio · built on ACE-Step v1.5</span> |
| <div class="links"> |
| <a href="https://github.com/ACE-Step/ACE-Step-1.5" target="_blank">GitHub</a> |
| <a href="https://huggingface.co/collections/ACE-Step/ace-step-15" target="_blank">Hugging Face</a> |
| <a href="https://discord.gg/PeWDxrkdj7" target="_blank">Discord</a> |
| </div> |
| </footer> |
|
|
| </div> |
|
|
| <script type="module"> |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.15.2/dist/index.min.js"; |
| |
| const descEl = document.getElementById("desc"); |
| const durationEl = document.getElementById("duration"); |
| const btnEl = document.getElementById("generate-btn"); |
| const statusRow = document.getElementById("status-row"); |
| const reelSm = document.getElementById("reel-sm"); |
| const statusText = document.getElementById("status-text"); |
| const resultEl = document.getElementById("result"); |
| const reelLg = document.getElementById("reel-lg"); |
| const titleEl = document.getElementById("result-title"); |
| const tagsEl = document.getElementById("result-tags"); |
| const audioEl = document.getElementById("result-audio"); |
| const lyricsToggle = document.getElementById("lyrics-toggle"); |
| const lyricsBody = document.getElementById("lyrics-body"); |
| const downloadLink = document.getElementById("download-link"); |
| |
| let client = null; |
| async function getClient() { |
| if (!client) client = await Client.connect(window.location.origin); |
| return client; |
| } |
| |
| function cleanErrorText(msg) { |
| return String(msg || "").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)"); |
| } |
| |
| function setStatus(msg, isError) { |
| statusText.textContent = msg || ""; |
| statusText.classList.toggle("error", !!isError); |
| } |
| |
| function setBusy(busy) { |
| btnEl.disabled = busy; |
| btnEl.textContent = busy ? "Producing…" : "Write & Produce →"; |
| reelSm.classList.toggle("spin", busy); |
| reelLg.classList.toggle("spin", busy); |
| } |
| |
| lyricsToggle.addEventListener("click", () => { |
| const show = !lyricsBody.classList.contains("show"); |
| lyricsBody.classList.toggle("show", show); |
| lyricsToggle.textContent = show ? "Hide lyrics" : "Show lyrics"; |
| }); |
| |
| async function generate() { |
| const description = descEl.value.trim(); |
| if (!description) { descEl.focus(); return; } |
| |
| setBusy(true); |
| setStatus("Waking up the studio…"); |
| resultEl.classList.remove("show"); |
| |
| try { |
| const c = await getClient(); |
| const job = c.submit("/create", { |
| description, |
| audio_duration: parseFloat(durationEl.value), |
| seed: -1, |
| }); |
| |
| for await (const msg of job) { |
| if (msg.type === "status") { |
| if (msg.stage === "error") { |
| throw new Error(msg.message || "Generation failed on the server."); |
| } else if (msg.queue_size > 0) { |
| const eta = msg.eta != null ? ` · ~${Math.round(msg.eta)}s` : ""; |
| setStatus(`In queue (${msg.position}/${msg.queue_size})${eta}`); |
| } else { |
| setStatus("Writing lyrics & producing the track…"); |
| } |
| } else if (msg.type === "data") { |
| const raw = msg.data && msg.data[0]; |
| if (!raw) throw new Error("No data returned"); |
| const result = JSON.parse(raw); |
| |
| titleEl.textContent = result.title || "Untitled"; |
| tagsEl.innerHTML = ""; |
| (result.tags || "").split(",").map(t => t.trim()).filter(Boolean).forEach(t => { |
| const span = document.createElement("span"); |
| span.className = "tag-pill"; |
| span.textContent = t; |
| tagsEl.appendChild(span); |
| }); |
| |
| audioEl.src = result.audio; |
| downloadLink.href = result.audio; |
| |
| lyricsBody.textContent = result.lyrics || ""; |
| lyricsBody.classList.remove("show"); |
| lyricsToggle.textContent = "Show lyrics"; |
| |
| resultEl.classList.add("show"); |
| setStatus("Done."); |
| } |
| } |
| |
| if (!resultEl.classList.contains("show")) { |
| throw new Error("The studio stopped responding before finishing. Please try again."); |
| } |
| } catch (err) { |
| console.error(err); |
| setStatus(`Something went wrong: ${cleanErrorText(err.message)}`, true); |
| } finally { |
| setBusy(false); |
| } |
| } |
| |
| btnEl.addEventListener("click", generate); |
| descEl.addEventListener("keydown", (e) => { |
| if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); generate(); } |
| }); |
| |
| getClient().catch(console.error); |
| </script> |
| </body> |
| </html> |
|
|