From 00c5b75ffb7c27752cbacc58b462010de720066e Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 14 Mar 2026 08:34:58 -0700 Subject: [PATCH] Regenerate as new version, UI polish, and bugfixes - Add /generate/{id}/regenerate endpoint that creates a new take version - Wire regenerate into history dropdown with SSE progress + autoplay - Add normalize_audio to regenerate path - Remove duplicate Regenerate menu item - Show disabled ellipsis menu during generation instead of hiding it - Fix sf.write format kwarg broken by asyncio.to_thread migration - Rename clean version label to 'original', effects to 'version-N' - Show effects chain names in version list instead of 'N fx' - Include all versions in export package - Clean up version panel padding --- .../components/AudioPlayer/AudioPlayer.tsx | 15 ++- app/src/components/History/HistoryTable.tsx | 31 ++++- app/src/lib/api/client.ts | 6 + backend/export_import.py | 48 +++++-- backend/main.py | 118 +++++++++++++++++- 5 files changed, 197 insertions(+), 21 deletions(-) diff --git a/app/src/components/AudioPlayer/AudioPlayer.tsx b/app/src/components/AudioPlayer/AudioPlayer.tsx index 44ebb266..75e1b4e5 100644 --- a/app/src/components/AudioPlayer/AudioPlayer.tsx +++ b/app/src/components/AudioPlayer/AudioPlayer.tsx @@ -157,8 +157,21 @@ export function AudioPlayer() { const wavesurfer = wavesurferRef.current; if (!wavesurfer) return; - // Update store when time changes + // Update store when time changes, stop if past duration wavesurfer.on('timeupdate', (time) => { + const dur = usePlayerStore.getState().duration; + if (dur > 0 && time >= dur) { + setCurrentTime(dur); + const loop = usePlayerStore.getState().isLooping; + if (loop) { + wavesurfer.seekTo(0); + wavesurfer.play(); + } else { + wavesurfer.pause(); + setIsPlaying(false); + } + return; + } setCurrentTime(time); }); diff --git a/app/src/components/History/HistoryTable.tsx b/app/src/components/History/HistoryTable.tsx index 72a1fc90..7900cd44 100644 --- a/app/src/components/History/HistoryTable.tsx +++ b/app/src/components/History/HistoryTable.tsx @@ -224,6 +224,20 @@ export function HistoryTable() { } }; + const handleRegenerate = async (generationId: string) => { + try { + await apiClient.regenerateGeneration(generationId); + addPendingGeneration(generationId); + queryClient.invalidateQueries({ queryKey: ['history'] }); + } catch (error) { + toast({ + title: 'Regenerate failed', + description: error instanceof Error ? error.message : 'Could not regenerate', + variant: 'destructive', + }); + } + }; + const handleApplyEffects = (generationId: string) => { setEffectsTargetId(generationId); setEffectsChain([]); @@ -457,7 +471,7 @@ export function HistoryTable() { > - ) : isPlayable ? ( + ) : ( <> @@ -466,6 +480,7 @@ export function HistoryTable() { size="icon" className="h-8 w-8" aria-label="Actions" + disabled={isGenerating} > @@ -495,6 +510,10 @@ export function HistoryTable() { Apply Effects + handleRegenerate(gen.id)}> + + Regenerate + handleDeleteClick(gen.id, gen.profile_name)} disabled={deleteGeneration.isPending} @@ -519,7 +538,7 @@ export function HistoryTable() { )} - ) : null} + )} @@ -533,13 +552,13 @@ export function HistoryTable() { transition={{ duration: 0.2, ease: 'easeOut' }} className="overflow-hidden" > -
+
{gen.versions.map((v) => (