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 { 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 { 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 ", " Referenzaudio\n ", 'reference tab label', ); text = replaceOnce( text, " {t('cover')}\n ", " Quellaudio / Cover\n ", 'source tab label', ); text = replaceOnce( text, ' {/* Audio Content */}\n
', ` {/* Audio Content */}

{audioTab === 'reference' ? 'Referenzaudio beeinflusst nur Klang, Instrumentierung und Produktion – nicht die Melodie.' : 'Quellaudio / Cover erhält Melodie, Rhythmus und Akkorde des hochgeladenen Titels.'}

`, 'audio semantics explanation', ); return text; });