Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/next/changed-minimax-music-progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Music generation now shows active processing feedback and elapsed time during long renders.
31 changes: 31 additions & 0 deletions client/src/components/music/MusicGenPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default function MusicGenPanel({ track, title = '', artistId = '', artist
const [modelId, setModelId] = useState('');
const [durationSec, setDurationSec] = useState(null);
const [generating, setGenerating] = useState(false);
const [generationElapsedSec, setGenerationElapsedSec] = useState(0);
// Inline HF model install.
const [installRepo, setInstallRepo] = useState('');
const [installing, setInstalling] = useState(false);
Expand All @@ -64,6 +65,16 @@ export default function MusicGenPanel({ track, title = '', artistId = '', artist

useEffect(() => { loadEngines(); }, []);

useEffect(() => {
if (!generating) return undefined;
const startedAt = Date.now();
setGenerationElapsedSec(0);
const timer = setInterval(() => {
if (mountedRef.current) setGenerationElapsedSec(Math.floor((Date.now() - startedAt) / 1000));
}, 1000);
return () => clearInterval(timer);
}, [generating]);

const engine = useMemo(() => engines.find((e) => e.id === engineId) || null, [engines, engineId]);

// Remix: seed the engine / model / duration from a past render. Keyed on
Expand Down Expand Up @@ -282,6 +293,26 @@ export default function MusicGenPanel({ track, title = '', artistId = '', artist
</button>
) : null}
</div>
{generating ? (
<div role="status" className="rounded-lg border border-port-accent/30 bg-port-accent/10 px-3 py-2">
<div className="flex items-center justify-between gap-3 text-xs">
<span className="text-gray-300">
{engine?.cudaRequired ? 'Processing on the GPU' : 'Rendering audio'}
</span>
<span className="font-mono tabular-nums text-port-accent">
{Math.floor(generationElapsedSec / 60)}:{String(generationElapsedSec % 60).padStart(2, '0')} elapsed
</span>
</div>
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-port-border" aria-hidden="true">
<div className="h-full w-1/3 animate-pulse rounded-full bg-port-accent" />
</div>
<p className="mt-1.5 text-[11px] text-gray-500">
{engine?.id === 'minimax-music3'
? 'MiniMax Music 3 does not report an exact percentage yet; a 60-second track can take tens of minutes on a 24 GB GPU.'
: 'Generation is still active. Longer requested durations take more time.'}
</p>
</div>
) : null}

{/* Install an additional model from HuggingFace — only for engines that
can render an arbitrary checkpoint (musicgen/audioldm2). ACE-Step uses
Expand Down
22 changes: 22 additions & 0 deletions client/src/components/music/MusicGenPanel.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,26 @@ describe('MusicGenPanel', () => {
expect(screen.getByRole('button', { name: /install model/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /install runtime/i })).not.toBeInTheDocument();
});

it('shows honest elapsed GPU feedback while MiniMax generation is running', async () => {
api.listMusicEngines.mockResolvedValue({
defaultEngine: 'minimax-music3',
engines: [engine({
id: 'minimax-music3', name: 'MiniMax Music 3 (CUDA only)', ready: true,
cudaRequired: true, cudaState: 'available', customModels: false, lyrics: true,
maxDurationSec: 300, defaultDurationSec: 60,
models: [{ id: 'minimax-music3', repo: 'MiniMaxAI/MiniMax-Music3', name: 'MiniMax Music 3' }],
defaultModelId: 'minimax-music3',
})],
});
api.generateMusic.mockReturnValue(new Promise(() => {}));

render(<MusicGenPanel track={{ id: 'track-1' }} prompt="cinematic score" lyrics="Example lyrics" />);

fireEvent.click(await screen.findByRole('button', { name: /generate/i }));
expect(await screen.findByRole('status')).toHaveTextContent('Processing on the GPU');
expect(screen.getByRole('status')).toHaveTextContent('0:00 elapsed');
expect(screen.getByRole('status')).toHaveTextContent(/does not report an exact percentage/i);
expect(screen.getByRole('status')).toHaveTextContent(/tens of minutes on a 24 GB GPU/i);
});
});