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:
+260
@@ -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 }),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user