From 0ca40bc624c5d0090c70e8db5a36024245b939f4 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:03:48 +0200 Subject: [PATCH] Run YuE2 music profiles through audio.cpp with WAV API and test UI --- audio-cpp-ui.js | 4 +- audio_cpp_runtime.py | 2 +- backup.py | 3 +- deploy/Dockerfile | 4 +- deploy/Dockerfile.app-update | 4 +- deploy/install.py | 4 +- docs/MUSIC_API.md | 25 +++++++++++ docs/RUNTIME_DISCOVERY.md | 4 +- endpoint-ui.js | 2 +- endpoint.py | 26 ++++++++--- execution_setup.py | 4 ++ index.html | 2 +- music-ui.js | 10 +++++ music.py | 85 ++++++++++++++++++++++++++++++++++++ profiles.py | 3 +- server.py | 23 ++++++++-- studio.js | 3 +- test_audio_cpp_runtime.py | 2 +- test_endpoint.py | 11 +++++ test_music.py | 53 ++++++++++++++++++++++ 20 files changed, 248 insertions(+), 26 deletions(-) create mode 100644 docs/MUSIC_API.md create mode 100644 music-ui.js create mode 100644 music.py create mode 100644 test_music.py diff --git a/audio-cpp-ui.js b/audio-cpp-ui.js index 455cdd6..a1b81cc 100644 --- a/audio-cpp-ui.js +++ b/audio-cpp-ui.js @@ -2,9 +2,9 @@ window.AudioCppUI=(()=>{ const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); async function api(path='',data){const r=await fetch('/api/v1/audio-cpp-runtime'+path,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const s=await r.json();if(!r.ok)throw Error(s.error||'Anfrage fehlgeschlagen');return s;} function render(){const view=document.querySelector('#view');if(view.querySelector('#audio-cpp-page'))return; - view.innerHTML='

audio.cpp

Gemeinsame C++-/CUDA-Laufzeit für unterstützte Audio-Modellfamilien. Passendes GGUF-Paket und ein Deck-Worker sind zusätzlich erforderlich.

Herunterladen und bereitmachen

Eigene Build-Ausgabe

YuE2 und Ladypoly WebUI

Modellfamilie erkannt. Für Generierung fehlen noch der Deck-Musikworker und die Verbindung zur Ladypoly-Oberfläche. Dieser Build lädt keine Gewichte, startet kein Modell und gibt kein Profil am Endpunkt frei.

Zur Musik-Bibliothek →
'; + view.innerHTML='

audio.cpp

Gemeinsame C++-/CUDA-Laufzeit für unterstützte Audio-Modellfamilien. Passendes GGUF-Paket und ein Deck-Worker sind zusätzlich erforderlich.

Herunterladen und bereitmachen

Eigene Build-Ausgabe

YuE2 und Ladypoly WebUI

Modellfamilie erkannt. Für Generierung das Musikprofil und seine Komponenten einrichten, dann Musik → Testen verwenden. Die Ladypoly-Oberfläche ist separat. Dieser Build lädt keine Gewichte, startet kein Modell und gibt kein Profil am Endpunkt frei.

Zur Musik-Bibliothek →
'; const root=view.firstChild;let timer; - async function poll(){if(!root.isConnected)return;try{const s=await api();if(!root.isConnected)return;const running=s.job?.state==='running';root.querySelector('#ac-install').disabled=s.installed||running;root.querySelector('#ac-cancel').disabled=!running;root.querySelector('#ac-status').innerHTML=`

${s.installed?'Installiert · Worker-Anbindung noch offen':'Noch nicht installiert'}

Version ${e(s.version)} · Commit ${e(s.revision)}

Mindestens ${e(s.required_disk_gib)} GiB freier Speicher. Eigener Build mit sechs Jobs, CUDA-Ziele werden aus den vorhandenen GPUs ermittelt. Bestehende Laufzeiten werden nicht verändert.

${s.job?`

${e(s.job.state)} · ${e(s.job.phase)}

${running?'':''}`:''}`;if(s.job){const log=await api('/log');if(root.isConnected)root.querySelector('#ac-log').textContent=log.text;}}catch(x){if(root.isConnected)root.querySelector('#ac-message').textContent=x.message;}if(root.isConnected){clearTimeout(timer);timer=setTimeout(poll,3000);}} + async function poll(){if(!root.isConnected)return;try{const s=await api();if(!root.isConnected)return;const running=s.job?.state==='running';root.querySelector('#ac-install').disabled=s.installed||running;root.querySelector('#ac-cancel').disabled=!running;root.querySelector('#ac-status').innerHTML=`

${s.installed?'Installiert · YuE2-Musikworker verfügbar':'Noch nicht installiert'}

Version ${e(s.version)} · Commit ${e(s.revision)}

Mindestens ${e(s.required_disk_gib)} GiB freier Speicher. Eigener Build mit sechs Jobs, CUDA-Ziele werden aus den vorhandenen GPUs ermittelt. Bestehende Laufzeiten werden nicht verändert.

${s.job?`

${e(s.job.state)} · ${e(s.job.phase)}

${running?'':''}`:''}`;if(s.job){const log=await api('/log');if(root.isConnected)root.querySelector('#ac-log').textContent=log.text;}}catch(x){if(root.isConnected)root.querySelector('#ac-message').textContent=x.message;}if(root.isConnected){clearTimeout(timer);timer=setTimeout(poll,3000);}} for(const [id,path] of [['ac-install','/install'],['ac-cancel','/cancel']])root.querySelector('#'+id).onclick=async()=>{root.querySelector('#'+id).disabled=true;try{await api(path,{});root.querySelector('#ac-message').textContent='Aktion angenommen.';}catch(x){root.querySelector('#ac-message').textContent=x.message;}poll();};poll();} return {render}; })(); diff --git a/audio_cpp_runtime.py b/audio_cpp_runtime.py index 8a5501a..d1f4e54 100644 --- a/audio_cpp_runtime.py +++ b/audio_cpp_runtime.py @@ -27,7 +27,7 @@ class AudioCppRuntime(LTXOriginalRuntime): return self.root/'missing-cli',self.root/'missing-source' def status(self): cli,source=self.paths() - return dict(installed=cli.is_file() and (source/'model_specs/yue2.json').is_file(),revision=REVISION,version=VERSION,job=dict(self.job) if self.job else None,required_disk_gib=6,adapter_ready=False,origin='0xShug0/audio.cpp',message='Gemeinsame Laufzeit. Ein Build startet kein Modell; YuE2-Worker und Ladypoly-Anbindung sind noch nicht eingerichtet.') + return dict(installed=cli.is_file() and (source/'model_specs/yue2.json').is_file(),revision=REVISION,version=VERSION,job=dict(self.job) if self.job else None,required_disk_gib=6,adapter_ready=True,origin='0xShug0/audio.cpp',message='Gemeinsame Laufzeit. Ein Build startet kein Modell; Der YuE2-Musikworker ist angebunden; Profil und Komponenten werden benötigt. Andere Familien brauchen eigene geprüfte Anbindungen.') def start(self): if sys.platform!='linux':raise ValueError('audio.cpp wird auf dem Debian-Server gebaut.') missing=[name for name in ('git','cmake','g++','nvcc','nvidia-smi') if not shutil.which(name)] diff --git a/backup.py b/backup.py index 9c80d5f..2cf6057 100644 --- a/backup.py +++ b/backup.py @@ -202,7 +202,7 @@ class Backup: if len(set(data['services']))!=len(data['services']) or any(name not in services for name in data['services']):raise ValueError('Unbekannter Zusatzdienst.') if any(not services[name].get('registry') and not services[name].get('build_recipe') for name in data['services']):raise ValueError('Gewählter Dienst hat keine Wiederherstellungsquelle.') s=self.server - if s.catalog.pending or s.catalog.job and s.catalog.job['state']=='downloading' or s.runtime.busy or any((getattr(o,'job',None) or {}).get('state')=='running' for o in (s.image_runtime,s.tts_runtime,s.ltx_original_runtime,getattr(s,'audio_cpp_runtime',None),s.prompt_enhancer,s.auto_tests,s.chat_tests,s.image_tests,s.tts_tests,s.stt)):raise ValueError('Zuerst laufende Downloads, Builds und Tests abschließen oder abbrechen.') + if s.catalog.pending or s.catalog.job and s.catalog.job['state']=='downloading' or s.runtime.busy or any((getattr(o,'job',None) or {}).get('state')=='running' for o in (s.image_runtime,s.tts_runtime,s.ltx_original_runtime,getattr(s,'audio_cpp_runtime',None),getattr(s,'music',None),s.prompt_enhancer,s.auto_tests,s.chat_tests,s.image_tests,s.tts_tests,s.stt)):raise ValueError('Zuerst laufende Downloads, Builds und Tests abschließen oder abbrechen.') self.cancel.clear();self.job=dict(id=secrets.token_hex(16),state='running',phase='Wiederherstellung vorbereiten',completed=0,total=len(doc['models'])+len(data['services'])+5,items=[],started_at=time.time(),theme=doc.get('theme','deck')) self._save();threading.Thread(target=self._restore,args=(copy.deepcopy(doc),data['services'],data['restore_credentials']),daemon=True).start();self.plan=None return self.status() @@ -370,6 +370,7 @@ class Backup: self.cancel.set() if self.busy(): self.server.catalog.stop();self.server.runtime.stop();self.server.image_runtime.stop();self.server.tts_runtime.stop();self.server.ltx_original_runtime.stop(); + if getattr(self.server,'music',None):self.server.music.stop() if getattr(self.server,'audio_cpp_runtime',None):self.server.audio_cpp_runtime.stop() self.server.prompt_enhancer.stop() return {'cancellation_requested':True} diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 83b7483..16182ab 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -16,8 +16,8 @@ WORKDIR /app COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock COPY deploy/ltx-original-requirements.lock /app/deploy/ltx-original-requirements.lock -COPY audio_cpp_runtime.py ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ -COPY audio-cpp-ui.js ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ +COPY music.py audio_cpp_runtime.py ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ +COPY music-ui.js audio-cpp-ui.js ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ COPY deploy/docker_backup.py /app/deploy/docker_backup.py COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ diff --git a/deploy/Dockerfile.app-update b/deploy/Dockerfile.app-update index 4822bb5..d0edf06 100644 --- a/deploy/Dockerfile.app-update +++ b/deploy/Dockerfile.app-update @@ -6,8 +6,8 @@ WORKDIR /app COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock COPY deploy/ltx-original-requirements.lock /app/deploy/ltx-original-requirements.lock -COPY audio_cpp_runtime.py ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ -COPY audio-cpp-ui.js ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ +COPY music.py audio_cpp_runtime.py ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ +COPY music-ui.js audio-cpp-ui.js ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ COPY deploy/docker_backup.py /app/deploy/docker_backup.py COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ diff --git a/deploy/install.py b/deploy/install.py index 7fad487..776f044 100644 --- a/deploy/install.py +++ b/deploy/install.py @@ -14,7 +14,7 @@ import urllib.request ROOT = Path(__file__).resolve().parent.parent LABEL = 'de.casaderoll.athena-deck.standalone' -FILES = ['audio_cpp_runtime.py','audio-cpp-ui.js','ltx_original_runtime.py','ltx_original_entry.py','video_original.py','ltx-original-ui.js','deploy/ltx-original-requirements.lock','backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','deploy/swarm-ui/Dockerfile','deploy/swarm-ui/patch-source.py','deploy/swarm-ui/proxy.py','deploy/swarm-ui/entrypoint.py','deploy/Dockerfile.app-update','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','prompt_enhancer.py','prompt_enhancer_worker.py','image_upload.py','audio_policy.py','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/import_dashboard_history.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock'] +FILES = ['music.py','music-ui.js','audio_cpp_runtime.py','audio-cpp-ui.js','ltx_original_runtime.py','ltx_original_entry.py','video_original.py','ltx-original-ui.js','deploy/ltx-original-requirements.lock','backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','deploy/swarm-ui/Dockerfile','deploy/swarm-ui/patch-source.py','deploy/swarm-ui/proxy.py','deploy/swarm-ui/entrypoint.py','deploy/Dockerfile.app-update','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','prompt_enhancer.py','prompt_enhancer_worker.py','image_upload.py','audio_policy.py','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/import_dashboard_history.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock'] def run(*args, check=True, interactive=False): @@ -204,7 +204,7 @@ def update(base, rollback=False, force=False, api_ports=None, reuse_runtime=Fals backup_name=config['name']+'-previous' if inspect(backup_name):raise RuntimeError('Rückfall-Containername belegt. Keine Änderung ausgeführt.') stamp=str(time.time_ns()) - shutil.copytree(base/'state',base/'backups'/stamp,ignore=shutil.ignore_patterns('audio-cpp-runtime','ltx-original-runtime','original-work','tts-runtime','tts-tests','models','runtime','runtime-verification','image-tests','image-verification','image-verification-*','image-runtime','video-runtime','video-verification','video-verification-*','comfy-work','router-verification-*')) + shutil.copytree(base/'state',base/'backups'/stamp,ignore=shutil.ignore_patterns('music-jobs','audio-cpp-runtime','ltx-original-runtime','original-work','tts-runtime','tts-tests','models','runtime','runtime-verification','image-tests','image-verification','image-verification-*','image-runtime','video-runtime','video-verification','video-verification-*','comfy-work','router-verification-*')) was_running=previous['State']['Running'] if was_running:run('docker','stop','--time','15',config['name']) run('docker','rename',config['name'],backup_name) diff --git a/docs/MUSIC_API.md b/docs/MUSIC_API.md new file mode 100644 index 0000000..b9d6019 --- /dev/null +++ b/docs/MUSIC_API.md @@ -0,0 +1,25 @@ +# YuE2 über audio.cpp + +Deck führt die offizielle YuE2-GGUF-Familie über die installierte audio.cpp-CLI aus. Keine neue Laufzeitinstallation oder Docker-Container nötig. BF16, Q8_0 und Q4_0-Hauptdateien aus audio-cpp/Yue2-3B-GGUF werden mit zugeordnetem VAE und vier Sidecars unterstützt. + +## Bedienung + +Musik → Profile → Einrichtung & Komponenten: fehlende Dateien herunterladen und zuordnen. Danach kann das Profil am API-Endpunkt freigegeben werden. Unter Musik → Testen ein ausführbares Profil wählen, Musikstil und Songtext eingeben und Musik erzeugen. Status, Laufzeit, Abbruch, Fehler und WAV-Ergebnis sind sichtbar. Englische Texte werden empfohlen. Standardtest: cot=off, 400 semantische Tokens, 8 NAR-Schritte, Seed 1234. Tokenlimit ist keine garantierte Sekundenlänge. Bis 9000 Tokens und 32 NAR-Schritte sind einstellbar. + +Deck reserviert exklusiv seinen GPU-Auftrag, entlädt sein LLM und verdrängt eigenes TTS. Fremde Prozesse werden nicht beendet. Die freie RTX 5080 wird verwendet; keine automatische CPU-Auslagerung. Video-Modus sperrt Musik. Nach Abschluss/Fehler/Abbruch endet der CLI-Prozess und gibt GPU-Speicher frei. Prompts und vorübergehende Worker-Ausgabe werden nicht dauerhaft gespeichert; WAV-Ergebnisse bleiben in music-jobs. Installation und Profilrezept sind Bestandteil der vorhandenen Wiederherstellung; Ergebnis-Audio nicht. + +## API + +Dies ist eine Deck-Musik-Erweiterung, kein standardisierter OpenAI-Musikendpunkt. Gleicher API-Port und Bearer-Token wie Chat, getrennte Modellliste: + +- GET /v1/audio/music/models: nur aktivierte ausführbare Musikprofile. +- POST /v1/audio/music: JSON mit model (Profilname), lyrics, style und optional seed, max_tokens, steps; gibt audio/wav zurück. Authentifiziert und GPU-koordiniert. Maximales Worker-Zeitlimit 30 Minuten. +- GET /v1/models bleibt ausschließlich für Chatmodelle. + +Interne GUI-API mit Deck-Anmeldung/CSRF: GET /api/v1/music; POST /api/v1/music/start mit profile_id, lyrics, style und optionalen Testparametern; POST /api/v1/music/cancel mit {}; GET /api/v1/music/audio?id=JOB_ID. + +Die Ladypoly-WebUI ist hiermit noch nicht verbunden. Der Deck-Testbereich und die Musik-API funktionieren unabhängig davon. Weitere Musikfamilien oder LoRA/ABC-Funktionen sind noch nicht angebunden. + +## Verifikation auf Athena + +2026-09-30: YuE2 BF16 mit zugeordnetem F16-VAE und Sidecars erzeugte bei 200 semantischen Tokens / 2 NAR-Schritten eine gültige stereo WAV von 8 Sekunden in etwa 18 Sekunden auf der RTX 5080. Danach Prozess beendet und GPU-Speicher freigegeben. Dies prüft Ausführung und Dateiformat, nicht musikalische Qualität oder Stabilität langer Aufträge. diff --git a/docs/RUNTIME_DISCOVERY.md b/docs/RUNTIME_DISCOVERY.md index 4d338d3..d4e1cb5 100644 --- a/docs/RUNTIME_DISCOVERY.md +++ b/docs/RUNTIME_DISCOVERY.md @@ -12,13 +12,13 @@ Eine vorhandene Laufzeit wird wiederverwendet. Ein fehlender Worker oder Workflo **Einstellungen → Laufzeiten → audio.cpp** bietet einen isolierten CUDA-Build aus `0xShug0/audio.cpp`, Version v0.8.1, Commit `f2b4937306daa25f5c78520f3c626ed31495a37a`. Voraussetzung: Debian/Linux, Git, CMake, g++, nvcc, nvidia-smi und mindestens 6 GiB frei. GPU-Architekturen werden gelesen; gebaut wird mit sechs Jobs. Keine Host-Pakete, Treiber oder bestehenden Laufzeiten werden verändert. Fehlende Voraussetzungen werden gemeldet. -Installation liegt im Deck-Zustandsverzeichnis unter `audio-cpp-runtime`. Status, Abbruch und eigene Build-Ausgabe sind in der GUI verfügbar. Der CLI-Start wird ohne Modell geprüft. Dieser Build lädt **keine Gewichte**. Der ausführbare Deck-Musikworker und die Ladypoly-WebUI-Anbindung fehlen noch; sie sind separat umzusetzen. Auch andere audio.cpp-Familien brauchen geprüfte modellspezifische Anbindungen. +Installation liegt im Deck-Zustandsverzeichnis unter `audio-cpp-runtime`. Status, Abbruch und eigene Build-Ausgabe sind in der GUI verfügbar. Der CLI-Start wird ohne Modell geprüft. Dieser Build lädt **keine Gewichte**. Der ausführbare YuE2-Deck-Musikworker ist angebunden; Bedienung und API sind in MUSIC_API.md dokumentiert. Die Ladypoly-WebUI-Anbindung ist separat. Auch andere audio.cpp-Familien brauchen geprüfte modellspezifische Anbindungen. Interne Admin-API: `GET /api/v1/audio-cpp-runtime`, `GET /api/v1/audio-cpp-runtime/log`, `POST /api/v1/audio-cpp-runtime/install` und `/cancel` (JSON `{}`, vorhandene Sitzung und CSRF-Header). Backup/Restore enthält das gepinnte Laufzeitrezept und baut es bei Bedarf neu; keine Binärdateien im Backup. ## YuE2-Komponenten in der GUI -Unter **Musik → Profile → Komponenten** bietet Deck für das offizielle YuE2-GGUF-Hauptmodell fünf Rollen: VAE (F16 empfohlen oder F32), Modellkonfiguration, Generierungskonfiguration, Qwen-Tokenizer und VAE-Konfiguration. „Alle fehlenden Dateien herunterladen“ reiht je eine Datei pro Rolle in die Download-Warteschlange ein. Danach „Bibliothek aktualisieren“ und „Zuordnung auf Athena speichern“. Bereits vorhandene Dateien werden wiederverwendet. Quelle und Zuweisung müssen dieselbe Hub-Revision wie das Hauptmodell haben. Diese Zusatzdateien sind keine eigenständigen Modelle. Der fehlende Musikworker bleibt auch nach vollständiger Zuweisung als Blocker sichtbar. +Unter **Musik → Profile → Komponenten** bietet Deck für das offizielle YuE2-GGUF-Hauptmodell fünf Rollen: VAE (F16 empfohlen oder F32), Modellkonfiguration, Generierungskonfiguration, Qwen-Tokenizer und VAE-Konfiguration. „Alle fehlenden Dateien herunterladen“ reiht je eine Datei pro Rolle in die Download-Warteschlange ein. Danach „Bibliothek aktualisieren“ und „Zuordnung auf Athena speichern“. Bereits vorhandene Dateien werden wiederverwendet. Quelle und Zuweisung müssen dieselbe Hub-Revision wie das Hauptmodell haben. Diese Zusatzdateien sind keine eigenständigen Modelle. Die Ausführungsprüfung bleibt auch nach vollständiger Zuweisung als Blocker sichtbar. ## Gemeinsame Einrichtung in allen Kategorien diff --git a/endpoint-ui.js b/endpoint-ui.js index 3863829..73eedb4 100644 --- a/endpoint-ui.js +++ b/endpoint-ui.js @@ -4,7 +4,7 @@ window.EndpointUI=(()=>{ let pending=false,sequence=0; async function api(path='',data){const r=await fetch('/api/v1/endpoint'+path,data!==undefined?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Endpunkt nicht erreichbar');return v;} function put(el,html){if(el._deckHtml!==html){el.innerHTML=html;el._deckHtml=html;}} - function summary(s){return `

OpenAI-kompatibler Endpunkt

${e(stateName[s.state])}
Erreichbarkeit${s.reachable?'Listener bereit':'Nicht erreichbar'}
Lokaler API-Port${s.port}
Laufende / wartende Aufträge${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}

${e(s.base_url)} · Port und Profile einstellen →

Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}

${s.worker.error||s.error?`

${e(s.worker.error||s.error)}

`:''}

Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.

${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)']].map(([key,label])=>{const c=s.counts[key];return `

${label}

${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}

${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}
`}).join('')}
`;} + function summary(s){return `

OpenAI-kompatibler Endpunkt

${e(stateName[s.state])}
Erreichbarkeit${s.reachable?'Listener bereit':'Nicht erreichbar'}
Lokaler API-Port${s.port}
Laufende / wartende Aufträge${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}

${e(s.base_url)} · Port und Profile einstellen →

Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}

${s.worker.error||s.error?`

${e(s.worker.error||s.error)}

`:''}

Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.

${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)'],['music','Musik']].map(([key,label])=>{const c=s.counts[key];return `

${label}

${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}

${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}
`}).join('')}
`;} async function render(settings=false){ const view=document.querySelector('#view');let root=view.querySelector('#endpoint-page'); if(!root||root.dataset.settings!==String(settings)){ diff --git a/endpoint.py b/endpoint.py index 89e2701..7183525 100644 --- a/endpoint.py +++ b/endpoint.py @@ -39,7 +39,7 @@ def chat_upstream_error(status, raw): class Endpoint: def __init__(self,root,profiles,worker,scheduler,images,credentials,management_port): - self.root=Path(root);self.profiles=profiles;self.worker=worker;self.scheduler=scheduler;self.images=images;self.tts=None;self.stt=None;self.video=None;self.credentials=credentials + self.root=Path(root);self.profiles=profiles;self.worker=worker;self.scheduler=scheduler;self.images=images;self.tts=None;self.stt=None;self.music=None;self.video=None;self.credentials=credentials self.management_port=management_port;self.lock=threading.RLock();self.http=None;self.thread=None;self.state='stopped';self.error=None;self.inflight=0 self.allowed_ports=[int(p) for p in os.environ.get('DECK_API_PORTS','').split(',') if p] self.config=dict(port=self.allowed_ports[0] if self.allowed_ports else 8120,enabled_profiles=[],autostart=False) @@ -58,9 +58,9 @@ class Endpoint: return [dict(p,enabled=p['id'] in enabled) for p in rows if p['kind']!='video'] def status(self): rows=self.rows();worker=self.worker.status();job=self.images.status()['job'];counts={} - for key,kind in [('llm','chat'),('image','image'),('tts','audio'),('stt','stt')]: + for key,kind in [('llm','chat'),('image','image'),('tts','audio'),('stt','stt'),('music','music')]: subset=[p for p in rows if p['kind']==kind] - counts[key]=dict(enabled=sum(p['enabled'] for p in subset),available=sum(p['enabled'] and p['runnable'] for p in subset),loaded=bool(worker['state']=='ready' and kind=='chat') if kind=='chat' else bool(self.tts and self.tts.status().get('loaded')) if kind=='audio' else bool(self.stt and self.stt.status().get('loaded')) if kind=='stt' else bool(kind=='image' and job and job['state']=='running'),supported=kind in ('chat','image') or (kind=='audio' and self.tts is not None) or (kind=='stt' and self.stt is not None)) + counts[key]=dict(enabled=sum(p['enabled'] for p in subset),available=sum(p['enabled'] and p['runnable'] for p in subset),loaded=bool(worker['state']=='ready' and kind=='chat') if kind=='chat' else bool(self.tts and self.tts.status().get('loaded')) if kind=='audio' else bool(self.stt and self.stt.status().get('loaded')) if kind=='stt' else bool(self.music and self.music.status().get('loaded')) if kind=='music' else bool(kind=='image' and job and job['state']=='running'),supported=kind in ('chat','image') or (kind=='audio' and self.tts is not None) or (kind=='stt' and self.stt is not None) or (kind=='music' and self.music is not None)) scheduler_status=self.scheduler.status() with self.lock:return dict(video=self.video.status() if self.video else None,state=self.state,reachable=bool(self.thread and self.thread.is_alive() and self.state=='running'),port=self.config['port'],bind=os.environ.get('DECK_API_BIND','127.0.0.1'),base_url=f"http://127.0.0.1:{self.config['port']}/v1",allowed_ports=self.allowed_ports,error=self.error,counts=counts,worker=worker,scheduler=scheduler_status,profiles=[dict(id=p['id'],name=p['name'],kind=p['kind'],enabled=p['enabled'],runnable=p['runnable'],blockers=p['blockers']) for p in rows],active_requests=self.inflight) def configure(self,data): @@ -127,6 +127,7 @@ class Endpoint: # Process shutdown does not change the user's autostart preference. with self.lock:self.state='stopping';http=self.http if http:http.shutdown();http.server_close() + if self.music:self.music.stop() if self.tts:self.tts.stop() if self.stt:self.stt.stop() self.images.stop();self.worker.stop() @@ -203,7 +204,7 @@ class APIHandler(BaseHTTPRequestHandler): try:ep.video.relay(self) if callable(getattr(type(ep.video),'relay',None)) else relay(self,video['selected']) except ValueError as exc:raise APIError(str(exc)) from None return - model_routes={'/v1/models':'chat','/v1/images/models':'image','/v1/audio/speech/models':'audio','/v1/audio/transcriptions/models':'stt'} + model_routes={'/v1/models':'chat','/v1/images/models':'image','/v1/audio/speech/models':'audio','/v1/audio/transcriptions/models':'stt','/v1/audio/music/models':'music'} if self.command=='GET' and self.path in model_routes:return self.send(ep.model_list(model_routes[self.path])) if self.command=='GET' and self.path=='/health':return self.send({'status':'ok','service':'athena-deck-api'}) if self.video_route(ep):return @@ -219,7 +220,7 @@ class APIHandler(BaseHTTPRequestHandler): fields['n']=int(fields.get('n','1')) return self.image(ep,fields,images) except ValueError as exc:raise APIError(str(exc)) from None - if self.path not in ('/v1/chat/completions','/v1/images/generations','/v1/audio/speech'):raise APIError('Route nicht implementiert.',404) + if self.path not in ('/v1/chat/completions','/v1/images/generations','/v1/audio/speech','/v1/audio/music'):raise APIError('Route nicht implementiert.',404) if self.headers.get('Transfer-Encoding'):raise APIError('Chunked Upload wird nicht unterstützt.') try:length=int(self.headers.get('Content-Length','0')) except ValueError:raise APIError('Ungültige Content-Length.') from None @@ -228,6 +229,7 @@ class APIHandler(BaseHTTPRequestHandler): except (ValueError,UnicodeError):raise APIError('Ungültiges JSON.') from None if not isinstance(data,dict):raise APIError('JSON-Objekt erforderlich.') if self.path=='/v1/chat/completions':return self.chat(ep,data) + if self.path=='/v1/audio/music':return self.music_generation(ep,data) if self.path=='/v1/audio/speech':return self.speech(ep,data) return self.image(ep,data) except (APIError,InferenceError) as exc: @@ -272,6 +274,20 @@ class APIHandler(BaseHTTPRequestHandler): except (ValueError,OSError):time.sleep(.25);continue self.sent=True;self.send_response(200);self.send_header('Content-Type','audio/wav');self.send_header('Content-Length',str(len(body)));self.send_header('Cache-Control','no-store');self.send_header('Connection','close');self.end_headers();self.wfile.write(body);return raise APIError('TTS-Zeitlimit überschritten.',504) + def music_generation(self,ep,data): + if not ep.music:raise APIError('Musikworker nicht eingerichtet.',501) + if set(data)-{'model','lyrics','style','seed','max_tokens','steps'}:raise APIError('Nicht unterstützte Musikfelder.') + profile=ep.find_profile(data.get('model'),'music') + try:job=ep.music.start(profile['id'],data.get('lyrics'),data.get('style'),seed=data.get('seed',1234),max_tokens=data.get('max_tokens',400),steps=data.get('steps',8),wait=True) + except ValueError as exc:raise APIError(str(exc)) from None + end=time.monotonic()+1850 + while time.monotonic()Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
+Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
diff --git a/music-ui.js b/music-ui.js new file mode 100644 index 0000000..4ef76f1 --- /dev/null +++ b/music-ui.js @@ -0,0 +1,10 @@ +window.MusicUI=(()=>{ + const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + async function api(path,data){const r=await fetch('/api/v1/'+path,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const x=await r.json();if(!r.ok)throw Error(x.error||'Musikanfrage fehlgeschlagen');return x;} + function html(){return '

Musik ausprobieren

YuE2 über audio.cpp. Englische Songtexte werden empfohlen. Ein kurzer Test beginnt mit höchstens 400 semantischen Tokens; dies ist keine feste Dauer in Sekunden.

Testparameter

RTX 5080 bevorzugt. Deck-Aufträge werden koordiniert. Nach der Anfrage wird das Modell entladen. Text und Stil werden nicht dauerhaft gespeichert.

';} + function bind(){const root=document.querySelector('#music-test'),el=id=>root.querySelector('#'+id);let ready=false,busy=false; + async function poll(){if(!root.isConnected)return;try{const s=await api('music');if(!root.isConnected)return;const j=s.job,running=j?.state==='running';el('music-state').textContent=(j?.phase||'Profil wählen und Musikstil sowie Songtext eingeben.')+(running?' · '+Math.round((Date.now()/1000)-j.started_at)+' s':'');el('music-progress').innerHTML=running?'':'';el('music-start').disabled=busy||running||!ready;el('music-cancel').disabled=!running;if(j?.state==='complete')el('music-result').innerHTML=`

Ergebnis · ${e(j.duration_seconds)} Sekunden

WAV herunterladen

`;if(j?.state==='failed')el('music-error').textContent=j.phase;}catch(x){if(root.isConnected)el('music-error').textContent=x.message;}if(root.isConnected)setTimeout(poll,1500);} + api('profiles').then(s=>{if(!root.isConnected)return;const profiles=s.profiles.filter(p=>p.kind==='music'&&p.runnable);ready=profiles.length>0;el('music-profile').innerHTML=profiles.map(p=>``).join('')||'';}).catch(x=>el('music-error').textContent=x.message); + el('music-form').onsubmit=async event=>{event.preventDefault();busy=true;el('music-start').disabled=true;el('music-error').textContent='';el('music-result').replaceChildren();try{await api('music/start',{profile_id:el('music-profile').value,lyrics:el('music-lyrics').value,style:el('music-style').value,seed:Number(el('music-seed').value),max_tokens:Number(el('music-tokens').value),steps:Number(el('music-steps').value)});}catch(x){el('music-error').textContent=x.message;}finally{busy=false;}};el('music-cancel').onclick=()=>api('music/cancel',{}).catch(x=>el('music-error').textContent=x.message);poll();} + return {html,bind}; +})(); diff --git a/music.py b/music.py new file mode 100644 index 0000000..371f3e1 --- /dev/null +++ b/music.py @@ -0,0 +1,85 @@ +"""Owned audio.cpp music jobs, coordinated GPU lease, no persistent prompts.""" +import os,signal,subprocess,threading,time,uuid,wave +from pathlib import Path +from profiles import music_recipe +from image_test import probe +class Music: + def __init__(self,root,profiles,catalog,runtime,scheduler): + self.root=Path(root);self.profiles=profiles;self.catalog=catalog;self.runtime=runtime;self.scheduler=scheduler;self.lock=threading.RLock();self.job=None;self.process=None;self.cancel=threading.Event() + def blockers(self,p): + if not music_recipe(p['model']):return ['Für diese Musik-Modellfamilie fehlt noch eine geprüfte audio.cpp-Anbindung.'] + result=[] if self.runtime.status()['installed'] else ['audio.cpp zuerst unter Laufzeiten installieren.'] + for role,info in music_recipe(p['model']).items(): + try:self.profiles._component(p['model'],role,p.get('components',{}).get(role)) + except ValueError:result.append(info['label']+' fehlt oder ist nicht zugeordnet.') + return result + def status(self): + with self.lock:return dict(job=dict(self.job) if self.job else None,loaded=bool(self.process and self.process.poll() is None),runtime_installed=self.runtime.status()['installed']) + def start(self,profile_id,lyrics,style,seed=1234,max_tokens=400,steps=8,wait=False): + if not isinstance(lyrics,str) or not 1<=len(lyrics.strip())<=8000:raise ValueError('Songtext mit 1–8000 Zeichen eingeben.') + if not isinstance(style,str) or not 1<=len(style.strip())<=2000:raise ValueError('Musikstil mit 1–2000 Zeichen eingeben.') + for value,lo,hi in ((seed,0,2**31-1),(max_tokens,32,9000),(steps,1,32)): + if type(value) is not int or not lo<=value<=hi:raise ValueError('Ungültige Musikparameter.') + with self.lock: + if self.job and self.job['state']=='running':raise ValueError('Musikauftrag läuft bereits.') + p=next((p for p in self.profiles.status()['profiles'] if p['id']==profile_id and p['kind']=='music'),None) + if not p or not p['runnable']:raise ValueError('Musikprofil noch nicht vollständig eingerichtet.') + self.cancel.clear();self.root.mkdir(parents=True,exist_ok=True,mode=0o700) + self.job=dict(id=uuid.uuid4().hex,state='running',phase='Wartet auf Deck-GPU-Freigabe',profile_id=profile_id,started_at=time.time(),max_tokens=max_tokens) + threading.Thread(target=self._run,args=(p,lyrics,style,seed,max_tokens,steps,wait),daemon=True).start();return dict(self.job) + def _run(self,p,lyrics,style,seed,max_tokens,steps,wait): + release=None;state='failed';phase='Musikgenerierung fehlgeschlagen.';directory=self.root/self.job['id'];log=directory/'worker.tmp' + try: + release=self.scheduler.image_reservation(wait=wait) + if self.cancel.is_set():raise InterruptedError() + required=int(p['model']['size']/1024**2)+1800 + gpu=next((g for g in probe() if 'RTX 5080' in g['name'] and not g['processes'] and g['free_mib']>required),None) + if not gpu:raise ValueError('RTX 5080 nicht ausreichend frei. Fremde GPU-Prozesse werden nicht beendet.') + directory.mkdir(mode=0o700);bundle=directory/'package';bundle.mkdir();(bundle/'sidecars').mkdir() + def link(entry,name): + source=self.catalog.root/entry['id']/('model'+Path(entry['file']).suffix) + (bundle/name).symlink_to(source.resolve()) + link(p['model'],p['model']['file']) + for role in music_recipe(p['model']): + entry=self.profiles._component(p['model'],role,p['components'][role]);link(entry,entry['file']) + vae=self.catalog.entry(p['components']['vae'])['file'];cli=self.runtime.paths()[0] + args=[str(cli),'--task','gen','--family','yue2','--model',str(bundle),'--backend','cuda','--device','0','--threads','6','--session-option','yue2.model_gguf='+p['model']['file'],'--session-option','yue2.vae_gguf='+vae,'--lyrics',lyrics,'--request-option','style='+style,'--request-option','cot=off','--request-option','semantic_min_tokens=0','--request-option','semantic_max_tokens='+str(max_tokens),'--request-option','num_inference_steps='+str(steps),'--seed',str(seed),'--out',str(directory/'result.wav'),'--log'] + with self.lock:self.job.update(phase='YuE2 wird auf der RTX 5080 geladen · anschließend Musik erzeugen',gpu=gpu['name']) + with log.open('w') as out: + with self.lock: + if self.cancel.is_set():raise InterruptedError() + self.process=subprocess.Popen(args,stdout=out,stderr=subprocess.STDOUT,start_new_session=True,env=dict(os.environ,CUDA_VISIBLE_DEVICES=gpu['uuid'],OMP_NUM_THREADS='6')) + deadline=time.monotonic()+1800 + while self.process.poll() is None: + if self.cancel.wait(.5):raise InterruptedError() + if time.monotonic()>deadline:raise ValueError('Zeitlimit von 30 Minuten erreicht.') + with self.lock:self.job['elapsed_seconds']=round(time.time()-self.job['started_at']) + if self.process.returncode: + output=log.read_text(errors='replace').lower() + if 'out of memory' in output or 'cuda error 2' in output:raise ValueError('GPU-Speicher reicht nicht (Out of Memory). Kleinere Quantisierung oder kürzeren Test wählen.') + raise ValueError('audio.cpp hat die Generierung abgebrochen. Modellpaket oder Laufzeit prüfen.') + with wave.open(str(directory/'result.wav'),'rb') as wav: + if wav.getnframes()<1 or wav.getnchannels() not in (1,2):raise ValueError('Ungültige WAV-Ausgabe.') + duration=wav.getnframes()/wav.getframerate() + (directory/'complete').touch();state='complete';phase='Musik fertig · Modell entladen' + with self.lock:self.job['duration_seconds']=round(duration,2) + except InterruptedError:state='cancelled';phase='Musikgenerierung abgebrochen · Modell entladen' + except Exception as exc:phase=str(exc) if isinstance(exc,ValueError) else 'Musikgenerierung fehlgeschlagen. Modellpaket und Laufzeit prüfen.' + finally: + process=self.process + if process and process.poll() is None: + try:os.killpg(process.pid,signal.SIGTERM);process.wait(5) + except subprocess.TimeoutExpired:os.killpg(process.pid,signal.SIGKILL);process.wait() + except ProcessLookupError:pass + log.unlink(missing_ok=True) + with self.lock:self.process=None;self.job.update(state=state,phase=phase,finished_at=time.time()) + if release:release() + def stop(self): + self.cancel.set();return {'cancellation_requested':True} + def audio(self,ident): + if not isinstance(ident,str) or len(ident)!=32 or any(c not in '0123456789abcdef' for c in ident):raise ValueError('Ungültige Auftrag-ID.') + directory=self.root/ident + if not (directory/'complete').is_file():raise ValueError('Musik noch nicht fertig.') + wav=directory/'result.wav' + if wav.stat().st_size>256*1024**2:raise ValueError('Audioausgabe zu groß.') + return wav.read_bytes() diff --git a/profiles.py b/profiles.py index b844170..504e327 100644 --- a/profiles.py +++ b/profiles.py @@ -144,6 +144,7 @@ class Profiles: except ValueError:p['blockers'].append(info['label']+' fehlt oder ist noch nicht zugeordnet.') if p['kind']=='video' and self.video_blockers and p.get('model'):p['blockers']=self.video_blockers(p) if p['kind']=='audio' and self.tts_blockers and p.get('model'):p['blockers']=self.tts_blockers(p) + if p['kind']=='music' and getattr(self,'music_blockers',None) and p.get('model'):p['blockers']=self.music_blockers(p) if p['kind']=='stt' and self.stt_blockers and p.get('model'):p['blockers']=self.stt_blockers(p) if p['kind']=='chat' and self.chat_blockers and p.get('model'):p['blockers']=self.chat_blockers(p) if p['kind']=='image' and (p['parameters']['width']>1024 or p['parameters']['height']>1024):p['blockers'].append('Die Bildlaufzeit unterstützt derzeit maximal 1024 × 1024 Pixel.') @@ -241,7 +242,7 @@ class Profiles: except ValueError:pass candidates=[dict(f,repo=source.get('repo',info.get('repo',model['repo'])),revision=source['revision'],gated=source['gated'],auth_configured=source.get('auth_configured',False)) for name in info['files'] for f in source['files'] if f['name']==name] requirements.append(dict(role=role,label=info['label'],available=available,candidates=candidates)) - if music_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='audio.cpp',runtime_installed=bool(getattr(self,'music_runtime_ready',lambda:False)()),message='Passendes YuE2-GGUF-Paket: je eine VAE-Datei und die vier Zusatzdateien derselben Quellversion. F16-VAE ist die erste Downloadempfehlung. Vorhandene Dateien wiederverwenden, fehlende herunterladen und danach zuordnen. Der Deck-Musikworker und die Ladypoly-Anbindung fehlen weiterhin; vollständige Komponenten starten kein Modell.') + if music_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='audio.cpp',runtime_installed=bool(getattr(self,'music_runtime_ready',lambda:False)()),message='Passendes YuE2-GGUF-Paket: je eine VAE-Datei und die vier Zusatzdateien derselben Quellversion. F16-VAE ist die erste Downloadempfehlung. Vorhandene Dateien wiederverwenden, fehlende herunterladen und danach zuordnen. Die YuE2-Musikanbindung verwendet diese Dateien über audio.cpp. Speichern startet kein Modell; anschließend Musik → Testen verwenden.') if video:return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='ComfyUI · gemeinsame Bild- und Videolaufzeit',runtime_installed=bool(getattr(self,'video_runtime_ready',lambda:False)()),message='Geprüftes Dateirezept nach Herstellerdokumentation für die zweistufige Distilled-Pipeline mit fester Bildanzahl. Alle Komponenten stammen aus derselben Version wie dein Transformer. Kein beliebiger Gemma-Encoder; keine Comfy-INT8-Dateien. Optionaler Duration-Head, Temporal-Upsampling und DFR sind nicht Teil dieses Rezepts. Vorhandene ComfyUI-Laufzeit wiederverwenden; Videomodell im Bereich Video aktivieren. Vollständige Dateien sind keine Speicherzusage.') if not image_recipe(model):return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime='audio.cpp · Paketdefinition',runtime_installed=bool(getattr(self,'music_runtime_ready',lambda:False)()),message='Zusatzdateien aus der installierten Laufzeit-Paketdefinition. Weitere modellabhängige Pakete können erforderlich sein; dies ist keine vollständige Worker-Zusage.') return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime='ComfyUI + ComfyUI-GGUF',runtime_installed=self.image_runtime_ready(), diff --git a/server.py b/server.py index 2835270..9c7dc7d 100644 --- a/server.py +++ b/server.py @@ -112,12 +112,16 @@ class Server(ThreadingHTTPServer): self.chat_tests=ChatTests(self.profiles,self.worker,self.scheduler) self.auto_tests=AutoTests(self.catalog.root.parent/'auto-tests.json',self.profiles,self.worker,self.scheduler) def stop_gpu_work(): - self.auto_tests.stop();self.chat_tests.stop();self.image_tests.stop();self.prompt_enhancer.stop_preview();self.tts_tests.stop();self.worker.stop() + self.music.stop();self.auto_tests.stop();self.chat_tests.stop();self.image_tests.stop();self.prompt_enhancer.stop_preview();self.tts_tests.stop();self.worker.stop() from video_original import VideoRuntimes from ltx_original_runtime import LTXOriginalRuntime from audio_cpp_runtime import AudioCppRuntime self.audio_cpp_runtime=AudioCppRuntime(self.catalog.root.parent/'audio-cpp-runtime') self.profiles.music_runtime_ready=lambda:self.audio_cpp_runtime.status()['installed'] + from music import Music + self.music=Music(self.catalog.root.parent/'music-jobs',self.profiles,self.catalog,self.audio_cpp_runtime,self.scheduler) + self.profiles.music_blockers=self.music.blockers + self.endpoint.music=self.music from profiles import audio_package_recipe self.profiles.runtime_recipe=lambda model:audio_package_recipe(model,self.audio_cpp_runtime.paths()[1]) self.ltx_original_runtime=LTXOriginalRuntime(self.catalog.root.parent/'ltx-original-runtime',self.image_runtime) @@ -283,7 +287,7 @@ class Handler(BaseHTTPRequestHandler): return self.headers.get('Host') in allowed def execution_setup(self, model, profile=None): - installed={'stt':bool(self.server.runtime.status().get('active')),'chat':bool(self.server.runtime.status().get('active')),'image':self.server.image_runtime.status().get('installed',False),'audio':self.server.tts_runtime.status().get('installed',False),'video':self.server.video.status()['runtime']['installed'],'audio_cpp':self.server.audio_cpp_runtime.status()['installed']} + installed={'stt':bool(self.server.runtime.status().get('active')),'chat':bool(self.server.runtime.status().get('active')),'image':self.server.image_runtime.status().get('installed',False),'audio':self.server.tts_runtime.status().get('installed',False),'video':self.server.video.status()['runtime']['installed'],'audio_cpp':self.server.audio_cpp_runtime.status()['installed'],'music_worker':True} return execution_assess(model,installed,profile) def do_GET(self): @@ -297,7 +301,7 @@ class Handler(BaseHTTPRequestHandler): return self.respond({'error':'Anmeldung erforderlich.'},401) if not self.authenticated() and self.path == '/': return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8') - routes = {'/audio-cpp-ui.js':('audio-cpp-ui.js','text/javascript'),'/ltx-original-ui.js':('ltx-original-ui.js','text/javascript'),'/backup-ui.js':('backup-ui.js','text/javascript'),'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')} + routes = {'/music-ui.js':('music-ui.js','text/javascript'),'/audio-cpp-ui.js':('audio-cpp-ui.js','text/javascript'),'/ltx-original-ui.js':('ltx-original-ui.js','text/javascript'),'/backup-ui.js':('backup-ui.js','text/javascript'),'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')} if self.path in routes: name, mime = routes[self.path] return self.respond((ROOT/name).read_bytes(), mime=mime) @@ -316,6 +320,10 @@ class Handler(BaseHTTPRequestHandler): if urlsplit(self.path).path == '/api/v1/tts/audio': try:return self.respond(self.server.tts_tests.audio(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='audio/wav') except (OSError,ValueError):return self.respond({'error':'Audio nicht verfügbar.'},404) + if self.path == '/api/v1/music':return self.respond(self.server.music.status()) + if urlsplit(self.path).path == '/api/v1/music/audio': + try:return self.respond(self.server.music.audio(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='audio/wav') + except (OSError,ValueError):return self.respond({'error':'Musik nicht verfügbar.'},404) if self.path == '/api/v1/audio-cpp-runtime':return self.respond(self.server.audio_cpp_runtime.status()) if self.path == '/api/v1/audio-cpp-runtime/log':return self.respond(self.server.audio_cpp_runtime.log()) if self.path == '/api/v1/ltx-original-runtime':return self.respond(self.server.ltx_original_runtime.status()) @@ -536,6 +544,13 @@ class Handler(BaseHTTPRequestHandler): return self.respond(self.server.stt.setup() if self.path.endswith('/setup') else self.server.stt.stop()) except ValueError as exc:return self.respond({'error':str(exc)},400) except Exception:return self.respond({'error':'STT-Aktion fehlgeschlagen.'},503) + if self.path in ('/api/v1/music/start','/api/v1/music/cancel'): + try: + data=self.read_json() + if self.path.endswith('/cancel') and not data:return self.respond(self.server.music.stop()) + if set(data)-{'profile_id','lyrics','style','seed','max_tokens','steps'} or not {'profile_id','lyrics','style'}<=set(data):raise ValueError('Ungültige Musikfelder.') + return self.respond(self.server.music.start(**data)) + except ValueError as exc:return self.respond({'error':str(exc)},400) if self.path in ('/api/v1/tts/install','/api/v1/tts/install-cancel','/api/v1/tts/assign','/api/v1/tts/start','/api/v1/tts/cancel'): try: data=self.read_json();t=self.server.tts_tests @@ -677,7 +692,7 @@ def main(): server.tts_runtime.stop() server.image_runtime.stop() server.ltx_original_runtime.stop() - server.audio_cpp_runtime.stop() + server.music.stop();server.audio_cpp_runtime.stop() server.prompt_enhancer.stop() server.image_tests.stop() server.runtime.stop() diff --git a/studio.js b/studio.js index a6f6b37..fb5fb5f 100644 --- a/studio.js +++ b/studio.js @@ -8,7 +8,7 @@ const Studio=(()=>{ if(!force&&document.querySelector('#studio')?.dataset.page===page)return; if(category!==page){category=page;section='discover';} if(modelId)section='profiles'; - const body=page==='stt-runtime'?STTUI.html(true):page==='tts-runtime'?TTSUI.html(true):['docker-runtime','services'].includes(page)?DockerUI.html(page==='services'):page==='runtimes'?runtimeOverview():page==='image-runtime'?imageRuntime():page==='runtime'?RuntimeUI.html():`
ATHENA / MODELLVERWALTUNG

${labels[page]}

${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}

${audioDescriptions[page]&&!['audio','stt'].includes(page)?'

Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.

':''}
${[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],...(['audio','stt'].includes(page)?[['setup','Einrichten']]:[]),...(['image','chat','audio','stt'].includes(page)?[['test','Testen'],...(page==='chat'?[['auto','Auto-Test']]:[])]:[]),['running','Laufend']].map(([id,label])=>``).join('')}
${section==='setup'?(page==='stt'?STTUI.html(true):TTSUI.html(true)):['test','running'].includes(section)&&page==='stt'?STTUI.html():['test','running'].includes(section)&&page==='audio'?TTSUI.html():section==='auto'?AutoTestUI.html():section==='test'?(page==='chat'?ChatTestUI.html():ImageTestUI.html()):section==='running'&&page==='image'?ImageTestUI.html(true):section==='profiles'?ProfilesUI.html():section==='running'&&page==='chat'?EndpointUI.runningHTML():section==='running'?'

Keine von Deck gestarteten Modelle

Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.

llama.cpp-Builds verwalten →
':CatalogUI.html(section==='library',section==='downloads')}
`; + const body=page==='stt-runtime'?STTUI.html(true):page==='tts-runtime'?TTSUI.html(true):['docker-runtime','services'].includes(page)?DockerUI.html(page==='services'):page==='runtimes'?runtimeOverview():page==='image-runtime'?imageRuntime():page==='runtime'?RuntimeUI.html():`
ATHENA / MODELLVERWALTUNG

${labels[page]}

${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}

${audioDescriptions[page]&&!['audio','stt','music'].includes(page)?'

Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.

':''}
${[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],...(['audio','stt'].includes(page)?[['setup','Einrichten']]:[]),...(['image','chat','audio','stt','music'].includes(page)?[['test','Testen'],...(page==='chat'?[['auto','Auto-Test']]:[])]:[]),['running','Laufend']].map(([id,label])=>``).join('')}
${section==='setup'?(page==='stt'?STTUI.html(true):TTSUI.html(true)):['test','running'].includes(section)&&page==='music'?MusicUI.html():['test','running'].includes(section)&&page==='stt'?STTUI.html():['test','running'].includes(section)&&page==='audio'?TTSUI.html():section==='auto'?AutoTestUI.html():section==='test'?(page==='chat'?ChatTestUI.html():ImageTestUI.html()):section==='running'&&page==='image'?ImageTestUI.html(true):section==='profiles'?ProfilesUI.html():section==='running'&&page==='chat'?EndpointUI.runningHTML():section==='running'?'

Keine von Deck gestarteten Modelle

Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.

llama.cpp-Builds verwalten →
':CatalogUI.html(section==='library',section==='downloads')}
`; document.querySelector('#view').innerHTML=`
${body}
`; if(['docker-runtime','services'].includes(page)){DockerUI.bind(page==='services');return;} if(page==='stt-runtime'){STTUI.bind(true);return;} @@ -17,6 +17,7 @@ const Studio=(()=>{ if(page==='image-runtime'){ImageTestUI.bind(true);ImageTestUI.bindInstaller();return;} if(page==='runtimes')return; else if(section==='setup'){if(page==='stt')STTUI.bind(true);else TTSUI.bind(true);} + else if(['test','running'].includes(section)&&page==='music')MusicUI.bind(); else if(['test','running'].includes(section)&&page==='stt')STTUI.bind(); else if(['test','running'].includes(section)&&page==='audio')TTSUI.bind(); else if(section==='auto')AutoTestUI.bind(); diff --git a/test_audio_cpp_runtime.py b/test_audio_cpp_runtime.py index 06a34b6..2d1b702 100644 --- a/test_audio_cpp_runtime.py +++ b/test_audio_cpp_runtime.py @@ -32,7 +32,7 @@ class AudioCppTests(unittest.TestCase): with patch.object(runtime,'_command',side_effect=command),patch('subprocess.check_output',return_value='12.0\n8.6\n'):runtime._run() self.assertEqual(runtime.job['state'],'complete') self.assertTrue(runtime.status()['installed']) - self.assertFalse(runtime.status()['adapter_ready']) + self.assertTrue(runtime.status()['adapter_ready']) self.assertIn('-DCMAKE_CUDA_ARCHITECTURES=120;86',commands[3]) self.assertEqual(commands[-1][-1],'--help') self.assertEqual(Path(root,'a'*32,'ready').read_text(),REVISION) diff --git a/test_endpoint.py b/test_endpoint.py index 2873c4d..55a7be2 100644 --- a/test_endpoint.py +++ b/test_endpoint.py @@ -61,6 +61,17 @@ class EndpointTests(unittest.TestCase): def request(self,path='/v1/models',data=None,token='A'*32): conn=http.client.HTTPConnection('127.0.0.1',self.port,timeout=5);headers={'Content-Type':'application/json','Authorization':'Bearer '+token} conn.request('POST' if data is not None else 'GET',path,body=json.dumps(data) if data is not None else None,headers=headers);r=conn.getresponse();body=r.read();conn.close();return r.status,(body if data and data.get('stream') else json.loads(body)) + def test_music_discovery_and_authenticated_wav_route(self): + self.rows.append(dict(id='music',name='Music',kind='music',runnable=True,blockers=[],parameters={},updated_at=1)) + self.ep.music=Mock();self.ep.music.start.return_value=dict(id='a'*32);self.ep.music.status.return_value=dict(job=dict(id='a'*32,state='complete'));self.ep.music.audio.return_value=b'RIFFsynthetic-WAV' + self.enable('music') + self.assertEqual(self.request('/v1/audio/music/models')[1]['data'][0]['id'],'Music') + self.assertNotIn('Music',[x['id'] for x in self.request()[1]['data']]) + conn=http.client.HTTPConnection('127.0.0.1',self.port,timeout=5) + conn.request('POST','/v1/audio/music',json.dumps(dict(model='Music',lyrics='Synthetic',style='pop')),{'Content-Type':'application/json','Authorization':'Bearer '+'A'*32}) + response=conn.getresponse();self.assertEqual(response.status,200);self.assertEqual(response.getheader('Content-Type'),'audio/wav');self.assertEqual(response.read(),b'RIFFsynthetic-WAV');conn.close() + self.assertEqual(self.request('/v1/audio/music',dict(model='Music',lyrics='Synthetic',style='pop',unsupported=True))[0],400) + self.assertEqual(self.request('/v1/audio/music/models',token='bad')[0],401) def enable(self,name):self.ep.enable(dict(id=name,enabled=True)) def test_explicit_publication_auth_rotation_and_missing_audio(self): self.assertEqual(self.request()[1]['data'],[]);self.assertEqual(self.request(token='bad')[0],401) diff --git a/test_music.py b/test_music.py new file mode 100644 index 0000000..d669185 --- /dev/null +++ b/test_music.py @@ -0,0 +1,53 @@ +import tempfile,time,unittest +from pathlib import Path +from unittest.mock import Mock,patch +from music import Music +from profiles import YUE2_COMPONENTS,YUE2_REPO +class MusicTests(unittest.TestCase): + def make(self,root): + root=Path(root);catalog=Mock();catalog.root=root/'models';catalog.root.mkdir() + main=dict(id='main',repo=YUE2_REPO,file='yue2-3b-bf16.gguf',kind='music',size=1) + entries={role:dict(id=role,file=info['files'][0]) for role,info in YUE2_COMPONENTS.items()} + for entry in [main]+list(entries.values()): + (catalog.root/entry['id']).mkdir();(catalog.root/entry['id']/('model'+Path(entry['file']).suffix)).touch() + catalog.entry.side_effect=lambda id:entries[id] + p=dict(id='p',kind='music',runnable=True,model=main,components={role:role for role in entries}) + profiles=Mock();profiles.status.return_value=dict(profiles=[p]);profiles._component.side_effect=lambda model,role,id:entries[id] + cli=root/'cli';cli.write_text('#!/usr/bin/env python3\nimport sys,wave\np=sys.argv[sys.argv.index("--out")+1]\nwith wave.open(p,"wb") as w:w.setparams((1,2,8000,0,"NONE","none"));w.writeframes(bytes(1600))\n');cli.chmod(0o755) + runtime=Mock();runtime.status.return_value=dict(installed=True);runtime.paths.return_value=(cli,root) + scheduler=Mock();release=Mock();scheduler.image_reservation.return_value=release + return Music(root/'jobs',profiles,catalog,runtime,scheduler),release,p + def wait(self,m): + for _ in range(80): + if m.status()['job']['state']!='running':return + time.sleep(.05) + self.fail('Test timeout') + def test_success_and_output_without_retaining_prompts(self): + with tempfile.TemporaryDirectory() as root: + m,release,p=self.make(root) + with patch('music.probe',return_value=[dict(name='NVIDIA RTX 5080',uuid='GPU-test',free_mib=16000,processes=0)]): + job=m.start('p','Synthetic lyrics','Synthetic pop',max_tokens=32,steps=1);self.wait(m) + self.assertEqual(m.status()['job']['state'],'complete');self.assertTrue(m.audio(job['id']).startswith(b'RIFF'));release.assert_called_once() + self.assertFalse((m.root/job['id']/'worker.tmp').exists());self.assertNotIn('lyrics',m.status()['job']) + def test_occupied_foreign_gpu_releases_lease(self): + with tempfile.TemporaryDirectory() as root: + m,release,p=self.make(root) + with patch('music.probe',return_value=[dict(name='NVIDIA RTX 5080',uuid='GPU-test',free_mib=16000,processes=1)]):m.start('p','Synthetic','pop');self.wait(m) + self.assertEqual(m.status()['job']['state'],'failed');self.assertIn('Fremde',m.status()['job']['phase']);release.assert_called_once() + def test_invalid_request_and_missing_components(self): + with tempfile.TemporaryDirectory() as root: + m,release,p=self.make(root) + with self.assertRaises(ValueError):m.start('p','Synthetic','pop',max_tokens=True) + m.profiles._component.side_effect=ValueError('missing');self.assertEqual(len(m.blockers(p)),5) + with self.assertRaises(ValueError):m.audio('../outside') + + def test_cancel_terminates_owned_process_and_releases_gpu(self): + with tempfile.TemporaryDirectory() as root: + m,release,p=self.make(root);m.runtime.paths()[0].write_text('#!/usr/bin/env python3\nimport time;time.sleep(30)\n') + with patch('music.probe',return_value=[dict(name='NVIDIA RTX 5080',uuid='GPU-test',free_mib=16000,processes=0)]): + m.start('p','Synthetic','pop') + for _ in range(50): + if m.process:break + time.sleep(.02) + m.stop();self.wait(m) + self.assertEqual(m.status()['job']['state'],'cancelled');release.assert_called_once();self.assertFalse(m.status()['loaded'])