Run YuE2 music profiles through audio.cpp with WAV API and test UI
This commit is contained in:
1 parent
94bfb1ac78
commit
0ca40bc624
20 files changed
+248
-26
No files matched your search
+2
-2
@@ -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='<div id="audio-cpp-page"><h1>audio.cpp</h1><p>Gemeinsame C++-/CUDA-Laufzeit für unterstützte Audio-Modellfamilien. Passendes GGUF-Paket und ein Deck-Worker sind zusätzlich erforderlich.</p><section class="card"><h2>Herunterladen und bereitmachen</h2><div id="ac-status"></div><button id="ac-install">audio.cpp herunterladen & CUDA-Build erstellen</button><button id="ac-cancel" class="secondary">Build abbrechen</button><p id="ac-message" role="status"></p><details><summary>Eigene Build-Ausgabe</summary><pre id="ac-log" style="white-space:pre-wrap;max-height:320px;overflow:auto"></pre></details></section><section class="card"><h2>YuE2 und Ladypoly WebUI</h2><p>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.</p><a href="#music">Zur Musik-Bibliothek →</a></section></div>';
|
||||
view.innerHTML='<div id="audio-cpp-page"><h1>audio.cpp</h1><p>Gemeinsame C++-/CUDA-Laufzeit für unterstützte Audio-Modellfamilien. Passendes GGUF-Paket und ein Deck-Worker sind zusätzlich erforderlich.</p><section class="card"><h2>Herunterladen und bereitmachen</h2><div id="ac-status"></div><button id="ac-install">audio.cpp herunterladen & CUDA-Build erstellen</button><button id="ac-cancel" class="secondary">Build abbrechen</button><p id="ac-message" role="status"></p><details><summary>Eigene Build-Ausgabe</summary><pre id="ac-log" style="white-space:pre-wrap;max-height:320px;overflow:auto"></pre></details></section><section class="card"><h2>YuE2 und Ladypoly WebUI</h2><p>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.</p><a href="#music">Zur Musik-Bibliothek →</a></section></div>';
|
||||
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=`<p><strong>${s.installed?'Installiert · Worker-Anbindung noch offen':'Noch nicht installiert'}</strong></p><p>Version ${e(s.version)} · Commit <code>${e(s.revision)}</code></p><p>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.</p>${s.job?`<p>${e(s.job.state)} · ${e(s.job.phase)}</p>${running?'<progress aria-label="audio.cpp-Build läuft"></progress>':''}`:''}`;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=`<p><strong>${s.installed?'Installiert · YuE2-Musikworker verfügbar':'Noch nicht installiert'}</strong></p><p>Version ${e(s.version)} · Commit <code>${e(s.revision)}</code></p><p>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.</p>${s.job?`<p>${e(s.job.state)} · ${e(s.job.phase)}</p>${running?'<progress aria-label="audio.cpp-Build läuft"></progress>':''}`:''}`;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};
|
||||
})();
|
||||
@@ -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)]
|
||||
|
||||
@@ -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}
|
||||
+2
-2
@@ -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 \
|
||||
|
||||
@@ -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 \
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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 `<div class="grid"><section class="card wide"><div class="card-top"><h2>OpenAI-kompatibler Endpunkt</h2><span class="pill">${e(stateName[s.state])}</span></div><div class="metrics"><div><small>Erreichbarkeit</small><strong>${s.reachable?'Listener bereit':'Nicht erreichbar'}</strong></div><div><small>Lokaler API-Port</small><strong>${s.port}</strong></div><div><small>Laufende / wartende Aufträge</small><strong>${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}</strong></div></div><p><code>${e(s.base_url)}</code> · <a class="link" href="#endpoint">Port und Profile einstellen →</a></p><p>Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}</p>${s.worker.error||s.error?`<p role="alert">${e(s.worker.error||s.error)}</p>`:''}<p>Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.</p></section>${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)']].map(([key,label])=>{const c=s.counts[key];return `<section class="card"><h3>${label}</h3><p>${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}</p><span class="pill">${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}</span></section>`}).join('')}</div>`;}
|
||||
function summary(s){return `<div class="grid"><section class="card wide"><div class="card-top"><h2>OpenAI-kompatibler Endpunkt</h2><span class="pill">${e(stateName[s.state])}</span></div><div class="metrics"><div><small>Erreichbarkeit</small><strong>${s.reachable?'Listener bereit':'Nicht erreichbar'}</strong></div><div><small>Lokaler API-Port</small><strong>${s.port}</strong></div><div><small>Laufende / wartende Aufträge</small><strong>${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}</strong></div></div><p><code>${e(s.base_url)}</code> · <a class="link" href="#endpoint">Port und Profile einstellen →</a></p><p>Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}</p>${s.worker.error||s.error?`<p role="alert">${e(s.worker.error||s.error)}</p>`:''}<p>Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.</p></section>${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)'],['music','Musik']].map(([key,label])=>{const c=s.counts[key];return `<section class="card"><h3>${label}</h3><p>${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}</p><span class="pill">${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}</span></section>`}).join('')}</div>`;}
|
||||
async function render(settings=false){
|
||||
const view=document.querySelector('#view');let root=view.querySelector('#endpoint-page');
|
||||
if(!root||root.dataset.settings!==String(settings)){
|
||||
|
||||
+21
-5
@@ -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()<end:
|
||||
current=ep.music.status()['job']
|
||||
if current and current['id']==job['id'] and current['state'] in ('failed','cancelled'):raise APIError(current['phase'],503)
|
||||
try:body=ep.music.audio(job['id'])
|
||||
except (ValueError,OSError):time.sleep(.5);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.end_headers();self.wfile.write(body);return
|
||||
ep.music.stop();raise APIError('Musik-Zeitlimit überschritten.',504)
|
||||
def chat(self,ep,data):
|
||||
try:data,self.compatibility=normalize_chat(data)
|
||||
except CompatibilityError as exc:raise APIError(str(exc)) from None
|
||||
|
||||
@@ -60,6 +60,10 @@ def assess(model, installed, profile=None):
|
||||
add('current',result['runtime'],kind,result['route'],'Vorhandene Deck-Zuordnung')
|
||||
if kind=='music' and repo=='audio-cpp/yue2-3b-gguf' and name.startswith('yue2-3b-') and name.endswith('.gguf'):
|
||||
result.update(family='YuE2',evidence='audio-cpp/Yue2-3B-GGUF · Hauptmodell',state='adapter_missing',label='YuE2 erkannt · Deck-Worker fehlt',message='Passende Laufzeit: audio.cpp. Sie kann einmalig heruntergeladen und gebaut werden. Zur Generierung fehlen noch der Deck-Musikworker und die Ladypoly-Anbindung. Installation allein schaltet dieses Modell nicht frei.',components=['YuE2-VAE GGUF','Modell-/Generierungskonfiguration, VAE-Konfiguration und Qwen-Tokenizer (sidecars)'])
|
||||
if installed.get('music_worker'):
|
||||
result.update(state='setup',label='YuE2-Musikworker verfügbar · Komponenten prüfen',message='audio.cpp-Laufzeit und benötigte Komponenten einrichten. Danach Musik → Testen verwenden oder Profil am Musik-API-Endpunkt freigeben.',runtime='audio.cpp',installed=bool(installed.get('audio_cpp')),route='#music')
|
||||
if profile:
|
||||
result.update(state='ready' if profile.get('runnable') else 'profile_incomplete',label='Ausführbar · audio.cpp-Musikworker' if profile.get('runnable') else 'Musikprofil einrichten',message='YuE2 wird mit den zugeordneten Komponenten über audio.cpp ausgeführt. Testen im Musikbereich; Ergebnisse als WAV. Ladypoly-WebUI ist separat.',blockers=profile.get('blockers',[]))
|
||||
add('audio_cpp','audio.cpp','audio_cpp','#audio-cpp-runtime','Geprüfte GGUF-Quelle für die YuE2-Modellfamilie')
|
||||
elif kind=='music' and ('yue2' in repo or 'yue2' in family):
|
||||
result.update(family='YuE2',evidence='Repository-/Architekturhinweis; Ausführung ungeprüft')
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena Deck</title><link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/dashboard.css"><link rel="stylesheet" href="/themes.css"><script src="/network-ui.js" defer></script><script src="/access-ui.js" defer></script><script src="/catalog-ui.js" defer></script><script src="/runtime-ui.js" defer></script><script src="/profiles-ui.js" defer></script><script src="/auto-test-ui.js" defer></script><script src="/chat-test-ui.js" defer></script><script src="/image-test-ui.js" defer></script><script src="/docker-ui.js" defer></script><script src="/stt-ui.js" defer></script><script src="/tts-ui.js" defer></script><script src="/studio.js" defer></script><script src="/audio-cpp-ui.js" defer></script><script src="/ltx-original-ui.js" defer></script><script src="/video-ui.js" defer></script><script src="/endpoint-ui.js" defer></script><script src="/dashboard-ui.js" defer></script><script src="/backup-ui.js" defer></script><script src="/app.js" defer></script></head><body><aside><a class="brand" href="#dashboard"><span>Α</span> ATHENA <b>DECK</b></a><p class="eyebrow">WORKSPACE / ATHENA</p><nav aria-label="Hauptnavigation"><a href="#dashboard">Dashboard <span>01</span></a><a href="#home">Übersicht <span>02</span></a><a href="#chat">Sprachmodelle / Chat <span>02</span></a><a href="#image">Bildgenerierung <span>03</span></a><p class="nav-heading">AUDIO</p><a class="nav-sub" href="#audio">Text-to-Speech (TTS)</a><a class="nav-sub" href="#stt">Spracherkennung (STT)</a><a class="nav-sub" href="#music">Musik</a><a class="nav-sub" href="#voice">Stimme / Voice</a><a href="#video">Video <span>05</span></a><a href="#services">Weitere Dienste <span>06</span></a><p class="nav-heading">EINSTELLUNGEN</p><a href="#backup">Backup & Restore <span>↗</span></a><a href="#appearance">Darstellung <span>↗</span></a><a href="#runtimes">Laufzeiten <span>↗</span></a><a class="nav-sub" href="#runtime">llama.cpp</a><a class="nav-sub" href="#image-runtime">Bildgenerierung</a><a class="nav-sub" href="#stt-runtime">Spracherkennung</a><a class="nav-sub" href="#tts-runtime">Text-to-Speech</a><a class="nav-sub" href="#audio-cpp-runtime">audio.cpp</a><a class="nav-sub" href="#ltx-original-runtime">LTX Original</a><a class="nav-sub" href="#docker-runtime">Docker</a><a href="#endpoint">Endpunkt & Profile <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang & API <span>↗</span></a></nav><footer><i></i> Isolierte Entwicklungsumgebung<p>Eigener Modell-Router<br>Hardware · nur lesend</p></footer></aside><main><header><span>ATHENA CONTROL SURFACE</span><span class="pill">v0.7 · Router</span></header><div id="view"></div><p id="error" role="alert"></p></main></body></html>
|
||||
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena Deck</title><link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/dashboard.css"><link rel="stylesheet" href="/themes.css"><script src="/network-ui.js" defer></script><script src="/access-ui.js" defer></script><script src="/catalog-ui.js" defer></script><script src="/runtime-ui.js" defer></script><script src="/profiles-ui.js" defer></script><script src="/auto-test-ui.js" defer></script><script src="/chat-test-ui.js" defer></script><script src="/image-test-ui.js" defer></script><script src="/docker-ui.js" defer></script><script src="/stt-ui.js" defer></script><script src="/tts-ui.js" defer></script><script src="/studio.js" defer></script><script src="/music-ui.js" defer></script><script src="/audio-cpp-ui.js" defer></script><script src="/ltx-original-ui.js" defer></script><script src="/video-ui.js" defer></script><script src="/endpoint-ui.js" defer></script><script src="/dashboard-ui.js" defer></script><script src="/backup-ui.js" defer></script><script src="/app.js" defer></script></head><body><aside><a class="brand" href="#dashboard"><span>Α</span> ATHENA <b>DECK</b></a><p class="eyebrow">WORKSPACE / ATHENA</p><nav aria-label="Hauptnavigation"><a href="#dashboard">Dashboard <span>01</span></a><a href="#home">Übersicht <span>02</span></a><a href="#chat">Sprachmodelle / Chat <span>02</span></a><a href="#image">Bildgenerierung <span>03</span></a><p class="nav-heading">AUDIO</p><a class="nav-sub" href="#audio">Text-to-Speech (TTS)</a><a class="nav-sub" href="#stt">Spracherkennung (STT)</a><a class="nav-sub" href="#music">Musik</a><a class="nav-sub" href="#voice">Stimme / Voice</a><a href="#video">Video <span>05</span></a><a href="#services">Weitere Dienste <span>06</span></a><p class="nav-heading">EINSTELLUNGEN</p><a href="#backup">Backup & Restore <span>↗</span></a><a href="#appearance">Darstellung <span>↗</span></a><a href="#runtimes">Laufzeiten <span>↗</span></a><a class="nav-sub" href="#runtime">llama.cpp</a><a class="nav-sub" href="#image-runtime">Bildgenerierung</a><a class="nav-sub" href="#stt-runtime">Spracherkennung</a><a class="nav-sub" href="#tts-runtime">Text-to-Speech</a><a class="nav-sub" href="#audio-cpp-runtime">audio.cpp</a><a class="nav-sub" href="#ltx-original-runtime">LTX Original</a><a class="nav-sub" href="#docker-runtime">Docker</a><a href="#endpoint">Endpunkt & Profile <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang & API <span>↗</span></a></nav><footer><i></i> Isolierte Entwicklungsumgebung<p>Eigener Modell-Router<br>Hardware · nur lesend</p></footer></aside><main><header><span>ATHENA CONTROL SURFACE</span><span class="pill">v0.7 · Router</span></header><div id="view"></div><p id="error" role="alert"></p></main></body></html>
|
||||
+10
@@ -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 '<section id="music-test" class="card"><h2>Musik ausprobieren</h2><p>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.</p><p id="music-state" role="status"></p><p id="music-error" role="alert"></p><div id="music-progress"></div><form id="music-form"><label>Profil<select id="music-profile" required></select></label><label>Musikstil<textarea id="music-style" maxlength="2000" required placeholder="English, acoustic pop, warm vocals, gentle guitar"></textarea></label><label>Songtext<textarea id="music-lyrics" maxlength="8000" rows="6" required placeholder="[Verse] … [Chorus] …"></textarea></label><details><summary>Testparameter</summary><label>Maximale semantische Tokens<input type="number" id="music-tokens" min="32" max="9000" value="400"></label><label>NAR-Schritte<input type="number" id="music-steps" min="1" max="32" value="8"></label><label>Seed<input type="number" id="music-seed" min="0" max="2147483647" value="1234"></label></details><button id="music-start">Musik erzeugen</button><button type="button" class="secondary" id="music-cancel">Abbrechen</button></form><div id="music-result"></div><p>RTX 5080 bevorzugt. Deck-Aufträge werden koordiniert. Nach der Anfrage wird das Modell entladen. Text und Stil werden nicht dauerhaft gespeichert.</p></section>';}
|
||||
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?'<progress aria-label="Musikgenerierung läuft"></progress>':'';el('music-start').disabled=busy||running||!ready;el('music-cancel').disabled=!running;if(j?.state==='complete')el('music-result').innerHTML=`<h3>Ergebnis · ${e(j.duration_seconds)} Sekunden</h3><audio controls src="/api/v1/music/audio?id=${e(j.id)}"></audio><p><a download="athena-musik.wav" href="/api/v1/music/audio?id=${e(j.id)}">WAV herunterladen</a></p>`;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=>`<option value="${e(p.id)}">${e(p.name)}</option>`).join('')||'<option value="">Zuerst Profil und Komponenten einrichten</option>';}).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};
|
||||
})();
|
||||
@@ -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()
|
||||
+2
-1
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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():`<div class="kicker">ATHENA / MODELLVERWALTUNG</div><h1>${labels[page]}</h1><p>${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}</p>${audioDescriptions[page]&&!['audio','stt'].includes(page)?'<p class="note">Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.</p>':''}<div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${[['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])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-section="${id}">${label}</button>`).join('')}</div><div role="tabpanel">${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'?'<section class="card"><h2>Keine von Deck gestarteten Modelle</h2><p>Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.</p><a class="link" href="#runtime">llama.cpp-Builds verwalten →</a></section>':CatalogUI.html(section==='library',section==='downloads')}</div>`;
|
||||
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():`<div class="kicker">ATHENA / MODELLVERWALTUNG</div><h1>${labels[page]}</h1><p>${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}</p>${audioDescriptions[page]&&!['audio','stt','music'].includes(page)?'<p class="note">Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.</p>':''}<div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${[['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])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-section="${id}">${label}</button>`).join('')}</div><div role="tabpanel">${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'?'<section class="card"><h2>Keine von Deck gestarteten Modelle</h2><p>Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.</p><a class="link" href="#runtime">llama.cpp-Builds verwalten →</a></section>':CatalogUI.html(section==='library',section==='downloads')}</div>`;
|
||||
document.querySelector('#view').innerHTML=`<div id="studio" data-page="${page}">${body}</div>`;
|
||||
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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'])
|
||||
Reference in new issue
Block a user