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