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 - 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 - Whisper.cpp `large-v3-turbo` auf der CPU für lokale deutsche Spracherkennung
- Live-Dashboard mit 21 Tagen Detailhistorie auf Port 8099 - 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 - Portainer CE als optionale Container-Ansicht auf Port 9443
- WireGuard-Gateway, Datenbackup und Athena-Operator - WireGuard-Gateway, Datenbackup und Athena-Operator
- keine produktive Hermes-, OpenWebUI- oder portable Fach-MCP-Instanz - 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` - Router: `http://192.168.1.212:8081/v1`
- Athena-Dashboard: `http://192.168.1.212:8099` - 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 Der Betriebsmodus lässt sich dort direkt umschalten. In Hermes funktionieren
außerdem `/athena music`, `/athena llm` und `/athena status`; Details stehen in außerdem `/athena music`, `/athena llm` und `/athena status`; Details stehen in
+2 -1
View File
@@ -856,7 +856,8 @@ services:
DASHBOARD_PORT: "8099" DASHBOARD_PORT: "8099"
ROUTER_URL: http://router:8081 ROUTER_URL: http://router:8081
ROUTER_API_KEY: "${ROUTER_API_KEY:?ROUTER_API_KEY is required}" 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_PROC: /host/proc
HOST_DATA: /host/data HOST_DATA: /host/data
HOST_MODELS: /host/models HOST_MODELS: /host/models
+19 -2
View File
@@ -12,8 +12,25 @@ wird persistent gespeichert und beim Verlassen des Musikmodus wieder geladen.
## Bedienung ## Bedienung
Im Athena-Dashboard stehen die Schaltflächen **LLM-Betrieb** und Im Athena-Dashboard stehen die Schaltflächen **LLM-Betrieb** und
**Musikstudio** bereit. Nach dem Start des Musikmodus öffnet **Studio öffnen** **Musikstudio starten** bereit. Im Musikmodus werden zwei Oberflächen angeboten:
die über den SSH-Tunnel bereitgestellte ACE-Step-Oberfläche.
- **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 Hermes benötigt dafür kein Plugin. Exakt eingegebene Steuerbefehle werden vom
Router lokal beantwortet, auch wenn gerade kein LLM geladen ist: 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 | | 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 | | 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 | | 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 | | 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 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 gemeldete maximale CUDA-Allokation lag bei 9,38 GiB. Es gab weder OOM noch
CUDA-Fehler. Der technische Test ist damit bestanden; die subjektive 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) 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). 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 # ACE-Step 1.5 XL SFT: isolierter Athena-Test
Dieser Compose-Dienst ist absichtlich noch nicht Teil des produktiven Stacks. Der GPU-Worker startet nur im Musikmodus und bindet seine rohe
Er startet nicht automatisch, bindet die Testoberflaeche nur an localhost und Gradio-Oberflaeche nur an localhost. Die separate `fspecii/ace-step-ui`
kann mitsamt seinem Container entfernt werden, ohne Router oder Hermes zu bleibt als leichte React/Express-Oberflaeche aktiv; ihre SQLite-Datenbank,
veraendern. Gewichte und Ergebnisse liegen getrennt unter `/data`. Bibliothek und Uploads liegen persistent unter `/data/music/ace-step-ui`.
Das offizielle Image ist auf den am 8. September 2026 geladenen Digest Das offizielle Image ist auf den am 8. September 2026 geladenen Digest
`sha256:95652cd780c78a1b1a7f6f0335530430f0ae53d96c7c12d59f9f39fa23d38567` `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 Beim Wechsel des gepinnten Image-Digests muessen die Overrides gegen die neue
Upstream-Fassung geprueft werden. 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 ## Start
Vor dem Start muessen das aktive llama.cpp-Profil und Qwen3-TTS beendet sein. 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 ```bash
export ACESTEP_GPU_UUID="GPU-..." 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 --profile music-test up -d music-worker
docker compose logs -f 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 ```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 ## Beenden
```bash ```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, Der Befehl stoppt nur den GPU-Worker. Die Musikoberflaeche, ihre Bibliothek,
Modellgewichte und Ausgaben bleiben fuer reproduzierbare Messungen erhalten. Caches, Modellgewichte und Ausgaben bleiben erhalten.
Sie werden erst nach dokumentierter Ablehnung gezielt geloescht.
@@ -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 ACESTEP_DEVICE: cuda
# The image entrypoint forwards only ACESTEP_EXTRA_ARGS to the UI CLI. # The image entrypoint forwards only ACESTEP_EXTRA_ARGS to the UI CLI.
# One result per run avoids the batch=2 VRAM/time penalty. # 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" TOKENIZERS_PARALLELISM: "false"
NVIDIA_VISIBLE_DEVICES: ${ACESTEP_GPU_UUID:?set ACESTEP_GPU_UUID to the RTX 5080 UUID} NVIDIA_VISIBLE_DEVICES: ${ACESTEP_GPU_UUID:?set ACESTEP_GPU_UUID to the RTX 5080 UUID}
deploy: deploy:
@@ -26,7 +27,8 @@ services:
device_ids: ["${ACESTEP_GPU_UUID:?set ACESTEP_GPU_UUID to the RTX 5080 UUID}"] device_ids: ["${ACESTEP_GPU_UUID:?set ACESTEP_GPU_UUID to the RTX 5080 UUID}"]
capabilities: [gpu] capabilities: [gpu]
ports: 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: volumes:
- ${ACESTEP_CHECKPOINTS_DIR:-/data/models/acestep/checkpoints}:/app/checkpoints - ${ACESTEP_CHECKPOINTS_DIR:-/data/models/acestep/checkpoints}:/app/checkpoints
- ${ACESTEP_HF_CACHE_DIR:-/data/models/acestep/hf-cache}:/root/.cache/huggingface - ${ACESTEP_HF_CACHE_DIR:-/data/models/acestep/hf-cache}:/root/.cache/huggingface
@@ -45,3 +47,45 @@ services:
timeout: 10s timeout: 10s
start_period: 300s start_period: 300s
retries: 3 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 8085 tts-gateway:8085
start_proxy 8091 piper:8085 start_proxy 8091 piper:8085
start_proxy 8099 llama-dashboard:8099 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 8202 mcp-athena-operator:8000
start_proxy 9443 portainer:9443 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")) PORT = int(os.getenv("DASHBOARD_PORT", "8099"))
ROUTER_URL = os.getenv("ROUTER_URL", "http://router:8081").rstrip("/") ROUTER_URL = os.getenv("ROUTER_URL", "http://router:8081").rstrip("/")
ROUTER_API_KEY = os.getenv("ROUTER_API_KEY", "") 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_PROC = Path(os.getenv("HOST_PROC", "/host/proc"))
HOST_DATA = os.getenv("HOST_DATA", "/host/data") HOST_DATA = os.getenv("HOST_DATA", "/host/data")
HOST_MODELS = Path(os.getenv("HOST_MODELS", "/host/models")) 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)} .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} .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} .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> </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> <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"> <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">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">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> <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; 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 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); 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''' FULL_JS = r'''