Add dual ACE-Step music studio interfaces

This commit is contained in:
Mikei386
2026-09-08 18:46:54 +02:00
parent f58d61140e
commit a4e894fe70
12 changed files with 404 additions and 23 deletions
+4 -1
View File
@@ -15,7 +15,8 @@ Bild- und Sprachausgabe. **Hermes und die Fach-MCPs laufen auf Unraid.**
- Qwen3-TTS 1.7B auf der RTX 3060 mit Piper als CPU-Fallback
- Whisper.cpp `large-v3-turbo` auf der CPU für lokale deutsche Spracherkennung
- Live-Dashboard mit 21 Tagen Detailhistorie auf Port 8099
- Dashboard-Umschaltung zwischen LLM-Betrieb und ACE-Step-Musikstudio
- Dashboard-Umschaltung zwischen LLM-Betrieb und ACE-Step-Musikstudio mit
persistenter `fspecii/ace-step-ui`-Bibliothek
- Portainer CE als optionale Container-Ansicht auf Port 9443
- WireGuard-Gateway, Datenbackup und Athena-Operator
- keine produktive Hermes-, OpenWebUI- oder portable Fach-MCP-Instanz
@@ -121,6 +122,8 @@ Details, Installation, Prüfung und Rollback stehen in
- Router: `http://192.168.1.212:8081/v1`
- Athena-Dashboard: `http://192.168.1.212:8099`
- Musikstudio, Original UI (stabil): `http://192.168.1.212:7862`
- Musikstudio, Community UI (experimentell): `http://192.168.1.212:7861`
Der Betriebsmodus lässt sich dort direkt umschalten. In Hermes funktionieren
außerdem `/athena music`, `/athena llm` und `/athena status`; Details stehen in
+2 -1
View File
@@ -856,7 +856,8 @@ services:
DASHBOARD_PORT: "8099"
ROUTER_URL: http://router:8081
ROUTER_API_KEY: "${ROUTER_API_KEY:?ROUTER_API_KEY is required}"
MUSIC_UI_URL: "${MUSIC_UI_URL:-http://127.0.0.1:7861/}"
MUSIC_COMMUNITY_UI_URL: "${MUSIC_COMMUNITY_UI_URL:-http://192.168.1.212:7861/}"
MUSIC_ORIGINAL_UI_URL: "${MUSIC_ORIGINAL_UI_URL:-http://192.168.1.212:7862/}"
HOST_PROC: /host/proc
HOST_DATA: /host/data
HOST_MODELS: /host/models
+19 -2
View File
@@ -12,8 +12,25 @@ wird persistent gespeichert und beim Verlassen des Musikmodus wieder geladen.
## Bedienung
Im Athena-Dashboard stehen die Schaltflächen **LLM-Betrieb** und
**Musikstudio** bereit. Nach dem Start des Musikmodus öffnet **Studio öffnen**
die über den SSH-Tunnel bereitgestellte ACE-Step-Oberfläche.
**Musikstudio starten** bereit. Im Musikmodus werden zwei Oberflächen angeboten:
- **Original UI · stabil** öffnet die zum laufenden ACE-Step-Image gehörende
Gradio-Oberfläche. Sie ist für Cover, Remix und erweiterte Workflows der
verbindliche Produktionspfad.
- **Community UI · experimentell** öffnet `fspecii/ace-step-ui`. Die
CPU-leichte React/Express-Anwendung hält Bibliothek, Playlists und
Einstellungen in `/data/music/ace-step-ui`. Ein noch nicht übernommener
Upstream-Kompatibilitätsfix für die aktuelle 72-Felder-Gradio-API ist lokal
zurückportiert; normale Generierung funktioniert, Cover und Remix gelten bis
zu eigenen Ende-zu-Ende-Tests weiterhin als experimentell.
Die Community-Oberfläche ist im WireGuard-Netz unter
`http://192.168.1.212:7861`, die originale Gradio-Oberfläche unter
`http://192.168.1.212:7862` erreichbar. Beide Host-Ports bleiben zusätzlich
auf `127.0.0.1` gebunden und werden auf der Universitäts-Schnittstelle nicht
veröffentlicht. `ace-step-ui` ist reproduzierbar auf Commit
`a1fdf91829ec6f7b98844f80e323529cd155dbf2` fixiert und greift intern über das
Docker-Netz `mike-ai-music` auf `http://music-worker:7860` zu.
Hermes benötigt dafür kein Plugin. Exakt eingegebene Steuerbefehle werden vom
Router lokal beantwortet, auch wenn gerade kein LLM geladen ist:
+9 -2
View File
@@ -8,7 +8,7 @@ im zentralen Register `TESTED_MODELS.md` dokumentiert wurde.
| Prioritaet | Aufgabe | Kandidat | Geplanter Betrieb | Status |
|---:|---|---|---|---|
| 1 | Musik erzeugen und bearbeiten | `ACE-Step 1.5 XL SFT` mit `acestep-5Hz-lm-1.7B` | exklusives On-Demand-Profil auf der RTX 5080; CPU-Offload; Qwen, Vision und TTS werden waehrenddessen entladen | **technisch bestanden; Klangabnahme und Integration offen** |
| 1 | Musik erzeugen und bearbeiten | `ACE-Step 1.5 XL SFT` mit `acestep-5Hz-lm-1.7B` | exklusives On-Demand-Profil auf der RTX 5080; CPU-Offload; Qwen, Vision und TTS werden waehrenddessen entladen | **integriert; Klangabnahme laeuft** |
| 2 | Gesang und Instrumente trennen | BS-RoFormer Viperx 1297; alternativ MelBand-RoFormer, fuer Mehrspur `htdemucs_ft` | eigener Audio-Worker; GPU bevorzugt, CPU als langsamer Fallback | offen |
| 3 | Voice Cloning | vorhandenes `Qwen3-TTS-12Hz-1.7B-Base` | bestehender TTS-Worker auf der RTX 3060; zunaechst den eingebauten 3-Sekunden-Klonpfad freilegen | offen |
| 4 | Objekte lokalisieren und zaehlen | Grounding DINO oder RF-DETR | optionaler Vision-Worker; normales Erkennen bleibt beim vorhandenen Qwen-Vision-Projektor | offen |
@@ -47,7 +47,14 @@ Erster Messlauf am 8. September 2026: Ein 30-Sekunden-Instrumental wurde in
15,39 Sekunden erzeugt (LM 8,00 s, DiT 7,39 s, MP3-Encoding 0,82 s). Die
gemeldete maximale CUDA-Allokation lag bei 9,38 GiB. Es gab weder OOM noch
CUDA-Fehler. Der technische Test ist damit bestanden; die subjektive
Klangabnahme erfolgt ueber die geoeffnete Gradio-Oberflaeche.
Die originale ACE-Step-Gradio-Oberflaeche ist der stabile Produktionspfad. Die
persistente `fspecii/ace-step-ui`-Oberflaeche bleibt bis zur Abnahme aller
Audio-zu-Audio-Modi experimentell. Der lokale Backport des offenen Upstream-PR
[#109](https://github.com/fspecii/ace-step-ui/pull/109) wurde gegen die 72
Parameter des live veroeffentlichten `/generation_wrapper`-Schemas getestet.
Ein zehnsekündiger FLAC-Textauftrag lief am 8. September 2026 erfolgreich durch
UI, Express-Backend und Gradio-API und wurde in der persistenten Bibliothek
gespeichert; dieser Test belegt Cover und Remix ausdrücklich noch nicht.
Offizielle Referenzen: [ACE-Step 1.5](https://github.com/ace-step/ACE-Step-1.5)
und [REST-API](https://github.com/ace-step/ACE-Step-1.5/blob/main/docs/en/API.md).
+32 -11
View File
@@ -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);
});
+46 -2
View File
@@ -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
@@ -78,6 +78,8 @@ start_proxy 8081 router:8081
start_proxy 8085 tts-gateway:8085
start_proxy 8091 piper:8085
start_proxy 8099 llama-dashboard:8099
start_proxy 7861 music-ui:3000
start_proxy 7862 music-worker:7860
start_proxy 8202 mcp-athena-operator:8000
start_proxy 9443 portainer:9443
+12 -4
View File
@@ -20,7 +20,13 @@ HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0")
PORT = int(os.getenv("DASHBOARD_PORT", "8099"))
ROUTER_URL = os.getenv("ROUTER_URL", "http://router:8081").rstrip("/")
ROUTER_API_KEY = os.getenv("ROUTER_API_KEY", "")
MUSIC_UI_URL = os.getenv("MUSIC_UI_URL", "http://127.0.0.1:7861/")
MUSIC_COMMUNITY_UI_URL = os.getenv(
"MUSIC_COMMUNITY_UI_URL",
os.getenv("MUSIC_UI_URL", "http://192.168.1.212:7861/"),
)
MUSIC_ORIGINAL_UI_URL = os.getenv(
"MUSIC_ORIGINAL_UI_URL", "http://192.168.1.212:7862/"
)
HOST_PROC = Path(os.getenv("HOST_PROC", "/host/proc"))
HOST_DATA = os.getenv("HOST_DATA", "/host/data")
HOST_MODELS = Path(os.getenv("HOST_MODELS", "/host/models"))
@@ -624,11 +630,11 @@ HTML = r'''<!doctype html>
.span4 .metrics{grid-template-columns:repeat(2,1fr)}
.history-controls{display:flex;flex-wrap:wrap;gap:7px;margin:10px 0 14px}.history-controls button{border:1px solid var(--line);background:#09111b;color:var(--muted);padding:6px 10px;border-radius:8px;cursor:pointer}.history-controls button.active{color:var(--cyan);border-color:var(--cyan)}.chart-legend{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0 10px}.chart-legend button{border:1px solid var(--series);background:#09111b;color:var(--text);padding:6px 10px;border-radius:8px;cursor:pointer}.chart-legend button::before{content:'';display:inline-block;width:10px;height:3px;background:var(--series);margin:0 7px 3px 0}.chart-legend button.off{opacity:.4;text-decoration:line-through}.chart{width:100%;height:250px;display:block}.token-total{font-size:24px;font-weight:750;margin-top:5px}.history-note{color:var(--muted);font-size:11px;margin-top:8px}
.usage-list{display:grid;gap:13px;margin-top:8px}.usage-head{display:flex;justify-content:space-between;gap:14px;align-items:baseline}.usage-head b{font-size:16px}.usage-head span{color:var(--muted)}.usage-meta{display:flex;justify-content:space-between;gap:12px;color:var(--muted);font-size:11px;margin-top:5px}
.mode-row{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap}.mode-buttons{display:flex;gap:9px;flex-wrap:wrap}.mode-buttons button,.mode-buttons a{border:1px solid var(--line);background:#09111b;color:var(--text);padding:9px 14px;border-radius:9px;cursor:pointer;text-decoration:none;font:inherit}.mode-buttons button.active{border-color:var(--cyan);color:var(--cyan);box-shadow:inset 0 0 0 1px #45d7ff33}.mode-buttons button:disabled{opacity:.45;cursor:wait}.mode-buttons a[hidden]{display:none}.mode-error{color:var(--red)}
.mode-row{display:flex;align-items:center;justify-content:space-between;gap:18px;flex-wrap:wrap}.mode-buttons,.mode-buttons span{display:flex;gap:9px;flex-wrap:wrap}.mode-buttons button,.mode-buttons a{border:1px solid var(--line);background:#09111b;color:var(--text);padding:9px 14px;border-radius:9px;cursor:pointer;text-decoration:none;font:inherit}.mode-buttons button.active{border-color:var(--cyan);color:var(--cyan);box-shadow:inset 0 0 0 1px #45d7ff33}.mode-buttons button:disabled{opacity:.45;cursor:wait}.mode-buttons span[hidden]{display:none}.mode-buttons a.stable{border-color:#66e3a466;color:var(--green)}.mode-buttons a.experimental{border-color:#ffc65c66;color:var(--amber)}.mode-error{color:var(--red)}
</style></head><body><main>
<div class="top"><div><div class="eyebrow">Mike AI · Live Telemetry</div><h1>Athena llama.cpp Dashboard</h1></div><div class="live"><span class="dot" id="dot"></span><span id="updated">verbinde …</span></div></div>
<section class="grid">
<article class="card span12"><div class="mode-row"><div><div class="label">Athena Betriebsmodus</div><div class="value" id="operatingMode">–</div><div class="sub" id="modeStatus">Status wird geladen …</div></div><div class="mode-buttons"><button id="llmMode" onclick="setMode('llm')">LLM-Betrieb</button><button id="musicMode" onclick="setMode('music')">Musikstudio</button><a id="musicOpen" href="__MUSIC_UI_URL__" target="_blank" rel="noopener" hidden>Studio öffnen</a></div></div></article>
<article class="card span12"><div class="mode-row"><div><div class="label">Athena Betriebsmodus</div><div class="value" id="operatingMode">–</div><div class="sub" id="modeStatus">Status wird geladen …</div></div><div class="mode-buttons"><button id="llmMode" onclick="setMode('llm')">LLM-Betrieb</button><button id="musicMode" onclick="setMode('music')">Musikstudio starten</button><span id="musicOpen" hidden><a class="stable" href="__MUSIC_ORIGINAL_UI_URL__" target="_blank" rel="noopener">Original UI · stabil</a><a class="experimental" href="__MUSIC_COMMUNITY_UI_URL__" target="_blank" rel="noopener">Community UI · experimentell</a></span></div></div></article>
<article class="card span3"><div class="label">Aktives Profil</div><div class="value" id="profile">–</div><div class="sub" id="profileSub">Router wird abgefragt</div></article>
<article class="card span3"><div class="label">Modell</div><div class="value" id="model">–</div><div class="sub" id="modelSub">–</div></article>
<article class="card span3"><div class="label">CPU</div><div class="value" id="cpu">–</div><div class="bar"><div class="fill" id="cpuBar"></div></div><div class="sub" id="load">–</div></article>
@@ -669,7 +675,9 @@ const imagePhaseLabel=p=>({"stopping-qwen":"Qwen wird entladen","loading-image":
let modeBusy=false;
async function setMode(mode){if(modeBusy)return;modeBusy=true;$('llmMode').disabled=$('musicMode').disabled=true;$('modeStatus').textContent='Umschaltung angefordert …';try{let r=await fetch('/api/mode',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode})});let d=await r.json();if(!r.ok)throw Error(d?.error?.message||d?.error||`HTTP ${r.status}`);$('modeStatus').textContent='Umschaltung läuft …'}catch(e){$('modeStatus').textContent=e.message;$('modeStatus').classList.add('mode-error')}finally{modeBusy=false;setTimeout(refresh,250)}}
async function refresh(){try{let r=await fetch('/api/status',{cache:'no-store'});if(!r.ok)throw Error(`HTTP ${r.status}`);let d=await r.json(),c=d.cpu||{},m=c.memory||{},rt=d.router||{},up=rt.upstream||{},q=rt.qwen||{},lr=d.llama_runtime||{},img=rt.image||{},imageActive=img.phase&&img.phase!=='idle';let md=rt.mode||{},switchingMode=md.phase&&md.phase!=='ready';$('operatingMode').textContent=md.active==='music'?'Musikstudio':'LLM-Betrieb';$('modeStatus').textContent=switchingMode?`Umschaltung: ${md.phase}`:(md.last_error||`Musik-Worker: ${md.music_worker||'–'}${md.return_profile?` · Rückkehr zu ${md.return_profile}`:''}`);$('modeStatus').classList.toggle('mode-error',!!md.last_error);$('llmMode').classList.toggle('active',md.active==='llm');$('musicMode').classList.toggle('active',md.active==='music');$('llmMode').disabled=$('musicMode').disabled=modeBusy||switchingMode||!md.enabled;$('musicOpen').hidden=md.active!=='music';$('profile').textContent=imageActive?'Bildgenerierung':(rt.current_profile||'nicht geladen');$('profileSub').textContent=imageActive?imagePhaseLabel(img.phase):(rt.switching?`Wechsel zu ${rt.switching}`:`Kontext: ${up.ctx?up.ctx.toLocaleString('de-DE'):'–'} Token`);$('model').textContent=imageActive?(img.model||'Bildmodell'):(up.model||'–');$('modelSub').textContent=imageActive?`${img.model_loaded?'geladen':'wird vorbereitet'} · Worker ${img.worker||'–'}`:(lr.model_file|| (up.reachable?'llama.cpp erreichbar':'llama.cpp nicht erreichbar'));$('cpu').textContent=pct(c.usage_percent);$('cpuBar').style.width=`${c.usage_percent||0}%`;$('load').textContent=`${c.logical_cpus||'–'} Threads · Load ${(c.load||[]).join(' / ')}`;let rp=m.total?m.used/m.total*100:0;$('ram').textContent=pct(rp);$('ramBar').style.width=`${rp}%`;$('ramSub').textContent=`${gib(m.used)} / ${gib(m.total)}`;$('gpuCards').innerHTML=(d.gpus||[]).map(gpuCard).join('')||'<article class="card span12 error">Keine GPU-Daten verfügbar</article>';$('availability').textContent=imageActive?imagePhaseLabel(img.phase):(q.available?'bereit':'nicht bereit');$('availability').className=`value status ${(imageActive||q.available)?'':'bad'}`;$('activeChats').textContent=q.active_chats??'–';$('routerUptime').textContent=dur(rt.uptime_seconds);$('switching').textContent=imageActive?imagePhaseLabel(img.phase):(rt.switching||'nein');let disk=c.disk_data||{},dp=disk.total?disk.used/disk.total*100:null;$('dataDisk').textContent=pct(dp);$('processes').innerHTML=(d.gpu_processes||[]).map(p=>`<tr><td>${(d.gpus||[]).find(g=>g.uuid===p.gpu_uuid)?.index??'–'}</td><td>${p.name}</td><td>${p.pid}</td><td>${p.memory_mib??'–'} MiB</td></tr>`).join('')||'<tr><td colspan="4">Keine Compute-Prozesse gemeldet</td></tr>';let runtime=[['Modell-Datei',lr.model_file],['PID',lr.pid],['Kontext',lr.context_size?lr.context_size.toLocaleString('de-DE'):'–'],['Batch / µBatch',`${lr.batch_size??'–'} / ${lr.ubatch_size??'–'}`],['Parallel',lr.parallel],['Threads',`${lr.threads??'–'} / ${lr.threads_batch??'–'}`],['Geräte',lr.device],['Tensor-Split',lr.tensor_split],['KV-Cache',`${lr.cache_k??'–'} / ${lr.cache_v??'–'}`],['Flash Attention',lr.flash_attention?'an':'aus'],['Prompt-Cache',lr.prompt_cache?'an':'aus'],['MTP Draft',lr.mtp_draft_tokens]];$('runtime').innerHTML=runtime.map(([k,v])=>`<div class="metric"><b>${v??'–'}</b><span>${k}</span></div>`).join('');let es=Object.entries(d.errors||{}).filter(([,v])=>v);$('errors').hidden=!es.length;$('errors').textContent=es.map(([k,v])=>`${k}: ${v}`).join('\n');$('updated').textContent=`Live · ${new Date(d.timestamp*1000).toLocaleTimeString('de-DE')}`;$('dot').style.background='var(--green)'}catch(e){$('updated').textContent=`Verbindung gestört: ${e.message}`;$('dot').style.background='var(--red)'}}refresh();setInterval(refresh,1000);
</script><script src="/full.js"></script><script src="/history.js"></script></body></html>'''.replace("__MUSIC_UI_URL__", MUSIC_UI_URL)
</script><script src="/full.js"></script><script src="/history.js"></script></body></html>'''.replace(
"__MUSIC_ORIGINAL_UI_URL__", MUSIC_ORIGINAL_UI_URL
).replace("__MUSIC_COMMUNITY_UI_URL__", MUSIC_COMMUNITY_UI_URL)
FULL_JS = r'''