MA
feat: init hf space
7bcde8c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sample Generator</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
background: #c0c0c0;
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
color: #000;
}
body {
display: flex;
flex-direction: column;
padding: 6px;
}
/* ─── Window fills viewport ───────────────────────────────────── */
.window {
flex: 1;
display: flex;
flex-direction: column;
border: 3px solid;
border-color: #fff #404040 #404040 #fff;
min-height: 0;
}
/* ─── Title bar ───────────────────────────────────────────────── */
.title-bar {
background: #1a44d8;
color: #fff;
padding: 5px 8px;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
flex-shrink: 0;
}
.title-bar-title { font-weight: bold; font-size: 14px; letter-spacing: 0.5px; }
.title-bar-btns { display: flex; gap: 3px; }
.tbtn {
width: 24px; height: 20px;
background: #c0c0c0;
border: 2px solid; border-color: #fff #404040 #404040 #fff;
font-size: 10px; font-weight: bold; color: #000;
cursor: pointer; display: flex; align-items: center; justify-content: center;
font-family: 'Courier New', monospace;
}
.tbtn:active { border-color: #404040 #fff #fff #404040; }
/* ─── Nav ─────────────────────────────────────────────────────── */
.nav {
background: #c0c0c0;
padding: 7px 18px;
display: flex; gap: 44px;
flex-shrink: 0;
}
.nav-item { font-size: 13px; cursor: pointer; color: #000; }
.nav-item.active { font-weight: bold; }
/* ─── White content box β€” fills remaining space ───────────────── */
.content-wrap {
flex: 1;
padding: 6px 7px 0;
display: flex;
flex-direction: column;
min-height: 0;
}
.content {
flex: 1;
background: #fff;
border: 2px solid; border-color: #404040 #fff #fff #404040;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* ─── Pages (all fill content box) ───────────────────────────── */
.page {
display: none;
width: 100%; height: 100%;
align-items: center;
justify-content: center;
}
.page.active { display: flex; }
/* ─── HOME ────────────────────────────────────────────────────── */
.home-inner { text-align: center; }
.home-eyebrow { font-size: 14px; letter-spacing: 2px; margin-bottom: 6px; }
.home-big {
font-size: 96px; font-weight: bold;
color: #1a44d8; line-height: 1;
letter-spacing: -4px; margin-bottom: 4px;
}
.home-sub { font-size: 15px; letter-spacing: 6px; margin-bottom: 34px; }
/* ─── GENERATE β€” centered narrow column ───────────────────────── */
.form-inner {
width: 480px;
max-width: calc(100% - 40px);
}
.field { margin-bottom: 18px; }
.field-lbl {
display: block; font-size: 11px; letter-spacing: 1px;
color: #666; margin-bottom: 6px;
}
.win-input {
width: 100%; padding: 5px 7px;
font-family: 'Courier New', monospace; font-size: 13px;
border: 0; border-bottom: 1px solid #999;
background: transparent; outline: none; color: #000;
}
.win-input:focus { border-bottom-color: #1a44d8; }
.input-row { display: flex; gap: 8px; align-items: flex-end; }
.input-row .win-input { flex: 1; }
.win-select {
padding: 4px 6px;
font-family: 'Courier New', monospace; font-size: 12px;
border: 1px solid #999;
background: #fff; cursor: pointer; color: #000; outline: none;
}
.win-select:focus { border-color: #1a44d8; }
/* Tags */
.tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
.tag {
background: #c0c0c0;
border: 1px solid; border-color: #fff #808080 #808080 #fff;
padding: 2px 9px; font-size: 11px; cursor: pointer;
}
.tag:hover { background: #1a44d8; color: #fff; border-color: #1a44d8; }
.tag:active { border-color: #808080 #fff #fff #808080; }
.options-row {
display: flex; gap: 24px; flex-wrap: wrap;
margin-bottom: 22px; align-items: flex-end;
}
.options-row .field { margin-bottom: 0; }
/* Buttons */
.btn {
background: #c0c0c0;
border: 2px solid; border-color: #fff #404040 #404040 #fff;
padding: 4px 18px;
font-family: 'Courier New', monospace; font-size: 13px;
cursor: pointer; color: #000;
}
.btn:active { border-color: #404040 #fff #fff #404040; padding: 5px 17px 3px 19px; }
.btn:disabled { color: #808080; cursor: default; }
.btn.default { font-weight: bold; border-width: 3px; padding: 5px 24px; }
.btn.ghost {
background: transparent; border: 1px solid #999;
font-size: 12px; padding: 4px 14px;
}
.btn.ghost:hover { background: #f0f0f0; }
.btn.ghost:active { border-color: #404040; }
.action-row { display: flex; gap: 8px; align-items: center; }
/* Progress */
.progress-wrap { display: none; margin-top: 18px; }
.progress-lbl { font-size: 11px; color: #666; margin-bottom: 5px; }
.progress-track {
width: 100%; height: 4px;
background: #e0e0e0; overflow: hidden;
}
.progress-fill {
height: 100%;
background: repeating-linear-gradient(
90deg, #1a44d8 0px, #1a44d8 60%, #c0d0ff 60%, #c0d0ff 100%
);
background-size: 200% 100%;
animation: slide 1.2s linear infinite;
}
@keyframes slide { to { background-position: -200% 0; } }
/* Result */
.result-box { display: none; margin-top: 18px; }
audio { width: 100%; display: block; margin-bottom: 10px; }
.result-meta { font-size: 10px; color: #888; margin-top: 8px; }
/* ─── HISTORY ─────────────────────────────────────────────────── */
.history-inner { width: 600px; max-width: calc(100% - 40px); max-height: 80%; overflow-y: auto; }
.history-empty { text-align: center; color: #999; padding: 40px 0; font-size: 13px; }
.h-item {
border-bottom: 1px solid #eee;
padding: 8px 0;
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.h-item:first-child { border-top: 1px solid #eee; }
.h-num { font-size: 10px; color: #aaa; width: 20px; }
.h-desc { flex: 1; font-size: 12px; min-width: 120px; }
.h-dur { font-size: 10px; color: #999; white-space: nowrap; }
.h-audio audio { height: 24px; width: 180px; }
/* ─── ABOUT ───────────────────────────────────────────────────── */
.about-inner {
width: 420px; max-width: calc(100% - 40px);
font-size: 13px; line-height: 1.85; color: #222;
text-align: center;
}
.about-name {
font-size: 28px; font-weight: bold;
color: #1a44d8; letter-spacing: -1px;
margin-bottom: 4px;
}
.about-role {
font-size: 11px; letter-spacing: 3px;
color: #888; margin-bottom: 22px;
text-transform: uppercase;
}
.about-divider {
border: none; border-top: 1px solid #ddd;
margin: 18px 0;
}
.about-bio {
font-size: 13px; color: #444;
line-height: 1.9; margin-bottom: 22px;
}
.about-links {
display: flex; flex-direction: column; gap: 8px;
align-items: center;
}
.about-link {
display: inline-flex; align-items: center; gap: 8px;
font-size: 12px; color: #1a44d8;
text-decoration: none; letter-spacing: 0.3px;
}
.about-link:hover { text-decoration: underline; }
.about-link-icon { font-size: 13px; }
/* ─── Scrollbar ───────────────────────────────────────────────── */
.scrollbar {
background: #c0c0c0; height: 20px;
display: flex; align-items: stretch; flex-shrink: 0;
margin-top: 0;
}
.sb-arrow {
width: 20px; flex-shrink: 0;
border: 2px solid; border-color: #fff #404040 #404040 #fff;
display: flex; align-items: center; justify-content: center;
font-size: 9px; cursor: default; background: #c0c0c0;
}
.sb-arrow:active { border-color: #404040 #fff #fff #404040; }
.sb-track {
flex: 1; position: relative;
background: repeating-linear-gradient(
45deg, #b8b8b8 0, #b8b8b8 1px, #c0c0c0 1px, #c0c0c0 4px
);
}
.sb-thumb {
position: absolute; left: 38%; top: 0;
width: 56px; height: 100%;
border: 2px solid; border-color: #fff #404040 #404040 #fff;
background: #c0c0c0;
}
</style>
</head>
<body>
<div class="window">
<!-- Title bar -->
<div class="title-bar">
<span class="title-bar-title">Sample Generator</span>
<div class="title-bar-btns">
<button class="tbtn">_</button>
<button class="tbtn">β–‘</button>
<button class="tbtn">Γ—</button>
</div>
</div>
<!-- Nav -->
<div class="nav">
<span class="nav-item active" id="nav-home" onclick="show('home')">Home</span>
<span class="nav-item" id="nav-generate" onclick="show('generate')">Generate</span>
<span class="nav-item" id="nav-history" onclick="show('history')">History</span>
<span class="nav-item" id="nav-about" onclick="show('about')">About</span>
</div>
<!-- White content box -->
<div class="content-wrap">
<div class="content">
<!-- HOME -->
<div class="page active" id="page-home">
<div class="home-inner">
<p class="home-eyebrow">Generate a</p>
<div class="home-big">SAMPLE</div>
<p class="home-sub">FROM SCRATCH</p>
<button class="btn default" onclick="show('generate')">Start</button>
</div>
</div>
<!-- GENERATE -->
<div class="page" id="page-generate">
<div class="form-inner">
<div class="field">
<label class="field-lbl">DESCRIPTION</label>
<div class="input-row">
<input type="text" class="win-input" id="desc"
placeholder="e.g. lo-fi hip hop, ambient, jazz piano..."
value="lo-fi hip hop with rain and vinyl crackle">
<button class="btn ghost" onclick="randomPrompt()">🎲</button>
</div>
<div class="tags" id="tagRow"></div>
</div>
<div class="options-row">
<div class="field">
<label class="field-lbl">DURATION</label>
<select class="win-select" id="dur">
<option value="5">5 seconds</option>
<option value="8" selected>8 seconds</option>
<option value="15">15 seconds</option>
<option value="30">30 seconds</option>
</select>
</div>
<div class="field">
<label class="field-lbl">MODEL</label>
<select class="win-select" id="modelSel">
<option value="facebook/musicgen-small" selected>musicgen-small</option>
<option value="facebook/musicgen-medium">musicgen-medium</option>
</select>
</div>
</div>
<div class="action-row">
<button class="btn default" id="genBtn" onclick="generate()">β–Ί Generate</button>
<button class="btn ghost" onclick="clearResult()">Clear</button>
</div>
<div class="progress-wrap" id="progressWrap">
<div class="progress-lbl" id="progressLbl">Generating…</div>
<div class="progress-track"><div class="progress-fill"></div></div>
</div>
<div class="result-box" id="resultBox">
<audio id="player" controls></audio>
<div class="action-row">
<button class="btn ghost" onclick="dlSample()">↓ Download WAV</button>
<button class="btn ghost" onclick="generate()">β†Ί Regenerate</button>
</div>
<div class="result-meta" id="resultMeta"></div>
</div>
</div>
</div>
<!-- HISTORY -->
<div class="page" id="page-history">
<div class="history-inner">
<div id="historyList">
<div class="history-empty">No samples generated yet.</div>
</div>
</div>
</div>
<!-- ABOUT -->
<div class="page" id="page-about">
<div class="about-inner">
<div class="about-name">M - A</div>
<div class="about-role">Applied AI Engineer &amp; Artist</div>
<p class="about-bio">
Designing and developing AI solutions for all kinds of use cases β€”
and also a song-writer, musician and painter.<br>
This sample generator is one small piece of that creative chaos 🀍
</p>
<hr class="about-divider">
<div class="about-links">
<a class="about-link" href="https://m-aofficialmusic.org/" target="_blank">
<span class="about-link-icon">🌐</span> m-aofficialmusic.org
</a>
<a class="about-link" href="https://open.spotify.com/artist/5v8fSIV6a2nJwHNHWHHHa5" target="_blank">
<span class="about-link-icon">🎡</span> Spotify
</a>
<a class="about-link" href="https://www.youtube.com/channel/UCFuuOTOczyOkD5aUUBfKy0A" target="_blank">
<span class="about-link-icon">β–Ά</span> YouTube
</a>
<a class="about-link" href="https://www.instagram.com/maofficialmusique" target="_blank">
<span class="about-link-icon">β—ˆ</span> Instagram
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Scrollbar -->
<div class="scrollbar">
<div class="sb-arrow">β—„</div>
<div class="sb-track"><div class="sb-thumb"></div></div>
<div class="sb-arrow">β–Ί</div>
</div>
</div>
<script>
const PROMPTS = [
"lo-fi hip hop with rain and vinyl crackle",
"epic orchestral film score with choir",
"happy 8-bit chiptune adventure",
"dark minimal techno with heavy bass",
"smooth jazz piano trio late night",
"acoustic folk guitar campfire song",
"ambient drone meditation",
"upbeat synthpop 80s dance floor",
"heavy metal guitar riff",
"bossa nova guitar and piano",
"deep house groove with soft pads",
"funky slap bass groove",
"melancholic piano ballad",
"tropical reggaeton with steel drums",
"blues harmonica and acoustic guitar",
];
const TAGS = ["lo-fi hip hop", "ambient", "jazz piano", "techno", "orchestral", "chiptune", "folk guitar", "EDM"];
(function() {
const row = document.getElementById('tagRow');
TAGS.forEach(t => {
const el = document.createElement('span');
el.className = 'tag'; el.textContent = t;
el.onclick = () => document.getElementById('desc').value = t;
row.appendChild(el);
});
})();
let currentBlob = null, history = [];
function show(page) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('page-' + page).classList.add('active');
document.getElementById('nav-' + page).classList.add('active');
}
function randomPrompt() {
document.getElementById('desc').value = PROMPTS[Math.floor(Math.random() * PROMPTS.length)];
}
async function generate() {
const desc = document.getElementById('desc').value.trim();
const dur = parseInt(document.getElementById('dur').value);
const model = document.getElementById('modelSel').value;
if (!desc) { alert('Please enter a description!'); return; }
const btn = document.getElementById('genBtn');
const progressWrap = document.getElementById('progressWrap');
const progressLbl = document.getElementById('progressLbl');
const resultBox = document.getElementById('resultBox');
btn.disabled = true;
progressWrap.style.display = 'block';
resultBox.style.display = 'none';
progressLbl.textContent = 'Loading model & generating… (first run may take a few minutes)';
try {
const res = await fetch('/api/generate', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: desc, duration: dur, model })
});
if (!res.ok) {
let msg = 'Generation failed';
try { msg = (await res.json()).error || msg; } catch(_) {}
throw new Error(msg);
}
const blob = await res.blob();
currentBlob = blob;
document.getElementById('player').src = URL.createObjectURL(blob);
const ts = new Date().toLocaleTimeString();
document.getElementById('resultMeta').textContent = '"' + desc + '" Β· ' + dur + 's Β· ' + ts;
resultBox.style.display = 'block';
progressLbl.textContent = 'Complete!';
history.unshift({ desc, dur, url: URL.createObjectURL(blob), ts });
renderHistory();
} catch(e) {
progressLbl.textContent = 'Error: ' + e.message;
} finally {
btn.disabled = false;
setTimeout(() => { progressWrap.style.display = 'none'; }, 3000);
}
}
function dlSample() {
if (!currentBlob) return;
const a = document.createElement('a');
a.href = URL.createObjectURL(currentBlob);
const slug = document.getElementById('desc').value.replace(/[^a-z0-9 ]/gi,'').trim().replace(/\s+/g,'_').toLowerCase().slice(0,40);
a.download = 'sample_' + slug + '.wav'; a.click();
}
function clearResult() {
document.getElementById('resultBox').style.display = 'none';
document.getElementById('player').src = ''; currentBlob = null;
}
function renderHistory() {
const el = document.getElementById('historyList');
if (!history.length) { el.innerHTML = '<div class="history-empty">No samples generated yet.</div>'; return; }
el.innerHTML = history.map((item, i) => `
<div class="h-item">
<span class="h-num">#${i+1}</span>
<span class="h-desc">${item.desc}</span>
<span class="h-dur">${item.dur}s Β· ${item.ts}</span>
<div class="h-audio"><audio controls src="${item.url}"></audio></div>
<a href="${item.url}" download="sample_${i+1}.wav"><button class="btn ghost" style="font-size:11px;padding:2px 8px;">↓</button></a>
</div>`).join('');
}
document.getElementById('desc').addEventListener('keydown', e => { if (e.key === 'Enter') generate(); });
</script>
</body>
</html>