Mina Emadi commited on
Commit
59dcfdf
·
1 Parent(s): 25d51c9

added the music generation functionality

Browse files
frontend/src/App.jsx CHANGED
@@ -7,8 +7,10 @@ import ScalesDisplay from './components/ScalesDisplay'
7
  import StemMixer from './components/StemMixer'
8
  import TransportBar from './components/TransportBar'
9
  import ProcessingOverlay from './components/ProcessingOverlay'
 
10
  import { useSession } from './hooks/useSession'
11
  import { useAudioEngine } from './hooks/useAudioEngine'
 
12
  import { useProcessingProgress } from './hooks/useProcessingProgress'
13
  import { usePresetCache } from './hooks/usePresetCache'
14
  import { getCachedPreset } from './utils/presetCache'
@@ -25,6 +27,9 @@ function App() {
25
  detect,
26
  process,
27
  generate,
 
 
 
28
  loadPreset,
29
  clearError
30
  } = useSession()
@@ -60,6 +65,14 @@ function App() {
60
 
61
  const { cacheStatus } = usePresetCache()
62
 
 
 
 
 
 
 
 
 
63
  const currentPresetNameRef = useRef(null)
64
 
65
  const [isProcessing, setIsProcessing] = useState(false)
@@ -239,6 +252,17 @@ function App() {
239
  hasRegion={hasRegion}
240
  />
241
 
 
 
 
 
 
 
 
 
 
 
 
242
  {/* Chord Summary (all unique chords) */}
243
  {chordsData && (
244
  <ChordDisplay
 
7
  import StemMixer from './components/StemMixer'
8
  import TransportBar from './components/TransportBar'
9
  import ProcessingOverlay from './components/ProcessingOverlay'
10
+ import GenerationPanel from './components/generation/GenerationPanel'
11
  import { useSession } from './hooks/useSession'
12
  import { useAudioEngine } from './hooks/useAudioEngine'
13
+ import { useGeneration } from './hooks/useGeneration'
14
  import { useProcessingProgress } from './hooks/useProcessingProgress'
15
  import { usePresetCache } from './hooks/usePresetCache'
16
  import { getCachedPreset } from './utils/presetCache'
 
27
  detect,
28
  process,
29
  generate,
30
+ extractSeed,
31
+ generateAceStep,
32
+ checkModels,
33
  loadPreset,
34
  clearError
35
  } = useSession()
 
65
 
66
  const { cacheStatus } = usePresetCache()
67
 
68
+ const generation = useGeneration({
69
+ sessionId,
70
+ generate,
71
+ extractSeed,
72
+ generateAceStep,
73
+ checkModels,
74
+ })
75
+
76
  const currentPresetNameRef = useRef(null)
77
 
78
  const [isProcessing, setIsProcessing] = useState(false)
 
252
  hasRegion={hasRegion}
253
  />
254
 
255
+ {/* AI Generation Panel */}
256
+ <GenerationPanel
257
+ sessionId={sessionId}
258
+ detection={detection}
259
+ regionStart={regionStart}
260
+ regionEnd={regionEnd}
261
+ hasRegion={hasRegion}
262
+ onPlaySection={handlePlaySection}
263
+ generation={generation}
264
+ />
265
+
266
  {/* Chord Summary (all unique chords) */}
267
  {chordsData && (
268
  <ChordDisplay
frontend/src/components/StemMixer.jsx CHANGED
@@ -57,7 +57,7 @@ function VolumeSlider({ value, onChange, disabled }) {
57
  <div className="w-full h-full rounded-full bg-white/[0.08]" />
58
  <div
59
  className="absolute top-0 left-0 h-full rounded-full"
60
- style={{ width: `${pct}%`, background: disabled ? '#374151' : '#14B8A6' }}
61
  />
62
  </div>
63
  <div
@@ -69,7 +69,7 @@ function VolumeSlider({ value, onChange, disabled }) {
69
  left: `${pct}%`,
70
  transform: 'translate(-50%, -50%)',
71
  background: disabled ? '#374151' : 'white',
72
- border: `2px solid ${disabled ? '#4b5563' : '#14B8A6'}`,
73
  }}
74
  />
75
  </div>
@@ -268,7 +268,7 @@ function StemMixer({
268
  max={1}
269
  onChange={(v) => onPanChange && onPanChange(stem, v)}
270
  label="Pan"
271
- color="#14B8A6"
272
  showCenterTick
273
  disabled={isMuted}
274
  />
@@ -278,7 +278,7 @@ function StemMixer({
278
  max={0.5}
279
  onChange={(v) => onReverbChange && onReverbChange(stem, v)}
280
  label="Reverb"
281
- color="#14B8A6"
282
  disabled={isMuted}
283
  />
284
  </div>
 
57
  <div className="w-full h-full rounded-full bg-white/[0.08]" />
58
  <div
59
  className="absolute top-0 left-0 h-full rounded-full"
60
+ style={{ width: `${pct}%`, background: disabled ? '#374151' : '#F0A500' }}
61
  />
62
  </div>
63
  <div
 
69
  left: `${pct}%`,
70
  transform: 'translate(-50%, -50%)',
71
  background: disabled ? '#374151' : 'white',
72
+ border: `2px solid ${disabled ? '#4b5563' : '#F0A500'}`,
73
  }}
74
  />
75
  </div>
 
268
  max={1}
269
  onChange={(v) => onPanChange && onPanChange(stem, v)}
270
  label="Pan"
271
+ color="#F0A500"
272
  showCenterTick
273
  disabled={isMuted}
274
  />
 
278
  max={0.5}
279
  onChange={(v) => onReverbChange && onReverbChange(stem, v)}
280
  label="Reverb"
281
+ color="#F0A500"
282
  disabled={isMuted}
283
  />
284
  </div>
frontend/src/components/TransportBar.jsx CHANGED
@@ -482,7 +482,7 @@ function TransportBar({
482
  onClick={() => setSnapMode(mode)}
483
  className={`px-2 py-0.5 rounded transition-colors capitalize ${
484
  snapMode === mode
485
- ? 'bg-cyan-500/30 text-cyan-300 border border-cyan-500/40'
486
  : 'text-gray-400 hover:text-gray-200 hover:bg-white/[0.06]'
487
  }`}
488
  >
 
482
  onClick={() => setSnapMode(mode)}
483
  className={`px-2 py-0.5 rounded transition-colors capitalize ${
484
  snapMode === mode
485
+ ? 'bg-accent-500/30 text-accent-400 border border-accent-500/40'
486
  : 'text-gray-400 hover:text-gray-200 hover:bg-white/[0.06]'
487
  }`}
488
  >
frontend/src/components/generation/ACEStepParams.jsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react'
2
+ import ParamSlider from './ParamSlider'
3
+
4
+ export default function ACEStepParams({ params, onParamChange, detection }) {
5
+ const [showAdvanced, setShowAdvanced] = useState(false)
6
+
7
+ return (
8
+ <div className="space-y-3">
9
+ <div className="text-[10px] text-gray-500 bg-white/[0.02] rounded-lg px-3 py-2">
10
+ Caption is auto-generated from your seed's analysis (BPM, key, brightness, instruments) for musical coherence. Override below if needed.
11
+ </div>
12
+
13
+ {/* Optional caption override (collapsed by default) */}
14
+ <button
15
+ onClick={() => setShowAdvanced(!showAdvanced)}
16
+ className="text-xs text-gray-500 hover:text-gray-300 transition-colors flex items-center gap-1"
17
+ >
18
+ <span className={`transition-transform ${showAdvanced ? 'rotate-90' : ''}`}>&#9654;</span>
19
+ Custom Caption & Advanced
20
+ </button>
21
+
22
+ {showAdvanced && (
23
+ <div className="space-y-3 pl-3 border-l border-white/[0.06]">
24
+ <div>
25
+ <label className="text-xs text-gray-400 font-medium mb-1 block">Caption Override</label>
26
+ <textarea
27
+ value={params.caption}
28
+ onChange={e => onParamChange('caption', e.target.value)}
29
+ placeholder="Leave empty for auto-generated caption"
30
+ className="w-full bg-surface-elevated border border-white/[0.08] rounded-lg px-3 py-2 text-sm text-white placeholder-gray-600 focus:outline-none focus:border-accent-500/40 resize-none font-mono"
31
+ rows={2}
32
+ />
33
+ </div>
34
+
35
+ <ParamSlider
36
+ label="CFG Scale"
37
+ value={params.cfg_scale || 7}
38
+ min={1}
39
+ max={15}
40
+ step={0.5}
41
+ onChange={v => onParamChange('cfg_scale', v)}
42
+ />
43
+
44
+ <div>
45
+ <label className="text-xs text-gray-400 font-medium mb-1 block">Scheduler</label>
46
+ <select
47
+ value={params.scheduler}
48
+ onChange={e => onParamChange('scheduler', e.target.value)}
49
+ className="bg-surface-elevated border border-white/[0.08] rounded-lg px-3 py-1.5 text-sm text-white focus:outline-none focus:border-accent-500/40"
50
+ >
51
+ <option value="ode">ODE (recommended)</option>
52
+ <option value="sde">SDE</option>
53
+ </select>
54
+ </div>
55
+ </div>
56
+ )}
57
+
58
+ <ParamSlider
59
+ label="Duration"
60
+ value={params.duration}
61
+ min={10}
62
+ max={120}
63
+ step={5}
64
+ onChange={v => onParamChange('duration', v)}
65
+ leftLabel="10s"
66
+ rightLabel="120s"
67
+ />
68
+
69
+ <ParamSlider
70
+ label="Source Fidelity"
71
+ value={params.audio_cover_strength}
72
+ min={0}
73
+ max={1}
74
+ step={0.05}
75
+ onChange={v => onParamChange('audio_cover_strength', v)}
76
+ leftLabel="Creative"
77
+ rightLabel="Faithful"
78
+ />
79
+
80
+ {/* Model Variant */}
81
+ <div>
82
+ <label className="text-xs text-gray-400 font-medium mb-1 block">Model Variant</label>
83
+ <select
84
+ value={params.model_variant}
85
+ onChange={e => onParamChange('model_variant', e.target.value)}
86
+ className="bg-surface-elevated border border-white/[0.08] rounded-lg px-3 py-1.5 text-sm text-white focus:outline-none focus:border-accent-500/40"
87
+ >
88
+ <option value="xl-turbo">XL Turbo (best for 32GB)</option>
89
+ <option value="standard-turbo">Standard Turbo (faster)</option>
90
+ <option value="standard-sft">Standard SFT (balanced)</option>
91
+ </select>
92
+ </div>
93
+
94
+ <ParamSlider
95
+ label="Inference Steps"
96
+ value={params.inference_steps}
97
+ min={1}
98
+ max={50}
99
+ step={1}
100
+ onChange={v => onParamChange('inference_steps', v)}
101
+ leftLabel="Fast (1)"
102
+ rightLabel="Quality (50)"
103
+ />
104
+
105
+ {/* Seed */}
106
+ <div>
107
+ <label className="text-xs text-gray-400 font-medium mb-1 block">Seed</label>
108
+ <div className="flex gap-2">
109
+ <input
110
+ type="number"
111
+ value={params.seed}
112
+ onChange={e => onParamChange('seed', parseInt(e.target.value) || -1)}
113
+ className="flex-1 bg-surface-elevated border border-white/[0.08] rounded-lg px-3 py-1.5 text-sm text-white font-mono focus:outline-none focus:border-accent-500/40"
114
+ />
115
+ <button
116
+ onClick={() => onParamChange('seed', Math.floor(Math.random() * 999999))}
117
+ className="px-2 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-sm transition-colors"
118
+ title="Random seed"
119
+ >
120
+ 🎲
121
+ </button>
122
+ </div>
123
+ <span className="text-[10px] text-gray-600 mt-0.5 block">-1 = random</span>
124
+ </div>
125
+ </div>
126
+ )
127
+ }
frontend/src/components/generation/GenerationOutput.jsx ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useState, useEffect } from 'react'
2
+
3
+ export default function GenerationOutput({
4
+ sessionId,
5
+ generationResult,
6
+ onRegenerate,
7
+ onUseAsSource,
8
+ onPlaySection,
9
+ }) {
10
+ const audioRef = useRef(null)
11
+ const [isPlaying, setIsPlaying] = useState(false)
12
+ const [currentTime, setCurrentTime] = useState(0)
13
+ const [duration, setDuration] = useState(0)
14
+ const [abSource, setAbSource] = useState('generated')
15
+ const [downloadFormat, setDownloadFormat] = useState('wav')
16
+ const [audioSrc, setAudioSrc] = useState(null)
17
+ const [loadError, setLoadError] = useState(false)
18
+
19
+ // Only set the audio src when we get a NEW generation result
20
+ useEffect(() => {
21
+ if (generationResult && sessionId) {
22
+ setAudioSrc(`/api/stem/${sessionId}/_continuation?format=wav&v=${Date.now()}`)
23
+ setIsPlaying(false)
24
+ setCurrentTime(0)
25
+ setDuration(0)
26
+ setAbSource('generated')
27
+ setLoadError(false)
28
+ } else {
29
+ setAudioSrc(null)
30
+ }
31
+ }, [generationResult, sessionId])
32
+
33
+ const handleTimeUpdate = () => {
34
+ if (audioRef.current) setCurrentTime(audioRef.current.currentTime)
35
+ }
36
+
37
+ const handleLoadedMetadata = () => {
38
+ if (audioRef.current) setDuration(audioRef.current.duration)
39
+ }
40
+
41
+ const handleEnded = () => setIsPlaying(false)
42
+
43
+ // Stop retrying on error — clear the src so the browser doesn't loop
44
+ const handleError = () => {
45
+ setLoadError(true)
46
+ setIsPlaying(false)
47
+ }
48
+
49
+ const togglePlay = () => {
50
+ if (!audioRef.current || loadError) return
51
+ if (isPlaying) {
52
+ audioRef.current.pause()
53
+ setIsPlaying(false)
54
+ } else {
55
+ audioRef.current.play().catch(() => setIsPlaying(false))
56
+ setIsPlaying(true)
57
+ }
58
+ }
59
+
60
+ const handleSeek = (e) => {
61
+ if (!audioRef.current || !duration) return
62
+ const rect = e.currentTarget.getBoundingClientRect()
63
+ const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
64
+ audioRef.current.currentTime = pct * duration
65
+ }
66
+
67
+ const formatTime = (t) => {
68
+ const m = Math.floor(t / 60)
69
+ const s = Math.floor(t % 60).toString().padStart(2, '0')
70
+ return `${m}:${s}`
71
+ }
72
+
73
+ const handleDownload = () => {
74
+ if (!sessionId) return
75
+ const url = `/api/stem/${sessionId}/_continuation?format=${downloadFormat}`
76
+ const a = document.createElement('a')
77
+ a.href = url
78
+ a.download = `generation.${downloadFormat}`
79
+ a.click()
80
+ }
81
+
82
+ // Don't render anything until we have a generation result
83
+ if (!generationResult) return null
84
+
85
+ return (
86
+ <div className="space-y-3 pt-3 border-t border-white/[0.06]">
87
+ {/* Only mount <audio> when we have a valid src */}
88
+ {audioSrc && !loadError && (
89
+ <audio
90
+ ref={audioRef}
91
+ src={audioSrc}
92
+ preload="auto"
93
+ onTimeUpdate={handleTimeUpdate}
94
+ onLoadedMetadata={handleLoadedMetadata}
95
+ onEnded={handleEnded}
96
+ onError={handleError}
97
+ />
98
+ )}
99
+
100
+ {loadError && (
101
+ <div className="text-xs text-red-400 bg-red-500/10 rounded px-3 py-2">
102
+ Audio playback unavailable — session may have expired. Try generating again.
103
+ </div>
104
+ )}
105
+
106
+ {/* A/B Toggle */}
107
+ <div className="flex gap-1.5">
108
+ <button
109
+ onClick={() => setAbSource('generated')}
110
+ className={`px-3 py-1 rounded text-xs font-medium transition-all ${
111
+ abSource === 'generated'
112
+ ? 'bg-accent-500/20 text-accent-400 border border-accent-500/30'
113
+ : 'bg-white/[0.04] text-gray-500 border border-transparent hover:bg-white/[0.06]'
114
+ }`}
115
+ >
116
+ Seed + Generated
117
+ </button>
118
+ <button
119
+ onClick={() => {
120
+ setAbSource('seed')
121
+ if (audioRef.current) { audioRef.current.pause(); setIsPlaying(false) }
122
+ if (onPlaySection) onPlaySection()
123
+ }}
124
+ className={`px-3 py-1 rounded text-xs font-medium transition-all ${
125
+ abSource === 'seed'
126
+ ? 'bg-accent-500/20 text-accent-400 border border-accent-500/30'
127
+ : 'bg-white/[0.04] text-gray-500 border border-transparent hover:bg-white/[0.06]'
128
+ }`}
129
+ >
130
+ Seed Only
131
+ </button>
132
+ </div>
133
+
134
+ {/* Playback controls */}
135
+ {!loadError && (
136
+ <div className="space-y-2">
137
+ <div className="flex items-center gap-3">
138
+ <button
139
+ onClick={togglePlay}
140
+ disabled={abSource === 'seed'}
141
+ className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors text-white text-sm ${
142
+ abSource === 'seed' ? 'bg-gray-700 cursor-not-allowed' : 'bg-accent-500 hover:bg-accent-400'
143
+ }`}
144
+ >
145
+ {isPlaying ? '⏸' : '▶'}
146
+ </button>
147
+ <div
148
+ className="flex-1 h-1.5 rounded-full bg-white/[0.06] cursor-pointer"
149
+ onClick={handleSeek}
150
+ >
151
+ <div
152
+ className="h-full bg-accent-500 rounded-full transition-all"
153
+ style={{ width: duration ? `${(currentTime / duration) * 100}%` : '0%' }}
154
+ />
155
+ </div>
156
+ <span className="text-xs font-mono text-gray-400 w-16 text-right">
157
+ {formatTime(currentTime)} / {formatTime(duration)}
158
+ </span>
159
+ </div>
160
+ </div>
161
+ )}
162
+
163
+ {/* Metadata */}
164
+ <div className="flex flex-wrap gap-2 text-[10px]">
165
+ <span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500">
166
+ Model: {generationResult.model || 'unknown'}
167
+ </span>
168
+ <span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500">
169
+ Duration: {generationResult.duration_seconds?.toFixed(1)}s
170
+ </span>
171
+ <span className="px-2 py-0.5 rounded bg-white/[0.04] text-gray-500">
172
+ Generated in {generationResult.generation_time_seconds?.toFixed(1)}s
173
+ </span>
174
+ </div>
175
+
176
+ {/* Action buttons */}
177
+ <div className="flex items-center gap-2">
178
+ <button
179
+ onClick={onRegenerate}
180
+ className="px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-xs text-gray-300 transition-colors"
181
+ >
182
+ Regenerate
183
+ </button>
184
+ <button
185
+ onClick={onUseAsSource}
186
+ className="px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-xs text-gray-300 transition-colors"
187
+ >
188
+ Use as Source
189
+ </button>
190
+
191
+ {/* Download */}
192
+ <div className="ml-auto flex items-center gap-1">
193
+ <select
194
+ value={downloadFormat}
195
+ onChange={e => setDownloadFormat(e.target.value)}
196
+ className="bg-surface-elevated border border-white/[0.06] rounded px-1.5 py-1 text-[10px] text-gray-400 focus:outline-none"
197
+ >
198
+ <option value="wav">.wav</option>
199
+ <option value="flac">.flac</option>
200
+ </select>
201
+ <button
202
+ onClick={handleDownload}
203
+ className="px-2 py-1 rounded bg-white/[0.06] hover:bg-white/[0.1] text-[10px] text-gray-400 transition-colors"
204
+ >
205
+ Download
206
+ </button>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ )
211
+ }
frontend/src/components/generation/GenerationPanel.jsx ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useCallback, useEffect, useRef } from 'react'
2
+ import ModelSelector from './ModelSelector'
3
+ import SeedRegionInfo from './SeedRegionInfo'
4
+ import TaskTypeSelector from './TaskTypeSelector'
5
+ import MusicGenParams from './MusicGenParams'
6
+ import ACEStepParams from './ACEStepParams'
7
+ import GenerationProgress from './GenerationProgress'
8
+ import GenerationOutput from './GenerationOutput'
9
+
10
+ // Map model+taskType to the param key used in the request
11
+ function getTaskTypeKey(model) {
12
+ if (model === 'ace-step') return 'task_type'
13
+ return null
14
+ }
15
+
16
+ export default function GenerationPanel({
17
+ sessionId,
18
+ detection,
19
+ regionStart,
20
+ regionEnd,
21
+ hasRegion,
22
+ onPlaySection,
23
+ generation, // all state from useGeneration hook
24
+ }) {
25
+ const [isCollapsed, setIsCollapsed] = useState(false)
26
+ const [isExtracting, setIsExtracting] = useState(false)
27
+
28
+ const {
29
+ selectedModel,
30
+ setSelectedModel,
31
+ modelParams,
32
+ setModelParam,
33
+ isGenerating,
34
+ generationResult,
35
+ generationProgress,
36
+ modelAvailability,
37
+ seedData,
38
+ doExtractSeed,
39
+ startGeneration,
40
+ setGenerationResult,
41
+ } = generation
42
+
43
+ const currentParams = modelParams[selectedModel] || {}
44
+
45
+ // Get the current task type for the selected model
46
+ const taskTypeKey = getTaskTypeKey(selectedModel)
47
+ const taskType = taskTypeKey ? currentParams[taskTypeKey] : 'continuation'
48
+
49
+ const handleTaskTypeChange = useCallback((newType) => {
50
+ if (taskTypeKey) {
51
+ setModelParam(selectedModel, taskTypeKey, newType)
52
+ }
53
+ }, [selectedModel, taskTypeKey, setModelParam])
54
+
55
+ const handleParamChange = useCallback((key, value) => {
56
+ setModelParam(selectedModel, key, value)
57
+ }, [selectedModel, setModelParam])
58
+
59
+ // Auto-extract seed when region changes (debounced with longer delay)
60
+ const regionRef = useRef({ start: regionStart, end: regionEnd })
61
+ useEffect(() => {
62
+ regionRef.current = { start: regionStart, end: regionEnd }
63
+ }, [regionStart, regionEnd])
64
+
65
+ useEffect(() => {
66
+ if (!hasRegion || !sessionId) return
67
+ if (regionEnd - regionStart < 3) return
68
+
69
+ const timer = setTimeout(async () => {
70
+ // Only extract if region hasn't changed during the delay
71
+ if (regionRef.current.start === regionStart && regionRef.current.end === regionEnd) {
72
+ setIsExtracting(true)
73
+ await doExtractSeed(regionStart, regionEnd)
74
+ setIsExtracting(false)
75
+ }
76
+ }, 1000)
77
+
78
+ return () => clearTimeout(timer)
79
+ // eslint-disable-next-line react-hooks/exhaustive-deps
80
+ }, [regionStart, regionEnd, hasRegion, sessionId])
81
+
82
+ const handleGenerate = useCallback(async () => {
83
+ if (!hasRegion) return
84
+ await startGeneration(regionStart, regionEnd)
85
+ }, [hasRegion, regionStart, regionEnd, startGeneration])
86
+
87
+ const handleRegenerate = useCallback(async () => {
88
+ // Set a new random seed before regenerating
89
+ if (selectedModel === 'musicgen') {
90
+ setModelParam('musicgen', 'seed', Math.floor(Math.random() * 999999))
91
+ } else if (selectedModel === 'ace-step') {
92
+ setModelParam('ace-step', 'seed', Math.floor(Math.random() * 999999))
93
+ }
94
+ // Small delay for state to update
95
+ setTimeout(() => handleGenerate(), 50)
96
+ }, [selectedModel, setModelParam, handleGenerate])
97
+
98
+ const handleUseAsSource = useCallback(() => {
99
+ // The generated continuation is already stored in the session.
100
+ // Clear the current result so the user can generate again.
101
+ setGenerationResult(null)
102
+ }, [setGenerationResult])
103
+
104
+ const canGenerate = hasRegion && !isGenerating && (regionEnd - regionStart >= 3)
105
+
106
+ return (
107
+ <div className="card rounded-xl p-4 animate-fade-in">
108
+ {/* Header */}
109
+ <button
110
+ onClick={() => setIsCollapsed(!isCollapsed)}
111
+ className="w-full flex items-center justify-between mb-3"
112
+ >
113
+ <div className="flex items-center gap-2">
114
+ <span className="text-[10px] uppercase tracking-widest text-accent-500 font-medium">
115
+ AI Generation
116
+ </span>
117
+ </div>
118
+ <span className={`text-gray-500 text-xs transition-transform ${isCollapsed ? '' : 'rotate-180'}`}>
119
+ &#9660;
120
+ </span>
121
+ </button>
122
+
123
+ {!isCollapsed && (
124
+ <div className="space-y-4">
125
+ {/* Model selector tabs */}
126
+ <ModelSelector
127
+ selectedModel={selectedModel}
128
+ onModelChange={setSelectedModel}
129
+ availability={modelAvailability}
130
+ />
131
+
132
+ {/* Seed region info */}
133
+ <SeedRegionInfo
134
+ seedData={seedData}
135
+ regionStart={regionStart}
136
+ regionEnd={regionEnd}
137
+ hasRegion={hasRegion}
138
+ onPreviewRegion={onPlaySection}
139
+ isExtracting={isExtracting}
140
+ />
141
+
142
+ {/* Task type selector (per model) */}
143
+ <TaskTypeSelector
144
+ model={selectedModel}
145
+ taskType={taskType}
146
+ onTaskTypeChange={handleTaskTypeChange}
147
+ />
148
+
149
+ {/* Model-specific parameters */}
150
+ <div className="transition-all">
151
+ {selectedModel === 'musicgen' && (
152
+ <MusicGenParams
153
+ params={currentParams}
154
+ onParamChange={handleParamChange}
155
+ />
156
+ )}
157
+ {selectedModel === 'ace-step' && (
158
+ <ACEStepParams
159
+ params={currentParams}
160
+ onParamChange={handleParamChange}
161
+ detection={detection}
162
+ />
163
+ )}
164
+ </div>
165
+
166
+ {/* Generate button */}
167
+ <button
168
+ onClick={handleGenerate}
169
+ disabled={!canGenerate}
170
+ className={`w-full py-2.5 rounded-lg text-sm font-medium transition-all ${
171
+ canGenerate
172
+ ? 'bg-accent-500 hover:bg-accent-400 text-white'
173
+ : 'bg-white/[0.04] text-gray-600 cursor-not-allowed'
174
+ }`}
175
+ >
176
+ {isGenerating
177
+ ? 'Generating...'
178
+ : `\u25B6 Generate with ${selectedModel === 'musicgen' ? 'MusicGen' : 'ACE-Step'}`
179
+ }
180
+ </button>
181
+
182
+ {/* Progress */}
183
+ <GenerationProgress
184
+ model={selectedModel}
185
+ progress={generationProgress}
186
+ isGenerating={isGenerating}
187
+ />
188
+
189
+ {/* Output */}
190
+ <GenerationOutput
191
+ sessionId={sessionId}
192
+ generationResult={generationResult}
193
+ onRegenerate={handleRegenerate}
194
+ onUseAsSource={handleUseAsSource}
195
+ onPlaySection={onPlaySection}
196
+ />
197
+ </div>
198
+ )}
199
+ </div>
200
+ )
201
+ }
frontend/src/components/generation/GenerationProgress.jsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ export default function GenerationProgress({ model, progress, isGenerating }) {
4
+ if (!isGenerating) return null
5
+
6
+ return (
7
+ <div className="py-3">
8
+ {model === 'ace-step' && (
9
+ <div className="space-y-1.5">
10
+ <div className="flex items-center justify-between text-xs">
11
+ <span className="text-gray-400">Generating with ACE-Step...</span>
12
+ {progress?.step && progress?.totalSteps && (
13
+ <span className="font-mono text-accent-400">
14
+ Step {progress.step}/{progress.totalSteps}
15
+ </span>
16
+ )}
17
+ </div>
18
+ <div className="h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
19
+ <div
20
+ className="h-full bg-accent-500 rounded-full transition-all duration-300"
21
+ style={{
22
+ width: progress?.step && progress?.totalSteps
23
+ ? `${(progress.step / progress.totalSteps) * 100}%`
24
+ : '60%',
25
+ animation: (!progress?.step) ? 'pulse 2s ease-in-out infinite' : 'none',
26
+ }}
27
+ />
28
+ </div>
29
+ <span className="text-[10px] text-gray-600">
30
+ {progress?.estimateSeconds
31
+ ? `~${progress.estimateSeconds}s remaining`
32
+ : 'XL Turbo: ~20-60s depending on duration'}
33
+ </span>
34
+ </div>
35
+ )}
36
+
37
+ {model === 'musicgen' && (
38
+ <div className="space-y-1.5">
39
+ <span className="text-xs text-gray-400">Generating with MusicGen...</span>
40
+ <div className="h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
41
+ <div
42
+ className="h-full bg-accent-500 rounded-full"
43
+ style={{
44
+ width: '100%',
45
+ animation: 'pulse 2s ease-in-out infinite',
46
+ opacity: 0.7,
47
+ }}
48
+ />
49
+ </div>
50
+ </div>
51
+ )}
52
+ </div>
53
+ )
54
+ }
frontend/src/components/generation/ModelSelector.jsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ const MODELS = [
4
+ {
5
+ id: 'musicgen',
6
+ name: 'MusicGen',
7
+ provider: 'META',
8
+ location: 'LOCAL',
9
+ subtitle: 'Melody-conditioned continuation',
10
+ },
11
+ {
12
+ id: 'ace-step',
13
+ name: 'ACE-Step v1.5',
14
+ provider: 'ACE STUDIO',
15
+ location: 'LOCAL',
16
+ subtitle: 'Seed continuation & style transfer',
17
+ },
18
+ ]
19
+
20
+ function getTooltip(model, availability) {
21
+ if (!availability) return null
22
+ const info = availability[model.id]
23
+ if (!info) return null
24
+ if (info.status === 'offline') {
25
+ return model.id === 'ace-step'
26
+ ? 'Start ACE-Step: ./start_api_server_macos.sh'
27
+ : 'Model offline'
28
+ }
29
+ return null
30
+ }
31
+
32
+ export default function ModelSelector({ selectedModel, onModelChange, availability }) {
33
+ return (
34
+ <div className="flex gap-2">
35
+ {MODELS.map(model => {
36
+ const tooltip = getTooltip(model, availability)
37
+ const isAvailable = !tooltip
38
+ const isActive = selectedModel === model.id
39
+
40
+ return (
41
+ <button
42
+ key={model.id}
43
+ onClick={() => isAvailable && onModelChange(model.id)}
44
+ disabled={!isAvailable}
45
+ title={tooltip || model.subtitle}
46
+ className={`flex-1 px-3 py-2.5 rounded-lg border text-left transition-all ${
47
+ isActive
48
+ ? 'bg-accent-500/20 border-accent-500/40'
49
+ : isAvailable
50
+ ? 'bg-white/[0.03] border-white/[0.06] hover:bg-white/[0.06] hover:border-white/[0.12]'
51
+ : 'bg-white/[0.02] border-white/[0.04] opacity-40 cursor-not-allowed'
52
+ }`}
53
+ >
54
+ <div className="flex items-center gap-2 mb-0.5">
55
+ <span className={`text-sm font-medium ${isActive ? 'text-accent-400' : 'text-gray-300'}`}>
56
+ {model.name}
57
+ </span>
58
+ </div>
59
+ <div className="flex items-center gap-1.5">
60
+ <span className="text-[9px] uppercase tracking-wider font-medium px-1.5 py-0.5 rounded bg-white/[0.06] text-gray-500">
61
+ {model.provider}
62
+ </span>
63
+ <span className={`text-[9px] uppercase tracking-wider font-medium px-1.5 py-0.5 rounded ${
64
+ model.location === 'LOCAL'
65
+ ? 'bg-emerald-500/10 text-emerald-500'
66
+ : 'bg-blue-500/10 text-blue-400'
67
+ }`}>
68
+ {model.location}
69
+ </span>
70
+ </div>
71
+ </button>
72
+ )
73
+ })}
74
+ </div>
75
+ )
76
+ }
frontend/src/components/generation/MusicGenParams.jsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ParamSlider from './ParamSlider'
3
+
4
+ export default function MusicGenParams({ params, onParamChange }) {
5
+ return (
6
+ <div className="space-y-3">
7
+ <div className="text-[10px] text-gray-500 bg-white/[0.02] rounded-lg px-3 py-2">
8
+ Prompt is auto-generated from your seed's BPM, key, and instrumentation to ensure musical coherence.
9
+ </div>
10
+
11
+ <ParamSlider
12
+ label="Duration"
13
+ value={params.duration}
14
+ min={1}
15
+ max={30}
16
+ step={1}
17
+ onChange={v => onParamChange('duration', v)}
18
+ leftLabel="1s"
19
+ rightLabel="30s"
20
+ />
21
+
22
+ {/* Seed */}
23
+ <div>
24
+ <label className="text-xs text-gray-400 font-medium mb-1 block">Seed</label>
25
+ <div className="flex gap-2">
26
+ <input
27
+ type="number"
28
+ value={params.seed}
29
+ onChange={e => onParamChange('seed', parseInt(e.target.value) || -1)}
30
+ className="flex-1 bg-surface-elevated border border-white/[0.08] rounded-lg px-3 py-1.5 text-sm text-white font-mono focus:outline-none focus:border-accent-500/40"
31
+ />
32
+ <button
33
+ onClick={() => onParamChange('seed', Math.floor(Math.random() * 999999))}
34
+ className="px-2 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1] text-sm transition-colors"
35
+ title="Random seed"
36
+ >
37
+ 🎲
38
+ </button>
39
+ </div>
40
+ <span className="text-[10px] text-gray-600 mt-0.5 block">-1 = random</span>
41
+ </div>
42
+ </div>
43
+ )
44
+ }
frontend/src/components/generation/ParamSlider.jsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ export default function ParamSlider({ label, value, min, max, step = 1, onChange, leftLabel, rightLabel, disabled }) {
4
+ const displayValue = step < 1 ? value.toFixed(2) : Math.round(value)
5
+
6
+ return (
7
+ <div className="space-y-1">
8
+ <div className="flex items-center justify-between">
9
+ <label className="text-xs text-gray-400 font-medium">{label}</label>
10
+ <span className="text-xs font-mono text-accent-400">{displayValue}</span>
11
+ </div>
12
+ <input
13
+ type="range"
14
+ min={min}
15
+ max={max}
16
+ step={step}
17
+ value={value}
18
+ onChange={e => onChange(parseFloat(e.target.value))}
19
+ disabled={disabled}
20
+ className="w-full"
21
+ />
22
+ {(leftLabel || rightLabel) && (
23
+ <div className="flex justify-between">
24
+ <span className="text-[10px] text-gray-500">{leftLabel}</span>
25
+ <span className="text-[10px] text-gray-500">{rightLabel}</span>
26
+ </div>
27
+ )}
28
+ </div>
29
+ )
30
+ }
frontend/src/components/generation/SeedRegionInfo.jsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ function formatTime(seconds) {
4
+ const m = Math.floor(seconds / 60)
5
+ const s = (seconds % 60).toFixed(1)
6
+ return `${m}:${s.padStart(4, '0')}`
7
+ }
8
+
9
+ export default function SeedRegionInfo({ seedData, regionStart, regionEnd, hasRegion, onPreviewRegion, isExtracting }) {
10
+ if (!hasRegion) {
11
+ return (
12
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-500/5 border border-amber-500/20">
13
+ <span className="text-amber-400 text-xs">
14
+ Select a region in the transport bar to use as seed audio
15
+ </span>
16
+ </div>
17
+ )
18
+ }
19
+
20
+ const duration = (regionEnd - regionStart).toFixed(1)
21
+
22
+ return (
23
+ <div className="space-y-2">
24
+ <div className="flex items-center justify-between">
25
+ <div className="flex items-center gap-2 text-xs text-gray-400">
26
+ <span className="font-mono text-accent-400">
27
+ {formatTime(regionStart)} &rarr; {formatTime(regionEnd)}
28
+ </span>
29
+ <span className="text-gray-600">({duration}s)</span>
30
+ </div>
31
+ <button
32
+ onClick={onPreviewRegion}
33
+ className="text-[10px] px-2 py-1 rounded bg-white/[0.06] hover:bg-white/[0.1] text-gray-400 hover:text-white transition-colors"
34
+ >
35
+ Preview
36
+ </button>
37
+ </div>
38
+
39
+ {seedData?.analysis && (
40
+ <div className="flex flex-wrap gap-2">
41
+ <Badge label="BPM" value={seedData.analysis.bpm} />
42
+ <Badge label="Key" value={`${seedData.analysis.key} ${seedData.analysis.mode}`} />
43
+ <Badge label="Brightness" value={seedData.analysis.brightness.toFixed(2)} />
44
+ <Badge label="Density" value={seedData.analysis.density.toFixed(2)} />
45
+ </div>
46
+ )}
47
+
48
+ {isExtracting && (
49
+ <div className="text-[10px] text-gray-500 animate-pulse">Analyzing seed...</div>
50
+ )}
51
+ </div>
52
+ )
53
+ }
54
+
55
+ function Badge({ label, value }) {
56
+ const isUnknown = value === '?' || value === '0' || value === 0 || value === '? ?'
57
+ return (
58
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-white/[0.04] border border-white/[0.06]">
59
+ <span className="text-[9px] uppercase tracking-wider text-gray-500">{label}</span>
60
+ <span className={`text-xs font-mono ${isUnknown ? 'text-amber-400' : 'text-accent-400'}`}>
61
+ {isUnknown ? '?' : value}
62
+ </span>
63
+ </span>
64
+ )
65
+ }
frontend/src/components/generation/TaskTypeSelector.jsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+
3
+ const TASK_TYPES = {
4
+ musicgen: [
5
+ { id: 'continuation', label: 'Continue' },
6
+ ],
7
+ 'ace-step': [
8
+ { id: 'repaint', label: 'Continue' },
9
+ { id: 'cover', label: 'Style Transfer' },
10
+ ],
11
+ }
12
+
13
+ export default function TaskTypeSelector({ model, taskType, onTaskTypeChange }) {
14
+ const types = TASK_TYPES[model] || []
15
+ if (types.length <= 1) return null
16
+
17
+ return (
18
+ <div className="flex gap-1.5">
19
+ {types.map(t => (
20
+ <button
21
+ key={t.id}
22
+ onClick={() => onTaskTypeChange(t.id)}
23
+ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
24
+ taskType === t.id
25
+ ? 'bg-accent-500/20 text-accent-400 border border-accent-500/30'
26
+ : 'bg-white/[0.04] text-gray-400 border border-transparent hover:bg-white/[0.06] hover:text-gray-300'
27
+ }`}
28
+ >
29
+ {t.label}
30
+ </button>
31
+ ))}
32
+ </div>
33
+ )
34
+ }
frontend/src/hooks/useGeneration.js ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useRef, useEffect } from 'react'
2
+
3
+ const DEFAULT_PARAMS = {
4
+ musicgen: {
5
+ prompt: '',
6
+ duration: 15,
7
+ seed: -1,
8
+ },
9
+ 'ace-step': {
10
+ task_type: 'repaint',
11
+ caption: '',
12
+ lyrics: '[Instrumental]',
13
+ bpm: null,
14
+ duration: 30,
15
+ audio_cover_strength: 0.8,
16
+ model_variant: 'xl-turbo',
17
+ inference_steps: 15,
18
+ seed: -1,
19
+ scheduler: 'ode',
20
+ cfg_scale: null,
21
+ timestep_shift: null,
22
+ },
23
+ }
24
+
25
+ export function useGeneration({
26
+ sessionId,
27
+ generate,
28
+ extractSeed,
29
+ generateAceStep,
30
+ checkModels,
31
+ }) {
32
+ const [selectedModel, setSelectedModel] = useState('musicgen')
33
+ const [modelParams, setModelParams] = useState(() => JSON.parse(JSON.stringify(DEFAULT_PARAMS)))
34
+ const [isGenerating, setIsGenerating] = useState(false)
35
+ const [generationResult, setGenerationResult] = useState(null)
36
+ const [generationProgress, setGenerationProgress] = useState(null)
37
+ const [modelAvailability, setModelAvailability] = useState(null)
38
+ const [seedData, setSeedData] = useState(null)
39
+ const abortRef = useRef(null)
40
+
41
+ // Check model availability and clear stale results when session changes
42
+ useEffect(() => {
43
+ // Clear previous generation when session changes
44
+ setGenerationResult(null)
45
+ setSeedData(null)
46
+ setGenerationProgress(null)
47
+
48
+ if (checkModels) {
49
+ checkModels().then(data => {
50
+ if (data?.models) {
51
+ const avail = {}
52
+ data.models.forEach(m => { avail[m.name] = m })
53
+ setModelAvailability(avail)
54
+ }
55
+ })
56
+ }
57
+ }, [sessionId, checkModels])
58
+
59
+ const setModelParam = useCallback((model, key, value) => {
60
+ setModelParams(prev => ({
61
+ ...prev,
62
+ [model]: { ...prev[model], [key]: value },
63
+ }))
64
+ }, [])
65
+
66
+ const resetParams = useCallback((model) => {
67
+ setModelParams(prev => ({
68
+ ...prev,
69
+ [model]: JSON.parse(JSON.stringify(DEFAULT_PARAMS[model])),
70
+ }))
71
+ }, [])
72
+
73
+ // Extract seed from region
74
+ const doExtractSeed = useCallback(async (regionStart, regionEnd) => {
75
+ if (!extractSeed) return null
76
+ const result = await extractSeed(regionStart, regionEnd)
77
+ if (result) {
78
+ setSeedData(result)
79
+ // Pre-fill BPM from analysis
80
+ if (result.analysis?.bpm) {
81
+ setModelParams(prev => ({
82
+ ...prev,
83
+ 'ace-step': { ...prev['ace-step'], bpm: result.analysis.bpm },
84
+ }))
85
+ }
86
+ }
87
+ return result
88
+ }, [extractSeed])
89
+
90
+ // Start generation
91
+ const startGeneration = useCallback(async (regionStart, regionEnd) => {
92
+ setIsGenerating(true)
93
+ setGenerationResult(null)
94
+ setGenerationProgress({ status: 'starting' })
95
+
96
+ try {
97
+ let result = null
98
+
99
+ if (selectedModel === 'musicgen') {
100
+ const params = modelParams.musicgen
101
+ result = await generate(regionStart, regionEnd, params.duration, params.prompt || null)
102
+ } else if (selectedModel === 'ace-step') {
103
+ if (!seedData?.seed_id) {
104
+ // Auto-extract seed if not done
105
+ const extracted = await doExtractSeed(regionStart, regionEnd)
106
+ if (!extracted) throw new Error('Failed to extract seed')
107
+ }
108
+ const params = { ...modelParams['ace-step'] }
109
+ params.seed_id = seedData.seed_id
110
+ result = await generateAceStep(params)
111
+ }
112
+
113
+ if (result) {
114
+ setGenerationResult({ ...result, model: selectedModel })
115
+ setGenerationProgress({ status: 'done' })
116
+ } else {
117
+ setGenerationProgress({ status: 'error' })
118
+ }
119
+
120
+ return result
121
+ } catch (err) {
122
+ setGenerationProgress({ status: 'error', message: err.message })
123
+ return null
124
+ } finally {
125
+ setIsGenerating(false)
126
+ }
127
+ }, [selectedModel, modelParams, seedData, generate, generateAceStep, doExtractSeed])
128
+
129
+ const cancelGeneration = useCallback(() => {
130
+ if (abortRef.current) {
131
+ abortRef.current.abort()
132
+ abortRef.current = null
133
+ }
134
+ setIsGenerating(false)
135
+ setGenerationProgress(null)
136
+ }, [])
137
+
138
+ return {
139
+ selectedModel,
140
+ setSelectedModel,
141
+ modelParams,
142
+ setModelParam,
143
+ resetParams,
144
+ isGenerating,
145
+ generationResult,
146
+ setGenerationResult,
147
+ generationProgress,
148
+ modelAvailability,
149
+ seedData,
150
+ setSeedData,
151
+ doExtractSeed,
152
+ startGeneration,
153
+ cancelGeneration,
154
+ }
155
+ }
frontend/src/hooks/useSession.js CHANGED
@@ -245,6 +245,78 @@ export function useSession() {
245
  }
246
  }, [sessionId])
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  return {
249
  sessionId,
250
  stems,
@@ -256,6 +328,9 @@ export function useSession() {
256
  detect,
257
  process,
258
  generate,
 
 
 
259
  fetchChords,
260
  getStems,
261
  loadPreset,
 
245
  }
246
  }, [sessionId])
247
 
248
+ // Extract and analyze a seed from a selected region
249
+ const extractSeed = useCallback(async (regionStart, regionEnd) => {
250
+ if (!sessionId) {
251
+ setError('No session')
252
+ return null
253
+ }
254
+
255
+ try {
256
+ const response = await fetch('/api/seed/extract', {
257
+ method: 'POST',
258
+ headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify({
260
+ session_id: sessionId,
261
+ start_time: regionStart,
262
+ end_time: regionEnd,
263
+ }),
264
+ })
265
+
266
+ if (!response.ok) {
267
+ const data = await response.json().catch(() => ({}))
268
+ throw new Error(data.detail || `Seed extraction failed: ${response.status}`)
269
+ }
270
+
271
+ return await response.json()
272
+ } catch (err) {
273
+ setError(err.message)
274
+ return null
275
+ }
276
+ }, [sessionId])
277
+
278
+ // Generate with ACE-Step
279
+ const generateAceStep = useCallback(async (params) => {
280
+ if (!sessionId) {
281
+ setError('No session')
282
+ return null
283
+ }
284
+
285
+ setLoading(true)
286
+ setError(null)
287
+
288
+ try {
289
+ const response = await fetch('/api/generate/acestep', {
290
+ method: 'POST',
291
+ headers: { 'Content-Type': 'application/json' },
292
+ body: JSON.stringify({ session_id: sessionId, ...params }),
293
+ })
294
+
295
+ if (!response.ok) {
296
+ const data = await response.json().catch(() => ({}))
297
+ throw new Error(data.detail || `ACE-Step generation failed: ${response.status}`)
298
+ }
299
+
300
+ return await response.json()
301
+ } catch (err) {
302
+ setError(err.message)
303
+ return null
304
+ } finally {
305
+ setLoading(false)
306
+ }
307
+ }, [sessionId])
308
+
309
+ // Check available models
310
+ const checkModels = useCallback(async () => {
311
+ try {
312
+ const response = await fetch('/api/models')
313
+ if (response.ok) return await response.json()
314
+ } catch (err) {
315
+ console.warn('Model check failed:', err.message)
316
+ }
317
+ return null
318
+ }, [])
319
+
320
  return {
321
  sessionId,
322
  stems,
 
328
  detect,
329
  process,
330
  generate,
331
+ extractSeed,
332
+ generateAceStep,
333
+ checkModels,
334
  fetchChords,
335
  getStems,
336
  loadPreset,
frontend/src/index.css CHANGED
@@ -1,4 +1,5 @@
1
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
 
2
 
3
  @tailwind base;
4
  @tailwind components;
@@ -28,12 +29,12 @@ body {
28
  }
29
 
30
  ::-webkit-scrollbar-thumb {
31
- background: rgba(20, 184, 166, 0.3);
32
  border-radius: 3px;
33
  }
34
 
35
  ::-webkit-scrollbar-thumb:hover {
36
- background: rgba(20, 184, 166, 0.5);
37
  }
38
 
39
  /* Slider styling */
@@ -54,7 +55,7 @@ input[type="range"]::-webkit-slider-thumb {
54
  appearance: none;
55
  width: 14px;
56
  height: 14px;
57
- background: #14B8A6;
58
  border-radius: 50%;
59
  cursor: pointer;
60
  margin-top: -5px;
@@ -85,8 +86,8 @@ input[type="range"]::-webkit-slider-thumb:hover {
85
  }
86
 
87
  @keyframes pulse-border {
88
- 0%, 100% { border-color: rgba(20, 184, 166, 0.3); }
89
- 50% { border-color: rgba(20, 184, 166, 0.8); }
90
  }
91
 
92
  .animate-pulse-border {
 
1
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
2
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap');
3
 
4
  @tailwind base;
5
  @tailwind components;
 
29
  }
30
 
31
  ::-webkit-scrollbar-thumb {
32
+ background: rgba(240, 165, 0, 0.3);
33
  border-radius: 3px;
34
  }
35
 
36
  ::-webkit-scrollbar-thumb:hover {
37
+ background: rgba(240, 165, 0, 0.5);
38
  }
39
 
40
  /* Slider styling */
 
55
  appearance: none;
56
  width: 14px;
57
  height: 14px;
58
+ background: #F0A500;
59
  border-radius: 50%;
60
  cursor: pointer;
61
  margin-top: -5px;
 
86
  }
87
 
88
  @keyframes pulse-border {
89
+ 0%, 100% { border-color: rgba(240, 165, 0, 0.3); }
90
+ 50% { border-color: rgba(240, 165, 0, 0.8); }
91
  }
92
 
93
  .animate-pulse-border {
frontend/tailwind.config.js CHANGED
@@ -13,13 +13,15 @@ export default {
13
  elevated: '#1C1F33',
14
  },
15
  accent: {
16
- 400: '#2DD4BF',
17
- 500: '#14B8A6',
18
- 600: '#0D9488',
 
19
  }
20
  },
21
  fontFamily: {
22
  sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
 
23
  },
24
  animation: {
25
  'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
 
13
  elevated: '#1C1F33',
14
  },
15
  accent: {
16
+ 300: '#FCD34D',
17
+ 400: '#FBBF24',
18
+ 500: '#F0A500',
19
+ 600: '#D97706',
20
  }
21
  },
22
  fontFamily: {
23
  sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
24
+ mono: ['JetBrains Mono', 'Fira Code', 'ui-monospace', 'monospace'],
25
  },
26
  animation: {
27
  'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',