Some enhancements to UI functionality
This commit is contained in:
@@ -46,6 +46,7 @@ export default function App() {
|
||||
const [songs, setSongs] = useState<Song[]>([]);
|
||||
const [playlists, setPlaylists] = useState<Playlist[]>([]);
|
||||
const [likedSongIds, setLikedSongIds] = useState<Set<string>>(new Set());
|
||||
const [referenceTracks, setReferenceTracks] = useState<ReferenceTrack[]>([]);
|
||||
const [playQueue, setPlayQueue] = useState<Song[]>([]);
|
||||
const [queueIndex, setQueueIndex] = useState(-1);
|
||||
|
||||
@@ -107,6 +108,17 @@ export default function App() {
|
||||
isVisible: false,
|
||||
});
|
||||
|
||||
interface ReferenceTrack {
|
||||
id: string;
|
||||
filename: string;
|
||||
storage_key: string;
|
||||
duration: number | null;
|
||||
file_size_bytes: number | null;
|
||||
tags: string[] | null;
|
||||
created_at: string;
|
||||
audio_url: string;
|
||||
}
|
||||
|
||||
const showToast = (message: string, type: ToastType = 'success') => {
|
||||
setToast({ message, type, isVisible: true });
|
||||
};
|
||||
@@ -307,6 +319,31 @@ export default function App() {
|
||||
loadSongs();
|
||||
}, [isAuthenticated, token]);
|
||||
|
||||
const loadReferenceTracks = useCallback(async () => {
|
||||
if (!isAuthenticated || !token) return;
|
||||
try {
|
||||
const response = await fetch('/api/reference-tracks', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
setReferenceTracks(data.tracks || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load reference tracks:', error);
|
||||
}
|
||||
}, [isAuthenticated, token]);
|
||||
|
||||
// Load reference tracks for Library
|
||||
useEffect(() => {
|
||||
loadReferenceTracks();
|
||||
}, [loadReferenceTracks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentView === 'library') {
|
||||
loadReferenceTracks();
|
||||
}
|
||||
}, [currentView, loadReferenceTracks]);
|
||||
|
||||
// Player Logic
|
||||
const getActiveQueue = (song?: Song) => {
|
||||
if (playQueue.length > 0) return playQueue;
|
||||
@@ -816,6 +853,26 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteReferenceTrack = async (trackId: string) => {
|
||||
if (!token) return;
|
||||
const confirmed = window.confirm('Delete this upload? This action cannot be undone.');
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const response = await fetch(`/api/reference-tracks/${trackId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete upload');
|
||||
}
|
||||
setReferenceTracks(prev => prev.filter(track => track.id !== trackId));
|
||||
showToast('Upload deleted successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete upload:', error);
|
||||
showToast('Failed to delete upload', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const createPlaylist = async (name: string, description: string) => {
|
||||
if (!token) return;
|
||||
try {
|
||||
@@ -882,19 +939,28 @@ export default function App() {
|
||||
// Render Layout Logic
|
||||
const renderContent = () => {
|
||||
switch (currentView) {
|
||||
case 'library':
|
||||
case 'library': {
|
||||
const allSongs = user ? songs.filter(s => s.userId === user.id) : [];
|
||||
return (
|
||||
<LibraryView
|
||||
allSongs={allSongs}
|
||||
likedSongs={songs.filter(s => likedSongIds.has(s.id))}
|
||||
playlists={playlists}
|
||||
referenceTracks={referenceTracks}
|
||||
onPlaySong={playSong}
|
||||
onCreatePlaylist={() => {
|
||||
setSongToAddToPlaylist(null);
|
||||
setIsCreatePlaylistModalOpen(true);
|
||||
}}
|
||||
onSelectPlaylist={(p) => handleNavigateToPlaylist(p.id)}
|
||||
onAddToPlaylist={openAddToPlaylistModal}
|
||||
onOpenVideo={openVideoGenerator}
|
||||
onReusePrompt={handleReuse}
|
||||
onDeleteSong={handleDeleteSong}
|
||||
onDeleteReferenceTrack={handleDeleteReferenceTrack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'profile':
|
||||
if (!viewingUsername) return null;
|
||||
|
||||
+98
-19
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Sparkles, ChevronDown, Settings2, Trash2, Music2, Sliders, Dices, Hash, RefreshCw, Plus, Upload, Play, Pause } from 'lucide-react';
|
||||
import { Sparkles, ChevronDown, Settings2, Trash2, Music2, Sliders, Dices, Hash, RefreshCw, Plus, Upload, Play, Pause, Loader2 } from 'lucide-react';
|
||||
import { GenerationParams, Song } from '../types';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { generateApi } from '../services/api';
|
||||
@@ -174,8 +174,10 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
const [isUploadingSource, setIsUploadingSource] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const [isFormatting, setIsFormatting] = useState(false);
|
||||
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
||||
const referenceInputRef = useRef<HTMLInputElement>(null);
|
||||
const sourceInputRef = useRef<HTMLInputElement>(null);
|
||||
const dragDepthRef = useRef(0);
|
||||
const [showAudioModal, setShowAudioModal] = useState(false);
|
||||
const [audioModalTarget, setAudioModalTarget] = useState<'reference' | 'source'>('reference');
|
||||
const [tempAudioUrl, setTempAudioUrl] = useState('');
|
||||
@@ -273,6 +275,50 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
useEffect(() => {
|
||||
const isFileDrag = (e: DragEvent) =>
|
||||
!!(e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files'));
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
if (!isFileDrag(e)) return;
|
||||
dragDepthRef.current += 1;
|
||||
setIsDraggingFile(true);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
if (!isFileDrag(e)) return;
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
if (!isFileDrag(e)) return;
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
||||
if (dragDepthRef.current === 0) {
|
||||
setIsDraggingFile(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
if (!isFileDrag(e)) return;
|
||||
e.preventDefault();
|
||||
dragDepthRef.current = 0;
|
||||
setIsDraggingFile(false);
|
||||
};
|
||||
|
||||
window.addEventListener('dragenter', handleDragEnter);
|
||||
window.addEventListener('dragover', handleDragOver);
|
||||
window.addEventListener('dragleave', handleDragLeave);
|
||||
window.addEventListener('drop', handleDrop);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('dragenter', handleDragEnter);
|
||||
window.removeEventListener('dragover', handleDragOver);
|
||||
window.removeEventListener('dragleave', handleDragLeave);
|
||||
window.removeEventListener('drop', handleDrop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const startResizing = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
@@ -303,13 +349,13 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>, target: 'reference' | 'source') => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
void uploadAudio(file, target);
|
||||
void uploadReferenceTrack(file, target);
|
||||
}
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
// Format handler - uses LLM to enhance style and auto-fill parameters
|
||||
const handleFormat = async () => {
|
||||
// Format handler - uses LLM to enhance style/lyrics and auto-fill parameters
|
||||
const handleFormat = async (target: 'style' | 'lyrics') => {
|
||||
if (!token || !style.trim()) return;
|
||||
setIsFormatting(true);
|
||||
try {
|
||||
@@ -327,14 +373,14 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
|
||||
if (result.success) {
|
||||
// Update fields with LLM-generated values
|
||||
if (result.caption) setStyle(result.caption);
|
||||
if (result.lyrics) setLyrics(result.lyrics);
|
||||
if (target === 'style' && result.caption) setStyle(result.caption);
|
||||
if (target === 'lyrics' && result.lyrics) setLyrics(result.lyrics);
|
||||
if (result.bpm && result.bpm > 0) setBpm(result.bpm);
|
||||
if (result.duration && result.duration > 0) setDuration(result.duration);
|
||||
if (result.key_scale) setKeyScale(result.key_scale);
|
||||
if (result.time_signature) setTimeSignature(result.time_signature);
|
||||
if (result.language) setVocalLanguage(result.language);
|
||||
setIsFormatCaption(true);
|
||||
if (target === 'style') setIsFormatCaption(true);
|
||||
} else {
|
||||
console.error('Format failed:', result.error || result.status_message);
|
||||
alert(result.error || result.status_message || 'Format failed. Make sure the LLM is initialized.');
|
||||
@@ -372,7 +418,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const uploadReferenceTrack = async (file: File) => {
|
||||
const uploadReferenceTrack = async (file: File, target?: 'reference' | 'source') => {
|
||||
if (!token) {
|
||||
setUploadError('Please sign in to upload audio.');
|
||||
return;
|
||||
@@ -398,7 +444,8 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
setReferenceTracks(prev => [data.track, ...prev]);
|
||||
|
||||
// Also set as current reference/source
|
||||
if (audioModalTarget === 'reference') {
|
||||
const selectedTarget = target ?? audioModalTarget;
|
||||
if (selectedTarget === 'reference') {
|
||||
setReferenceAudioUrl(data.track.audio_url);
|
||||
} else {
|
||||
setSourceAudioUrl(data.track.audio_url);
|
||||
@@ -494,7 +541,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) {
|
||||
void uploadAudio(file, target);
|
||||
void uploadReferenceTrack(file, target);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -502,6 +549,18 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleWorkspaceDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
if (e.dataTransfer.files?.length) {
|
||||
handleDrop(e, audioTab);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkspaceDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = () => {
|
||||
// Bulk generation: loop bulkCount times
|
||||
for (let i = 0; i < bulkCount; i++) {
|
||||
@@ -582,7 +641,27 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-zinc-50 dark:bg-suno-panel w-full overflow-y-auto custom-scrollbar transition-colors duration-300">
|
||||
<div
|
||||
className="relative flex flex-col h-full bg-zinc-50 dark:bg-suno-panel w-full overflow-y-auto custom-scrollbar transition-colors duration-300"
|
||||
onDrop={handleWorkspaceDrop}
|
||||
onDragOver={handleWorkspaceDragOver}
|
||||
>
|
||||
{isDraggingFile && (
|
||||
<div className="absolute inset-0 z-[90] pointer-events-none">
|
||||
<div className="absolute inset-0 bg-white/70 dark:bg-black/50 backdrop-blur-sm" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2 rounded-2xl border border-zinc-200 dark:border-white/10 bg-white/90 dark:bg-zinc-900/90 px-6 py-5 shadow-xl">
|
||||
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 text-white flex items-center justify-center shadow-lg">
|
||||
<Upload size={22} />
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-zinc-900 dark:text-white">Drop to upload</div>
|
||||
<div className="text-[11px] text-zinc-500 dark:text-zinc-400">
|
||||
Uploading as {audioTab === 'reference' ? 'Reference' : 'Cover'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 pt-14 md:pt-4 space-y-5">
|
||||
<input
|
||||
ref={referenceInputRef}
|
||||
@@ -971,12 +1050,12 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
{instrumental ? 'Instrumental' : 'Vocal'}
|
||||
</button>
|
||||
<button
|
||||
className={`p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors ${isFormatting ? 'text-pink-500 animate-pulse' : 'text-zinc-500 hover:text-black dark:hover:text-white'}`}
|
||||
className={`p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors ${isFormatting ? 'text-pink-500' : 'text-zinc-500 hover:text-black dark:hover:text-white'}`}
|
||||
title="AI Format - Enhance style & auto-fill parameters"
|
||||
onClick={handleFormat}
|
||||
onClick={() => handleFormat('lyrics')}
|
||||
disabled={isFormatting || !style.trim()}
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
{isFormatting ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className="p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded text-zinc-500 hover:text-black dark:hover:text-white transition-colors"
|
||||
@@ -1011,12 +1090,12 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
<p className="text-[11px] text-zinc-400 dark:text-zinc-500 mt-0.5">Genre, mood, instruments, vibe</p>
|
||||
</div>
|
||||
<button
|
||||
className={`p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors ${isFormatting ? 'text-pink-500 animate-pulse' : 'text-zinc-500 hover:text-black dark:hover:text-white'}`}
|
||||
className={`p-1.5 hover:bg-zinc-200 dark:hover:bg-white/10 rounded transition-colors ${isFormatting ? 'text-pink-500' : 'text-zinc-500 hover:text-black dark:hover:text-white'}`}
|
||||
title="AI Format - Enhance style & auto-fill parameters"
|
||||
onClick={handleFormat}
|
||||
onClick={() => handleFormat('style')}
|
||||
disabled={isFormatting || !style.trim()}
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
{isFormatting ? <Loader2 size={14} className="animate-spin" /> : <Sparkles size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
@@ -1708,7 +1787,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
onClick={() => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.mp3,.wav,.flac,audio/*';
|
||||
input.accept = '.mp3,.wav,.flac,.m4a,.mp4,audio/*';
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) void uploadReferenceTrack(file);
|
||||
@@ -1727,7 +1806,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
<>
|
||||
<Upload size={16} />
|
||||
Upload audio
|
||||
<span className="text-xs text-zinc-400 ml-1">MP3, WAV, FLAC</span>
|
||||
<span className="text-xs text-zinc-400 ml-1">MP3, WAV, FLAC, M4A, MP4</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
+175
-5
@@ -1,26 +1,70 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Song, Playlist } from '../types';
|
||||
import { Heart, Plus, Music, Play } from 'lucide-react';
|
||||
import { Heart, Plus, Music, Play, MoreHorizontal, Trash2 } from 'lucide-react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { SongDropdownMenu } from './SongDropdownMenu';
|
||||
import { ShareModal } from './ShareModal';
|
||||
import { AlbumCover } from './AlbumCover';
|
||||
|
||||
interface LibraryViewProps {
|
||||
allSongs: Song[];
|
||||
likedSongs: Song[];
|
||||
playlists: Playlist[];
|
||||
referenceTracks: ReferenceTrack[];
|
||||
onPlaySong: (song: Song, list?: Song[]) => void;
|
||||
onCreatePlaylist: () => void;
|
||||
onSelectPlaylist: (playlist: Playlist) => void;
|
||||
onAddToPlaylist: (song: Song) => void;
|
||||
onOpenVideo?: (song: Song) => void;
|
||||
onReusePrompt?: (song: Song) => void;
|
||||
onDeleteSong?: (song: Song) => void;
|
||||
onDeleteReferenceTrack?: (trackId: string) => void;
|
||||
}
|
||||
|
||||
interface ReferenceTrack {
|
||||
id: string;
|
||||
filename: string;
|
||||
storage_key: string;
|
||||
duration: number | null;
|
||||
file_size_bytes: number | null;
|
||||
tags: string[] | null;
|
||||
created_at: string;
|
||||
audio_url: string;
|
||||
}
|
||||
|
||||
export const LibraryView: React.FC<LibraryViewProps> = ({
|
||||
allSongs,
|
||||
likedSongs,
|
||||
playlists,
|
||||
referenceTracks,
|
||||
onPlaySong,
|
||||
onCreatePlaylist,
|
||||
onSelectPlaylist
|
||||
onSelectPlaylist,
|
||||
onAddToPlaylist,
|
||||
onOpenVideo,
|
||||
onReusePrompt,
|
||||
onDeleteSong,
|
||||
onDeleteReferenceTrack,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<'playlists' | 'liked'>('liked');
|
||||
const { user } = useAuth();
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'playlists' | 'liked' | 'uploads'>('all');
|
||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
||||
const [shareSong, setShareSong] = useState<Song | null>(null);
|
||||
|
||||
const formatBytes = (bytes?: number | null) => {
|
||||
if (!bytes || bytes <= 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unit = 0;
|
||||
while (size >= 1024 && unit < units.length - 1) {
|
||||
size /= 1024;
|
||||
unit += 1;
|
||||
}
|
||||
return `${size.toFixed(size >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 bg-white dark:bg-black overflow-y-auto custom-scrollbar p-6 lg:p-10 pb-32 transition-colors duration-300">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">Your Library</h1>
|
||||
@@ -35,6 +79,13 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex items-center gap-4 mb-8 border-b border-zinc-200 dark:border-white/10 pb-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('all')}
|
||||
className={`pb-3 text-sm font-bold transition-colors relative ${activeTab === 'all' ? 'text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'}`}
|
||||
>
|
||||
All Songs
|
||||
{activeTab === 'all' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-green-500 rounded-full"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('liked')}
|
||||
className={`pb-3 text-sm font-bold transition-colors relative ${activeTab === 'liked' ? 'text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'}`}
|
||||
@@ -49,10 +100,68 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
|
||||
Playlists
|
||||
{activeTab === 'playlists' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-green-500 rounded-full"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('uploads')}
|
||||
className={`pb-3 text-sm font-bold transition-colors relative ${activeTab === 'uploads' ? 'text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'}`}
|
||||
>
|
||||
Uploads
|
||||
{activeTab === 'uploads' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-green-500 rounded-full"></div>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{activeTab === 'liked' ? (
|
||||
{activeTab === 'all' && (
|
||||
<div className="space-y-1">
|
||||
{allSongs.length === 0 ? (
|
||||
<div className="text-sm text-zinc-500 dark:text-zinc-400">No songs yet.</div>
|
||||
) : (
|
||||
allSongs.map((song, idx) => (
|
||||
<div key={song.id} className="group flex items-center gap-4 p-2 rounded hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors" onClick={() => onPlaySong(song, allSongs)}>
|
||||
<span className="text-zinc-400 dark:text-zinc-500 w-6 text-center group-hover:hidden">{idx + 1}</span>
|
||||
<span className="text-zinc-900 dark:text-white w-6 text-center hidden group-hover:block"><Play size={14} fill="currentColor" /></span>
|
||||
|
||||
{song.coverUrl ? (
|
||||
<img src={song.coverUrl} className="w-10 h-10 rounded object-cover shadow-sm" alt="" onError={(e) => { e.currentTarget.style.display = 'none'; }} />
|
||||
) : (
|
||||
<AlbumCover seed={song.id || song.title} size="sm" className="w-10 h-10" />
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-zinc-900 dark:text-white font-medium truncate">{song.title}</div>
|
||||
<div className="text-zinc-500 dark:text-zinc-400 text-xs">{song.style}</div>
|
||||
</div>
|
||||
|
||||
<div className="text-zinc-500 dark:text-zinc-400 text-sm font-mono">{song.duration}</div>
|
||||
<div className="relative ml-2">
|
||||
<button
|
||||
className="p-2 rounded-full hover:bg-zinc-200 dark:hover:bg-white/5 text-zinc-400 hover:text-black dark:hover:text-white transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShareSong(prev => prev?.id === song.id ? null : song);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal size={16} />
|
||||
</button>
|
||||
<SongDropdownMenu
|
||||
song={song}
|
||||
isOpen={shareSong?.id === song.id}
|
||||
onClose={() => setShareSong(null)}
|
||||
isOwner={user ? song.userId === user.id : false}
|
||||
onCreateVideo={() => onOpenVideo?.(song)}
|
||||
onReusePrompt={() => onReusePrompt?.(song)}
|
||||
onAddToPlaylist={() => onAddToPlaylist(song)}
|
||||
onDelete={() => onDeleteSong?.(song)}
|
||||
onShare={() => {
|
||||
setShareModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'liked' && (
|
||||
<div>
|
||||
<div className="bg-gradient-to-b from-indigo-500/10 to-zinc-50 dark:from-indigo-800/50 dark:to-zinc-900/50 p-6 rounded-xl flex items-end gap-6 mb-8 cursor-pointer hover:bg-zinc-100 dark:hover:bg-white/5 transition-colors group border border-zinc-200 dark:border-white/5" onClick={() => likedSongs.length > 0 && onPlaySong(likedSongs[0], likedSongs)}>
|
||||
<div className="w-40 h-40 bg-gradient-to-br from-indigo-500 to-purple-400 rounded shadow-2xl flex items-center justify-center">
|
||||
@@ -91,11 +200,36 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
|
||||
|
||||
<div className="text-zinc-500 dark:text-zinc-400 text-sm font-mono">{song.duration}</div>
|
||||
<div className="text-green-500"><Heart fill="#22c55e" size={16} /></div>
|
||||
<div className="relative ml-2">
|
||||
<button
|
||||
className="p-2 rounded-full hover:bg-zinc-200 dark:hover:bg-white/5 text-zinc-400 hover:text-black dark:hover:text-white transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShareSong(prev => prev?.id === song.id ? null : song);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal size={16} />
|
||||
</button>
|
||||
<SongDropdownMenu
|
||||
song={song}
|
||||
isOpen={shareSong?.id === song.id}
|
||||
onClose={() => setShareSong(null)}
|
||||
isOwner={user ? song.userId === user.id : false}
|
||||
onCreateVideo={() => onOpenVideo?.(song)}
|
||||
onReusePrompt={() => onReusePrompt?.(song)}
|
||||
onAddToPlaylist={() => onAddToPlaylist(song)}
|
||||
onDelete={() => onDeleteSong?.(song)}
|
||||
onShare={() => {
|
||||
setShareModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
{activeTab === 'playlists' && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
||||
{playlists.map((playlist) => (
|
||||
<div key={playlist.id} className="bg-white dark:bg-zinc-900/40 p-4 rounded-lg border border-zinc-200 dark:border-white/5 hover:border-zinc-300 dark:hover:border-white/10 hover:shadow-lg dark:hover:bg-zinc-900 transition-all group cursor-pointer" onClick={() => onSelectPlaylist(playlist)}>
|
||||
@@ -112,6 +246,42 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'uploads' && (
|
||||
<div className="space-y-2">
|
||||
{referenceTracks.length === 0 ? (
|
||||
<div className="text-sm text-zinc-500 dark:text-zinc-400">No uploads yet.</div>
|
||||
) : (
|
||||
referenceTracks.map((track) => (
|
||||
<div key={track.id} className="flex items-center gap-4 p-3 rounded-lg border border-zinc-200 dark:border-white/5 bg-white dark:bg-zinc-900/40">
|
||||
<div className="w-10 h-10 rounded bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center">
|
||||
<Music size={18} className="text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-zinc-900 dark:text-white truncate">{track.filename}</div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{formatBytes(track.file_size_bytes)} • {new Date(track.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="p-2 rounded-full hover:bg-zinc-200 dark:hover:bg-white/5 text-zinc-500 hover:text-red-600 transition-colors"
|
||||
onClick={() => onDeleteReferenceTrack?.(track.id)}
|
||||
title="Delete upload"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{shareSong && (
|
||||
<ShareModal
|
||||
isOpen={shareModalOpen}
|
||||
onClose={() => { setShareModalOpen(false); setShareSong(null); }}
|
||||
song={shareSong}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import torch
|
||||
|
||||
# Get ACE-Step path from environment or use default
|
||||
ACESTEP_PATH = os.environ.get('ACESTEP_PATH', '/home/ambsd/Desktop/aceui/ACE-Step-1.5')
|
||||
@@ -27,12 +28,18 @@ def get_llm_handler():
|
||||
# Initialize the LLM with the 0.6B model (lighter on VRAM)
|
||||
checkpoint_dir = os.path.join(ACESTEP_PATH, "checkpoints")
|
||||
lm_model_path = "acestep-5Hz-lm-0.6B" # Use the smaller 0.6B model
|
||||
if torch.cuda.is_available():
|
||||
device = "cuda"
|
||||
elif torch.backends.mps.is_available():
|
||||
device = "mps"
|
||||
else:
|
||||
device = "cpu"
|
||||
|
||||
status, success = _llm_handler.initialize(
|
||||
checkpoint_dir=checkpoint_dir,
|
||||
lm_model_path=lm_model_path,
|
||||
backend="pt", # Use PyTorch backend
|
||||
device="cuda",
|
||||
device=device,
|
||||
offload_to_cpu=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -34,13 +34,15 @@ const audioUpload = multer({
|
||||
'audio/flac',
|
||||
'audio/x-flac',
|
||||
'audio/mp4',
|
||||
'audio/x-m4a',
|
||||
'audio/aac',
|
||||
'audio/ogg',
|
||||
'audio/webm',
|
||||
'video/mp4',
|
||||
];
|
||||
|
||||
// Also check file extension as fallback
|
||||
const allowedExtensions = ['.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg', '.webm', '.opus'];
|
||||
const allowedExtensions = ['.mp3', '.wav', '.flac', '.m4a', '.mp4', '.aac', '.ogg', '.webm', '.opus'];
|
||||
const fileExt = file.originalname.toLowerCase().match(/\.[^.]+$/)?.[0];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype) || (fileExt && allowedExtensions.includes(fileExt))) {
|
||||
@@ -141,10 +143,13 @@ router.post('/upload-audio', authMiddleware, audioUpload.single('audio'), async
|
||||
case 'audio/ogg':
|
||||
return '.ogg';
|
||||
case 'audio/mp4':
|
||||
case 'audio/x-m4a':
|
||||
case 'audio/aac':
|
||||
return '.m4a';
|
||||
case 'audio/webm':
|
||||
return '.webm';
|
||||
case 'video/mp4':
|
||||
return '.mp4';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -370,7 +375,8 @@ router.get('/status/:jobId', authMiddleware, async (req: AuthenticatedRequest, r
|
||||
const { buffer } = await downloadAudioToBuffer(audioUrl);
|
||||
const ext = audioUrl.includes('.flac') ? '.flac' : '.mp3';
|
||||
const storageKey = `${req.user!.id}/${songId}${ext}`;
|
||||
const storedPath = await storage.upload(storageKey, buffer, `audio/${ext.slice(1)}`);
|
||||
await storage.upload(storageKey, buffer, `audio/${ext.slice(1)}`);
|
||||
const storedPath = storage.getPublicUrl(storageKey);
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO songs (id, user_id, title, lyrics, style, caption, audio_url,
|
||||
@@ -593,7 +599,6 @@ router.post('/format', authMiddleware, async (req: AuthenticatedRequest, res: Re
|
||||
cwd: ACESTEP_DIR,
|
||||
env: {
|
||||
...process.env,
|
||||
CUDA_VISIBLE_DEVICES: '0',
|
||||
ACESTEP_PATH: ACESTEP_DIR,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,11 +11,22 @@ const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB max
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/flac', 'audio/mp3', 'audio/x-wav', 'audio/x-flac'];
|
||||
if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(mp3|wav|flac)$/i)) {
|
||||
const allowedTypes = [
|
||||
'audio/mpeg',
|
||||
'audio/wav',
|
||||
'audio/flac',
|
||||
'audio/mp3',
|
||||
'audio/x-wav',
|
||||
'audio/x-flac',
|
||||
'audio/mp4',
|
||||
'audio/x-m4a',
|
||||
'audio/aac',
|
||||
'video/mp4',
|
||||
];
|
||||
if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(mp3|wav|flac|m4a|mp4)$/i)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Invalid file type. Only MP3, WAV, and FLAC are allowed.'));
|
||||
cb(new Error('Invalid file type. Only MP3, WAV, FLAC, M4A, and MP4 are allowed.'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export class LocalStorageProvider implements StorageProvider {
|
||||
const filepath = path.join(this.audioDir, key);
|
||||
await mkdir(path.dirname(filepath), { recursive: true });
|
||||
await writeFile(filepath, data);
|
||||
return `/audio/${key}`;
|
||||
return key;
|
||||
}
|
||||
|
||||
async getUrl(key: string, _expiresIn?: number): Promise<string> {
|
||||
@@ -26,6 +26,9 @@ export class LocalStorageProvider implements StorageProvider {
|
||||
}
|
||||
|
||||
getPublicUrl(key: string): string {
|
||||
if (key.startsWith('/audio/')) {
|
||||
return key;
|
||||
}
|
||||
return `/audio/${key}`;
|
||||
}
|
||||
|
||||
|
||||
Regular → Executable
Regular → Executable
Reference in New Issue
Block a user