From 66b035c8d76bd3ea757ba43b91ce7791f245981e Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:00:36 +0200 Subject: [PATCH] Install native audio separator with model packages WAV tests and restore recipe --- README.md | 5 ++- STUDIO.md | 41 +++++++++++++++++++++ app.js | 1 + backup.py | 23 ++++++++++-- deploy/Dockerfile | 6 +-- deploy/Dockerfile.app-update | 6 +-- deploy/install.py | 4 +- execution_setup.py | 3 ++ index.html | 2 +- inference.py | 2 +- separator-ui.js | 20 ++++++++++ separator.py | 71 ++++++++++++++++++++++++++++++++++++ separator_runtime.py | 71 ++++++++++++++++++++++++++++++++++++ separator_worker.py | 17 +++++++++ server.py | 38 +++++++++++++++++-- stt.py | 4 +- studio.js | 4 +- test_backup.py | 8 ++++ test_separator.py | 55 ++++++++++++++++++++++++++++ test_server.py | 10 +++++ 20 files changed, 368 insertions(+), 23 deletions(-) create mode 100644 separator-ui.js create mode 100644 separator.py create mode 100644 separator_runtime.py create mode 100644 separator_worker.py create mode 100644 test_separator.py diff --git a/README.md b/README.md index 6eebb7d..e71a377 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,9 @@ Installations-, Versions- und Statusseiten. Der Laufzeiten-Katalog zeigt den aktuellen Installationsstatus, lässt sich nach Name und Bereich filtern und bietet eine direkte llama.cpp-Releaseprüfung mit Änderungsnotizen. „Build vorbereiten“ übernimmt das Release in das Buildformular; erst dessen -Bestätigung startet den Build. Audio Separator ist als noch nicht angebundene -Laufzeit sichtbar und kann derzeit nicht über Deck installiert werden. Die einzelnen Laufzeiten stehen +Bestätigung startet den Build. Audio Separator besitzt einen eigenen Installer und WAV-Trenntest. Unterstützte +BS-/MelBand-RoFormer-Pakete werden einschließlich passender YAML-Konfiguration +geladen; ungeprüfte Hub-Dateien werden dadurch nicht freigeschaltet. Die einzelnen Laufzeiten stehen nicht mehr separat in der Seitenleiste. Bibliothek und Profile verwenden kompakte, aufklappbare Zeilen: Details, API-Freigabe, Komponenten und Aktionen stehen in der geöffneten Zeile. Downloads behalten Fortschritt, Geschwindigkeit, diff --git a/STUDIO.md b/STUDIO.md index d46ede0..d7e1a00 100644 --- a/STUDIO.md +++ b/STUDIO.md @@ -291,3 +291,44 @@ Laufzeiten müssen weiter über die Steuerung verwaltet werden. Löschen benöti einen gestoppten Container und eine Bestätigung; weder Images noch Volumes werden gelöscht. Daten nur in der beschreibbaren Container-Schicht gehen verloren. Es gibt keine pauschalen Stop-, Prune- oder Löschaktionen für den Docker-Stack. + +## Audio Separator + +Unter Einstellungen → Laufzeiten → Audio Separator steht ein nativer Installer +für `audio-separator[gpu]==0.47.0` bereit. PyTorch/Torchaudio 2.11.0 mit CUDA 12.8 +und ONNX Runtime GPU 1.22.0 werden in einer eigenen Python-Umgebung installiert. +FFmpeg und libsndfile werden vom Debian-Testinstaller im Deck-Image bereitgestellt. +Die Laufzeit wird pro Server, nicht pro Modell installiert. + +Unterstützte Pakete: BS-RoFormer (`model_bs_roformer_ep_317_sdr_12.9755.ckpt`) +und MelBand RoFormer (`vocals_mel_band_roformer.ckpt`). „Modell und Konfiguration +herunterladen“ verwendet den Audio-Separator-Modellkatalog und lädt die passende +YAML mit. Diese Pakete liegen in `separator-runtime/models`; sie werden derzeit +auf der Laufzeitseite verwaltet, getrennt von ungeprüften Hub-Downloads in der +Bibliothek. CoreML-, ONNX- und Safetensors-Abwandlungen werden nicht automatisch +als kompatibel freigegeben. Für unbekannte RoFormer-Dateien zeigt Deck den Weg +zu diesem Einrichtungsbereich, behauptet aber keine Kompatibilität. + +Stimmwerkzeuge → Audio trennen bzw. die Laufzeitseite bieten einen Trenntest: +16-Bit-PCM-WAV, Mono/Stereo, 8–48 kHz, maximal 30 Sekunden/8 MiB. Der Worker +nutzt bevorzugt eine freie RTX 3060, sonst eine freie RTX 5080. Deck koordiniert +eigene LLM/TTS-Prozesse über den GPU-Scheduler; fremde Prozesse werden nicht +beendet. Video-/Musikmodus sperren diesen Test. Es gibt keinen stillen CPU-Fallback. +Nach Abschluss/Abbruch wird der Prozess beendet und die Eingabedatei gelöscht. +Ergebnis-WAVs bleiben unter `separator-tests/` auf Athena. + +Interne, nur mit GUI-Anmeldung erreichbare API: +- `GET /api/v1/separator-runtime`: Status, Installationsauftrag, Modellpakete. +- `POST /api/v1/separator-runtime/install` / `cancel`: leeres JSON-Objekt. +- `POST /api/v1/separator-runtime/download`: `{"model":""}`. +- `GET /api/v1/separator-tests`: Teststatus und Ergebnisliste. +- `POST /api/v1/separator-tests/start`: Multipart-Felder `model` und `file`. +- `POST /api/v1/separator-tests/cancel`: leeres JSON-Objekt. +- `GET /api/v1/separator-tests/audio?id=&index=`: WAV. + +Backups enthalten Version und ausgewählte Modellpakete als Restore-Rezept, +keine Laufzeit-Binaries, Gewichte, Eingabe- oder Ergebnis-Audiodateien. Restore +installiert eine fehlende Laufzeit und lädt die Modellpakete erneut. Der +Audio-Separator-Upstream-Katalog und seine öffentlichen Modellquellen können +sich ändern; Wiederherstellung ist von deren Verfügbarkeit abhängig. +Quelle: https://github.com/nomadkaraoke/python-audio-separator/tree/v0.47.0 diff --git a/app.js b/app.js index d6504f2..979c51a 100644 --- a/app.js +++ b/app.js @@ -24,6 +24,7 @@ function appearancePage(){ sync(); } async function refresh(){if(refreshing)return;refreshing=true;const page=location.hash.slice(1)||'dashboard';document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('active',a.hash==='#'+page));try{error.textContent='';let html; +if(page==='separator-runtime'||page==='separator-test'){SeparatorUI.render(page==='separator-test');return;} if(page==='audio-cpp-runtime'){AudioCppUI.render();return;} if(page==='ltx-original-runtime'){LTXOriginalUI.render();return;} if(['video','video-runtime'].includes(page)){VideoUI.runtime();return;} diff --git a/backup.py b/backup.py index 2cf6057..d84a59b 100644 --- a/backup.py +++ b/backup.py @@ -72,7 +72,11 @@ def validate(doc): if 'video/original-work/settings.json' in settings and (not isinstance(settings['video/original-work/settings.json'],dict) or len(json.dumps(settings['video/original-work/settings.json']))>65536):raise ValueError('Ungültige LTX-Einstellungen.') validate_record(doc.get('credentials')) runtimes=doc.get('runtimes') - if not isinstance(runtimes,dict) or set(runtimes)-{'llama','llama_builds','image','tts','enhancers','swarm_nodes','ltx_original','audio_cpp'} or not {'llama','llama_builds','image','tts','enhancers','swarm_nodes'}<=set(runtimes):raise ValueError('Ungültiges Laufzeitrezept.') + if not isinstance(runtimes,dict) or set(runtimes)-{'llama','llama_builds','image','tts','enhancers','swarm_nodes','ltx_original','audio_cpp','separator'} or not {'llama','llama_builds','image','tts','enhancers','swarm_nodes'}<=set(runtimes):raise ValueError('Ungültiges Laufzeitrezept.') + if 'separator' in runtimes: + from separator_runtime import MODELS + value=runtimes['separator'] + if not isinstance(value,dict) or set(value)!={'installed','models'} or type(value['installed']) is not bool or not isinstance(value['models'],list) or len(value['models'])>len(MODELS) or any(not isinstance(m,str) or m not in MODELS for m in value['models']) or len(set(value['models']))!=len(value['models']):raise ValueError('Ungültiges Audio-Separator-Rezept.') if 'audio_cpp' in runtimes and type(runtimes['audio_cpp']) is not bool:raise ValueError('Ungültige audio.cpp-Laufzeit.') if 'ltx_original' in runtimes and type(runtimes['ltx_original']) is not bool:raise ValueError('Ungültige LTX-Laufzeit.') if not isinstance(runtimes['llama_builds'],list) or len(runtimes['llama_builds'])>50 or any(not isinstance(b,dict) or set(b)!={'commit','backend'} or not re.fullmatch('[a-f0-9]{40}',b.get('commit','')) or b.get('backend') not in ('CUDA','CPU') for b in runtimes['llama_builds']):raise ValueError('Ungültige Buildrezepte.') @@ -139,6 +143,8 @@ class Backup: network=s.network.call('backup-export') else:warnings.append('Deck-Netzwerkmodul deaktiviert; fremder WireGuard-Gateway wird nicht gesichert.') document=dict(format='athena-deck',version=VERSION,created_at=time.time(),settings=settings,models=models,credentials=s.credentials.read(),theme=theme,network=network,services=services,runtime_versions=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']),runtimes=dict(audio_cpp=bool(getattr(s,'audio_cpp_runtime',None) and s.audio_cpp_runtime.status()['installed']),ltx_original=bool(getattr(s,'ltx_original_runtime',None) and s.ltx_original_runtime.status()['installed']),llama=llama,llama_builds=[dict(commit=b['commit'],backend=b['backend']) for b in state['builds']],image=s.image_runtime.status()['installed'],tts=s.tts_runtime.status()['installed'],enhancers={task:data['revision'] for task in ('t2i','i2i') if (data:=s.prompt_enhancer.installed(task))},swarm_nodes=(self.root/'video/swarm-comfy-nodes').is_dir()),warnings=warnings) + if getattr(s,'separator_runtime',None): + separator=s.separator_runtime.status();document['runtimes']['separator']=dict(installed=separator['installed'],models=[m['id'] for m in separator['models'] if m['installed']]);document['runtime_versions']['separator']=separator['version'] if document['runtimes']['audio_cpp']: from audio_cpp_runtime import REVISION document['runtime_versions']['audio_cpp']=REVISION @@ -171,13 +177,16 @@ class Backup: elif s.network.status().get('enabled') or s.network.status().get('mode')!='lan':blockers.append('Deck-Tunnel vor Netzwerk-Restore im bestätigten LAN-Modus deaktivieren.') if doc['network']['mode']!='lan':warnings.append('Tunnelzugang wird mit Rückfallzeit aktiviert und muss über den Tunnel bestätigt werden.') versions=doc.get('runtime_versions',{}) + if versions.get('separator'): + from separator_runtime import VERSION as SEPARATOR_VERSION + if versions['separator']!=SEPARATOR_VERSION:blockers.append('Backup verlangt eine andere Audio-Separator-Version.') if versions.get('audio_cpp'): from audio_cpp_runtime import REVISION if versions['audio_cpp']!=REVISION:blockers.append('Backup verlangt eine andere audio.cpp-Version. Passende Deck-Version verwenden.') if versions.get('ltx_original'): from ltx_original_runtime import DESKTOP_REV,LTX_REV if versions['ltx_original']!=dict(desktop=DESKTOP_REV,inference=LTX_REV):blockers.append('Backup verlangt eine andere originale LTX-Version. Passende Deck-Version verwenden.') - if versions and {k:v for k,v in versions.items() if k not in ('ltx_original','audio_cpp')}!=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']):blockers.append('Backup verlangt andere Bild-/TTS-Laufzeitversionen. Passende Deck-Version oder geprüftes Runtime-Upgrade verwenden.') + if versions and {k:v for k,v in versions.items() if k not in ('ltx_original','audio_cpp','separator')}!=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']):blockers.append('Backup verlangt andere Bild-/TTS-Laufzeitversionen. Passende Deck-Version oder geprüftes Runtime-Upgrade verwenden.') for c in doc['services']: if not c.get('registry') and c.get('build_recipe')!='swarm-ui':warnings.append(c['name']+': Image nur lokal vorhanden; Registry-/Buildquelle fehlt.') ports=s.endpoint.allowed_ports @@ -202,7 +211,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),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.') + 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),getattr(s,'separator_runtime',None),getattr(s,'separator_tests',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() @@ -232,6 +241,12 @@ class Backup: self._phase('audio.cpp-Laufzeit installieren');s.audio_cpp_runtime.start();self._wait(s.audio_cpp_runtime.status) if r.get('ltx_original') and not s.ltx_original_runtime.status()['installed']: self._phase('Originale LTX-Laufzeit installieren');s.ltx_original_runtime.start();self._wait(s.ltx_original_runtime.status) + if r.get('separator',{}).get('installed'): + if not s.separator_runtime.status()['installed']: + self._phase('Audio Separator installieren');s.separator_runtime.start();self._wait(s.separator_runtime.status) + for name in r['separator']['models']: + if not s.separator_runtime.model_ready(name): + self._phase('Audio-Separator-Modellpaket laden');s.separator_runtime.download(name);self._wait(s.separator_runtime.status) targets=r['llama_builds']+([r['llama']] if r['llama'] and r['llama'] not in r['llama_builds'] else []) for target in targets: state=s.runtime.status();matching=next((b for b in state['builds'] if b['commit']==target['commit'] and b['backend']==target['backend']),None) @@ -370,6 +385,8 @@ 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,'separator_runtime',None):self.server.separator_runtime.stop() + if getattr(self.server,'separator_tests',None):self.server.separator_tests.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() diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 16182ab..ec4b3a3 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -11,13 +11,13 @@ RUN git clone https://github.com/Comfy-Org/ComfyUI.git /opt/deck-comfy \ && /opt/deck-image-python/bin/pip install --no-cache-dir -c /tmp/image-requirements.lock -r /opt/deck-comfy/requirements.txt -r /opt/deck-comfy/custom_nodes/ComfyUI-GGUF/requirements.txt accelerate==1.15.0 \ && /opt/deck-image-python/bin/pip freeze > /opt/deck-comfy/deck-requirements.lock RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3-dev && rm -rf /var/lib/apt/lists/* -RUN apt-get update && apt-get install -y --no-install-recommends python3-cryptography && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends python3-cryptography ffmpeg libsndfile1 && rm -rf /var/lib/apt/lists/* 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 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 separator.py separator_runtime.py separator_worker.py 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 separator-ui.js 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 d0edf06..b4eb780 100644 --- a/deploy/Dockerfile.app-update +++ b/deploy/Dockerfile.app-update @@ -1,13 +1,13 @@ ARG DECK_RUNTIME_IMAGE FROM ${DECK_RUNTIME_IMAGE} USER root -RUN apt-get update && apt-get install -y --no-install-recommends python3-cryptography && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends python3-cryptography ffmpeg libsndfile1 && rm -rf /var/lib/apt/lists/* 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 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 separator.py separator_runtime.py separator_worker.py 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 separator-ui.js 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 ae3c521..3d6369d 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 = ['deploy/ladypoly/Dockerfile','deploy/ladypoly/bridge.py','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'] +FILES = ['separator.py','separator_runtime.py','separator_worker.py','separator-ui.js','deploy/ladypoly/Dockerfile','deploy/ladypoly/bridge.py','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('music-verification','music-test-temp','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-*')) + shutil.copytree(base/'state',base/'backups'/stamp,ignore=shutil.ignore_patterns('music-verification','music-test-temp','music-jobs','separator-runtime','separator-tests','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/execution_setup.py b/execution_setup.py index 5db9698..e77db79 100644 --- a/execution_setup.py +++ b/execution_setup.py @@ -73,6 +73,9 @@ def assess(model, installed, profile=None): add('audio_cpp','audio.cpp','audio_cpp','#audio-cpp-runtime','audio-cpp-Repository; konkrete Familie und Zusatzdateien noch prüfen') elif meta.get('library_name')=='transformers': add('transformers','Hugging Face Transformers / PyTorch','transformers',None,'Hub nennt Transformers; modellabhängiger Worker erforderlich',False) + if kind=='voice' and any(token in (repo+' '+name+' '+family) for token in ('roformer','demucs')): + add('separator','Audio Separator','separator','#separator-runtime','Trennmodell-Hinweis; exakte Datei und Konfiguration müssen zum unterstützten Paket passen') + result['candidate_message']='Unter Audio Separator sind geprüfte Modellpakete mit Konfiguration verfügbar. Beliebige CoreML-, ONNX- oder Safetensors-Abwandlungen sind nicht automatisch kompatibel.' if kind=='video' and not result['candidates'] and meta.get('library_name')=='diffusers': add('diffusers','Diffusers / PyTorch','diffusers',None,'Hub nennt Diffusers; passende Video-Pipeline noch prüfen',False) if not result['candidates']: diff --git a/index.html b/index.html index a86d788..32ba879 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
+Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
diff --git a/inference.py b/inference.py index 39155e3..d280b05 100644 --- a/inference.py +++ b/inference.py @@ -47,7 +47,7 @@ class Scheduler: if self.transition: try: if switch:self.worker.stop() - if key==('image',) or switch:self.evict_tts() + if key in (('image',),('separator',)) or switch:self.evict_tts() if prepare:prepare() except Exception: self.worker.stop() diff --git a/separator-ui.js b/separator-ui.js new file mode 100644 index 0000000..8a54d90 --- /dev/null +++ b/separator-ui.js @@ -0,0 +1,20 @@ +window.SeparatorUI=(()=>{ + const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + async function api(path,data){const response=await fetch('/api/v1/'+path,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const value=await response.json();if(!response.ok)throw Error(value.error||'Anfrage fehlgeschlagen');return value;} + function render(test=false){if(document.querySelector('#separator-panel')?.dataset.test===String(test))return; + document.querySelector('#view').innerHTML=`
${test?'STIMMWERKZEUGE':'EINSTELLUNGEN / LAUFZEITEN'}

${test?'Audio trennen':'Audio Separator'}

BS-RoFormer und MelBand RoFormer trennen Gesang und Instrumental. Die native Laufzeit wird einmal installiert und für passende Modellpakete wiederverwendet.

Version 0.47.0 · mindestens 15 GiB frei. Installiert CUDA-PyTorch und Audio Separator in einer eigenen Umgebung. Keine Änderungen an Host-Treibern oder alten Diensten.

Installationsausgabe

Kurzer Trenntest

PCM-WAV, 16 Bit, Mono oder Stereo, bis 30 Sekunden und 8 MiB. Im LLM-Modus koordiniert Deck die GPU-Nutzung und entlädt gegebenenfalls eigene Modelle. Fremde Dienste bleiben unberührt. Das Trennmodell wird nach dem Test entladen.

`; + const root=document.querySelector('#separator-panel'),msg=root.querySelector('#sep-message');let pending=false,signature=''; + async function action(path,data){pending=true;try{await api(path,data);msg.textContent='Aktion gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;signature='';poll();}} + async function poll(){if(!root.isConnected)return;try{const [s,t]=await Promise.all([api('separator-runtime'),api('separator-tests')]);if(!root.isConnected)return; + const running=s.job?.state==='running';if(s.job){const log=await api('separator-runtime/log');if(!root.isConnected)return;root.querySelector('#sep-log').textContent=log.text;}root.querySelector('#sep-status').innerHTML=`

${s.installed?'Installiert':'Noch nicht installiert'} · ${e(s.version)}

${s.job?`

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

${running?'':''}`:''}`; + root.querySelector('#sep-install').disabled=pending||running||s.installed;root.querySelector('#sep-cancel').disabled=pending||!running; + const key=JSON.stringify(s.models);if(key!==signature){signature=key;root.querySelector('#sep-models').innerHTML='

Unterstützte Modellpakete

Gewichte und passende Konfiguration werden zusammen beschafft. Diese Pakete sind getrennt von ungeprüften Hugging-Face-Dateien.

'+s.models.map(m=>`

${e(m.name)} · ${m.installed?'Bereit':'Fehlt'}
${e(m.id)} + ${e(m.configuration)}

`).join('');root.querySelector('#sep-model').innerHTML=s.models.filter(m=>m.installed).map(m=>``).join('');root.querySelectorAll('[data-sep-model]').forEach(b=>b.onclick=()=>action('separator-runtime/download',{model:b.dataset.sepModel}));} + root.querySelectorAll('[data-sep-model]').forEach(b=>b.disabled=pending||running||!s.installed||s.models.find(m=>m.id===b.dataset.sepModel)?.installed); + const j=t.job;root.querySelector('#sep-test button').disabled=pending||running||!s.models.some(m=>m.installed)||j?.state==='running';root.querySelector('#sep-test-cancel').disabled=j?.state!=='running';root.querySelector('#sep-job').innerHTML=j?`

${e(j.state)} · ${e(j.phase)} ${j.elapsed_seconds?e(j.elapsed_seconds)+' s':''}

${j.state==='running'?'':''}`:''; + const results=root.querySelector('#sep-results');if(results.dataset.job!==String(j?.id+':'+j?.state)){results.dataset.job=String(j?.id+':'+j?.state);results.innerHTML=j?.state==='complete'?j.files.map(f=>`

${e(f.name)}

Spur herunterladen

`).join(''):'';} + }catch(error){if(root.isConnected)msg.textContent=error.message;}clearTimeout(root.pollTimer);if(root.isConnected)root.pollTimer=setTimeout(poll,3000);} + root.querySelector('#sep-install').onclick=()=>action('separator-runtime/install',{});root.querySelector('#sep-cancel').onclick=()=>action('separator-runtime/cancel',{});root.querySelector('#sep-test-cancel').onclick=()=>action('separator-tests/cancel',{}); + root.querySelector('#sep-test').onsubmit=async event=>{event.preventDefault();pending=true;msg.textContent='Audio wird hochgeladen …';try{const response=await fetch('/api/v1/separator-tests/start',{method:'POST',headers:{'X-Athena-Deck':'1'},body:new FormData(event.target)}),value=await response.json();if(!response.ok)throw Error(value.error);msg.textContent='Trenntest gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;poll();}};poll(); + } + return {render}; +})(); diff --git a/separator.py b/separator.py new file mode 100644 index 0000000..864ad78 --- /dev/null +++ b/separator.py @@ -0,0 +1,71 @@ +"""Bounded WAV tests with coordinated GPU ownership and disposable input.""" +import io,json,os,signal,subprocess,threading,time,uuid,wave +from pathlib import Path +from image_test import probe +from separator_runtime import MODELS +class SeparatorTests: + def __init__(self,root,runtime,scheduler): + self.root=Path(root);self.runtime=runtime;self.scheduler=scheduler;self.lock=threading.RLock();self.job=None;self.process=None;self.cancel=threading.Event();self.thread=None + def status(self): + with self.lock:return dict(job=dict(self.job) if self.job else None) + def start(self,model,audio): + if not isinstance(model,str) or model not in MODELS or not self.runtime.status()['installed'] or not self.runtime.model_ready(model):raise ValueError('Laufzeit und Modellpaket zuerst unter Audio Separator einrichten.') + if not isinstance(audio,bytes) or not 44<=len(audio)<=8*1024**2:raise ValueError('WAV bis 8 MiB erforderlich.') + try: + with wave.open(io.BytesIO(audio)) as wav: + if wav.getnchannels() not in (1,2) or wav.getsampwidth()!=2 or not 8000<=wav.getframerate()<=48000 or not 04000),None) + if not gpu:raise ValueError('Keine ausreichend freie GPU. Fremde Dienste werden nicht beendet.') + directory.mkdir(mode=0o700);(directory/'input.wav').write_bytes(audio);diagnostics=(directory/'worker.tmp').open('wb') + with self.lock: + if self.cancel.is_set():raise InterruptedError() + self.job.update(phase='Modell laden und Audio trennen',gpu=gpu['name']) + self.process=subprocess.Popen([str(self.runtime.paths()[0]),str(Path(__file__).with_name('separator_worker.py')),str(self.runtime.root/'models'),model,str(directory/'input.wav'),str(directory)],stdout=subprocess.DEVNULL,stderr=diagnostics,start_new_session=True,env=dict(os.environ,CUDA_VISIBLE_DEVICES=gpu['uuid'],OMP_NUM_THREADS='6')) + # Drain diagnostics into a bounded temporary file rather than retaining user data. + deadline=time.monotonic()+600 + while self.process.poll() is None: + if self.cancel.wait(.2):raise InterruptedError() + if time.monotonic()>deadline:raise ValueError('Trenntest überschreitet zehn Minuten.') + with self.lock:self.job['elapsed_seconds']=round(time.time()-self.job['started_at']) + diagnostics.close();error=(directory/'worker.tmp').read_bytes()[-65536:].decode(errors='replace') + if self.process.returncode:raise ValueError('GPU-Speicher reicht nicht (OOM).' if 'out of memory' in error.lower() else 'Audio Separator hat den Test abgebrochen. Modellpaket und Laufzeit prüfen.') + names=json.loads((directory/'results.json').read_text()) + if not names or any(Path(n).name!=n or not n.endswith('.wav') or not (directory/n).is_file() for n in names):raise ValueError('Ungültige Ergebnisdateien.') + with self.lock:self.job['files']=[dict(index=i,name=n) for i,n in enumerate(names)] + (directory/'complete').touch();state='complete';phase='Spuren fertig · Modell entladen' + except InterruptedError:state='cancelled';phase='Trenntest abgebrochen' + except Exception as exc:phase=str(exc) if isinstance(exc,ValueError) else 'Trenntest fehlgeschlagen.' + 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 + if 'diagnostics' in locals():diagnostics.close() + (directory/'input.wav').unlink(missing_ok=True);(directory/'worker.tmp').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() + if self.thread:self.thread.join(7) + return {'cancellation_requested':True} + def audio(self,ident,index): + 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('Auftrag nicht fertig.') + names=json.loads((directory/'results.json').read_text()) + if type(index) is not int or not 0<=index32*1024**2:raise ValueError('Spur zu groß.') + return path.read_bytes() diff --git a/separator_runtime.py b/separator_runtime.py new file mode 100644 index 0000000..dc9608d --- /dev/null +++ b/separator_runtime.py @@ -0,0 +1,71 @@ +"""Owned audio-separator environment and explicitly supported model packages.""" +import hashlib,json,os,shutil,sys,time +from pathlib import Path +from ltx_original_runtime import LTXOriginalRuntime +VERSION='0.47.0' +MODELS={ + 'vocals_mel_band_roformer.ckpt':('MelBand RoFormer · Gesang / Instrumental','vocals_mel_band_roformer.yaml'), + 'model_bs_roformer_ep_317_sdr_12.9755.ckpt':('BS-RoFormer · Gesang / Instrumental','model_bs_roformer_ep_317_sdr_12.9755.yaml')} +class SeparatorRuntime(LTXOriginalRuntime): + name='Audio Separator' + def __init__(self,root):super().__init__(root,None) + def paths(self): + marker=self.root/'active.json' + if marker.is_file(): + ident=json.loads(marker.read_text()).get('id','') + if len(ident)==32 and all(c in '0123456789abcdef' for c in ident): + base=self.root/ident + if (base/'ready').is_file():return base/'python/bin/python',base + return self.root/'missing-python',self.root/'missing-runtime' + def status(self): + python,base=self.paths() + return dict(installed=python.is_file() and (base/'ready').is_file(),version=VERSION,required_disk_gib=15,job=dict(self.job) if self.job else None,models=[dict(id=name,name=label,configuration=config,installed=self.model_ready(name)) for name,(label,config) in MODELS.items()]) + def model_ready(self,name):return name in MODELS and (self.root/'models'/(name+'.ready')).is_file() and all((self.root/'models'/f).is_file() and (self.root/'models'/f).stat().st_size>0 for f in (name,MODELS[name][1])) + def start(self): + if not shutil.which('ffmpeg'):raise ValueError('FFmpeg fehlt. Deck-Systemvoraussetzungen installieren.') + with self.lock: + if self.job and self.job['state']=='running':raise ValueError('Einrichtungsauftrag läuft bereits.') + self.requested_model=None + return super().start() + def download(self,name): + import threading,uuid + if not isinstance(name,str) or name not in MODELS:raise ValueError('Dieses Modellpaket ist nicht unterstützt.') + with self.lock: + if not self.status()['installed']:raise ValueError('Zuerst Audio Separator installieren.') + if self.job and self.job['state']=='running':raise ValueError('Einrichtungsauftrag läuft bereits.') + if shutil.disk_usage(self.root).free<3*1024**3:raise ValueError('Mindestens 3 GiB freier Speicher erforderlich.') + self.cancel.clear();self.requested_model=name;self.job=dict(id=uuid.uuid4().hex,state='running',phase='Modellpaket herunterladen',started_at=time.time());self._save() + self.worker=threading.Thread(target=self._run,daemon=True);self.worker.start();return self.status() + def _run(self): + base=self.root/self.job['id'];state='failed';phase='Einrichtung fehlgeschlagen.';model=getattr(self,'requested_model',None) + try: + if model: + directory=base;directory.mkdir(mode=0o700) + self._command([self.paths()[0],'-c','from audio_separator.separator import Separator; import sys; Separator(model_file_dir=sys.argv[1],info_only=True).download_model_files(sys.argv[2])',directory,model]) + filenames=(model,MODELS[model][1]) + if any(not (directory/f).is_file() or (directory/f).stat().st_size==0 for f in filenames):raise ValueError('Modell oder YAML-Konfiguration fehlt.') + if self.cancel.is_set():raise InterruptedError() + destination=self.root/'models';destination.mkdir(exist_ok=True);hashes={} + for f in filenames: + digest=hashlib.sha256() + with (directory/f).open('rb') as source: + for chunk in iter(lambda:source.read(1024**2),b''):digest.update(chunk) + hashes[f]=digest.hexdigest();(directory/f).replace(destination/f) + (destination/(model+'.ready')).write_text(json.dumps(hashes)) + state='complete';phase='Modell und passende Konfiguration heruntergeladen. Kein Modell geladen.' + else: + base.mkdir(mode=0o700);tmp=base/'tmp';tmp.mkdir() + env=dict(os.environ,TMPDIR=str(tmp),PIP_NO_INPUT='1',PIP_DISABLE_PIP_VERSION_CHECK='1') + python=base/'python/bin/python';self._phase('Eigene Python-Umgebung erstellen');self._command([sys.executable,'-m','venv',base/'python'],env) + self._phase('CUDA-PyTorch herunterladen · mehrere GiB');self._command([python,'-m','pip','install','--no-cache-dir','torch==2.11.0','torchaudio==2.11.0','--index-url','https://download.pytorch.org/whl/cu128'],env) + self._phase('Audio Separator und GPU-Abhängigkeiten installieren');self._command([python,'-m','pip','install','--no-cache-dir',f'audio-separator[gpu]=={VERSION}','onnxruntime-gpu==1.22.0','audioread==3.1.0'],env) + self._phase('Paketkonsistenz und CUDA-Unterstützung prüfen');self._command([python,'-m','pip','check'],env) + self._command([python,'-c','import torch; from audio_separator.separator import Separator; assert torch.version.cuda'],env) + if self.cancel.is_set():raise InterruptedError() + (base/'ready').write_text(VERSION);marker=self.root/'active.tmp';marker.write_text(json.dumps({'id':base.name}));marker.replace(self.root/'active.json') + state='complete';phase='Audio Separator installiert. Jetzt Modellpaket auswählen.' + except InterruptedError:state='cancelled';phase='Einrichtung abgebrochen.' + except Exception as exc:phase=str(exc) + finally: + if model or state!='complete':shutil.rmtree(base,ignore_errors=True) + with self.lock:self.process=None;self.job.update(state=state,phase=phase,finished_at=time.time());self._save() diff --git a/separator_worker.py b/separator_worker.py new file mode 100644 index 0000000..684fc6a --- /dev/null +++ b/separator_worker.py @@ -0,0 +1,17 @@ +"""One native separation process; exact pinned package, no arbitrary model code.""" +import json,sys +from pathlib import Path +from audio_separator.separator import Separator +import torch +model_dir,model,input_file,output_dir=sys.argv[1:] +if not torch.cuda.is_available():raise RuntimeError('CUDA nicht verfügbar; kein stiller CPU-Fallback.') +separator=Separator(model_file_dir=model_dir,output_dir=output_dir,output_format='WAV',use_soundfile=True,mdxc_params={'segment_size':256,'override_model_segment_size':False,'batch_size':1,'overlap':2,'pitch_shift':0}) +separator.load_model(model) +files=separator.separate(input_file) +root=Path(output_dir).resolve();names=[] +for name in files: + path=(root/name).resolve() + if path.parent!=root or not path.is_file() or path.suffix.lower()!='.wav':raise RuntimeError('Ungültige Ausgabe.') + names.append(path.name) +if not names:raise RuntimeError('Keine Spuren erzeugt.') +(root/'results.json').write_text(json.dumps(names)) diff --git a/server.py b/server.py index 8b71156..61f8262 100644 --- a/server.py +++ b/server.py @@ -17,6 +17,8 @@ from profiles import Profiles from capacity import assess, overview from runtime import Runtime from image_runtime import ImageRuntime +from separator_runtime import SeparatorRuntime +from separator import SeparatorTests from image_test import ImageTests from prompt_enhancer import PromptEnhancer from docker_support import DockerSupport @@ -99,6 +101,8 @@ class Server(ThreadingHTTPServer): raise RuntimeError('Serverzugang muss vor dem Start eingerichtet werden.') self.worker=LlamaWorker(self.catalog.root.parent/'llama-worker',self.catalog,self.runtime) self.scheduler=Scheduler(self.worker) + self.separator_runtime=SeparatorRuntime(self.catalog.root.parent/'separator-runtime') + self.separator_tests=SeparatorTests(self.catalog.root.parent/'separator-tests',self.separator_runtime,self.scheduler) self.profiles.chat_blockers=self.worker.blockers self.image_tests.acquire=self.scheduler.image_reservation self.prompt_enhancer.acquire=self.scheduler.image_reservation @@ -112,7 +116,7 @@ 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.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() + self.separator_tests.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 @@ -298,7 +302,7 @@ class Handler(BaseHTTPRequestHandler): 'audio':self.server.tts_runtime.status().get('installed',False), 'video':video_runtime.status().get('installed',False), 'audio_cpp':self.server.audio_cpp_runtime.status()['installed'], - 'music_worker':True} + 'music_worker':True,'separator':self.server.separator_runtime.status()['installed']} def execution_setup(self, model, profile=None, installed=None): return execution_assess(model, self.execution_runtime_snapshot() if installed is None else installed, profile) @@ -314,7 +318,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 = {'/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')} + routes = {'/separator-ui.js':('separator-ui.js','text/javascript'),'/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) @@ -337,6 +341,13 @@ class Handler(BaseHTTPRequestHandler): 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/separator-runtime':return self.respond(self.server.separator_runtime.status()) + if self.path == '/api/v1/separator-runtime/log':return self.respond(self.server.separator_runtime.log()) + if self.path == '/api/v1/separator-tests':return self.respond(self.server.separator_tests.status()) + if urlsplit(self.path).path == '/api/v1/separator-tests/audio': + try: + query=parse_qs(urlsplit(self.path).query);return self.respond(self.server.separator_tests.audio(query.get('id',[''])[0],int(query.get('index',['-1'])[0])),mime='audio/wav') + except (ValueError,OSError):return self.respond({'error':'Spur 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()) @@ -527,6 +538,25 @@ class Handler(BaseHTTPRequestHandler): if data:raise ValueError('Keine Parameter erwartet.') return self.respond(self.server.chat_tests.stop() if self.path.endswith('/cancel') else self.server.chat_tests.unload()) except ValueError as exc:return self.respond({'error':str(exc)},400) + if self.path in ('/api/v1/separator-runtime/install','/api/v1/separator-runtime/cancel','/api/v1/separator-runtime/download'): + try: + data=self.read_json() + if self.path.endswith('/download'): + if set(data)!={'model'}:raise ValueError('Nur Modell-ID erwartet.') + return self.respond(self.server.separator_runtime.download(data['model'])) + if data:raise ValueError('Keine Parameter erwartet.') + return self.respond(self.server.separator_runtime.stop() if self.path.endswith('/cancel') else self.server.separator_runtime.start()) + except (ValueError,OSError) as exc:return self.respond({'error':str(exc)},400) + if self.path in ('/api/v1/separator-tests/start','/api/v1/separator-tests/cancel'): + try: + if self.path.endswith('/cancel'): + if self.read_json():raise ValueError('Keine Parameter erwartet.') + return self.respond(self.server.separator_tests.stop()) + # Reuse bounded multipart parsing; separation permits stereo WAV. + fields,audio=read_upload(self,validate=False) + if set(fields)!={'model'}:raise ValueError('Nur Modell und WAV-Datei erwartet.') + return self.respond(self.server.separator_tests.start(fields['model'],audio)) + except (ValueError,OSError) as exc:return self.respond({'error':str(exc)},400) if self.path in ('/api/v1/audio-cpp-runtime/install','/api/v1/audio-cpp-runtime/cancel'): try: if self.read_json():raise ValueError('Keine Installationsparameter erwartet.') @@ -715,7 +745,7 @@ def main(): server.tts_runtime.stop() server.image_runtime.stop() server.ltx_original_runtime.stop() - server.music.stop();server.audio_cpp_runtime.stop() + server.separator_tests.stop();server.separator_runtime.stop();server.music.stop();server.audio_cpp_runtime.stop() server.prompt_enhancer.stop() server.image_tests.stop() server.runtime.stop() diff --git a/stt.py b/stt.py index e72315b..8730e68 100644 --- a/stt.py +++ b/stt.py @@ -22,7 +22,7 @@ def validate_wav(audio): if len(w.readframes(w.getnframes()))!=w.getnframes()*2:raise ValueError('WAV-Datei ist unvollständig.') except (wave.Error,EOFError):raise ValueError('Ungültige WAV-Datei.') from None -def read_upload(handler): +def read_upload(handler,validate=True): handler.connection.settimeout(30) if handler.headers.get('Transfer-Encoding'):raise ValueError('Chunked Upload wird nicht unterstützt.') try:length=int(handler.headers.get('Content-Length','0')) @@ -44,7 +44,7 @@ def read_upload(handler): if name not in ('model','profile_id','language','response_format') or name in fields or len(data)>256:raise ValueError('Ungültiges oder doppeltes Feld.') try:fields[name]=data.decode('utf-8') except UnicodeError:raise ValueError('Ungültiges Textfeld.') from None - validate_wav(audio) + if validate:validate_wav(audio) if fields.get('response_format','json')!='json':raise ValueError('Aktuell wird response_format=json unterstützt.') return fields,audio diff --git a/studio.js b/studio.js index 2828072..c9e738a 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','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']]:[])]:[])].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')}
`; + 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.'}

${page==='voice'?'

Audio Separator einrichten → · Audio trennen →

Audiotrennung nutzt die unterstützten Pakete im Audio-Separator-Bereich. Andere Hub-Dateien und Voice-Profile benötigen weiterhin eine geprüfte Anbindung.

':''}${audioDescriptions[page]&&!['audio','stt','music','voice'].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']]:[])]:[])].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;} @@ -36,7 +36,7 @@ const Studio=(()=>{ ['audio.cpp','Musik & Audio','audio-cpp-runtime','/api/v1/audio-cpp-runtime','CUDA-Build einmal erstellen. YuE2 ist angebunden; andere Modellfamilien brauchen eine geprüfte Worker-Anbindung.'], ['LTX Original','Video','ltx-original-runtime','/api/v1/ltx-original-runtime','Originales LTX-Backend für LTX DeskWEB. Modelle und Zusatzdateien bleiben in der Bibliothek.'], ['Docker Engine','Weitere Dienste','docker-runtime','/api/v1/docker','Optionale Container-Umgebung. Erstinstallation über den Systemhelfer; vorhandenes Docker wird nicht automatisch aktualisiert.'], - ['Audio Separator','Audiotrennung',null,null,'Für BS-RoFormer, MelBand RoFormer und weitere Trennmodelle vorgesehen. Installer und Deck-Worker sind noch nicht angebunden; ein Download der Gewichte genügt hier noch nicht.'] + ['Audio Separator','Audiotrennung','separator-runtime','/api/v1/separator-runtime','Native CUDA-Laufzeit für unterstützte BS- und MelBand-RoFormer-Pakete. Modelle samt YAML-Konfiguration herunterladen und einen kurzen WAV-Trenntest starten.'] ]; const runtimeEscape=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); function runtimeOverview(){return `
EINSTELLUNGEN

Laufzeiten

Einmal einrichten, für passende Modelle wiederverwenden. Dieser Katalog zeigt die in Deck unterstützten und vorgesehenen Laufzeiten. Zusatzdateien findest du beim Modell unter Einrichtung & Komponenten.

Installationsstatus wird geprüft …

${runtimeRows.map(([name,category,route,path,description],i)=>`
${name}${category}${path?'Wird geprüft …':'Noch nicht angebunden'}

${description}

${route?`${i===0?'Installation, Build & Updates öffnen': 'Einrichtung öffnen'} →`:''}${i===0?'

':''}
`).join('')}
`;} diff --git a/test_backup.py b/test_backup.py index e46c50e..78c2c85 100644 --- a/test_backup.py +++ b/test_backup.py @@ -51,6 +51,14 @@ class RestoreTests(unittest.TestCase): doc=open_backup(self.server.backup.export('backup secure password'),'backup secure password') self.assertNotIn('PRIVATE-CHAT-LOG',json.dumps(doc));self.assertNotIn('model-weights-not-in-backup',json.dumps(doc));self.assertIn('history',doc);validate(doc) self.assertEqual(doc['models'][0]['id'],ident);self.assertEqual(len(doc['settings']['profiles.json']),1) + def test_separator_restore_installs_runtime_and_redownloads_package(self): + from separator_runtime import MODELS + doc=self.server.backup.snapshot();name=next(iter(MODELS));doc['runtimes']['separator']={'installed':True,'models':[name]} + runtime=Mock();runtime.job=None;runtime.status.return_value={'installed':False,'job':{'state':'complete'}};runtime.model_ready.return_value=False + self.server.separator_runtime=runtime + summary=self.server.backup.inspect(seal(doc,'backup secure password'),'backup secure password');self.assertFalse(summary['blockers']) + self.server.backup.start(dict(id=summary['id'],services=[],confirm=True,restore_credentials=False));job=self.wait() + self.assertEqual(job['state'],'complete',job);runtime.start.assert_called_once();runtime.download.assert_called_once_with(name) def test_restore_reuses_verified_models_restores_profiles_and_keeps_login_when_selected(self): ident=self.model();self.profile(ident);raw=self.server.backup.export('backup secure password');self.server.profiles.rows=[];(self.root/'profiles.json').write_text('[]') current=self.server.credentials.read();summary=self.server.backup.inspect(raw,'backup secure password');self.assertFalse(summary['blockers']) diff --git a/test_separator.py b/test_separator.py new file mode 100644 index 0000000..944f249 --- /dev/null +++ b/test_separator.py @@ -0,0 +1,55 @@ +import io,json,tempfile,unittest,wave +from pathlib import Path +from unittest.mock import Mock,patch +from separator_runtime import SeparatorRuntime,MODELS +from separator import SeparatorTests + +def audio(channels=2,duration=.1): + data=io.BytesIO() + with wave.open(data,'wb') as wav: + wav.setnchannels(channels);wav.setsampwidth(2);wav.setframerate(44100);wav.writeframes(b'\x00'*(int(44100*duration)*channels*2)) + return data.getvalue() +class Tests(unittest.TestCase): + def test_marker_cannot_escape_and_failed_install_not_published(self): + with tempfile.TemporaryDirectory() as root: + Path(root,'active.json').write_text(json.dumps({'id':'../../outside'}));runtime=SeparatorRuntime(root) + self.assertFalse(runtime.status()['installed']);runtime.job={'id':'a'*32,'state':'running'} + with patch.object(runtime,'_command',side_effect=RuntimeError('synthetic failure')):runtime._run() + self.assertEqual(runtime.job['state'],'failed');self.assertFalse(runtime.status()['installed']) + def test_model_requires_configuration_and_unknown_download_rejected(self): + with tempfile.TemporaryDirectory() as root: + runtime=SeparatorRuntime(root);models=Path(root,'models');models.mkdir();name=next(iter(MODELS));(models/name).write_bytes(b'weights') + self.assertFalse(runtime.model_ready(name));(models/MODELS[name][1]).write_text('model: {}');(models/(name+'.ready')).write_text('{}');self.assertTrue(runtime.model_ready(name)) + with self.assertRaises(ValueError):runtime.download('../other') + with self.assertRaises(ValueError):runtime.download([]) + def test_stereo_accepted_and_long_or_truncated_wav_rejected(self): + with tempfile.TemporaryDirectory() as root: + runtime=Mock();runtime.status.return_value={'installed':True};runtime.model_ready.return_value=True + worker=SeparatorTests(root,runtime,Mock());model=next(iter(MODELS)) + with patch('separator.threading.Thread') as thread: + self.assertEqual(worker.start(model,audio())['state'],'running');thread.return_value.start.assert_called_once() + worker.job=None + for data in (audio(duration=31),audio()[:-2],b'not a wav'): + with self.assertRaises(ValueError):worker.start(model,data) + def test_busy_gpu_fails_without_spawning_and_releases_lease(self): + with tempfile.TemporaryDirectory() as root: + scheduler=Mock();release=Mock();scheduler.image_reservation.return_value=release + worker=SeparatorTests(root,Mock(),scheduler);worker.job={'id':'a'*32,'state':'running','started_at':0} + with patch('separator.probe',return_value=[{'name':'RTX 3060','processes':1,'free_mib':11000}]),patch('separator.subprocess.Popen') as process:worker._run(next(iter(MODELS)),audio()) + self.assertEqual(worker.job['state'],'failed');process.assert_not_called();release.assert_called_once() + def test_success_publishes_only_finished_outputs_and_removes_input(self): + with tempfile.TemporaryDirectory() as root: + scheduler=Mock();release=Mock();scheduler.image_reservation.return_value=release + runtime=Mock();runtime.paths.return_value=(Path('/fake/python'),Path('/fake'));runtime.root=Path(root,'runtime') + worker=SeparatorTests(root,runtime,scheduler);worker.job={'id':'a'*32,'state':'running','started_at':0} + directory=Path(root,'a'*32) + def launch(*args,**kwargs): + (directory/'vocals.wav').write_bytes(audio());(directory/'results.json').write_text('["vocals.wav"]') + process=Mock();process.poll.return_value=0;process.returncode=0;return process + with patch('separator.probe',return_value=[{'name':'RTX 3060','uuid':'GPU-test','processes':0,'free_mib':11000}]),patch('separator.subprocess.Popen',side_effect=launch):worker._run(next(iter(MODELS)),audio()) + self.assertEqual(worker.job['state'],'complete');self.assertEqual(worker.audio('a'*32,0),audio());self.assertFalse((directory/'input.wav').exists());self.assertFalse((directory/'worker.tmp').exists());release.assert_called_once() + def test_result_traversal_rejected(self): + with tempfile.TemporaryDirectory() as root: + worker=SeparatorTests(root,Mock(),Mock());directory=Path(root,'a'*32);directory.mkdir();(directory/'complete').touch();(directory/'results.json').write_text('["../../secret.wav"]') + with self.assertRaises(ValueError):worker.audio('a'*32,0) +if __name__=='__main__':unittest.main() diff --git a/test_server.py b/test_server.py index 1574fc3..23e4b29 100644 --- a/test_server.py +++ b/test_server.py @@ -58,6 +58,16 @@ class Tests(unittest.TestCase): self.assertEqual(error.exception.code,401);error.exception.close() self.assertEqual(action.call_count,count) + def test_separator_runtime_is_admin_only_and_rejects_unknown_package(self): + status=self.request('separator-runtime') + self.assertFalse(status['installed']);self.assertEqual(len(status['models']),2) + req=urllib.request.Request(self.url+'/api/v1/separator-runtime/download',data=b'{"model":"unknown"}',headers={'Cookie':self.cookie,'X-Athena-Deck':'1','Content-Type':'application/json'}) + with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req) + self.assertEqual(error.exception.code,400);error.exception.close() + req=urllib.request.Request(self.url+'/api/v1/separator-runtime/install',data=b'{}',headers={'Authorization':'Bearer '+self.api_token,'X-Athena-Deck':'1','Content-Type':'application/json'}) + with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req) + self.assertEqual(error.exception.code,401);error.exception.close() + def test_demo_removed_from_status(self): state=self.request('status') self.assertNotIn('demo',state)