Add dual ACE-Step music studio interfaces
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# ACE-Step 1.5 XL SFT: isolierter Athena-Test
|
||||
|
||||
Dieser Compose-Dienst ist absichtlich noch nicht Teil des produktiven Stacks.
|
||||
Er startet nicht automatisch, bindet die Testoberflaeche nur an localhost und
|
||||
kann mitsamt seinem Container entfernt werden, ohne Router oder Hermes zu
|
||||
veraendern. Gewichte und Ergebnisse liegen getrennt unter `/data`.
|
||||
Der GPU-Worker startet nur im Musikmodus und bindet seine rohe
|
||||
Gradio-Oberflaeche nur an localhost. Die separate `fspecii/ace-step-ui`
|
||||
bleibt als leichte React/Express-Oberflaeche aktiv; ihre SQLite-Datenbank,
|
||||
Bibliothek und Uploads liegen persistent unter `/data/music/ace-step-ui`.
|
||||
|
||||
Das offizielle Image ist auf den am 8. September 2026 geladenen Digest
|
||||
`sha256:95652cd780c78a1b1a7f6f0335530430f0ae53d96c7c12d59f9f39fa23d38567`
|
||||
@@ -30,6 +30,23 @@ bleiben bewusst vorgenommene Aenderungen wieder im jeweiligen Browser erhalten.
|
||||
Beim Wechsel des gepinnten Image-Digests muessen die Overrides gegen die neue
|
||||
Upstream-Fassung geprueft werden.
|
||||
|
||||
## Zwei Musikoberflaechen
|
||||
|
||||
Die originale Gradio-Oberflaeche aus demselben ACE-Step-Image ist der stabile
|
||||
Produktionspfad fuer Simple, Custom, Cover, Remix und Repaint. Sie ist im
|
||||
WireGuard-Netz unter `http://192.168.1.212:7862` erreichbar.
|
||||
|
||||
`music-ui` baut [fspecii/ace-step-ui](https://github.com/fspecii/ace-step-ui)
|
||||
reproduzierbar von Commit `a1fdf91829ec6f7b98844f80e323529cd155dbf2`.
|
||||
Ein lokaler Backport des noch offenen Upstream-PR
|
||||
[#109](https://github.com/fspecii/ace-step-ui/pull/109) gleicht das Frontend an
|
||||
das aktuelle 72-Felder-Gradio-Schema an. Athena-spezifisch sind nur die
|
||||
persistente Ablage, die XL-SFT-Anzeige und die gemeldeten Laufzeitlimits.
|
||||
Schlaegt bei einer spaeteren Upstream-Fassung ein Patch-Anker fehl, bricht der
|
||||
Image-Build ab. Die Community-UI unter `http://192.168.1.212:7861` ist bis zu
|
||||
vollstaendigen Ende-zu-Ende-Tests von Cover und Remix als experimentell
|
||||
gekennzeichnet.
|
||||
|
||||
## Start
|
||||
|
||||
Vor dem Start muessen das aktive llama.cpp-Profil und Qwen3-TTS beendet sein.
|
||||
@@ -37,24 +54,28 @@ Die RTX 5080 wird ueber ihre UUID exklusiv an den Container uebergeben.
|
||||
|
||||
```bash
|
||||
export ACESTEP_GPU_UUID="GPU-..."
|
||||
export ACESTEP_UI_JWT_SECRET="$(openssl rand -hex 32)"
|
||||
docker compose up -d music-ui
|
||||
docker compose --profile music-test up -d music-worker
|
||||
docker compose logs -f music-worker
|
||||
```
|
||||
|
||||
Die lokale Oberflaeche kann vom Mac aus ueber einen SSH-Tunnel erreicht werden:
|
||||
Alternativ zum WireGuard-Zugriff lassen sich beide Oberflaechen per SSH-Tunnel
|
||||
erreichen:
|
||||
|
||||
```bash
|
||||
ssh -L 7861:127.0.0.1:7861 root@athena.scc.kit.edu
|
||||
ssh -L 7861:127.0.0.1:7861 -L 7862:127.0.0.1:7862 root@athena.scc.kit.edu
|
||||
```
|
||||
|
||||
Danach im Browser `http://127.0.0.1:7861` aufrufen.
|
||||
`ace-step-ui` spricht den Worker ausschliesslich ueber das interne Docker-Netz
|
||||
an. Dafuer startet ACE-Step mit aktivierten, benannten API-Endpunkten
|
||||
(`--enable-api`).
|
||||
|
||||
## Beenden
|
||||
|
||||
```bash
|
||||
docker compose --profile music-test down
|
||||
docker compose --profile music-test stop music-worker
|
||||
```
|
||||
|
||||
Der Befehl entfernt nur den Testcontainer und das Testnetz. Caches,
|
||||
Modellgewichte und Ausgaben bleiben fuer reproduzierbare Messungen erhalten.
|
||||
Sie werden erst nach dokumentierter Ablehnung gezielt geloescht.
|
||||
Der Befehl stoppt nur den GPU-Worker. Die Musikoberflaeche, ihre Bibliothek,
|
||||
Caches, Modellgewichte und Ausgaben bleiben erhalten.
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
FROM node:22-bookworm AS build
|
||||
|
||||
ARG ACE_STEP_UI_COMMIT
|
||||
RUN test -n "$ACE_STEP_UI_COMMIT"
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN git clone https://github.com/fspecii/ace-step-ui.git /src \
|
||||
&& cd /src \
|
||||
&& git checkout --detach "$ACE_STEP_UI_COMMIT"
|
||||
COPY patch-source.mjs /tmp/patch-source.mjs
|
||||
RUN node /tmp/patch-source.mjs /src
|
||||
RUN cd /src \
|
||||
&& npm ci \
|
||||
&& npm run build
|
||||
RUN cd /src/server \
|
||||
&& npm ci \
|
||||
&& npm run build \
|
||||
&& npm prune --omit=dev
|
||||
|
||||
FROM node:22-bookworm-slim AS runtime
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nginx curl ca-certificates ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=build /src/dist /usr/share/nginx/html
|
||||
COPY --from=build /src/server/dist /app/server/dist
|
||||
COPY --from=build /src/server/node_modules /app/server/node_modules
|
||||
COPY --from=build /src/server/package.json /app/server/package.json
|
||||
COPY --from=build /src/server/public /app/server/public
|
||||
COPY --from=build /src/server/audio-editor /app/server/audio-editor
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY entrypoint.sh /usr/local/bin/ace-step-ui-entrypoint
|
||||
RUN chmod 0755 /usr/local/bin/ace-step-ui-entrypoint \
|
||||
&& mkdir -p /data/audio /data/datasets/uploads
|
||||
|
||||
EXPOSE 3000 3001
|
||||
ENTRYPOINT ["/usr/local/bin/ace-step-ui-entrypoint"]
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
node /app/server/dist/index.js &
|
||||
server_pid=$!
|
||||
|
||||
trap 'kill "$server_pid" 2>/dev/null || true' INT TERM EXIT
|
||||
nginx -g 'daemon off;' &
|
||||
nginx_pid=$!
|
||||
|
||||
while kill -0 "$server_pid" 2>/dev/null && kill -0 "$nginx_pid" 2>/dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
kill "$server_pid" "$nginx_pid" 2>/dev/null || true
|
||||
wait "$server_pid" 2>/dev/null || true
|
||||
wait "$nginx_pid" 2>/dev/null || true
|
||||
exit 1
|
||||
@@ -0,0 +1,35 @@
|
||||
worker_processes auto;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
client_max_body_size 512m;
|
||||
|
||||
server {
|
||||
listen 3000;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location ~ ^/(api|audio|editor|blog|demucs-web)(/|$) {
|
||||
proxy_pass http://127.0.0.1:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 1800s;
|
||||
proxy_send_timeout 1800s;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
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',
|
||||
);
|
||||
|
||||
const oldArgs = ` params.instruction || 'Fill the audio semantic mask with the style described in the text prompt.', // 17: Instruction
|
||||
params.audioCoverStrength ?? 1.0, // 18: Audio Cover Strength
|
||||
0.0, // 19: Cover Noise Strength (ACE-Step v1.5 new param, default 0.0)
|
||||
(params.taskType === 'audio2audio' ? 'cover' : params.taskType) || 'text2music', // 20: Task Type
|
||||
params.useAdg ?? false, // 21: Use ADG
|
||||
params.cfgIntervalStart ?? 0.0, // 22: CFG Interval Start
|
||||
params.cfgIntervalEnd ?? 1.0, // 23: CFG Interval End
|
||||
params.shift ?? 3.0, // 24: Shift
|
||||
params.inferMethod || 'ode', // 25: Inference Method
|
||||
params.customTimesteps || '', // 26: Custom Timesteps
|
||||
params.audioFormat || 'mp3', // 27: Audio Format
|
||||
params.lmTemperature ?? 0.85, // 28: LM Temperature
|
||||
isThinking, // 29: Think
|
||||
params.lmCfgScale ?? 2.0, // 30: LM CFG Scale
|
||||
params.lmTopK ?? 0, // 31: LM Top-K
|
||||
params.lmTopP ?? 0.9, // 32: LM Top-P
|
||||
params.lmNegativePrompt || 'NO USER INPUT', // 33: LM Negative Prompt
|
||||
useCot ? (params.useCotMetas ?? true) : false, // 34: CoT Metas
|
||||
useCot ? (params.useCotCaption ?? true) : false, // 35: CaptionRewrite
|
||||
useCot ? (params.useCotLanguage ?? true) : false, // 36: CoT Language
|
||||
params.isFormatCaption ?? false, // 37: Is Format Caption State
|
||||
params.constrainedDecodingDebug ?? false, // 38: Constrained Decoding Debug
|
||||
params.allowLmBatch ?? true, // 39: ParallelThinking
|
||||
params.getScores ?? false, // 40: Auto Score
|
||||
params.getLrc ?? false, // 41: Auto LRC (timestamped lyrics)
|
||||
params.scoreScale ?? 0.5, // 42: Quality Score Sensitivity (0.01-1.0)
|
||||
params.lmBatchChunkSize ?? 8, // 43: LM Batch Chunk Size
|
||||
params.trackName || null, // 44: Track Name
|
||||
params.completeTrackClasses || [], // 45: Track Names
|
||||
true, // 46: Enable Normalization (ACE-Step v1.5, default true)
|
||||
-1.0, // 47: Normalization DB (ACE-Step v1.5, default -1.0)
|
||||
0.0, // 48: Latent Shift (ACE-Step v1.5, default 0.0)
|
||||
1.0, // 49: Latent Rescale (ACE-Step v1.5, default 1.0)
|
||||
params.autogen ?? false, // 50: AutoGen`;
|
||||
|
||||
const newArgs = ` params.instruction || 'Fill the audio semantic mask based on the given conditions:', // 17: Instruction
|
||||
params.audioCoverStrength ?? 1.0, // 18: LM Codes Strength
|
||||
0.0, // 19: Cover Strength
|
||||
false, // 20: no_fsq
|
||||
params.useAdg ?? false, // 21: Use ADG
|
||||
params.cfgIntervalStart ?? 0.0, // 22: CFG Interval Start
|
||||
params.cfgIntervalEnd ?? 1.0, // 23: CFG Interval End
|
||||
params.shift ?? 1.0, // 24: Shift
|
||||
params.inferMethod || 'ode', // 25: Inference Method
|
||||
'euler', // 26: Sampler Mode
|
||||
0.0, // 27: Velocity Norm Threshold
|
||||
0.0, // 28: Velocity EMA Factor
|
||||
false, // 29: Enable DCW
|
||||
'double', // 30: DCW Mode
|
||||
0.02, // 31: DCW Scaler
|
||||
0.06, // 32: DCW High Scaler
|
||||
'haar', // 33: DCW Wavelet
|
||||
params.customTimesteps || '', // 34: Custom Timesteps
|
||||
params.audioFormat || 'flac', // 35: Audio Format
|
||||
'320k', // 36: MP3 Bitrate
|
||||
48000, // 37: MP3 Sample Rate
|
||||
params.lmTemperature ?? 0.85, // 38: LM Temperature
|
||||
isThinking, // 39: Think
|
||||
params.lmCfgScale ?? 2.0, // 40: LM CFG Scale
|
||||
params.lmTopK ?? 0, // 41: LM Top-K
|
||||
params.lmTopP ?? 0.9, // 42: LM Top-P
|
||||
params.lmNegativePrompt || 'NO USER INPUT', // 43: LM Negative Prompt
|
||||
useCot ? (params.useCotMetas ?? true) : false, // 44: CoT Metas
|
||||
useCot ? (params.useCotCaption ?? true) : false, // 45: CaptionRewrite
|
||||
useCot ? (params.useCotLanguage ?? true) : false, // 46: CoT Language
|
||||
params.constrainedDecodingDebug ?? false, // 47: Constrained Decoding Debug
|
||||
params.allowLmBatch ?? true, // 48: ParallelThinking
|
||||
params.getScores ?? false, // 49: Auto Score
|
||||
params.getLrc ?? false, // 50: Auto LRC
|
||||
params.scoreScale ?? 0.5, // 51: Quality Score Sensitivity
|
||||
params.lmBatchChunkSize ?? 8, // 52: LM Batch Chunk Size
|
||||
params.trackName || null, // 53: Track Name
|
||||
params.completeTrackClasses || [], // 54: Track Names
|
||||
true, // 55: Enable Normalization
|
||||
-1.0, // 56: Target Peak (dB)
|
||||
0.0, // 57: Fade In
|
||||
0.0, // 58: Fade Out
|
||||
0.0, // 59: Latent Shift
|
||||
1.0, // 60: Latent Rescale
|
||||
'balanced', // 61: Repaint Mode
|
||||
0.5, // 62: Repaint Strength
|
||||
0.0, // 63: variance
|
||||
'', // 64: repaint seed
|
||||
false, // 65: Edit
|
||||
null, // 66: source caption
|
||||
null, // 67: source lyrics
|
||||
0.0, // 68: n_min
|
||||
1.0, // 69: n_max
|
||||
1, // 70: n_avg
|
||||
params.autogen ?? false, // 71: AutoGen`;
|
||||
|
||||
return replaceOnce(text, oldArgs, newArgs, 'Athena Gradio argument schema');
|
||||
});
|
||||
|
||||
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,
|
||||
" 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);
|
||||
});
|
||||
@@ -15,7 +15,8 @@ services:
|
||||
ACESTEP_DEVICE: cuda
|
||||
# The image entrypoint forwards only ACESTEP_EXTRA_ARGS to the UI CLI.
|
||||
# One result per run avoids the batch=2 VRAM/time penalty.
|
||||
ACESTEP_EXTRA_ARGS: "--batch_size 1"
|
||||
# Named Gradio endpoints are consumed by the separate ace-step-ui service.
|
||||
ACESTEP_EXTRA_ARGS: "--batch_size 1 --enable-api"
|
||||
TOKENIZERS_PARALLELISM: "false"
|
||||
NVIDIA_VISIBLE_DEVICES: ${ACESTEP_GPU_UUID:?set ACESTEP_GPU_UUID to the RTX 5080 UUID}
|
||||
deploy:
|
||||
@@ -26,7 +27,8 @@ services:
|
||||
device_ids: ["${ACESTEP_GPU_UUID:?set ACESTEP_GPU_UUID to the RTX 5080 UUID}"]
|
||||
capabilities: [gpu]
|
||||
ports:
|
||||
- "127.0.0.1:${ACESTEP_UI_PORT:-7861}:7860"
|
||||
# Raw Gradio remains available for diagnostics; users open music-ui below.
|
||||
- "127.0.0.1:${ACESTEP_GRADIO_PORT:-7862}:7860"
|
||||
volumes:
|
||||
- ${ACESTEP_CHECKPOINTS_DIR:-/data/models/acestep/checkpoints}:/app/checkpoints
|
||||
- ${ACESTEP_HF_CACHE_DIR:-/data/models/acestep/hf-cache}:/root/.cache/huggingface
|
||||
@@ -45,3 +47,45 @@ services:
|
||||
timeout: 10s
|
||||
start_period: 300s
|
||||
retries: 3
|
||||
networks:
|
||||
- music
|
||||
- frontend
|
||||
|
||||
music-ui:
|
||||
build:
|
||||
context: ./ace-step-ui
|
||||
args:
|
||||
ACE_STEP_UI_COMMIT: a1fdf91829ec6f7b98844f80e323529cd155dbf2
|
||||
image: mike-ai/ace-step-ui:a1fdf918
|
||||
container_name: mike-ai-music-ui
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: "3001"
|
||||
FRONTEND_URL: ${ACESTEP_UI_PUBLIC_URL:-http://192.168.1.212:7861}
|
||||
ACESTEP_API_URL: http://music-worker:7860
|
||||
DATABASE_PATH: /data/acestep.db
|
||||
AUDIO_DIR: /data/audio
|
||||
DATASETS_DIR: /data/datasets
|
||||
DATASETS_UPLOADS_DIR: /data/datasets/uploads
|
||||
JWT_SECRET: ${ACESTEP_UI_JWT_SECRET:?set ACESTEP_UI_JWT_SECRET}
|
||||
ports:
|
||||
- "127.0.0.1:${ACESTEP_UI_PORT:-7861}:3000"
|
||||
volumes:
|
||||
- ${ACESTEP_UI_DATA_DIR:-/data/music/ace-step-ui}:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/ >/dev/null && curl -fsS http://127.0.0.1:3001/health >/dev/null"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
start_period: 30s
|
||||
retries: 3
|
||||
networks:
|
||||
- music
|
||||
- frontend
|
||||
|
||||
networks:
|
||||
music:
|
||||
name: mike-ai-music
|
||||
frontend:
|
||||
external: true
|
||||
name: mike-ai_frontend
|
||||
|
||||
Reference in New Issue
Block a user