Gradio API migration, training pipeline, news page, and UI improvements

- Migrate backend from REST API to Gradio @gradio/client for generation
- Fix Gradio parameter alignment (positions 36-49) for reference/cover audio
- Add LoRA training pipeline with dataset upload, preprocessing, and export
- Add News page with dismiss/restore and GitHub star button
- Add localization info icon in Settings language section
- Fix upload audio URL prefix, add missing MIME types
- Add training API routes and Python preprocess script
- Update i18n with news keys for all languages
This commit is contained in:
fspecii
2026-02-09 22:30:15 +02:00
parent f42fde9b40
commit 565faacb7b
23 changed files with 3145 additions and 236 deletions
+20 -3
View File
@@ -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<HTMLAudioElement | null>(null);
const selectedSongRef = useRef<Song | null>(null);
const currentSongIdRef = useRef<string | null>(null);
const pendingSeekRef = useRef<number | null>(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 <TrainingPanel />;
case 'news':
return <NewsPage />;
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);
}}