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
+77 -21
View File
@@ -34,8 +34,8 @@ function resolveAceStepPath(): string {
if (envPath) {
return path.isAbsolute(envPath) ? envPath : path.resolve(process.cwd(), envPath);
}
// Default: sibling directory
return path.resolve(__dirname, '../../../../ACE-Step-1.5');
// Default: sibling directory (server/src/services -> ../../../ACE-Step-1.5 = app/ACE-Step-1.5)
return path.resolve(__dirname, '../../../ACE-Step-1.5');
}
// Resolve Python path cross-platform (supports venv and portable installations)
@@ -54,11 +54,22 @@ export function resolvePythonPath(baseDir: string): string {
return portablePath;
}
// Standard venv path (different structure on Windows vs Unix)
if (isWindows) {
return path.join(baseDir, '.venv', 'Scripts', pythonExe);
// Check common venv directory names (Pinokio uses 'env', others use '.venv' or 'venv')
const venvDirs = ['env', '.venv', 'venv'];
for (const venvDir of venvDirs) {
const venvPython = isWindows
? path.join(baseDir, venvDir, 'Scripts', pythonExe)
: path.join(baseDir, venvDir, 'bin', 'python');
if (existsSync(venvPython)) {
return venvPython;
}
}
return path.join(baseDir, '.venv', 'bin', 'python');
// Fallback to first option (will produce a clear error if not found)
if (isWindows) {
return path.join(baseDir, 'env', 'Scripts', pythonExe);
}
return path.join(baseDir, 'env', 'bin', 'python');
}
const ACESTEP_DIR = resolveAceStepPath();
@@ -99,7 +110,11 @@ async function prepareAudioFile(audioUrl: string | undefined): Promise<unknown>
try {
const buffer = await readFile(filePath);
const ext = path.extname(filePath).toLowerCase();
const mimeType = ext === '.flac' ? 'audio/flac' : ext === '.wav' ? 'audio/wav' : 'audio/mpeg';
const mimeMap: Record<string, string> = {
'.flac': 'audio/flac', '.wav': 'audio/wav', '.ogg': 'audio/ogg',
'.opus': 'audio/opus', '.m4a': 'audio/mp4', '.mp4': 'audio/mp4',
};
const mimeType = mimeMap[ext] || 'audio/mpeg';
const blob = new Blob([buffer], { type: mimeType });
return handle_file(blob);
} catch (error) {
@@ -113,7 +128,7 @@ async function prepareAudioFile(audioUrl: string | undefined): Promise<unknown>
}
/**
* Build the 45 positional arguments for the Gradio /generation_wrapper endpoint.
* Build the 50 positional arguments for the Gradio /generation_wrapper endpoint.
*/
async function buildGradioArgs(params: GenerationParams): Promise<unknown[]> {
const caption = params.style || 'pop music';
@@ -138,7 +153,7 @@ async function buildGradioArgs(params: GenerationParams): Promise<unknown[]> {
String(params.seed ?? -1), // 9: Seed
referenceAudio, // 10: Reference Audio (filepath | null)
params.duration && params.duration > 0 ? params.duration : -1, // 11: Audio Duration (-1 = auto)
params.batchSize ?? 1, // 12: Batch Size
Math.min(Math.max(params.batchSize ?? 1, 1), 16), // 12: Batch Size (clamped 1-16)
sourceAudio, // 13: Source Audio (filepath | null)
params.audioCodes || '', // 14: LM Codes Hints
params.repaintingStart ?? 0.0, // 15: Repainting Start
@@ -162,15 +177,20 @@ async function buildGradioArgs(params: GenerationParams): Promise<unknown[]> {
isThinking ? (params.useCotMetas ?? true) : false, // 33: CoT Metas
isThinking ? (params.useCotCaption ?? true) : false, // 34: CaptionRewrite
isThinking ? (params.useCotLanguage ?? true) : false, // 35: CoT Language
params.constrainedDecodingDebug ?? false, // 36: Constrained Decoding Debug
params.allowLmBatch ?? true, // 37: ParallelThinking
params.getScores ?? false, // 38: Auto Score
params.getLrc ?? false, // 39: Auto LRC
params.scoreScale ?? 0.5, // 40: Quality Score Sensitivity
params.lmBatchChunkSize ?? 8, // 41: LM Batch Chunk Size
params.trackName || '', // 42: Track Name
params.completeTrackClasses || [], // 43: Track Names
params.autogen ?? false, // 44: AutoGen
params.isFormatCaption ?? false, // 36: Is Format Caption State
params.constrainedDecodingDebug ?? false, // 37: Constrained Decoding Debug
params.allowLmBatch ?? true, // 38: ParallelThinking
params.getScores ?? false, // 39: Auto Score
params.getLrc ?? false, // 40: Auto LRC
params.scoreScale ?? 0.5, // 41: Quality Score Sensitivity
params.lmBatchChunkSize ?? 8, // 42: LM Batch Chunk Size
params.trackName || null, // 43: Track Name
params.completeTrackClasses || [], // 44: Track Names
params.autogen ?? false, // 45: AutoGen
0, // 46: Current Batch Index
1, // 47: Total Batches
[], // 48: Batch Queue
{}, // 49: Generation Params State
];
}
@@ -191,14 +211,20 @@ async function downloadGradioAudioFile(
return;
}
// Fall back to HTTP download via Gradio URL
// Fall back to HTTP download via Gradio URL (use temp file for atomicity)
if (fileObj.url) {
const response = await fetch(fileObj.url);
if (!response.ok) {
throw new Error(`Failed to download Gradio audio: ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
await writeFile(destPath, buffer);
if (buffer.length === 0) {
throw new Error('Downloaded audio file is empty');
}
const tmpPath = destPath + '.tmp';
await writeFile(tmpPath, buffer);
const { rename } = await import('fs/promises');
await rename(tmpPath, destPath);
return;
}
@@ -319,6 +345,9 @@ interface ActiveJob {
const activeJobs = new Map<string, ActiveJob>();
// Periodic cleanup of old jobs (every 10 minutes, remove jobs older than 1 hour)
setInterval(() => cleanupOldJobs(3600000), 600000);
// Job queue for sequential processing (GPU can only handle one job at a time)
const jobQueue: string[] = [];
let isProcessingQueue = false;
@@ -453,6 +482,10 @@ async function processGenerationViaGradio(
const result = await client.predict('/generation_wrapper', args);
const data = result.data as unknown[];
if (!Array.isArray(data) || data.length === 0) {
throw new Error(`Gradio returned unexpected data format: ${typeof data}`);
}
// Extract audio files from the result
// Outputs 0-7: individual audio samples (filepath objects)
// Output 8: "All Generated Files" as list[filepath]
@@ -696,7 +729,7 @@ interface PythonResult {
error?: string;
}
function runPythonGeneration(scriptArgs: string[]): Promise<PythonResult> {
function runPythonGeneration(scriptArgs: string[], timeoutMs = 600000): Promise<PythonResult> {
return new Promise((resolve) => {
const pythonPath = resolvePythonPath(ACESTEP_DIR);
const args = [PYTHON_SCRIPT, ...scriptArgs];
@@ -709,6 +742,13 @@ function runPythonGeneration(scriptArgs: string[]): Promise<PythonResult> {
},
});
// Kill process after timeout (default 10 minutes)
const timer = setTimeout(() => {
proc.kill('SIGTERM');
setTimeout(() => { if (!proc.killed) proc.kill('SIGKILL'); }, 5000);
resolve({ success: false, error: `Generation timed out after ${timeoutMs / 1000}s` });
}, timeoutMs);
let stdout = '';
let stderr = '';
@@ -727,6 +767,7 @@ function runPythonGeneration(scriptArgs: string[]): Promise<PythonResult> {
});
proc.on('close', (code) => {
clearTimeout(timer);
if (code !== 0) {
resolve({ success: false, error: stderr || `Process exited with code ${code}` });
return;
@@ -749,6 +790,7 @@ function runPythonGeneration(scriptArgs: string[]): Promise<PythonResult> {
});
proc.on('error', (err) => {
clearTimeout(timer);
resolve({ success: false, error: err.message });
});
});
@@ -831,6 +873,20 @@ export async function getAudioStream(audioPath: string): Promise<Response> {
}
}
// Absolute path — try reading directly from disk (Gradio output files)
if (audioPath.startsWith('/')) {
try {
const buffer = await readFile(audioPath);
const ext = audioPath.endsWith('.flac') ? 'flac' : audioPath.endsWith('.wav') ? 'wav' : 'mpeg';
return new Response(buffer, {
status: 200,
headers: { 'Content-Type': `audio/${ext}` }
});
} catch {
// Fall through to Gradio API
}
}
const url = `${ACESTEP_API}/v1/audio?path=${encodeURIComponent(audioPath)}`;
console.log('Fetching audio from:', url);
return fetch(url);