MA commited on
Commit
7bcde8c
·
1 Parent(s): 72a2276

feat: init hf space

Browse files
Files changed (7) hide show
  1. .gitattributes copy +35 -0
  2. .gitignore +43 -0
  3. .python-version +1 -0
  4. Dockerfile +31 -0
  5. README copy.md +18 -0
  6. app.py +86 -0
  7. index.html +525 -0
.gitattributes copy ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Generated audio files
18
+ *.wav
19
+ *.mid
20
+ *.mp3
21
+ /tmp/
22
+
23
+ # Model cache (heavy — never commit this)
24
+ .cache/
25
+ huggingface/
26
+ /data/
27
+
28
+ # OS
29
+ .DS_Store
30
+ Thumbs.db
31
+
32
+ # IDE
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+
37
+ # Env files
38
+ .env
39
+ .env.*
40
+
41
+ # Package manager artifacts
42
+ pyproject.toml
43
+ uv.lock
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # ── System dependencies ───────────────────────────────────────────────────────
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ git \
8
+ ffmpeg \
9
+ build-essential \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # ── Python: torch first (CPU build, works on Render's free/standard tier) ─────
13
+ # Swap the index URL for a CUDA build if you ever add a GPU service on Render.
14
+ RUN pip install --no-cache-dir \
15
+ torch torchaudio --index-url https://download.pytorch.org/whl/cpu
16
+
17
+ # ── Python: audiocraft + flask ────────────────────────────────────────────────
18
+ RUN pip install --no-cache-dir audiocraft flask
19
+
20
+ # ── App code ──────────────────────────────────────────────────────────────────
21
+ COPY app.py index.html ./
22
+
23
+ # ── Model cache directory (matches the Render persistent disk mount path) ─────
24
+ # The disk is mounted at /data; HF will read/write models there so they
25
+ # survive container restarts and don't need to re-download every deploy.
26
+ ENV HF_HOME=/data/huggingface
27
+
28
+ # Render injects $PORT at runtime; default to 10000 to match Render's standard
29
+ EXPOSE 10000
30
+
31
+ CMD ["python", "app.py"]
README copy.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Sample Generator
3
+ emoji: 🎵
4
+ colorFrom: blue
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # Sample Generator by M-A
12
+
13
+ A retro Windows 95-styled AI music sample generator powered by **Meta's MusicGen**.
14
+
15
+ Describe any genre, mood, or instrumentation in plain text and the model generates
16
+ an original audio sample — from lo-fi hip hop to epic orchestral scores.
17
+
18
+ **Made by [M-A](https://m-aofficialmusic.org/)** — Applied AI Engineer & Artist.
app.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ╔══════════════════════════════════════╗
4
+ ║ SAMPLE GENERATOR ║
5
+ ║ Retro-styled AI music sampler ║
6
+ ║ Powered by Meta's MusicGen ║
7
+ ╚══════════════════════════════════════╝
8
+
9
+ Local usage:
10
+ pip install -r requirements.txt
11
+ python app.py → http://localhost:7860
12
+
13
+ HF Spaces:
14
+ Push this folder to a Docker Space — the Dockerfile handles everything.
15
+ """
16
+
17
+ from flask import Flask, send_file, send_from_directory, jsonify, request
18
+ import os, uuid, traceback, threading
19
+
20
+ app = Flask(__name__)
21
+ _model = None
22
+ _model_name = None # model id from first load; reused for all later requests
23
+ _model_lock = threading.Lock()
24
+
25
+
26
+ # ─── Serve the frontend ────────────────────────────────────────────────────
27
+
28
+ @app.route('/')
29
+ def index():
30
+ return send_from_directory(os.path.dirname(__file__), 'index.html')
31
+
32
+
33
+ # ─── Generate endpoint ─────────────────────────────────────────────────────
34
+
35
+ @app.route('/api/generate', methods=['POST'])
36
+ def generate():
37
+ global _model, _model_name
38
+
39
+ data = request.get_json() or {}
40
+ description = data.get('description', 'ambient electronic music').strip()
41
+ duration = max(1, min(int(data.get('duration', 8)), 60))
42
+ model_id = data.get('model', 'facebook/musicgen-small')
43
+
44
+ # Whitelist allowed model IDs
45
+ allowed = {'facebook/musicgen-small', 'facebook/musicgen-medium'}
46
+ if model_id not in allowed:
47
+ return jsonify({'error': f'Unknown model: {model_id}'}), 400
48
+
49
+ try:
50
+ # ── Load model once per process (first request wins; concurrent first calls share one load)
51
+ with _model_lock:
52
+ if _model is None:
53
+ from audiocraft.models import MusicGen
54
+ _model = MusicGen.get_pretrained(model_id)
55
+ _model_name = model_id
56
+ # Already loaded: never call get_pretrained again (ignore later model_id changes)
57
+
58
+ _model.set_generation_params(duration=duration)
59
+ wav = _model.generate([description])
60
+
61
+ # ── Save to /tmp ───────────────────────────────────────────────
62
+ tmp_path = f'/tmp/sample_{uuid.uuid4().hex[:10]}'
63
+ from audiocraft.data.audio import audio_write
64
+ audio_write(tmp_path, wav[0].cpu(), _model.sample_rate,
65
+ strategy='loudness')
66
+
67
+ return send_file(f'{tmp_path}.wav', mimetype='audio/wav')
68
+
69
+ except Exception as e:
70
+ return jsonify({
71
+ 'error': str(e),
72
+ 'traceback': traceback.format_exc()
73
+ }), 500
74
+
75
+
76
+ # ─── Entry point ───────────────────────────────────────────────────────────
77
+
78
+ if __name__ == '__main__':
79
+ # HF Spaces requires port 7860; falls back to that locally too
80
+ port = int(os.environ.get('PORT', 7860))
81
+ print('\n' + '=' * 50)
82
+ print(' SAMPLE GENERATOR')
83
+ print('=' * 50)
84
+ print(f' → http://localhost:{port}')
85
+ print('=' * 50 + '\n')
86
+ app.run(host='0.0.0.0', port=port, debug=False)
index.html ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Sample Generator</title>
6
+ <style>
7
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
8
+
9
+ html, body {
10
+ height: 100%;
11
+ background: #c0c0c0;
12
+ font-family: 'Courier New', Courier, monospace;
13
+ font-size: 13px;
14
+ color: #000;
15
+ }
16
+
17
+ body {
18
+ display: flex;
19
+ flex-direction: column;
20
+ padding: 6px;
21
+ }
22
+
23
+ /* ─── Window fills viewport ───────────────────────────────────── */
24
+ .window {
25
+ flex: 1;
26
+ display: flex;
27
+ flex-direction: column;
28
+ border: 3px solid;
29
+ border-color: #fff #404040 #404040 #fff;
30
+ min-height: 0;
31
+ }
32
+
33
+ /* ─── Title bar ───────────────────────────────────────────────── */
34
+ .title-bar {
35
+ background: #1a44d8;
36
+ color: #fff;
37
+ padding: 5px 8px;
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
+ user-select: none;
42
+ flex-shrink: 0;
43
+ }
44
+ .title-bar-title { font-weight: bold; font-size: 14px; letter-spacing: 0.5px; }
45
+ .title-bar-btns { display: flex; gap: 3px; }
46
+ .tbtn {
47
+ width: 24px; height: 20px;
48
+ background: #c0c0c0;
49
+ border: 2px solid; border-color: #fff #404040 #404040 #fff;
50
+ font-size: 10px; font-weight: bold; color: #000;
51
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
52
+ font-family: 'Courier New', monospace;
53
+ }
54
+ .tbtn:active { border-color: #404040 #fff #fff #404040; }
55
+
56
+ /* ─── Nav ─────────────────────────────────────────────────────── */
57
+ .nav {
58
+ background: #c0c0c0;
59
+ padding: 7px 18px;
60
+ display: flex; gap: 44px;
61
+ flex-shrink: 0;
62
+ }
63
+ .nav-item { font-size: 13px; cursor: pointer; color: #000; }
64
+ .nav-item.active { font-weight: bold; }
65
+
66
+ /* ─── White content box — fills remaining space ───────────────── */
67
+ .content-wrap {
68
+ flex: 1;
69
+ padding: 6px 7px 0;
70
+ display: flex;
71
+ flex-direction: column;
72
+ min-height: 0;
73
+ }
74
+ .content {
75
+ flex: 1;
76
+ background: #fff;
77
+ border: 2px solid; border-color: #404040 #fff #fff #404040;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ overflow: hidden;
82
+ }
83
+
84
+ /* ─── Pages (all fill content box) ───────────────────────────── */
85
+ .page {
86
+ display: none;
87
+ width: 100%; height: 100%;
88
+ align-items: center;
89
+ justify-content: center;
90
+ }
91
+ .page.active { display: flex; }
92
+
93
+ /* ─── HOME ────────────────────────────────────────────────────── */
94
+ .home-inner { text-align: center; }
95
+ .home-eyebrow { font-size: 14px; letter-spacing: 2px; margin-bottom: 6px; }
96
+ .home-big {
97
+ font-size: 96px; font-weight: bold;
98
+ color: #1a44d8; line-height: 1;
99
+ letter-spacing: -4px; margin-bottom: 4px;
100
+ }
101
+ .home-sub { font-size: 15px; letter-spacing: 6px; margin-bottom: 34px; }
102
+
103
+ /* ─── GENERATE — centered narrow column ───────────────────────── */
104
+ .form-inner {
105
+ width: 480px;
106
+ max-width: calc(100% - 40px);
107
+ }
108
+
109
+ .field { margin-bottom: 18px; }
110
+ .field-lbl {
111
+ display: block; font-size: 11px; letter-spacing: 1px;
112
+ color: #666; margin-bottom: 6px;
113
+ }
114
+
115
+ .win-input {
116
+ width: 100%; padding: 5px 7px;
117
+ font-family: 'Courier New', monospace; font-size: 13px;
118
+ border: 0; border-bottom: 1px solid #999;
119
+ background: transparent; outline: none; color: #000;
120
+ }
121
+ .win-input:focus { border-bottom-color: #1a44d8; }
122
+
123
+ .input-row { display: flex; gap: 8px; align-items: flex-end; }
124
+ .input-row .win-input { flex: 1; }
125
+
126
+ .win-select {
127
+ padding: 4px 6px;
128
+ font-family: 'Courier New', monospace; font-size: 12px;
129
+ border: 1px solid #999;
130
+ background: #fff; cursor: pointer; color: #000; outline: none;
131
+ }
132
+ .win-select:focus { border-color: #1a44d8; }
133
+
134
+ /* Tags */
135
+ .tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
136
+ .tag {
137
+ background: #c0c0c0;
138
+ border: 1px solid; border-color: #fff #808080 #808080 #fff;
139
+ padding: 2px 9px; font-size: 11px; cursor: pointer;
140
+ }
141
+ .tag:hover { background: #1a44d8; color: #fff; border-color: #1a44d8; }
142
+ .tag:active { border-color: #808080 #fff #fff #808080; }
143
+
144
+ .options-row {
145
+ display: flex; gap: 24px; flex-wrap: wrap;
146
+ margin-bottom: 22px; align-items: flex-end;
147
+ }
148
+ .options-row .field { margin-bottom: 0; }
149
+
150
+ /* Buttons */
151
+ .btn {
152
+ background: #c0c0c0;
153
+ border: 2px solid; border-color: #fff #404040 #404040 #fff;
154
+ padding: 4px 18px;
155
+ font-family: 'Courier New', monospace; font-size: 13px;
156
+ cursor: pointer; color: #000;
157
+ }
158
+ .btn:active { border-color: #404040 #fff #fff #404040; padding: 5px 17px 3px 19px; }
159
+ .btn:disabled { color: #808080; cursor: default; }
160
+ .btn.default { font-weight: bold; border-width: 3px; padding: 5px 24px; }
161
+ .btn.ghost {
162
+ background: transparent; border: 1px solid #999;
163
+ font-size: 12px; padding: 4px 14px;
164
+ }
165
+ .btn.ghost:hover { background: #f0f0f0; }
166
+ .btn.ghost:active { border-color: #404040; }
167
+
168
+ .action-row { display: flex; gap: 8px; align-items: center; }
169
+
170
+ /* Progress */
171
+ .progress-wrap { display: none; margin-top: 18px; }
172
+ .progress-lbl { font-size: 11px; color: #666; margin-bottom: 5px; }
173
+ .progress-track {
174
+ width: 100%; height: 4px;
175
+ background: #e0e0e0; overflow: hidden;
176
+ }
177
+ .progress-fill {
178
+ height: 100%;
179
+ background: repeating-linear-gradient(
180
+ 90deg, #1a44d8 0px, #1a44d8 60%, #c0d0ff 60%, #c0d0ff 100%
181
+ );
182
+ background-size: 200% 100%;
183
+ animation: slide 1.2s linear infinite;
184
+ }
185
+ @keyframes slide { to { background-position: -200% 0; } }
186
+
187
+ /* Result */
188
+ .result-box { display: none; margin-top: 18px; }
189
+ audio { width: 100%; display: block; margin-bottom: 10px; }
190
+ .result-meta { font-size: 10px; color: #888; margin-top: 8px; }
191
+
192
+ /* ─── HISTORY ─────────────────────────────────────────────────── */
193
+ .history-inner { width: 600px; max-width: calc(100% - 40px); max-height: 80%; overflow-y: auto; }
194
+ .history-empty { text-align: center; color: #999; padding: 40px 0; font-size: 13px; }
195
+ .h-item {
196
+ border-bottom: 1px solid #eee;
197
+ padding: 8px 0;
198
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
199
+ }
200
+ .h-item:first-child { border-top: 1px solid #eee; }
201
+ .h-num { font-size: 10px; color: #aaa; width: 20px; }
202
+ .h-desc { flex: 1; font-size: 12px; min-width: 120px; }
203
+ .h-dur { font-size: 10px; color: #999; white-space: nowrap; }
204
+ .h-audio audio { height: 24px; width: 180px; }
205
+
206
+ /* ─── ABOUT ───────────────────────────────────────────────────── */
207
+ .about-inner {
208
+ width: 420px; max-width: calc(100% - 40px);
209
+ font-size: 13px; line-height: 1.85; color: #222;
210
+ text-align: center;
211
+ }
212
+ .about-name {
213
+ font-size: 28px; font-weight: bold;
214
+ color: #1a44d8; letter-spacing: -1px;
215
+ margin-bottom: 4px;
216
+ }
217
+ .about-role {
218
+ font-size: 11px; letter-spacing: 3px;
219
+ color: #888; margin-bottom: 22px;
220
+ text-transform: uppercase;
221
+ }
222
+ .about-divider {
223
+ border: none; border-top: 1px solid #ddd;
224
+ margin: 18px 0;
225
+ }
226
+ .about-bio {
227
+ font-size: 13px; color: #444;
228
+ line-height: 1.9; margin-bottom: 22px;
229
+ }
230
+ .about-links {
231
+ display: flex; flex-direction: column; gap: 8px;
232
+ align-items: center;
233
+ }
234
+ .about-link {
235
+ display: inline-flex; align-items: center; gap: 8px;
236
+ font-size: 12px; color: #1a44d8;
237
+ text-decoration: none; letter-spacing: 0.3px;
238
+ }
239
+ .about-link:hover { text-decoration: underline; }
240
+ .about-link-icon { font-size: 13px; }
241
+
242
+ /* ─── Scrollbar ───────────────────────────────────────────────── */
243
+ .scrollbar {
244
+ background: #c0c0c0; height: 20px;
245
+ display: flex; align-items: stretch; flex-shrink: 0;
246
+ margin-top: 0;
247
+ }
248
+ .sb-arrow {
249
+ width: 20px; flex-shrink: 0;
250
+ border: 2px solid; border-color: #fff #404040 #404040 #fff;
251
+ display: flex; align-items: center; justify-content: center;
252
+ font-size: 9px; cursor: default; background: #c0c0c0;
253
+ }
254
+ .sb-arrow:active { border-color: #404040 #fff #fff #404040; }
255
+ .sb-track {
256
+ flex: 1; position: relative;
257
+ background: repeating-linear-gradient(
258
+ 45deg, #b8b8b8 0, #b8b8b8 1px, #c0c0c0 1px, #c0c0c0 4px
259
+ );
260
+ }
261
+ .sb-thumb {
262
+ position: absolute; left: 38%; top: 0;
263
+ width: 56px; height: 100%;
264
+ border: 2px solid; border-color: #fff #404040 #404040 #fff;
265
+ background: #c0c0c0;
266
+ }
267
+ </style>
268
+ </head>
269
+ <body>
270
+ <div class="window">
271
+
272
+ <!-- Title bar -->
273
+ <div class="title-bar">
274
+ <span class="title-bar-title">Sample Generator</span>
275
+ <div class="title-bar-btns">
276
+ <button class="tbtn">_</button>
277
+ <button class="tbtn">□</button>
278
+ <button class="tbtn">×</button>
279
+ </div>
280
+ </div>
281
+
282
+ <!-- Nav -->
283
+ <div class="nav">
284
+ <span class="nav-item active" id="nav-home" onclick="show('home')">Home</span>
285
+ <span class="nav-item" id="nav-generate" onclick="show('generate')">Generate</span>
286
+ <span class="nav-item" id="nav-history" onclick="show('history')">History</span>
287
+ <span class="nav-item" id="nav-about" onclick="show('about')">About</span>
288
+ </div>
289
+
290
+ <!-- White content box -->
291
+ <div class="content-wrap">
292
+ <div class="content">
293
+
294
+ <!-- HOME -->
295
+ <div class="page active" id="page-home">
296
+ <div class="home-inner">
297
+ <p class="home-eyebrow">Generate a</p>
298
+ <div class="home-big">SAMPLE</div>
299
+ <p class="home-sub">FROM SCRATCH</p>
300
+ <button class="btn default" onclick="show('generate')">Start</button>
301
+ </div>
302
+ </div>
303
+
304
+ <!-- GENERATE -->
305
+ <div class="page" id="page-generate">
306
+ <div class="form-inner">
307
+
308
+ <div class="field">
309
+ <label class="field-lbl">DESCRIPTION</label>
310
+ <div class="input-row">
311
+ <input type="text" class="win-input" id="desc"
312
+ placeholder="e.g. lo-fi hip hop, ambient, jazz piano..."
313
+ value="lo-fi hip hop with rain and vinyl crackle">
314
+ <button class="btn ghost" onclick="randomPrompt()">🎲</button>
315
+ </div>
316
+ <div class="tags" id="tagRow"></div>
317
+ </div>
318
+
319
+ <div class="options-row">
320
+ <div class="field">
321
+ <label class="field-lbl">DURATION</label>
322
+ <select class="win-select" id="dur">
323
+ <option value="5">5 seconds</option>
324
+ <option value="8" selected>8 seconds</option>
325
+ <option value="15">15 seconds</option>
326
+ <option value="30">30 seconds</option>
327
+ </select>
328
+ </div>
329
+ <div class="field">
330
+ <label class="field-lbl">MODEL</label>
331
+ <select class="win-select" id="modelSel">
332
+ <option value="facebook/musicgen-small" selected>musicgen-small</option>
333
+ <option value="facebook/musicgen-medium">musicgen-medium</option>
334
+ </select>
335
+ </div>
336
+ </div>
337
+
338
+ <div class="action-row">
339
+ <button class="btn default" id="genBtn" onclick="generate()">► Generate</button>
340
+ <button class="btn ghost" onclick="clearResult()">Clear</button>
341
+ </div>
342
+
343
+ <div class="progress-wrap" id="progressWrap">
344
+ <div class="progress-lbl" id="progressLbl">Generating…</div>
345
+ <div class="progress-track"><div class="progress-fill"></div></div>
346
+ </div>
347
+
348
+ <div class="result-box" id="resultBox">
349
+ <audio id="player" controls></audio>
350
+ <div class="action-row">
351
+ <button class="btn ghost" onclick="dlSample()">↓ Download WAV</button>
352
+ <button class="btn ghost" onclick="generate()">↺ Regenerate</button>
353
+ </div>
354
+ <div class="result-meta" id="resultMeta"></div>
355
+ </div>
356
+
357
+ </div>
358
+ </div>
359
+
360
+ <!-- HISTORY -->
361
+ <div class="page" id="page-history">
362
+ <div class="history-inner">
363
+ <div id="historyList">
364
+ <div class="history-empty">No samples generated yet.</div>
365
+ </div>
366
+ </div>
367
+ </div>
368
+
369
+ <!-- ABOUT -->
370
+ <div class="page" id="page-about">
371
+ <div class="about-inner">
372
+ <div class="about-name">M - A</div>
373
+ <div class="about-role">Applied AI Engineer &amp; Artist</div>
374
+ <p class="about-bio">
375
+ Designing and developing AI solutions for all kinds of use cases —
376
+ and also a song-writer, musician and painter.<br>
377
+ This sample generator is one small piece of that creative chaos 🤍
378
+ </p>
379
+ <hr class="about-divider">
380
+ <div class="about-links">
381
+ <a class="about-link" href="https://m-aofficialmusic.org/" target="_blank">
382
+ <span class="about-link-icon">🌐</span> m-aofficialmusic.org
383
+ </a>
384
+ <a class="about-link" href="https://open.spotify.com/artist/5v8fSIV6a2nJwHNHWHHHa5" target="_blank">
385
+ <span class="about-link-icon">🎵</span> Spotify
386
+ </a>
387
+ <a class="about-link" href="https://www.youtube.com/channel/UCFuuOTOczyOkD5aUUBfKy0A" target="_blank">
388
+ <span class="about-link-icon">▶</span> YouTube
389
+ </a>
390
+ <a class="about-link" href="https://www.instagram.com/maofficialmusique" target="_blank">
391
+ <span class="about-link-icon">◈</span> Instagram
392
+ </a>
393
+ </div>
394
+ </div>
395
+ </div>
396
+
397
+ </div>
398
+ </div>
399
+
400
+ <!-- Scrollbar -->
401
+ <div class="scrollbar">
402
+ <div class="sb-arrow">◄</div>
403
+ <div class="sb-track"><div class="sb-thumb"></div></div>
404
+ <div class="sb-arrow">►</div>
405
+ </div>
406
+
407
+ </div>
408
+
409
+ <script>
410
+ const PROMPTS = [
411
+ "lo-fi hip hop with rain and vinyl crackle",
412
+ "epic orchestral film score with choir",
413
+ "happy 8-bit chiptune adventure",
414
+ "dark minimal techno with heavy bass",
415
+ "smooth jazz piano trio late night",
416
+ "acoustic folk guitar campfire song",
417
+ "ambient drone meditation",
418
+ "upbeat synthpop 80s dance floor",
419
+ "heavy metal guitar riff",
420
+ "bossa nova guitar and piano",
421
+ "deep house groove with soft pads",
422
+ "funky slap bass groove",
423
+ "melancholic piano ballad",
424
+ "tropical reggaeton with steel drums",
425
+ "blues harmonica and acoustic guitar",
426
+ ];
427
+
428
+ const TAGS = ["lo-fi hip hop", "ambient", "jazz piano", "techno", "orchestral", "chiptune", "folk guitar", "EDM"];
429
+
430
+ (function() {
431
+ const row = document.getElementById('tagRow');
432
+ TAGS.forEach(t => {
433
+ const el = document.createElement('span');
434
+ el.className = 'tag'; el.textContent = t;
435
+ el.onclick = () => document.getElementById('desc').value = t;
436
+ row.appendChild(el);
437
+ });
438
+ })();
439
+
440
+ let currentBlob = null, history = [];
441
+
442
+ function show(page) {
443
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
444
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
445
+ document.getElementById('page-' + page).classList.add('active');
446
+ document.getElementById('nav-' + page).classList.add('active');
447
+ }
448
+
449
+ function randomPrompt() {
450
+ document.getElementById('desc').value = PROMPTS[Math.floor(Math.random() * PROMPTS.length)];
451
+ }
452
+
453
+ async function generate() {
454
+ const desc = document.getElementById('desc').value.trim();
455
+ const dur = parseInt(document.getElementById('dur').value);
456
+ const model = document.getElementById('modelSel').value;
457
+ if (!desc) { alert('Please enter a description!'); return; }
458
+
459
+ const btn = document.getElementById('genBtn');
460
+ const progressWrap = document.getElementById('progressWrap');
461
+ const progressLbl = document.getElementById('progressLbl');
462
+ const resultBox = document.getElementById('resultBox');
463
+
464
+ btn.disabled = true;
465
+ progressWrap.style.display = 'block';
466
+ resultBox.style.display = 'none';
467
+ progressLbl.textContent = 'Loading model & generating… (first run may take a few minutes)';
468
+
469
+ try {
470
+ const res = await fetch('/api/generate', {
471
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
472
+ body: JSON.stringify({ description: desc, duration: dur, model })
473
+ });
474
+ if (!res.ok) {
475
+ let msg = 'Generation failed';
476
+ try { msg = (await res.json()).error || msg; } catch(_) {}
477
+ throw new Error(msg);
478
+ }
479
+ const blob = await res.blob();
480
+ currentBlob = blob;
481
+ document.getElementById('player').src = URL.createObjectURL(blob);
482
+ const ts = new Date().toLocaleTimeString();
483
+ document.getElementById('resultMeta').textContent = '"' + desc + '" · ' + dur + 's · ' + ts;
484
+ resultBox.style.display = 'block';
485
+ progressLbl.textContent = 'Complete!';
486
+ history.unshift({ desc, dur, url: URL.createObjectURL(blob), ts });
487
+ renderHistory();
488
+ } catch(e) {
489
+ progressLbl.textContent = 'Error: ' + e.message;
490
+ } finally {
491
+ btn.disabled = false;
492
+ setTimeout(() => { progressWrap.style.display = 'none'; }, 3000);
493
+ }
494
+ }
495
+
496
+ function dlSample() {
497
+ if (!currentBlob) return;
498
+ const a = document.createElement('a');
499
+ a.href = URL.createObjectURL(currentBlob);
500
+ const slug = document.getElementById('desc').value.replace(/[^a-z0-9 ]/gi,'').trim().replace(/\s+/g,'_').toLowerCase().slice(0,40);
501
+ a.download = 'sample_' + slug + '.wav'; a.click();
502
+ }
503
+
504
+ function clearResult() {
505
+ document.getElementById('resultBox').style.display = 'none';
506
+ document.getElementById('player').src = ''; currentBlob = null;
507
+ }
508
+
509
+ function renderHistory() {
510
+ const el = document.getElementById('historyList');
511
+ if (!history.length) { el.innerHTML = '<div class="history-empty">No samples generated yet.</div>'; return; }
512
+ el.innerHTML = history.map((item, i) => `
513
+ <div class="h-item">
514
+ <span class="h-num">#${i+1}</span>
515
+ <span class="h-desc">${item.desc}</span>
516
+ <span class="h-dur">${item.dur}s · ${item.ts}</span>
517
+ <div class="h-audio"><audio controls src="${item.url}"></audio></div>
518
+ <a href="${item.url}" download="sample_${i+1}.wav"><button class="btn ghost" style="font-size:11px;padding:2px 8px;">↓</button></a>
519
+ </div>`).join('');
520
+ }
521
+
522
+ document.getElementById('desc').addEventListener('keydown', e => { if (e.key === 'Enter') generate(); });
523
+ </script>
524
+ </body>
525
+ </html>