diff --git a/App.tsx b/App.tsx index 169f798..01f477c 100644 --- a/App.tsx +++ b/App.tsx @@ -293,6 +293,14 @@ export default function App() { viewCount: s.view_count || 0, userId: s.user_id, creator: s.creator, + generationParams: (() => { + try { + if (!s.generation_params) return undefined; + return typeof s.generation_params === 'string' ? JSON.parse(s.generation_params) : s.generation_params; + } catch { + return undefined; + } + })(), }); const mySongs = mySongsRes.songs.map(mapSong); @@ -671,6 +679,8 @@ export default function App() { return { ...s, queuePosition: status.status === 'queued' ? status.queuePosition : undefined, + progress: status.status === 'running' ? status.progress : undefined, + stage: status.status === 'running' ? status.stage : undefined, }; } return s; diff --git a/components/CreatePanel.tsx b/components/CreatePanel.tsx index d4f9825..6ea16a0 100644 --- a/components/CreatePanel.tsx +++ b/components/CreatePanel.tsx @@ -445,11 +445,7 @@ export const CreatePanel: React.FC = ({ onGenerate, isGenerati // Also set as current reference/source const selectedTarget = target ?? audioModalTarget; - if (selectedTarget === 'reference') { - setReferenceAudioUrl(data.track.audio_url); - } else { - setSourceAudioUrl(data.track.audio_url); - } + applyAudioTargetUrl(selectedTarget, data.track.audio_url); setShowAudioModal(false); } catch (err) { const message = err instanceof Error ? err.message : 'Upload failed'; @@ -481,11 +477,7 @@ export const CreatePanel: React.FC = ({ onGenerate, isGenerati }; const useReferenceTrack = (track: ReferenceTrack) => { - if (audioModalTarget === 'reference') { - setReferenceAudioUrl(track.audio_url); - } else { - setSourceAudioUrl(track.audio_url); - } + applyAudioTargetUrl(audioModalTarget, track.audio_url); setShowAudioModal(false); setPlayingTrackId(null); }; @@ -507,17 +499,24 @@ export const CreatePanel: React.FC = ({ onGenerate, isGenerati const applyAudioUrl = () => { if (!tempAudioUrl.trim()) return; - if (audioModalTarget === 'reference') { - setReferenceAudioUrl(tempAudioUrl.trim()); + applyAudioTargetUrl(audioModalTarget, tempAudioUrl.trim()); + setShowAudioModal(false); + setTempAudioUrl(''); + }; + + const applyAudioTargetUrl = (target: 'reference' | 'source', url: string) => { + if (target === 'reference') { + setReferenceAudioUrl(url); setReferenceTime(0); setReferenceDuration(0); } else { - setSourceAudioUrl(tempAudioUrl.trim()); + setSourceAudioUrl(url); setSourceTime(0); setSourceDuration(0); + if (taskType === 'text2music') { + setTaskType('cover'); + } } - setShowAudioModal(false); - setTempAudioUrl(''); }; const formatTime = (time: number) => { diff --git a/components/RightSidebar.tsx b/components/RightSidebar.tsx index 0b6efab..799d60c 100644 --- a/components/RightSidebar.tsx +++ b/components/RightSidebar.tsx @@ -39,6 +39,24 @@ export const RightSidebar: React.FC = ({ song, onClose, onOpe } }, [song, user]); + const getSourceLabel = (url?: string) => { + if (!url) return 'None'; + try { + const parsed = new URL(url, window.location.origin); + const name = parsed.pathname.split('/').pop(); + return decodeURIComponent(name || url); + } catch { + const parts = url.split('/'); + return decodeURIComponent(parts[parts.length - 1] || url); + } + }; + + const openSource = (url?: string) => { + if (!url) return; + const resolved = url.startsWith('http') ? url : `${window.location.origin}${url}`; + window.open(resolved, '_blank'); + }; + if (!song) return (
@@ -239,6 +257,55 @@ export const RightSidebar: React.FC = ({ song, onClose, onOpe
+ {(song.generationParams?.referenceAudioUrl || song.generationParams?.sourceAudioUrl) && ( +
+
+ + Sources +
+
+
+
+ +
+
Reference
+
+ {getSourceLabel(song.generationParams?.referenceAudioUrl)} +
+
+
+ {song.generationParams?.referenceAudioUrl && ( + + )} +
+
+
+ +
+
Cover
+
+ {getSourceLabel(song.generationParams?.sourceAudioUrl)} +
+
+
+ {song.generationParams?.sourceAudioUrl && ( + + )} +
+
+
+ )} +
{/* Tags / Style */} @@ -332,4 +399,4 @@ const ActionButton: React.FC<{ icon: React.ReactNode; label?: string; active?: b {icon} {label && {label}} -); \ No newline at end of file +); diff --git a/components/SongList.tsx b/components/SongList.tsx index c5b0996..1d76f90 100644 --- a/components/SongList.tsx +++ b/components/SongList.tsx @@ -355,6 +355,20 @@ const SongItem: React.FC = ({

{song.style}

+ {song.isGenerating && ( +
+
+ {song.progress !== undefined ? ( +
+ ) : ( +
+ )} +
+
+ )}
{/* Actions Row - Hidden while generating */} @@ -453,4 +467,4 @@ const SongItem: React.FC = ({ /> ); -}; \ No newline at end of file +}; diff --git a/server/src/routes/songs.ts b/server/src/routes/songs.ts index 93ddc4f..6dd9ec5 100644 --- a/server/src/routes/songs.ts +++ b/server/src/routes/songs.ts @@ -107,7 +107,7 @@ router.get('/', authMiddleware, async (req: AuthenticatedRequest, res: Response) const result = await pool.query( `SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url, s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public, - s.like_count, s.view_count, s.user_id, s.created_at, + s.like_count, s.view_count, s.user_id, s.created_at, s.generation_params, COALESCE(u.username, 'Anonymous') as creator FROM songs s LEFT JOIN users u ON s.user_id = u.id @@ -137,7 +137,7 @@ router.get('/public/featured', optionalAuthMiddleware, async (_req: Authenticate const result = await pool.query( `SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url, s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.like_count, s.view_count, s.created_at, s.user_id, - COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar + COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar, s.generation_params FROM songs s LEFT JOIN users u ON s.user_id = u.id ORDER BY RANDOM() @@ -184,7 +184,7 @@ router.get('/public', optionalAuthMiddleware, async (req: AuthenticatedRequest, const result = await pool.query( `SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url, s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.like_count, s.created_at, - COALESCE(u.username, 'Anonymous') as creator + COALESCE(u.username, 'Anonymous') as creator, s.generation_params FROM songs s LEFT JOIN users u ON s.user_id = u.id WHERE s.is_public = true @@ -213,7 +213,7 @@ router.get('/:id', optionalAuthMiddleware, async (req: AuthenticatedRequest, res const result = await pool.query( `SELECT s.id, s.user_id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url, s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public, s.like_count, s.view_count, s.created_at, - COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar + COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar, s.generation_params FROM songs s LEFT JOIN users u ON s.user_id = u.id WHERE s.id = $1`, @@ -252,7 +252,7 @@ router.get('/:id/full', optionalAuthMiddleware, async (req: AuthenticatedRequest pool.query( `SELECT s.id, s.user_id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url, s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public, - s.like_count, s.view_count, s.created_at, + s.like_count, s.view_count, s.created_at, s.generation_params, COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar FROM songs s LEFT JOIN users u ON s.user_id = u.id @@ -496,7 +496,7 @@ router.get('/liked/list', authMiddleware, async (req: AuthenticatedRequest, res: const result = await pool.query( `SELECT s.id, s.title, s.lyrics, s.style, s.cover_url, s.audio_url, s.duration, s.tags, s.like_count, s.created_at, s.is_public, - COALESCE(u.username, 'Anonymous') as creator + COALESCE(u.username, 'Anonymous') as creator, s.generation_params FROM liked_songs ls JOIN songs s ON ls.song_id = s.id LEFT JOIN users u ON s.user_id = u.id diff --git a/server/src/services/acestep.ts b/server/src/services/acestep.ts index a60765d..2115265 100644 --- a/server/src/services/acestep.ts +++ b/server/src/services/acestep.ts @@ -255,7 +255,12 @@ async function pollApiResult(taskId: string, maxWaitMs = 600000): Promise { }; } + if (job.status === 'running' && job.taskId) { + try { + const response = await fetch(`${ACESTEP_API}/query_result`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ task_id_list: [job.taskId] }), + }); + + if (response.ok) { + const result = await response.json(); + const taskData = result.data?.[0]; + if (taskData?.result) { + let resultData: unknown = taskData.result; + if (typeof resultData === 'string') { + try { + resultData = JSON.parse(resultData); + } catch { + resultData = null; + } + } + + const item = Array.isArray(resultData) ? resultData[0] : resultData; + if (item && typeof item === 'object') { + const progress = typeof (item as any).progress === 'number' ? (item as any).progress : undefined; + const stage = typeof (item as any).stage === 'string' ? (item as any).stage : undefined; + return { + status: job.status, + etaSeconds: Math.max(0, 180 - elapsed), + progress, + stage, + }; + } + } + } + } catch { + // ignore progress fetch failures, fall back to ETA only + } + } + return { status: job.status, etaSeconds: Math.max(0, 180 - elapsed), // 3 min estimate diff --git a/services/api.ts b/services/api.ts index b0f18b2..4691ad8 100644 --- a/services/api.ts +++ b/services/api.ts @@ -106,6 +106,7 @@ export interface Song { user_id?: string; created_at: string; creator?: string; + generation_params?: any; } // Transform songs to have proper audio URLs @@ -266,6 +267,8 @@ export interface GenerationJob { status: 'pending' | 'queued' | 'running' | 'succeeded' | 'failed'; queuePosition?: number; etaSeconds?: number; + progress?: number; + stage?: string; result?: { audioUrls: string[]; bpm?: number; diff --git a/types.ts b/types.ts index 9d2efad..43194ee 100644 --- a/types.ts +++ b/types.ts @@ -8,6 +8,9 @@ export interface Song { createdAt: Date; isGenerating?: boolean; queuePosition?: number; // Position in queue (undefined = actively generating, number = waiting in queue) + progress?: number; + stage?: string; + generationParams?: any; tags: string[]; audioUrl?: string; isPublic?: boolean;