diff --git a/App.tsx b/App.tsx index 789a970..461623a 100644 --- a/App.tsx +++ b/App.tsx @@ -20,6 +20,8 @@ import { List } from 'lucide-react'; import { PlaylistDetail } from './components/PlaylistDetail'; import { Toast, ToastType } from './components/Toast'; import { SearchPage } from './components/SearchPage'; +import { TrainingPanel } from './components/TrainingPanel'; +import { NewsPage } from './components/NewsPage'; import { ConfirmDialog } from './components/ConfirmDialog'; @@ -106,6 +108,7 @@ function AppContent() { const [reuseData, setReuseData] = useState<{ song: Song, timestamp: number } | null>(null); const audioRef = useRef(null); + const selectedSongRef = useRef(null); const currentSongIdRef = useRef(null); const pendingSeekRef = useRef(null); const playNextRef = useRef<() => void>(() => {}); @@ -164,6 +167,9 @@ function AppContent() { } }, [token]); + // Keep selectedSongRef in sync for use in callbacks without stale closures + useEffect(() => { selectedSongRef.current = selectedSong; }, [selectedSong]); + // Cleanup active jobs on unmount useEffect(() => { return () => { @@ -280,6 +286,8 @@ function AppContent() { } } else if (path === '/search') { setCurrentView('search'); + } else if (path === '/news') { + setCurrentView('news'); } }; @@ -622,7 +630,8 @@ function AppContent() { }); // If the current selection was a temp/generating song, replace it with newest real song - if (selectedSong?.isGenerating || (selectedSong && !loadedSongs.some(s => s.id === selectedSong.id))) { + const current = selectedSongRef.current; + if (current?.isGenerating || (current && !loadedSongs.some(s => s.id === current.id))) { setSelectedSong(loadedSongs[0] ?? null); } } catch (error) { @@ -1040,7 +1049,7 @@ function AppContent() { if (songToAddToPlaylist) { await playlistsApi.addSong(res.playlist.id, songToAddToPlaylist.id, token); setSongToAddToPlaylist(null); - playlistsApi.getMyPlaylists(token).then(r => setPlaylists(r.playlists)); + playlistsApi.getMyPlaylists(token).then(r => setPlaylists(r.playlists)).catch(() => {}); } showToast(t('playlistCreated')); } catch (error) { @@ -1060,7 +1069,7 @@ function AppContent() { await playlistsApi.addSong(playlistId, songToAddToPlaylist.id, token); setSongToAddToPlaylist(null); showToast(t('songAddedToPlaylist')); - playlistsApi.getMyPlaylists(token).then(r => setPlaylists(r.playlists)); + playlistsApi.getMyPlaylists(token).then(r => setPlaylists(r.playlists)).catch(() => {}); } catch (error) { console.error('Add song error:', error); showToast(t('failedToAddSong'), 'error'); @@ -1212,6 +1221,12 @@ function AppContent() { /> ); + case 'training': + return ; + + case 'news': + return ; + case 'create': default: return ( @@ -1311,6 +1326,8 @@ function AppContent() { window.history.pushState({}, '', '/library'); } else if (v === 'search') { window.history.pushState({}, '', '/search'); + } else if (v === 'news') { + window.history.pushState({}, '', '/news'); } if (isMobile) setShowLeftSidebar(false); }} diff --git a/components/CreatePanel.tsx b/components/CreatePanel.tsx index c02b466..79be27e 100644 --- a/components/CreatePanel.tsx +++ b/components/CreatePanel.tsx @@ -4,7 +4,7 @@ import { GenerationParams, Song } from '../types'; import { useAuth } from '../context/AuthContext'; import { useI18n } from '../context/I18nContext'; import { generateApi } from '../services/api'; -import { MAIN_STYLES, SUB_STYLES } from '../data/genres'; +import { MAIN_STYLES } from '../data/genres'; import { EditableSlider } from './EditableSlider'; interface ReferenceTrack { @@ -48,7 +48,12 @@ const KEY_SIGNATURES = [ 'B major', 'B minor' ]; -const TIME_SIGNATURES = ['', '2/4', '3/4', '4/4', '6/8']; +const TIME_SIGNATURES = ['', '2', '3', '4', '6', 'N/A']; + +const TRACK_NAMES = [ + 'woodwinds', 'brass', 'fx', 'synth', 'strings', 'percussion', + 'keyboard', 'guitar', 'bass', 'drums', 'backing_vocals', 'vocals', +]; const VOCAL_LANGUAGE_KEYS = [ { value: 'unknown', key: 'autoInstrumental' as const }, @@ -215,6 +220,7 @@ export const CreatePanel: React.FC = ({ const [showLoraPanel, setShowLoraPanel] = useState(false); const [loraPath, setLoraPath] = useState('./lora_output/final/adapter'); const [loraLoaded, setLoraLoaded] = useState(false); + const [loraEnabled, setLoraEnabled] = useState(true); const [loraScale, setLoraScale] = useState(1.0); const [loraError, setLoraError] = useState(null); const [isLoraLoading, setIsLoraLoading] = useState(false); @@ -263,19 +269,6 @@ export const CreatePanel: React.FC = ({ return modelId.includes('turbo'); }; - // Genre selection state (cascading) - const [selectedMainGenre, setSelectedMainGenre] = useState(''); - const [selectedSubGenre, setSelectedSubGenre] = useState(''); - - // Filter sub-genres based on selected main genre - const filteredSubGenres = useMemo(() => { - if (!selectedMainGenre) return []; - const mainLower = selectedMainGenre.toLowerCase().trim(); - return SUB_STYLES.filter(style => - style.toLowerCase().includes(mainLower) - ); - }, [selectedMainGenre]); - const [isUploadingReference, setIsUploadingReference] = useState(false); const [isUploadingSource, setIsUploadingSource] = useState(false); const [isTranscribingReference, setIsTranscribingReference] = useState(false); @@ -427,7 +420,7 @@ export const CreatePanel: React.FC = ({ const handleLoraScaleChange = async (newScale: number) => { setLoraScale(newScale); - + if (!token || !loraLoaded) return; try { @@ -437,6 +430,61 @@ export const CreatePanel: React.FC = ({ } }; + const handleLoraEnabledToggle = async () => { + if (!token || !loraLoaded) return; + const newEnabled = !loraEnabled; + setLoraEnabled(newEnabled); + try { + await generateApi.toggleLora({ enabled: newEnabled }, token); + } catch (err) { + console.error('Failed to toggle LoRA:', err); + setLoraEnabled(!newEnabled); // revert on error + } + }; + + // Load generation parameters from JSON file + const handleLoadParamsFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + try { + const data = JSON.parse(ev.target?.result as string); + if (data.lyrics !== undefined) setLyrics(data.lyrics); + if (data.style !== undefined) setStyle(data.style); + if (data.title !== undefined) setTitle(data.title); + if (data.caption !== undefined) setStyle(data.caption); + if (data.instrumental !== undefined) setInstrumental(data.instrumental); + if (data.vocal_language !== undefined) setVocalLanguage(data.vocal_language); + if (data.bpm !== undefined) setBpm(data.bpm); + if (data.key_scale !== undefined) setKeyScale(data.key_scale); + if (data.time_signature !== undefined) setTimeSignature(data.time_signature); + if (data.duration !== undefined) setDuration(data.duration); + if (data.inference_steps !== undefined) setInferenceSteps(data.inference_steps); + if (data.guidance_scale !== undefined) setGuidanceScale(data.guidance_scale); + if (data.audio_format !== undefined) setAudioFormat(data.audio_format); + if (data.infer_method !== undefined) setInferMethod(data.infer_method); + if (data.seed !== undefined) { setSeed(data.seed); setRandomSeed(false); } + if (data.shift !== undefined) setShift(data.shift); + if (data.lm_temperature !== undefined) setLmTemperature(data.lm_temperature); + if (data.lm_cfg_scale !== undefined) setLmCfgScale(data.lm_cfg_scale); + if (data.lm_top_k !== undefined) setLmTopK(data.lm_top_k); + if (data.lm_top_p !== undefined) setLmTopP(data.lm_top_p); + if (data.lm_negative_prompt !== undefined) setLmNegativePrompt(data.lm_negative_prompt); + if (data.task_type !== undefined) setTaskType(data.task_type); + if (data.audio_codes !== undefined) setAudioCodes(data.audio_codes); + if (data.repainting_start !== undefined) setRepaintingStart(data.repainting_start); + if (data.repainting_end !== undefined) setRepaintingEnd(data.repainting_end); + if (data.instruction !== undefined) setInstruction(data.instruction); + if (data.audio_cover_strength !== undefined) setAudioCoverStrength(data.audio_cover_strength); + } catch { + console.error('Failed to parse parameters JSON'); + } + }; + reader.readAsText(file); + e.target.value = ''; // reset so same file can be reloaded + }; + // Reuse Effect - must be after all state declarations useEffect(() => { if (initialData) { @@ -628,28 +676,6 @@ export const CreatePanel: React.FC = ({ setIsResizing(true); }; - const uploadAudio = async (file: File, target: 'reference' | 'source') => { - if (!token) { - setUploadError('Please sign in to upload audio.'); - return; - } - setUploadError(null); - const setUploading = target === 'reference' ? setIsUploadingReference : setIsUploadingSource; - const setUrl = target === 'reference' ? setReferenceAudioUrl : setSourceAudioUrl; - setUploading(true); - try { - const result = await generateApi.uploadAudio(file, token); - setUrl(result.url); - setShowAudioModal(false); - setTempAudioUrl(''); - } catch (err) { - const message = err instanceof Error ? err.message : 'Upload failed'; - setUploadError(message); - } finally { - setUploading(false); - } - }; - const handleFileSelect = (e: React.ChangeEvent, target: 'reference' | 'source') => { const file = e.target.files?.[0]; if (file) { @@ -1182,8 +1208,28 @@ export const CreatePanel: React.FC = ({
{/* Song Description */}
-
- {t('describeYourSong')} +
+ + {t('describeYourSong')} + +