Files
AI-Profile-Router/experiments/acestep15-xl-sft/ace-step-ui/patch-source.mjs
T
2026-09-10 19:52:16 +02:00

418 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs';
import path from 'node:path';
const root = process.argv[2];
if (!root) throw new Error('source root argument is required');
function patch(relativePath, transform) {
const filename = path.join(root, relativePath);
const before = fs.readFileSync(filename, 'utf8');
const after = transform(before);
if (after === before) throw new Error(`patch made no change: ${relativePath}`);
fs.writeFileSync(filename, after);
}
function replaceOnce(text, before, after, label) {
const first = text.indexOf(before);
if (first < 0) throw new Error(`patch anchor missing: ${label}`);
if (text.indexOf(before, first + 1) >= 0) throw new Error(`patch anchor repeated: ${label}`);
return text.slice(0, first) + after + text.slice(first + before.length);
}
patch('server/src/services/acestep.ts', (text) => {
text = replaceOnce(
text,
"const AUDIO_DIR = path.join(__dirname, '../../public/audio');",
'const AUDIO_DIR = config.storage.audioDir;',
'persistent generated audio',
);
text = text.replace("import { handle_file } from '@gradio/client';\n", '');
text = text.replace('getGradioClient, ', '');
const helperStart = text.indexOf('// Gradio generation: map params');
const helperEnd = text.indexOf('/**\n * Download a Gradio audio result file', helperStart);
if (helperStart < 0 || helperEnd < 0) throw new Error('legacy Gradio helper anchors missing');
const helperCommentStart = text.lastIndexOf('// ---------------------------------------------------------------------------', helperStart);
const namedHelpers = `// ---------------------------------------------------------------------------
// Named REST generation through ACE-Step's official /release_task endpoint
// ---------------------------------------------------------------------------
function resolveAudioPath(audioUrl: string): string {
if (audioUrl.startsWith('/audio/')) {
return path.join(AUDIO_DIR, audioUrl.replace('/audio/', ''));
}
if (audioUrl.startsWith('http')) {
try {
const parsed = new URL(audioUrl);
if (parsed.pathname.startsWith('/audio/')) {
return path.join(AUDIO_DIR, parsed.pathname.replace('/audio/', ''));
}
} catch { /* fall through */ }
}
return audioUrl;
}
function resolveWorkerAudioPath(audioUrl: string | undefined): string | undefined {
if (!audioUrl) return undefined;
const localPath = resolveAudioPath(audioUrl);
if (!existsSync(localPath)) {
throw new Error(\`Uploaded audio is missing: \${localPath}\`);
}
const relativePath = path.relative(AUDIO_DIR, localPath);
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
throw new Error('Audio path is outside the shared Community UI storage');
}
return path.posix.join('/data/community-audio', relativePath.split(path.sep).join('/'));
}
function buildReleaseTaskPayload(params: GenerationParams): Record<string, unknown> {
const caption = params.style || 'pop music';
const prompt = params.customMode ? caption : (params.songDescription || caption);
const thinking = params.thinking ?? true;
const enhance = params.enhance ?? false;
const taskType = params.taskType === 'audio2audio' ? 'cover' : (params.taskType || 'text2music');
// A reference in text-to-music mode is only a global style/timbre guide.
// ACE-Step's own API guide recommends a low value (~0.2) for style transfer.
// Cover/source-audio jobs retain the explicitly selected cover strength.
const isStyleReference = taskType === 'text2music' && Boolean(params.referenceAudioUrl) && !params.sourceAudioUrl;
const effectiveAudioStrength = isStyleReference ? 0.2 : (params.audioCoverStrength ?? 1.0);
return {
prompt,
lyrics: params.instrumental ? '[Instrumental]' : (params.lyrics || ''),
instrumental: params.instrumental,
vocal_language: params.vocalLanguage || 'en',
bpm: params.bpm && params.bpm > 0 ? params.bpm : 0,
key_scale: params.keyScale || '',
time_signature: params.timeSignature || '',
audio_duration: params.duration && params.duration > 0 ? params.duration : -1,
inference_steps: params.inferenceSteps ?? 80,
guidance_scale: params.guidanceScale ?? 8.0,
shift: params.shift ?? 3.0,
infer_method: params.inferMethod || 'ode',
batch_size: Math.min(Math.max(params.batchSize ?? 1, 1), 16),
use_random_seed: params.randomSeed !== false,
seed: params.seed ?? -1,
thinking,
use_format: enhance,
lm_temperature: params.lmTemperature ?? 0.85,
lm_cfg_scale: params.lmCfgScale ?? 2.0,
lm_top_k: params.lmTopK ?? 0,
lm_top_p: params.lmTopP ?? 0.9,
lm_negative_prompt: params.lmNegativePrompt || 'NO USER INPUT',
use_cot_metas: thinking ? (params.useCotMetas ?? true) : false,
use_cot_caption: thinking ? (params.useCotCaption ?? true) : false,
use_cot_language: thinking ? (params.useCotLanguage ?? true) : false,
allow_lm_batch: params.allowLmBatch ?? true,
constrained_decoding_debug: params.constrainedDecodingDebug ?? false,
lm_batch_chunk_size: params.lmBatchChunkSize ?? 8,
task_type: taskType,
instruction: params.instruction || 'Fill the audio semantic mask based on the given conditions:',
reference_audio_path: resolveWorkerAudioPath(params.referenceAudioUrl),
src_audio_path: resolveWorkerAudioPath(params.sourceAudioUrl),
audio_codes: params.audioCodes || '',
repainting_start: params.repaintingStart ?? 0.0,
repainting_end: params.repaintingEnd ?? -1,
audio_cover_strength: effectiveAudioStrength,
use_adg: params.useAdg ?? false,
cfg_interval_start: params.cfgIntervalStart ?? 0.0,
cfg_interval_end: params.cfgIntervalEnd ?? 1.0,
audio_format: params.audioFormat || 'flac',
mp3_bitrate: '320k',
mp3_sample_rate: 48000,
};
}
`;
text = text.slice(0, helperCommentStart) + namedHelpers + text.slice(helperEnd);
const processStart = text.indexOf('// processGeneration — Gradio primary');
const processEnd = text.indexOf('function isAudioFile', processStart);
if (processStart < 0 || processEnd < 0) throw new Error('legacy generation anchors missing');
const processCommentStart = text.lastIndexOf('// ---------------------------------------------------------------------------', processStart);
const namedProcess = `// ---------------------------------------------------------------------------
// processGeneration — official named REST API only
// ---------------------------------------------------------------------------
async function processGeneration(
jobId: string,
params: GenerationParams,
job: ActiveJob,
): Promise<void> {
job.status = 'running';
job.stage = 'Preparing named ACE-Step request...';
if ((params.taskType === 'cover' || params.taskType === 'audio2audio') && !params.sourceAudioUrl && !params.audioCodes) {
job.status = 'failed';
job.error = \`task_type='\${params.taskType}' requires source audio or audio codes\`;
return;
}
try {
if (params.ditModel) {
job.stage = \`Loading model \${params.ditModel}...\`;
await switchModelIfNeeded(params.ditModel);
}
const payload = buildReleaseTaskPayload(params);
console.log(\`Job \${jobId}: POST /release_task with named parameters\`, payload);
job.stage = 'Generating music via named ACE-Step API...';
const releaseResponse = await fetch(\`\${ACESTEP_API}/release_task\`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const releaseText = await releaseResponse.text();
if (!releaseResponse.ok) {
throw new Error(\`ACE-Step /release_task failed (\${releaseResponse.status}): \${releaseText}\`);
}
const release = JSON.parse(releaseText) as any;
if (release.code !== 200 || !release.data?.task_id) {
throw new Error(release.error || 'ACE-Step returned no task_id');
}
const taskId = String(release.data.task_id);
job.taskId = taskId;
const queryResponse = await fetch(\`\${ACESTEP_API}/query_result\`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id_list: [taskId] }),
});
const queryText = await queryResponse.text();
if (!queryResponse.ok) {
throw new Error(\`ACE-Step /query_result failed (\${queryResponse.status}): \${queryText}\`);
}
const query = JSON.parse(queryText) as any;
const taskResult = query.data?.[0];
const audioItems = taskResult?.result ? JSON.parse(taskResult.result) : [];
if (!Array.isArray(audioItems) || audioItems.length === 0) {
throw new Error('ACE-Step completed without downloadable audio results');
}
const audioUrls: string[] = [];
let actualDuration = 0;
for (const item of audioItems) {
if (!item?.url) continue;
const remoteUrl = new URL(item.url, ACESTEP_API).toString();
const remoteName = String(item.file || item.url);
const ext = path.extname(remoteName) || \`.\${params.audioFormat || 'flac'}\`;
const filename = \`\${jobId}_\${audioUrls.length}\${ext}\`;
const destPath = path.join(AUDIO_DIR, filename);
await downloadGradioAudioFile({ url: remoteUrl, orig_name: remoteName }, destPath);
if (audioUrls.length === 0) actualDuration = getAudioDuration(destPath);
audioUrls.push(\`/audio/\${filename}\`);
}
if (audioUrls.length === 0) throw new Error('ACE-Step returned no supported audio files');
const first = audioItems[0] || {};
job.status = 'succeeded';
job.result = {
audioUrls,
duration: actualDuration || Number(first.duration) || params.duration || 0,
bpm: Number(first.bpm) || params.bpm,
keyScale: first.keyscale || params.keyScale,
timeSignature: first.timesignature || params.timeSignature,
status: 'succeeded',
};
job.rawResponse = { release, query, transmittedParameters: payload };
console.log(\`Job \${jobId}: Completed via named REST API with \${audioUrls.length} audio files\`);
} catch (error) {
job.status = 'failed';
job.error = error instanceof Error ? error.message : String(error);
console.error(\`Job \${jobId}: Named REST generation failed\`, error);
}
}
`;
text = text.slice(0, processCommentStart) + namedProcess + text.slice(processEnd);
return text;
});
patch('server/src/services/storage/local.ts', (text) => {
text = replaceOnce(
text,
"import type { StorageProvider } from './index.js';",
"import type { StorageProvider } from './index.js';\nimport { config } from '../../config/index.js';",
'storage config import',
);
return replaceOnce(
text,
"const AUDIO_DIR = path.join(__dirname, '../../../public/audio');",
'const AUDIO_DIR = config.storage.audioDir;',
'persistent uploaded audio',
);
});
patch('server/src/index.ts', (text) => replaceOnce(
text,
"app.use('/audio', express.static(path.join(__dirname, '../public/audio')));",
"app.use('/audio', express.static(config.storage.audioDir));",
'persistent audio static route',
));
patch('server/src/routes/referenceTrack.ts', (text) => {
text = replaceOnce(
text,
"import { spawn } from 'child_process';",
"import { spawn } from 'child_process';\nimport { config } from '../config/index.js';",
'reference audio config import',
);
return replaceOnce(
text,
"const AUDIO_DIR = path.join(__dirname, '../../public/audio');",
'const AUDIO_DIR = config.storage.audioDir;',
'persistent reference audio',
);
});
patch('server/src/routes/generate.ts', (text) => {
text = replaceOnce(
text,
" thinking?: boolean;\n audioFormat?: 'mp3' | 'flac';",
" thinking?: boolean;\n enhance?: boolean;\n audioFormat?: 'mp3' | 'flac';",
'server enhance type',
);
const enhanceAnchor = ' thinking,\n audioFormat,';
if (text.split(enhanceAnchor).length - 1 !== 2) {
throw new Error('expected enhance anchor in destructuring and forwarding');
}
text = text.replaceAll(enhanceAnchor, ' thinking,\n enhance,\n audioFormat,');
text = replaceOnce(
text,
" const ALL_DIT_MODELS = [\n 'acestep-v15-turbo',",
" const ALL_DIT_MODELS = [\n 'acestep-v15-xl-sft', // Athena production model\n 'acestep-v15-turbo',",
'XL-SFT model list',
);
const start = text.indexOf("router.get('/limits'");
const end = text.indexOf("router.get('/debug/", start);
if (start < 0 || end < 0) throw new Error('limits route anchors missing');
const limits = `router.get('/limits', async (_req, res: Response) => {
// The UI container intentionally has no CUDA or ACE-Step Python runtime.
// These are the limits reported by Athena's dedicated RTX 5080 worker.
res.json({
tier: process.env.ACESTEP_TIER || 'tier5',
gpu_memory_gb: Number(process.env.ACESTEP_GPU_MEMORY_GB || 15.5),
max_duration_with_lm: Number(process.env.ACESTEP_MAX_DURATION_WITH_LM || 480),
max_duration_without_lm: Number(process.env.ACESTEP_MAX_DURATION_WITHOUT_LM || 600),
max_batch_size_with_lm: Number(process.env.ACESTEP_MAX_BATCH_WITH_LM || 4),
max_batch_size_without_lm: Number(process.env.ACESTEP_MAX_BATCH_WITHOUT_LM || 4),
});
});
`;
return text.slice(0, start) + limits + text.slice(end);
});
patch('services/api.ts', (text) => replaceOnce(
text,
" thinking?: boolean;\n audioFormat?: 'mp3' | 'flac';",
" thinking?: boolean;\n enhance?: boolean;\n audioFormat?: 'mp3' | 'flac';",
'client enhance type',
));
patch('App.tsx', (text) => {
text = replaceOnce(
text,
' thinking: params.thinking,\n audioFormat: params.audioFormat,',
' thinking: params.thinking,\n enhance: params.enhance,\n audioFormat: params.audioFormat,',
'client enhance forwarding',
);
return replaceOnce(
text,
' title: params.title,\n instrumental: params.instrumental,',
' title: params.title,\n ditModel: params.ditModel,\n instrumental: params.instrumental,',
'client model forwarding',
);
});
patch('components/CreatePanel.tsx', (text) => {
text = replaceOnce(text, 'useState(9.0);', 'useState(8.0);', 'guidance default');
text = replaceOnce(
text,
'useState(false); // Default false for GPU compatibility',
'useState(true); // Athena default: use the 1.7B planner for coherent structure',
'thinking default',
);
text = replaceOnce(text, "useState<'mp3' | 'flac'>('mp3');", "useState<'mp3' | 'flac'>('flac');", 'lossless default');
text = replaceOnce(text, 'useState(12);', 'useState(80);', 'XL-SFT steps default');
text = replaceOnce(text, "localStorage.getItem('ace-lmModel') || 'acestep-5Hz-lm-0.6B'", "localStorage.getItem('ace-lmModel') || 'acestep-5Hz-lm-1.7B'", 'planner model default');
// Upstream already defaults to Shift 3. Keep it instead of replacing it.
text = replaceOnce(
text,
' // Bulk generation: loop bulkCount times\n for (let i = 0; i < bulkCount; i++) {',
` const requestedText = customMode ? styleWithGender : songDescription;
const vocalRequestText = \`\${requestedText || ''}\\n\${lyrics}\`;
const explicitlyNoVocals = /\\b(no vocals?|without vocals?|instrumental only|kein(?:e[rs]?)? gesang|ohne gesang|keine stimme|ohne stimme)\\b/i.test(vocalRequestText);
const asksForVocals = !explicitlyNoVocals && /\\b(vocals?|singer|singing|male voice|female voice|gesang|stimme|sänger(?:in)?|singt)\\b/i.test(vocalRequestText);
if (!instrumental && asksForVocals && !lyrics.trim()) {
window.alert('Widerspruch: Der Auftrag verlangt Gesang, aber das Liedtextfeld ist leer. Bitte Text eintragen oder „Instrumental“ wählen.');
return;
}
if (instrumental && asksForVocals) {
window.alert('Widerspruch: „Instrumental“ ist aktiv, aber die Beschreibung verlangt Gesang. Bitte Gesangsbegriffe aus der Beschreibung entfernen oder „Instrumental“ deaktivieren und einen Liedtext eintragen.');
return;
}
if ((taskType === 'cover' || taskType === 'audio2audio') && !sourceAudioUrl.trim() && !audioCodes.trim()) {
window.alert('Für einen Cover-Auftrag fehlt das Quellaudio. Bitte unter „Quellaudio / Cover“ eine Datei auswählen.');
return;
}
const taskLabel = taskType === 'cover' || taskType === 'audio2audio' ? 'Cover / Audio-zu-Audio' : taskType;
const summary = [
'Folgende Parameter werden tatsächlich an Athena übertragen:',
'',
\`Aufgabe: \${taskLabel}\`,
\`Modell: \${selectedModel}\`,
\`Dauer: \${duration > 0 ? \`\${duration} Sekunden\` : 'automatisch'}\`,
\`Tempo: \${bpm > 0 ? \`\${bpm} BPM\` : 'automatisch'}\`,
\`Tonart: \${keyScale || 'automatisch'}\`,
\`Taktart: \${timeSignature || 'automatisch'}\`,
\`Thinking/Planung: \${thinking ? 'AN' : 'AUS'}\`,
\`AI Enhance: \${enhance ? 'AN' : 'AUS'}\`,
\`XL-SFT: \${inferenceSteps} Schritte, Guidance \${guidanceScale}, Shift \${shift}\`,
\`Gesang: \${instrumental ? 'nein (Instrumental)' : 'ja'}\`,
\`Referenzaudio (nur Klang/Produktion): \${referenceAudioUrl ? 'vorhanden' : 'keines'}\`,
\`Quellaudio (Melodie/Rhythmus/Akkorde): \${sourceAudioUrl ? 'vorhanden' : 'keines'}\`,
\`Audio-Einfluss: \${referenceAudioUrl && !sourceAudioUrl && taskType === 'text2music' ? '0,2 (sichere Stilreferenz)' : audioCoverStrength}\`,
\`Ausgabe: \${audioFormat.toUpperCase()}, \${batchSize} Variation(en), \${bulkCount} Auftrag/Aufträge\`,
'',
'Auftrag jetzt starten?',
].join('\\n');
if (!window.confirm(summary)) return;
// Bulk generation: loop bulkCount times
for (let i = 0; i < bulkCount; i++) {`,
'validation and transmitted parameter summary',
);
text = replaceOnce(
text,
" {t('reference')}\n </button>",
" Referenzaudio\n </button>",
'reference tab label',
);
text = replaceOnce(
text,
" {t('cover')}\n </button>",
" Quellaudio / Cover\n </button>",
'source tab label',
);
text = replaceOnce(
text,
' {/* Audio Content */}\n <div className="p-3 space-y-2">',
` {/* Audio Content */}
<div className="p-3 space-y-2">
<p className="text-[11px] leading-relaxed text-zinc-500 dark:text-zinc-400">
{audioTab === 'reference'
? 'Referenzaudio beeinflusst nur Klang, Instrumentierung und Produktion – nicht die Melodie.'
: 'Quellaudio / Cover erhält Melodie, Rhythmus und Akkorde des hochgeladenen Titels.'}
</p>`,
'audio semantics explanation',
);
return text;
});