Install native audio separator with model packages WAV tests and restore recipe

This commit is contained in:
Mikei386
2026-10-01 12:00:36 +02:00
parent c3db35dada
commit 66b035c8d7
20 changed files with 368 additions and 23 deletions
+3 -2
View File
@@ -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,
+41
View File
@@ -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/<job-id>` 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":"<Paket-Dateiname>"}`.
- `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=<job-id>&index=<Spur>`: 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
+1
View File
@@ -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;}
+20 -3
View File
@@ -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()
+3 -3
View File
@@ -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 \
+3 -3
View File
@@ -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 \
+2 -2
View File
@@ -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)
+3
View File
@@ -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']:
+1 -1
View File
@@ -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="/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"><p class="nav-heading">Übersicht</p><a href="#dashboard">Dashboard</a><a href="#home">Steuerung</a><p class="nav-heading">KI-Werkzeuge</p><a href="#chat">Chat &amp; Sprachmodelle</a><a href="#image">Bildgenerierung</a><a href="#music">Musikgenerierung</a><a href="#audio">Sprachausgabe (TTS)</a><a href="#stt">Spracherkennung (STT)</a><a href="#voice">Stimmwerkzeuge</a><a href="#video">Videogenerierung</a><p class="nav-heading">Anwendungen</p><a href="#services">Weitere Dienste</a><p class="nav-heading">Einstellungen</p><a href="#runtimes">Laufzeiten</a><a href="#endpoint">Endpunkt</a><a href="#network">Netzwerk</a><a href="#access">Zugang &amp; API-Token</a><a href="#appearance">Darstellung</a><a href="#backup">Backup &amp; Wiederherstellung</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="/separator-ui.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"><p class="nav-heading">Übersicht</p><a href="#dashboard">Dashboard</a><a href="#home">Steuerung</a><p class="nav-heading">KI-Werkzeuge</p><a href="#chat">Chat &amp; Sprachmodelle</a><a href="#image">Bildgenerierung</a><a href="#music">Musikgenerierung</a><a href="#audio">Sprachausgabe (TTS)</a><a href="#stt">Spracherkennung (STT)</a><a href="#voice">Stimmwerkzeuge</a><a href="#video">Videogenerierung</a><p class="nav-heading">Anwendungen</p><a href="#services">Weitere Dienste</a><p class="nav-heading">Einstellungen</p><a href="#runtimes">Laufzeiten</a><a href="#endpoint">Endpunkt</a><a href="#network">Netzwerk</a><a href="#access">Zugang &amp; API-Token</a><a href="#appearance">Darstellung</a><a href="#backup">Backup &amp; Wiederherstellung</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>
+1 -1
View File
@@ -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()
+20
View File
@@ -0,0 +1,20 @@
window.SeparatorUI=(()=>{
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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=`<div id="separator-panel" data-test="${test}"><div class="kicker">${test?'STIMMWERKZEUGE':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${test?'Audio trennen':'Audio Separator'}</h1><p>BS-RoFormer und MelBand RoFormer trennen Gesang und Instrumental. Die native Laufzeit wird einmal installiert und für passende Modellpakete wiederverwendet.</p><p id="sep-message" role="status"></p><section class="card"><div id="sep-status"></div><button id="sep-install">Laufzeit installieren</button><button class="secondary" id="sep-cancel">Einrichtung abbrechen</button><p>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.</p><div id="sep-models"></div><details><summary>Installationsausgabe</summary><pre id="sep-log" style="max-height:240px;overflow:auto;white-space:pre-wrap"></pre></details></section><section class="card"><h2>Kurzer Trenntest</h2><p>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.</p><form id="sep-test"><label>Modellpaket<select name="model" id="sep-model"></select></label><label>Audiodatei<input name="file" type="file" accept=".wav,audio/wav" required></label><button>Audio trennen</button><button type="button" class="secondary" id="sep-test-cancel">Test abbrechen</button></form><div id="sep-job" role="status"></div><div id="sep-results"></div></section></div>`;
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=`<p><strong>${s.installed?'Installiert':'Noch nicht installiert'}</strong> · ${e(s.version)}</p>${s.job?`<p>${e(s.job.state)} · ${e(s.job.phase)}</p>${running?'<progress aria-label="Einrichtung läuft"></progress>':''}`:''}`;
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='<h2>Unterstützte Modellpakete</h2><p>Gewichte und passende Konfiguration werden zusammen beschafft. Diese Pakete sind getrennt von ungeprüften Hugging-Face-Dateien.</p>'+s.models.map(m=>`<p><strong>${e(m.name)}</strong> · ${m.installed?'Bereit':'Fehlt'}<br><span class="small">${e(m.id)} + ${e(m.configuration)}</span><br><button data-sep-model="${e(m.id)}" ${m.installed?'disabled':''}>Modell und Konfiguration herunterladen</button></p>`).join('');root.querySelector('#sep-model').innerHTML=s.models.filter(m=>m.installed).map(m=>`<option value="${e(m.id)}">${e(m.name)}</option>`).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?`<p>${e(j.state)} · ${e(j.phase)} ${j.elapsed_seconds?e(j.elapsed_seconds)+' s':''}</p>${j.state==='running'?'<progress aria-label="Audio wird getrennt"></progress>':''}`:'';
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=>`<p>${e(f.name)}</p><audio controls src="/api/v1/separator-tests/audio?id=${e(j.id)}&index=${f.index}"></audio><p><a href="/api/v1/separator-tests/audio?id=${e(j.id)}&index=${f.index}" download="${e(f.name)}">Spur herunterladen</a></p>`).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};
})();
+71
View File
@@ -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 0<wav.getnframes()/wav.getframerate()<=30:raise ValueError('PCM-WAV, 16 Bit, Mono/Stereo, maximal 30 Sekunden erforderlich.')
if len(wav.readframes(wav.getnframes()))!=wav.getnframes()*wav.getnchannels()*2:raise ValueError('WAV-Datei ist unvollständig.')
except (wave.Error,EOFError):raise ValueError('Ungültige WAV-Datei.') from None
with self.lock:
if self.job and self.job['state']=='running':raise ValueError('Trenntest läuft bereits.')
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 GPU-Freigabe',started_at=time.time(),files=[])
self.thread=threading.Thread(target=self._run,args=(model,audio),daemon=True);self.thread.start();return dict(self.job)
def _run(self,model,audio):
release=None;directory=self.root/self.job['id'];state='failed';phase='Trennung fehlgeschlagen.'
try:
release=self.scheduler.image_reservation(kind='separator')
gpu=next((g for term in ('RTX 3060','RTX 5080') for g in probe() if term in g['name'] and not g['processes'] and g['free_mib']>4000),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<=index<len(names) or Path(names[index]).name!=names[index]:raise ValueError('Ungültige Spur.')
path=directory/names[index]
if path.is_symlink() or path.resolve().parent!=directory.resolve():raise ValueError('Ungültiger Ergebnispfad.')
if path.stat().st_size>32*1024**2:raise ValueError('Spur zu groß.')
return path.read_bytes()
+71
View File
@@ -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()
+17
View File
@@ -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))
+34 -4
View File
@@ -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()
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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','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']]:[])]:[])].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>`;
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>${page==='voice'?'<p><a class="link" href="#separator-runtime">Audio Separator einrichten →</a> · <a class="link" href="#separator-test">Audio trennen →</a></p><p class="note">Audiotrennung nutzt die unterstützten Pakete im Audio-Separator-Bereich. Andere Hub-Dateien und Voice-Profile benötigen weiterhin eine geprüfte Anbindung.</p>':''}${audioDescriptions[page]&&!['audio','stt','music','voice'].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']]:[])]:[])].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;}
@@ -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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function runtimeOverview(){return `<div class="kicker">EINSTELLUNGEN</div><h1>Laufzeiten</h1><p>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.</p><label>Laufzeit suchen<input id="runtime-search" type="search" placeholder="Name oder Bereich"></label><p id="runtime-catalog-message" role="status">Installationsstatus wird geprüft …</p><div class="detail-list">${runtimeRows.map(([name,category,route,path,description],i)=>`<details class="detail-row" data-runtime-row="${i}"><summary class="detail-summary"><strong>${name}</strong><span>${category}</span><span data-runtime-status>${path?'Wird geprüft …':'Noch nicht angebunden'}</span></summary><div class="detail-body"><p>${description}</p><p data-runtime-version></p>${route?`<a class="link" href="#${route}">${i===0?'Installation, Build & Updates öffnen': 'Einrichtung öffnen'} →</a>`:''}${i===0?'<p><button id="catalog-llama-updates">llama.cpp-Updates prüfen</button></p><div id="catalog-llama-releases"></div>':''}</div></details>`).join('')}</div>`;}
+8
View File
@@ -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'])
+55
View File
@@ -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()
+10
View File
@@ -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)