Mina Emadi Claude Sonnet 4.6 commited on
Commit
08bdfee
·
1 Parent(s): d1bbea4

add demo preset system with sample track

Browse files

- backend/routers/presets.py: new endpoints GET /api/presets and POST /api/preset/{name}
- loads stems + MIDI from backend/presets/{name}/ into a session (no upload needed)
- .gitignore exception + LFS tracking for preset WAV files
- frontend dropdown on upload screen to select and load a demo preset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* 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
 
 
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
36
+ backend/presets/**/*.wav filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -37,6 +37,9 @@ frontend/build/
37
  *.mp3
38
  *.flac
39
 
 
 
 
40
  # Testing
41
  .pytest_cache/
42
  .coverage
 
37
  *.mp3
38
  *.flac
39
 
40
+ # Exception: preset demo files should be committed (via LFS)
41
+ !backend/presets/**/*.wav
42
+
43
  # Testing
44
  .pytest_cache/
45
  .coverage
backend/main.py CHANGED
@@ -8,7 +8,7 @@ from fastapi import FastAPI
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.staticfiles import StaticFiles
10
 
11
- from .routers import upload_router, detection_router, processing_router, stems_router, generation_router
12
  from .models.session import cleanup_old_sessions
13
 
14
 
@@ -62,6 +62,7 @@ app.include_router(detection_router, prefix="/api", tags=["detection"])
62
  app.include_router(processing_router, prefix="/api", tags=["processing"])
63
  app.include_router(stems_router, prefix="/api", tags=["stems"])
64
  app.include_router(generation_router, prefix="/api", tags=["generation"])
 
65
 
66
 
67
  # Health check endpoint
 
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.staticfiles import StaticFiles
10
 
11
+ from .routers import upload_router, detection_router, processing_router, stems_router, generation_router, presets_router
12
  from .models.session import cleanup_old_sessions
13
 
14
 
 
62
  app.include_router(processing_router, prefix="/api", tags=["processing"])
63
  app.include_router(stems_router, prefix="/api", tags=["stems"])
64
  app.include_router(generation_router, prefix="/api", tags=["generation"])
65
+ app.include_router(presets_router, prefix="/api", tags=["presets"])
66
 
67
 
68
  # Health check endpoint
backend/presets/sample-track/MIDI.mid ADDED
Binary file (24.1 kB). View file
 
backend/presets/sample-track/bass.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34d0bf1999390c1846ec635d7d13f719f12235dd0c3b43aff37c978defd20166
3
+ size 70893820
backend/presets/sample-track/click_record.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:436b475a472014349fe08f7acee5916bf17d1ff475471b6f04b45e1cf3f67233
3
+ size 70893820
backend/presets/sample-track/drums.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:536d1b91df21e93ca9f1f1352da5b70f64e6b0b8ac884e7ef794122f63183cfd
3
+ size 70893820
backend/presets/sample-track/guitar.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ed112603fd68bed8a61c290b9ca36fcecee998b085eb283a21e127df0b2bc9e
3
+ size 70893820
backend/presets/sample-track/synth.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ba8a34d85b65a69e09ba0a319136bba8c9d92a6e0fa10e9dc3770d6e26d25a49
3
+ size 70893820
backend/routers/__init__.py CHANGED
@@ -5,6 +5,7 @@ from .detection import router as detection_router
5
  from .processing import router as processing_router
6
  from .stems import router as stems_router
7
  from .generation import router as generation_router
 
8
 
9
  __all__ = [
10
  "upload_router",
@@ -12,4 +13,5 @@ __all__ = [
12
  "processing_router",
13
  "stems_router",
14
  "generation_router",
 
15
  ]
 
5
  from .processing import router as processing_router
6
  from .stems import router as stems_router
7
  from .generation import router as generation_router
8
+ from .presets import router as presets_router
9
 
10
  __all__ = [
11
  "upload_router",
 
13
  "processing_router",
14
  "stems_router",
15
  "generation_router",
16
+ "presets_router",
17
  ]
backend/routers/presets.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Presets router — load pre-committed demo stems into a session."""
2
+
3
+ from pathlib import Path
4
+
5
+ import io
6
+ import numpy as np
7
+ import soundfile as sf
8
+ import mido
9
+ from fastapi import APIRouter, HTTPException
10
+
11
+ from ..models.session import create_session, StemData
12
+ from ..models.schemas import UploadResponse
13
+ from ..utils.audio_utils import normalize
14
+
15
+ router = APIRouter()
16
+
17
+ PRESETS_DIR = Path(__file__).parent.parent / "presets"
18
+ STEM_NAMES = ['guitar', 'drums', 'bass', 'synth', 'click_record']
19
+
20
+
21
+ @router.get("/presets")
22
+ async def list_presets():
23
+ """List available demo presets."""
24
+ if not PRESETS_DIR.exists():
25
+ return {"presets": []}
26
+ presets = sorted(d.name for d in PRESETS_DIR.iterdir() if d.is_dir())
27
+ return {"presets": presets}
28
+
29
+
30
+ @router.post("/preset/{preset_name}", response_model=UploadResponse)
31
+ async def load_preset(preset_name: str):
32
+ """Load a pre-committed preset into a session (no upload needed)."""
33
+ preset_dir = PRESETS_DIR / preset_name
34
+ if not preset_dir.exists():
35
+ raise HTTPException(status_code=404, detail=f"Preset '{preset_name}' not found")
36
+
37
+ stems = {}
38
+ sample_rates = []
39
+
40
+ for stem_name in STEM_NAMES:
41
+ wav_path = preset_dir / f"{stem_name}.wav"
42
+ if not wav_path.exists():
43
+ continue
44
+ audio, sr = sf.read(str(wav_path))
45
+ audio = audio.astype(np.float32)
46
+ if audio.ndim == 2:
47
+ audio = np.mean(audio, axis=1).astype(np.float32)
48
+ stems[stem_name] = StemData(name=stem_name, audio=audio, sample_rate=sr)
49
+ sample_rates.append(sr)
50
+
51
+ if not stems:
52
+ raise HTTPException(status_code=404, detail=f"No WAV stems found in preset '{preset_name}'")
53
+
54
+ # Load MIDI files from preset folder
55
+ midi_data = None
56
+ midi_files = sorted(preset_dir.glob("*.mid")) + sorted(preset_dir.glob("*.midi"))
57
+ if midi_files:
58
+ try:
59
+ if len(midi_files) == 1:
60
+ midi_data = mido.MidiFile(filename=str(midi_files[0]))
61
+ else:
62
+ midi_data = mido.MidiFile()
63
+ for midi_path in midi_files:
64
+ temp = mido.MidiFile(filename=str(midi_path))
65
+ for track in temp.tracks:
66
+ midi_data.tracks.append(track)
67
+ except Exception as e:
68
+ print(f"Warning: could not load MIDI from preset '{preset_name}': {e}")
69
+
70
+ # Build session
71
+ session = create_session()
72
+ session.stems = stems
73
+ session.original_sr = sample_rates[0]
74
+ session.midi_data = midi_data
75
+
76
+ # Generate mix
77
+ max_length = max(len(s.audio) for s in stems.values())
78
+ mixed = np.zeros(max_length, dtype=np.float64)
79
+ for stem in stems.values():
80
+ audio = stem.audio
81
+ if len(audio) < max_length:
82
+ padded = np.zeros(max_length, dtype=np.float64)
83
+ padded[:len(audio)] = audio
84
+ mixed += padded
85
+ else:
86
+ mixed += audio
87
+ mixed = normalize(mixed.astype(np.float32), peak=0.95)
88
+ session.full_mix = StemData(name="generated_mix", audio=mixed, sample_rate=sample_rates[0])
89
+
90
+ duration_seconds = max_length / sample_rates[0]
91
+
92
+ return UploadResponse(
93
+ session_id=session.id,
94
+ stems=list(stems.keys()),
95
+ has_full_mix=False,
96
+ duration_seconds=round(duration_seconds, 2),
97
+ sample_rate=sample_rates[0],
98
+ has_midi=midi_data is not None
99
+ )
frontend/src/App.jsx CHANGED
@@ -21,6 +21,7 @@ function App() {
21
  detect,
22
  process,
23
  generate,
 
24
  clearError
25
  } = useSession()
26
 
@@ -166,6 +167,7 @@ function App() {
166
 
167
  <FileUpload
168
  onUpload={handleUpload}
 
169
  loading={loading}
170
  error={error}
171
  onClearError={clearError}
 
21
  detect,
22
  process,
23
  generate,
24
+ loadPreset,
25
  clearError
26
  } = useSession()
27
 
 
167
 
168
  <FileUpload
169
  onUpload={handleUpload}
170
+ onLoadPreset={loadPreset}
171
  loading={loading}
172
  error={error}
173
  onClearError={clearError}
frontend/src/components/FileUpload.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback } from 'react'
2
 
3
  const STEM_TYPES = [
4
  { name: 'guitar', label: 'Guitar', icon: '🎸' },
@@ -8,7 +8,17 @@ const STEM_TYPES = [
8
  { name: 'click_record', label: 'Click Record', icon: '⏱️' }
9
  ]
10
 
11
- function FileUpload({ onUpload, loading, error, onClearError }) {
 
 
 
 
 
 
 
 
 
 
12
  const [stems, setStems] = useState({
13
  guitar: null,
14
  drums: null,
@@ -85,6 +95,33 @@ function FileUpload({ onUpload, loading, error, onClearError }) {
85
  <div className="glass rounded-2xl p-6 animate-fade-in max-w-4xl mx-auto">
86
  <h2 className="text-xl font-semibold mb-6 text-center">Upload Your Stems</h2>
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  {/* Stem Upload Grid */}
89
  <div className="grid grid-cols-2 gap-4 mb-6">
90
  {STEM_TYPES.map(({ name, label, icon }) => (
 
1
+ import React, { useState, useCallback, useEffect } from 'react'
2
 
3
  const STEM_TYPES = [
4
  { name: 'guitar', label: 'Guitar', icon: '🎸' },
 
8
  { name: 'click_record', label: 'Click Record', icon: '⏱️' }
9
  ]
10
 
11
+ function FileUpload({ onUpload, onLoadPreset, loading, error, onClearError }) {
12
+ const [presets, setPresets] = useState([])
13
+ const [selectedPreset, setSelectedPreset] = useState('')
14
+
15
+ useEffect(() => {
16
+ fetch('/api/presets')
17
+ .then(r => r.json())
18
+ .then(data => setPresets(data.presets || []))
19
+ .catch(() => {})
20
+ }, [])
21
+
22
  const [stems, setStems] = useState({
23
  guitar: null,
24
  drums: null,
 
95
  <div className="glass rounded-2xl p-6 animate-fade-in max-w-4xl mx-auto">
96
  <h2 className="text-xl font-semibold mb-6 text-center">Upload Your Stems</h2>
97
 
98
+ {/* Demo presets dropdown */}
99
+ {presets.length > 0 && (
100
+ <div className="mb-6 p-4 bg-gray-800/50 rounded-xl border border-gray-600">
101
+ <p className="text-sm text-gray-400 mb-3">Or try a demo track:</p>
102
+ <div className="flex gap-3">
103
+ <select
104
+ value={selectedPreset}
105
+ onChange={(e) => setSelectedPreset(e.target.value)}
106
+ disabled={loading}
107
+ className="flex-1 bg-gray-800 border border-gray-600 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-primary-500"
108
+ >
109
+ <option value="">Select a demo...</option>
110
+ {presets.map(p => (
111
+ <option key={p} value={p}>{p}</option>
112
+ ))}
113
+ </select>
114
+ <button
115
+ onClick={() => selectedPreset && onLoadPreset(selectedPreset)}
116
+ disabled={loading || !selectedPreset}
117
+ className="px-4 py-2 rounded-lg text-sm font-medium transition-all disabled:bg-gray-700 disabled:text-gray-500 disabled:cursor-not-allowed bg-gradient-to-r from-primary-600 to-accent-600 hover:from-primary-500 hover:to-accent-500"
118
+ >
119
+ {loading ? 'Loading...' : 'Load'}
120
+ </button>
121
+ </div>
122
+ </div>
123
+ )}
124
+
125
  {/* Stem Upload Grid */}
126
  <div className="grid grid-cols-2 gap-4 mb-6">
127
  {STEM_TYPES.map(({ name, label, icon }) => (
frontend/src/hooks/useSession.js CHANGED
@@ -147,6 +147,37 @@ export function useSession() {
147
  }
148
  }, [sessionId])
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  const generate = useCallback(async (regionStart, regionEnd, duration = 15.0, prompt = null) => {
151
  if (!sessionId) {
152
  setError('No session to generate')
@@ -196,6 +227,7 @@ export function useSession() {
196
  process,
197
  generate,
198
  getStems,
 
199
  clearError
200
  }
201
  }
 
147
  }
148
  }, [sessionId])
149
 
150
+ const loadPreset = useCallback(async (presetName) => {
151
+ setLoading(true)
152
+ setError(null)
153
+
154
+ try {
155
+ const uploadResponse = await fetch(`/api/preset/${presetName}`, { method: 'POST' })
156
+ if (!uploadResponse.ok) {
157
+ const data = await uploadResponse.json().catch(() => ({}))
158
+ throw new Error(data.detail || `Failed to load preset: ${uploadResponse.status}`)
159
+ }
160
+ const uploadData = await uploadResponse.json()
161
+ setSessionId(uploadData.session_id)
162
+ setStems(uploadData.stems)
163
+
164
+ const detectResponse = await fetch(`/api/detect/${uploadData.session_id}`, { method: 'POST' })
165
+ if (!detectResponse.ok) {
166
+ const data = await detectResponse.json().catch(() => ({}))
167
+ throw new Error(data.detail || `Detection failed: ${detectResponse.status}`)
168
+ }
169
+ const detectData = await detectResponse.json()
170
+ setDetection(detectData)
171
+
172
+ return { upload: uploadData, detection: detectData }
173
+ } catch (err) {
174
+ setError(err.message)
175
+ return null
176
+ } finally {
177
+ setLoading(false)
178
+ }
179
+ }, [])
180
+
181
  const generate = useCallback(async (regionStart, regionEnd, duration = 15.0, prompt = null) => {
182
  if (!sessionId) {
183
  setError('No session to generate')
 
227
  process,
228
  generate,
229
  getStems,
230
+ loadPreset,
231
  clearError
232
  }
233
  }