408 lines
17 KiB
JavaScript
408 lines
17 KiB
JavaScript
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');
|
||
|
||
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 ?? 50,
|
||
guidance_scale: params.guidanceScale ?? 7.0,
|
||
shift: params.shift ?? 1.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: params.audioCoverStrength ?? 1.0,
|
||
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(7.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(50);', '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');
|
||
text = replaceOnce(text, 'useState(3.0);', 'useState(1.0);', 'shift default');
|
||
|
||
text = replaceOnce(
|
||
text,
|
||
' // Bulk generation: loop bulkCount times\n for (let i = 0; i < bulkCount; i++) {',
|
||
` const requestedText = customMode ? styleWithGender : songDescription;
|
||
const asksForVocals = /\\b(vocals?|singer|singing|male voice|female voice|gesang|stimme|sänger(?:in)?|singt)\\b/i.test(
|
||
\`\${requestedText || ''}\\n\${lyrics}\`,
|
||
);
|
||
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 ((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'}\`,
|
||
\`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;
|
||
});
|