Install native audio separator with model packages WAV tests and restore recipe
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 & 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 & API-Token</a><a href="#appearance">Darstellung</a><a href="#backup">Backup & 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 & 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 & API-Token</a><a href="#appearance">Darstellung</a><a href="#backup">Backup & 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
@@ -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()
|
||||
|
||||
@@ -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=`<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};
|
||||
})();
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>`;}
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user