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
+260
View File
@@ -106,6 +106,7 @@ export interface Song {
user_id?: string;
created_at: string;
creator?: string;
creator_avatar?: string;
ditModel?: string;
generation_params?: any;
}
@@ -311,11 +312,14 @@ export interface GenerationParams {
export interface GenerationJob {
jobId: string;
id?: string;
status: 'pending' | 'queued' | 'running' | 'succeeded' | 'failed';
queuePosition?: number;
etaSeconds?: number;
progress?: number;
stage?: string;
params?: any;
created_at?: string;
result?: {
audioUrls: string[];
bpm?: number;
@@ -375,6 +379,13 @@ export const generateApi = {
error?: string;
}> => api('/api/generate/format', { method: 'POST', body: params, token }),
// Random description from Gradio's example library
getRandomDescription: (token: string): Promise<{
description: string;
instrumental: boolean;
vocalLanguage: string;
}> => api('/api/generate/random-description', { token }),
// LoRA Inference (requires ACE-Step training fork)
loadLora: (params: {
lora_path: string;
@@ -393,6 +404,20 @@ export const generateApi = {
message: string;
scale: number;
}> => api('/api/lora/scale', { method: 'POST', body: params, token }),
toggleLora: (params: {
enabled: boolean;
}, token: string): Promise<{
message: string;
active: boolean;
}> => api('/api/lora/toggle', { method: 'POST', body: params, token }),
getLoraStatus: (token: string): Promise<{
loaded: boolean;
active: boolean;
scale: number;
path: string;
}> => api('/api/lora/status', { token }),
};
// Users API
@@ -532,3 +557,238 @@ export const contactApi = {
submit: (data: ContactFormData): Promise<{ success: boolean; message: string; id: string }> =>
api('/api/contact', { method: 'POST', body: data }),
};
// Training API (LoRA fine-tuning via Gradio)
export interface TrainingSample {
audio: unknown;
filename: string;
caption: string;
genre: string;
promptOverride: string;
lyrics: string;
bpm: number;
key: string;
timeSignature: string;
duration: number;
language: string;
instrumental: boolean;
rawLyrics?: string;
}
export interface DatasetSettings {
datasetName: string;
customTag: string;
tagPosition: 'prepend' | 'append' | 'replace';
allInstrumental: boolean;
genreRatio: number;
}
export interface TrainingParams {
tensorDir?: string;
rank?: number;
alpha?: number;
dropout?: number;
learningRate?: number;
epochs?: number;
batchSize?: number;
gradientAccumulation?: number;
saveEvery?: number;
shift?: number;
seed?: number;
outputDir?: string;
resumeCheckpoint?: string | null;
}
// Helper: build proxy URL for training audio files
export function getTrainingAudioUrl(audioPath: unknown, token?: string): string | undefined {
if (!audioPath) return undefined;
// Handle Gradio FileData objects
if (typeof audioPath === 'object' && audioPath !== null) {
const fd = audioPath as Record<string, unknown>;
if (fd.url && typeof fd.url === 'string') return fd.url;
if (fd.path && typeof fd.path === 'string') {
return `${API_BASE}/api/training/audio?path=${encodeURIComponent(fd.path)}`;
}
return undefined;
}
// Handle absolute path string
if (typeof audioPath === 'string') {
if (audioPath.startsWith('http://') || audioPath.startsWith('https://') || audioPath.startsWith('/audio/')) {
return audioPath;
}
return `${API_BASE}/api/training/audio?path=${encodeURIComponent(audioPath)}`;
}
return undefined;
}
export const trainingApi = {
// Upload audio files for a dataset
uploadAudio: async (files: File[], datasetName: string, token: string): Promise<{
files: Array<{ filename: string; originalName: string; size: number; path: string }>;
uploadDir: string;
count: number;
}> => {
const formData = new FormData();
formData.append('datasetName', datasetName);
for (const file of files) {
formData.append('audio', file);
}
const response = await fetch(`${API_BASE}/api/training/upload-audio`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData,
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Upload failed' }));
throw new Error(error.error || 'Upload failed');
}
return response.json();
},
// Build dataset JSON from uploaded audio files
buildDataset: (params: {
datasetName: string;
customTag?: string;
tagPosition?: string;
allInstrumental?: boolean;
}, token: string): Promise<{
status: string;
dataframe: unknown;
sampleCount: number;
sample: TrainingSample;
settings: DatasetSettings;
datasetPath: string;
}> => api('/api/training/build-dataset', { method: 'POST', body: params, token }),
// Scan directory for audio files (Node.js implementation)
scanDirectory: (params: {
audioDir: string;
datasetName?: string;
customTag?: string;
tagPosition?: string;
allInstrumental?: boolean;
}, token: string): Promise<{
status: string;
dataframe: unknown;
sampleCount: number;
audioDir: string;
}> => api('/api/training/scan-directory', { method: 'POST', body: params, token }),
// Auto-label dataset samples (requires model loaded in Gradio)
autoLabel: (params: {
skipMetas?: boolean;
formatLyrics?: boolean;
transcribeLyrics?: boolean;
onlyUnlabeled?: boolean;
}, token: string): Promise<{
dataframe?: unknown;
status: string;
error?: string;
hint?: string;
}> => api('/api/training/auto-label', { method: 'POST', body: params, token }),
// Initialize model for training (requires Gradio)
initModel: (params: {
checkpoint?: string;
configPath?: string;
device?: string;
initLlm?: boolean;
lmModelPath?: string;
backend?: string;
useFlashAttention?: boolean;
offloadToCpu?: boolean;
offloadDitToCpu?: boolean;
compileModel?: boolean;
quantization?: boolean;
}, token: string): Promise<{
status: string;
modelReady?: boolean;
error?: string;
hint?: string;
}> => api('/api/training/init-model', { method: 'POST', body: params, token }),
// List available checkpoints
getCheckpoints: (token: string): Promise<{
checkpoints: string[];
configs: string[];
}> => api('/api/training/checkpoints', { token }),
// List LoRA training checkpoints
getLoraCheckpoints: (dir: string, token: string): Promise<{
checkpoints: string[];
outputDir: string;
}> => api(`/api/training/lora-checkpoints?dir=${encodeURIComponent(dir)}`, { token }),
// Preprocess dataset to tensors
preprocess: (params: {
datasetPath: string;
outputDir?: string;
}, token: string): Promise<{
status: string;
message?: string;
output_files?: number;
}> => api('/api/training/preprocess', { method: 'POST', body: params, token }),
loadDataset: (datasetPath: string, token: string): Promise<{
status: string;
dataframe: unknown;
sampleCount: number;
sample: TrainingSample;
settings: DatasetSettings;
}> => api('/api/training/load-dataset', { method: 'POST', body: { datasetPath }, token }),
getSamplePreview: (idx: number, token: string): Promise<TrainingSample> =>
api(`/api/training/sample-preview?idx=${idx}`, { token }),
saveSample: (params: {
sampleIdx: number;
caption: string;
genre: string;
promptOverride: string;
lyrics: string;
bpm: number;
key: string;
timeSignature: string;
language: string;
instrumental: boolean;
}, token: string): Promise<{ dataframe: unknown; status: string }> =>
api('/api/training/save-sample', { method: 'POST', body: params, token }),
updateSettings: (params: {
customTag: string;
tagPosition: string;
allInstrumental: boolean;
genreRatio: number;
}, token: string): Promise<{ success: boolean }> =>
api('/api/training/update-settings', { method: 'POST', body: params, token }),
saveDataset: (params: {
savePath?: string;
datasetName?: string;
}, token: string): Promise<{ status: string; path: string }> =>
api('/api/training/save-dataset', { method: 'POST', body: params, token }),
loadTensors: (tensorDir: string, token: string): Promise<{ status: string }> =>
api('/api/training/load-tensors', { method: 'POST', body: { tensorDir }, token }),
startTraining: (params: TrainingParams, token: string): Promise<{
progress: string;
log: string;
metrics: unknown;
}> => api('/api/training/start', { method: 'POST', body: params, token }),
stopTraining: (token: string): Promise<{ status: string }> =>
api('/api/training/stop', { method: 'POST', token }),
exportLora: (params: {
exportPath?: string;
loraOutputDir?: string;
}, token: string): Promise<{ status: string }> =>
api('/api/training/export', { method: 'POST', body: params, token }),
importDataset: (datasetType: string, token: string): Promise<{ status: string }> =>
api('/api/training/import-dataset', { method: 'POST', body: { datasetType }, token }),
};