From af25425aeee3da307a80db973cfbae0d7f174d73 Mon Sep 17 00:00:00 2001
From: Mikei386 <44135113+Mikei386@users.noreply.github.com>
Date: Thu, 10 Sep 2026 22:13:32 +0200
Subject: [PATCH] Integrate pinned YuE2 community WebUI
---
experiments/yue2-3b/Dockerfile | 15 ++++
experiments/yue2-3b/README.md | 30 +++++--
.../community-webui-patches/patch_webui.py | 83 +++++++++++++++++++
experiments/yue2-3b/compose.yaml | 54 ++++++++++--
4 files changed, 165 insertions(+), 17 deletions(-)
create mode 100644 experiments/yue2-3b/community-webui-patches/patch_webui.py
diff --git a/experiments/yue2-3b/Dockerfile b/experiments/yue2-3b/Dockerfile
index e0e170f..34d04df 100644
--- a/experiments/yue2-3b/Dockerfile
+++ b/experiments/yue2-3b/Dockerfile
@@ -1,6 +1,7 @@
FROM python:3.12-slim-bookworm
ARG YUE2_COMMIT=9c6c4b349be978b06a9d0d958471a07a6cdeff4d
+ARG YUE2_WEBUI_COMMIT=8fc05609bde5dcd345d7c6d57fff3da0839164c5
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git libsndfile1 \
@@ -10,6 +11,20 @@ RUN git clone https://github.com/multimodal-art-projection/YuE.git /opt/yue2 \
&& git -C /opt/yue2 checkout "${YUE2_COMMIT}" \
&& python -m pip install --no-cache-dir /opt/yue2
+# The community console is intentionally layered onto the already verified,
+# pinned YuE2 runtime. We use only its webui/ directory; its bundled YuE2 fork
+# and Windows installers never become part of the Athena image.
+RUN git clone https://github.com/Ladypoly/YuE2_WebUI.git /tmp/yue2-webui \
+ && git -C /tmp/yue2-webui checkout "${YUE2_WEBUI_COMMIT}" \
+ && cp -a /tmp/yue2-webui/webui /opt/yue2/webui \
+ && cp /tmp/yue2-webui/LICENSE /opt/yue2/YuE2_WebUI-LICENSE \
+ && python -m pip install --no-cache-dir -r /opt/yue2/webui/requirements.txt \
+ && rm -rf /tmp/yue2-webui
+
+COPY community-webui-patches /tmp/community-webui-patches
+RUN python /tmp/community-webui-patches/patch_webui.py /opt/yue2/webui \
+ && rm -rf /tmp/community-webui-patches
+
COPY ui /opt/yue2-playground
WORKDIR /workspace
diff --git a/experiments/yue2-3b/README.md b/experiments/yue2-3b/README.md
index 6eeb728..67e1a93 100644
--- a/experiments/yue2-3b/README.md
+++ b/experiments/yue2-3b/README.md
@@ -30,19 +30,33 @@ image is built, but no YuE2 container has been created or started.
The initial control request is a true empty-lyrics instrumental request. No
invented `[Instrumental]` lyrics marker is used.
-## Temporary playground
+## Community WebUI
-The optional `playground` profile provides a small German web interface for
-new-song generation. It lists persistent results, streams FLAC files with HTTP
-range support and permits explicit deletion. It is bound only to Athena's
-localhost on port 8014 and is not integrated into the profile router yet. The
-web process itself does not load the model; a click on **Song generieren**
-starts one exclusive offline generation job.
+The `community-ui` profile runs Ladypoly's YuE2 WebUI at the pinned commit
+`8fc05609bde5dcd345d7c6d57fff3da0839164c5`. Only the WebUI layer is copied
+from that repository. Athena continues to use the verified official YuE2
+`0.1.6` runtime and the existing model cache; the WebUI fork's bundled model
+code and Windows installers are not used.
+
+The UI is bound only to Athena's localhost on port 8014 and stores complete
+takes below `/data/music/yue2`. Optional Windows-only installers for llama.cpp
+and stable-diffusion.cpp are not part of the Athena setup. Manual composition,
+generation, result playback, score editing and the take library work without
+those optional components.
+
+The small integration patch under `community-webui-patches/` fixes the
+community release's missing `refreshArt()` function on Linux and permits the
+native YuE2 empty-lyrics request for true instrumentals. It deliberately does
+not modify the model runtime.
```sh
-docker compose --profile playground up -d yue2-ui
+docker compose --profile community-ui up -d yue2-ui
```
+The original small German playground remains available as a stopped fallback
+on localhost port 8016 through the `playground-fallback` profile. Do not run
+both frontends concurrently because both can submit work to the same GPU.
+
## Manual test (only after GPU availability was checked)
From `/opt/mike-ai/yue2-3b` on Athena:
diff --git a/experiments/yue2-3b/community-webui-patches/patch_webui.py b/experiments/yue2-3b/community-webui-patches/patch_webui.py
new file mode 100644
index 0000000..515e2f2
--- /dev/null
+++ b/experiments/yue2-3b/community-webui-patches/patch_webui.py
@@ -0,0 +1,83 @@
+"""Small Linux integration fixes for the pinned community WebUI.
+
+Keep these transformations explicit and fail the image build if upstream moves
+the expected code. That prevents a future upstream update from silently
+producing a half-patched console.
+"""
+from pathlib import Path
+import sys
+
+
+root = Path(sys.argv[1])
+app_js = root / "static" / "app.js"
+index_html = root / "static" / "index.html"
+server_py = root / "server.py"
+
+
+def replace_once(path: Path, old: str, new: str) -> None:
+ text = path.read_text(encoding="utf-8")
+ if text.count(old) != 1:
+ raise RuntimeError(f"expected exactly one patch marker in {path}: {old[:80]!r}")
+ path.write_text(text.replace(old, new, 1), encoding="utf-8")
+
+
+# The published UI calls refreshArt() during startup, but does not define it.
+# Athena does not install the Windows-only stable-diffusion.cpp helper, so show
+# that state honestly and keep the optional controls inert instead of crashing
+# the whole page.
+art_marker = ''' $("artDirAdd").addEventListener("click", function () {
+'''
+art_fix = ''' function refreshArt() {
+ return api("/api/art/status").then(function (data) {
+ STATE.art = data;
+ var installed = !!data.installed;
+ var supported = !!data.supported;
+ $("artState").textContent = data.busy || (installed ? "installed" :
+ (supported ? "not installed" : "not available on Linux"));
+ $("artState").dataset.s = data.busy ? "busy" : (installed ? "ready" : "missing");
+ $("artInstall").disabled = !!data.busy || !supported;
+ $("artInstall").textContent = installed ? "Reinstall" : "Install";
+ $("setArtAuto").checked = !!data.auto;
+ $("setArtAuto").disabled = !installed;
+ $("setArtModel").innerHTML = (data.models || []).length
+ ? data.models.map(function (m) {
+ return '';
+ }).join("")
+ : '';
+ if (data.selected) $("setArtModel").value = data.selected;
+ $("setArtModel").disabled = !installed;
+ $("artDir").disabled = !supported;
+ $("artDirAdd").disabled = !supported;
+ $("artDirs").textContent = supported
+ ? ((data.dirs || []).length ? "Also scanning: " + data.dirs.join(" · ") : "No extra folders configured.")
+ : "The community release only provides the cover-art installer for Windows; song generation is unaffected.";
+ $("artCatalog").innerHTML = "";
+ return data;
+ }).catch(function (error) {
+ $("artState").textContent = "unavailable";
+ $("artInstall").disabled = true;
+ $("artDirs").textContent = error.message;
+ });
+ }
+
+ $("artDirAdd").addEventListener("click", function () {
+'''
+replace_once(app_js, art_marker, art_fix)
+
+# YuE2 natively supports an empty lyric string for true instrumentals. The
+# community server rejected that valid request even though the browser did not.
+replace_once(
+ server_py,
+ ''' if not body.style.strip() or not body.lyrics.strip():
+ raise HTTPException(400, "A style prompt and lyrics are both required")
+''',
+ ''' if not body.style.strip():
+ raise HTTPException(400, "A style prompt is required")
+''',
+)
+
+replace_once(
+ index_html,
+ '''Lyrics section tags on their own line''',
+ '''Lyrics section tags on their own line · leave empty for a true instrumental''',
+)
diff --git a/experiments/yue2-3b/compose.yaml b/experiments/yue2-3b/compose.yaml
index d3d0a04..d76a35b 100644
--- a/experiments/yue2-3b/compose.yaml
+++ b/experiments/yue2-3b/compose.yaml
@@ -5,6 +5,7 @@ services:
context: .
args:
YUE2_COMMIT: 9c6c4b349be978b06a9d0d958471a07a6cdeff4d
+ YUE2_WEBUI_COMMIT: 8fc05609bde5dcd345d7c6d57fff3da0839164c5
image: mike-ai/yue2:3b-0.1.6
restart: "no"
environment:
@@ -23,22 +24,63 @@ services:
capabilities: [gpu]
yue2-ui:
- profiles: ["playground"]
+ profiles: ["community-ui"]
build:
context: .
args:
YUE2_COMMIT: 9c6c4b349be978b06a9d0d958471a07a6cdeff4d
+ YUE2_WEBUI_COMMIT: 8fc05609bde5dcd345d7c6d57fff3da0839164c5
image: mike-ai/yue2:3b-0.1.6
container_name: mike-ai-yue2-playground
restart: "no"
+ entrypoint: ["python", "/opt/yue2/webui/server.py"]
+ environment:
+ NVIDIA_VISIBLE_DEVICES: "GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe"
+ NVIDIA_DRIVER_CAPABILITIES: compute,utility
+ YUE2_HOST: "0.0.0.0"
+ YUE2_PORT: "8014"
+ YUE2_OUTPUTS: "/workspace/runs"
+ HF_HUB_OFFLINE: "1"
+ TRANSFORMERS_OFFLINE: "1"
+ ports:
+ - "127.0.0.1:8014:8014"
+ volumes:
+ - /data/models/yue2:/opt/yue2/models:ro
+ - /data/music/yue2:/workspace/runs
+ deploy:
+ resources:
+ reservations:
+ devices:
+ - driver: nvidia
+ device_ids: ["GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe"]
+ capabilities: [gpu]
+ healthcheck:
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8014/api/state', timeout=2)"]
+ interval: 5s
+ timeout: 3s
+ retries: 12
+ start_period: 5s
+
+ # Kept as a deliberately non-default fallback until the community console
+ # has completed a real generation on Athena.
+ yue2-playground-fallback:
+ profiles: ["playground-fallback"]
+ build:
+ context: .
+ args:
+ YUE2_COMMIT: 9c6c4b349be978b06a9d0d958471a07a6cdeff4d
+ YUE2_WEBUI_COMMIT: 8fc05609bde5dcd345d7c6d57fff3da0839164c5
+ image: mike-ai/yue2:3b-0.1.6
+ container_name: mike-ai-yue2-playground-fallback
+ restart: "no"
entrypoint: ["python", "/opt/yue2-playground/server.py"]
environment:
NVIDIA_VISIBLE_DEVICES: "GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe"
NVIDIA_DRIVER_CAPABILITIES: compute,utility
YUE2_UI_HOST: "0.0.0.0"
- YUE2_UI_PORT: "8014"
+ YUE2_UI_PORT: "8016"
ports:
- - "127.0.0.1:8014:8014"
+ - "127.0.0.1:8016:8016"
volumes:
- /data/models/yue2:/workspace/models:ro
- /data/music/yue2:/workspace/runs
@@ -49,9 +91,3 @@ services:
- driver: nvidia
device_ids: ["GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe"]
capabilities: [gpu]
- healthcheck:
- test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8014/health', timeout=2)"]
- interval: 5s
- timeout: 3s
- retries: 12
- start_period: 5s