Add audio separation web app and verified recovery sources

This commit is contained in:
Mikei386 committed 2026-10-01 16:19:33 +02:00
1 parent b5ee7c1e01
commit 433e6709cf
25 files changed
+381 -46

No files matched your search

+4
View File
@@ -234,3 +234,7 @@ Sprachmodell-GPUs und Vision-Projektor werden unabhängig zugeordnet. Der Modell
Referenzbilder: Alle derzeit ausführbaren Bildrezepte (Qwen Image 2.1 und FLUX.2 Klein 9B) erlauben bis zu vier Bilder pro Bearbeitungsauftrag. FLUX nutzt VAEEncode und verkettete ReferenceLatent-Nodes für beide Conditioning-Zweige nach dem offiziellen ComfyUI-Workflow: https://github.com/Comfy-Org/workflow_templates/blob/main/templates/image_flux2_klein_image_edit_9b_distilled.json . Neue Modellfamilien benötigen einen passenden Workflow; hochgeladene Referenzen werden niemals stillschweigend verworfen.
Bildadapter werden zentral in `profiles.IMAGE_ADAPTERS` definiert: Modellfamilie, unterstütztes Gewichtsformat, Komponenten, Laufzeit, Workflow, Referenzbildlimit und Speicherpolitik. Bibliothek, Profile und Bildworker verwenden denselben Resolver `image_adapter`. Bekannte Quellen bleiben kompatibel; neue Varianten können über gespeicherte präzise Architektur-Metadaten oder die Hugging-Face-Basismodell-Zuordnung dieselbe Anbindung wiederverwenden. Ein Name mit „Qwen“ oder „Flux“ genügt nicht. Allgemeine FLUX-Architekturangaben unterscheiden Klein 9B nicht sicher von anderen FLUX-Varianten und schalten daher keinen Worker frei. Metadaten sind eine Zuordnungshilfe, keine Garantie für den Inhalt oder die Qualität veränderter Gewichte. Neue Architekturen und Dateiformate benötigen einen registrierten Adapter. Bereits heruntergeladene Varianten ohne ausreichende Metadaten bleiben ungeprüft; bestehende bekannte Profile bleiben erhalten.
### Audio-Trennung
Eigener Werkzeugbereich `#separation` mit BS-Roformer, MelBand und Demucs FT/6s. Die alte Web-GUI läuft optional als CPU-only Container unter Weitere Dienste und nutzt den nativen Deck-Worker. [Installation und Grenzen](deploy/separator-web/README.md). Modellquellen, alle Ensemble-Dateien und SHA-256 sind in `separator_sources.json` festgelegt. Paket-Downloads und Backup-Restore verwenden diese geprüften Rezepte; Nutzmedien werden nicht exportiert. Bei lokalen Bibliotheksdateien wird eine belegte Originalquelle separat vom Übernahmeort angezeigt, ohne IDs oder Profilreferenzen zu ändern. Unbekannte lokale Quellen werden nicht geraten.
+1 -1
View File
@@ -24,7 +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(['separator-runtime','separator-test','separation'].includes(page)){SeparatorUI.render(page!=='separator-runtime');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;}
+8 -3
View File
@@ -76,7 +76,10 @@ def validate(doc):
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 not isinstance(value,dict) or set(value)-{'installed','models','sources'} or not {'installed','models'}<=set(value) 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 'sources' in value:
from separator_runtime import SOURCES
if value['sources']!={name:SOURCES[name] for name in value['models']}:raise ValueError('Audio-Separator-Quellen stimmen nicht mit dem geprüften Rezept überein.')
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.')
@@ -144,7 +147,8 @@ class Backup:
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']
from separator_runtime import SOURCES
separator=s.separator_runtime.status();document['runtimes']['separator']=dict(installed=separator['installed'],models=[m['id'] for m in separator['models'] if m['installed']],sources={m['id']:SOURCES[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
@@ -244,8 +248,9 @@ class Backup:
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)
from separator_runtime import verify_package
for name in r['separator']['models']:
if not s.separator_runtime.model_ready(name):
if not s.separator_runtime.model_ready(name) or not verify_package(s.separator_runtime.root/'models',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:
+1 -1
View File
@@ -34,7 +34,7 @@ window.CatalogUI = (() => {
const openRows=new Set([...root.querySelectorAll('.detail-row[open]')].map(x=>x.dataset.rowId));
const entries = state.entries.filter(x => x.kind === kind);
const groups = [['model', kind==='chat'?'Sprachmodelle · Hauptgewichte':'Modelle · Hauptgewichte'],['vision_projector','Vision-Projektoren'],['audio_projector','Audio-Projektoren'],['text_encoder','Textencoder'],['vae','VAE · Decoder'],['configuration','Konfigurationen'],['auxiliary','Tokenizer und weitere Zusatzkomponenten']];
const row = x => `<details class="detail-row" data-row-id="${escape(x.id)}" ${openRows.has(x.id)?'open':''}><summary class="detail-summary"><strong class="hub-filename">${escape(x.file)}</strong><span>${escape(x.role_label)}</span><span>${size(x.size)}</span><span>${x.used_by_profiles?.length?`${x.used_by_profiles.length} Profil(e)`:'Nicht zugeordnet'}</span></summary><div class="detail-body"><p class="hub-filename">Quelle: ${escape(x.repo)}</p><p>${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}</p><details><summary>Version und Integrität</summary><p class="hub-filename">Revision ${escape(x.revision)}</p><p>${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}</p></details>${executionHTML(x.execution)}${x.used_by_profiles?.length?`<p class="warning">Verwendet von Profil: ${x.used_by_profiles.map(p=>escape(p.name)).join(", ")}. Bitte zuerst das Profil löschen.</p>`:""}<div class="card-actions">${x.profile_eligible===true?`<button data-profile-model="${x.id}">${kind==='video'?'Als Videomodell verwenden':'Profil anlegen'}</button>`:""}<button type="button" class="secondary" data-delete-model="${x.id}" ${x.used_by_profiles?.length?"disabled":""}>Datei löschen</button></div></div></details>`;
const row = x => `<details class="detail-row" data-row-id="${escape(x.id)}" ${openRows.has(x.id)?'open':''}><summary class="detail-summary"><strong class="hub-filename">${escape(x.file)}</strong><span>${escape(x.role_label)}</span><span>${size(x.size)}</span><span>${x.used_by_profiles?.length?`${x.used_by_profiles.length} Profil(e)`:'Nicht zugeordnet'}</span></summary><div class="detail-body"><p class="hub-filename">Quelle: ${escape(x.download_source?.repo||x.repo)}</p>${x.download_source?`<p class="small">Lokal übernommen · Originaldatei: ${escape(x.download_source.file)} · Quellrevision: ${escape(x.download_source.revision)}</p>`:""}<p>${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}</p><details><summary>Version und Integrität</summary><p class="hub-filename">Revision ${escape(x.revision)}</p><p>${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}</p></details>${executionHTML(x.execution)}${x.used_by_profiles?.length?`<p class="warning">Verwendet von Profil: ${x.used_by_profiles.map(p=>escape(p.name)).join(", ")}. Bitte zuerst das Profil löschen.</p>`:""}<div class="card-actions">${x.profile_eligible===true?`<button data-profile-model="${x.id}">${kind==='video'?'Als Videomodell verwenden':'Profil anlegen'}</button>`:""}<button type="button" class="secondary" data-delete-model="${x.id}" ${x.used_by_profiles?.length?"disabled":""}>Datei löschen</button></div></div></details>`;
const known = new Set(groups.map(([role])=>role));
const sections = [...groups, ['other','Weitere Dateien · Zuordnung unbekannt']];
el('hub-results').innerHTML = sections.map(([role,label])=>{const files=entries.filter(x=>role==='other'?!known.has(x.role):x.role===role);return files.length?`<section class="library-group" aria-label="${escape(label)}"><h3>${escape(label)} <span class="small muted">(${files.length})</span></h3><div class="detail-list">${files.map(row).join('')}</div></section>`:'';}).join('') || '<div class="empty wide"><h3>Deine Bibliothek ist noch leer</h3><p>Unter „Entdecken“ kannst du Dateien für diesen Bereich auswählen und herunterladen.</p></div>';
+5 -1
View File
@@ -216,7 +216,11 @@ class Catalog:
if self.root.exists():
for p in self.root.glob('*/entry.json'):
try:
item=json.loads(p.read_text());item['id']=p.parent.name;item.update(file_role(item['file']));entries.append(item)
item=json.loads(p.read_text());item['id']=p.parent.name;item.update(file_role(item['file']))
if item['repo'].startswith('local/'):
from backup import LOCAL_SOURCES
item['download_source']=item.get('download_source') or LOCAL_SOURCES.get(item.get('sha256'))
entries.append(item)
except (OSError,ValueError):pass
return dict(entries=entries,downloads=[dict(x) for x in reversed(self.history) if not x.get("dismissed")],job=dict(self.job) if self.job else None,free_bytes=shutil.disk_usage(self.root if self.root.exists() else self.root.parent).free)
def start(self,repo,filename,revision,kind):
+1 -1
View File
@@ -16,7 +16,7 @@ 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 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_sources.json 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/
+1 -1
View File
@@ -6,7 +6,7 @@ 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 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_sources.json 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/
+7 -7
View File
@@ -53,7 +53,7 @@ def export(manager):
try:relative=str(Path(source).relative_to(deck))
except ValueError:pass
mounts.append(dict(index=i,target=m['Destination'],read_only=not m['RW'],deck_path=relative,files=config_files(source,m['Destination']),was_file=Path(source).is_file()))
result.append(dict(name=row['name'],image=c['Image'],registry=registry,application=c.get('Labels',{}).get(APP,''),labels={k:v for k,v in (c.get('Labels') or {}).items() if k.startswith('io.athena-deck.')},env=c.get('Env') or [],entrypoint=c.get('Entrypoint'),cmd=c.get('Cmd'),uid=app_uid(row['id'],c.get('User') or ''),user=c.get('User') or '',workdir=c.get('WorkingDir') or '',network=h['NetworkMode'],ports=h.get('PortBindings') or {},restart=h.get('RestartPolicy',{}).get('Name') or 'no',memory=h.get('Memory') or 0,nanocpus=h.get('NanoCpus') or 0,running=x['State']['Running'],mounts=mounts,build_recipe='swarm-ui' if row['name']=='athena-swarm-ui' else 'ladypoly' if row['name']=='athena-ladypoly' else None,unsupported=bool(h.get('Privileged') or h.get('DeviceRequests') or h.get('Devices') or h.get('CapAdd'))))
result.append(dict(name=row['name'],image=c['Image'],registry=registry,application=c.get('Labels',{}).get(APP,''),labels={k:v for k,v in (c.get('Labels') or {}).items() if k.startswith('io.athena-deck.')},env=c.get('Env') or [],entrypoint=c.get('Entrypoint'),cmd=c.get('Cmd'),uid=app_uid(row['id'],c.get('User') or ''),user=c.get('User') or '',workdir=c.get('WorkingDir') or '',network=h['NetworkMode'],ports=h.get('PortBindings') or {},restart=h.get('RestartPolicy',{}).get('Name') or 'no',memory=h.get('Memory') or 0,nanocpus=h.get('NanoCpus') or 0,running=x['State']['Running'],mounts=mounts,build_recipe='swarm-ui' if row['name']=='athena-swarm-ui' else 'ladypoly' if row['name']=='athena-ladypoly' else 'separator-web' if row['name']=='athena-separator-web' else None,unsupported=bool(h.get('Privileged') or h.get('DeviceRequests') or h.get('Devices') or h.get('CapAdd'))))
return {'services':result,'configured':bool(deck)}
def validate(c):
@@ -61,8 +61,8 @@ def validate(c):
if any(c.get('labels',{}).get(k)!=v for k,v in LABELS.items()):raise ValueError('Nur ausdrücklich Deck zugeordnete Anwendungscontainer erlaubt.')
if c.get('unsupported'):raise ValueError('GPU-/privilegierter Container benötigt manuelle Einrichtung.')
if c.get('network') not in ('bridge','default','host'):raise ValueError('Benutzerdefiniertes Docker-Netzwerk benötigt manuelle Einrichtung.')
if c.get('network')=='host' and c['name'] not in ('athena-swarm-ui','ltx-deskweb','athena-ladypoly'):raise ValueError('Host-Netzwerk nur für bekannte Deck-Oberflächen erlaubt.')
recipes={'swarm-ui':('athena-swarm-ui','athena-swarm-ui:de7b834'),'ladypoly':('athena-ladypoly','athena-ladypoly:3960afc-deck1')}
if c.get('network')=='host' and c['name'] not in ('athena-swarm-ui','ltx-deskweb','athena-ladypoly','athena-separator-web'):raise ValueError('Host-Netzwerk nur für bekannte Deck-Oberflächen erlaubt.')
recipes={'separator-web':('athena-separator-web','athena-separator-web:0.1.0'),'swarm-ui':('athena-swarm-ui','athena-swarm-ui:de7b834'),'ladypoly':('athena-ladypoly','athena-ladypoly:3960afc-deck1')}
if c.get('build_recipe') is not None and recipes.get(c['build_recipe'])!=(c['name'],c.get('image')):raise ValueError('Unbekanntes Build-Rezept.')
ref=c.get('registry') or c.get('image','')
if not isinstance(ref,str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._/@:-]{0,300}',ref):raise ValueError('Ungültige Image-Referenz.')
@@ -97,7 +97,7 @@ def validate(c):
if rel is not None and not native_media and (rel not in ('models','video/comfy-work/models','video/comfy-client-token') or not m['read_only']):raise ValueError('Deck-Volume nicht freigegeben; nur bekannte Modell-/Tokenpfade und LTX-Medienordner erlaubt.')
for name,data in m.get('files',{}).items():
allowed=('file','Settings.fds','Backends.fds')
if c['name']=='athena-ladypoly' and m['target']=='/data':allowed+=('settings.json','deck-connection.json')
if c['name'] in ('athena-ladypoly','athena-separator-web') and m['target']=='/data':allowed+=('settings.json','deck-connection.json')
if name not in allowed:raise ValueError('Unbekannte Konfigurationsdatei.')
if not isinstance(data,str) or len(base64.b64decode(data,validate=True))>MAX_FILE:raise ValueError('Ungültige Konfigurationsdatei.')
return c
@@ -115,9 +115,9 @@ def restore(manager,c):
# Never replace or restart an existing service during a restore.
if x['Config']['Image'] not in (c['image'],c.get('registry')):raise ValueError('Vorhandener Container verwendet ein anderes Image; manuell prüfen.')
return {'state':'reused','message':'Vorhandener markierter Container unverändert übernommen.'}
if c['name']=='athena-ladypoly':
with socket.socket() as probe:probe.bind(('127.0.0.1',8127))
if c.get('build_recipe') in ('swarm-ui','ladypoly'):
if c['name'] in ('athena-ladypoly','athena-separator-web'):
with socket.socket() as probe:probe.bind(('127.0.0.1',8127 if c['name']=='athena-ladypoly' else 8128))
if c.get('build_recipe') in ('swarm-ui','ladypoly','separator-web'):
source=Path(__file__).parent/c['build_recipe']
if not (source/'Dockerfile').is_file():raise ValueError('Gepinntes Swarm-Build-Rezept fehlt im Systemhelfer.')
command(['build','-t',c['image'],str(source)],1800)
+1 -1
View File
@@ -14,7 +14,7 @@ import urllib.request
ROOT = Path(__file__).resolve().parent.parent
LABEL = 'de.casaderoll.athena-deck.standalone'
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']
FILES = ['separator_sources.json','deploy/separator-web/Dockerfile','deploy/separator-web/app.py','deploy/separator-web/index.html','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):
+6
View File
@@ -0,0 +1,6 @@
FROM python:3.12-slim-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY app.py index.html ./
USER 65534:65534
CMD ["python","app.py"]
+29
View File
@@ -0,0 +1,29 @@
# Athena Audio-Trennung Web-GUI
Die bisherige Athena-GUI als CPU-only Container. Keine Modelle, CUDA oder Docker-Socket im Container. Native Verarbeitung durch Decks audio-separator 0.47.0.
Build: `docker build -t athena-separator-web:0.1.0 deploy/separator-web`
Start auf Debian nach Prüfung des freien Ports:
```sh
sudo install -d -o 65534 -g 65534 -m 700 /opt/athena-separator-web/data
docker run -d --name athena-separator-web --network host --restart unless-stopped \
--read-only --cap-drop ALL --security-opt no-new-privileges:true \
--memory 2g --cpus 2 --tmpfs /tmp:rw,noexec,nosuid,size=1g,uid=65534,gid=65534 \
--label io.athena-deck.managed=true --label io.athena-deck.role=application \
--label io.athena-deck.application=separator-web --label io.athena-deck.port=8128 \
-v /opt/athena-separator-web/data:/data:rw athena-separator-web:0.1.0
```
GUI bindet nur 127.0.0.1:8128; Verbindung zu Decks Management-API auf 127.0.0.1:8108. In der GUI einmal Deck-API-Token eintragen. Er wird serverseitig mit Modus 0600 gespeichert und niemals zurückgegeben. Loopback/SSH-Tunnel benutzen.
Mac-Tunnel: `ssh -i ~/.ssh/athena_key -o BatchMode=yes -N -L 8128:127.0.0.1:8128 root@192.168.1.212`
URL http://127.0.0.1:8128/ . Stoppen/starten/löschen über Weitere Dienste oder docker stop/start/rm athena-separator-web. Volume enthält ausschließlich Verbindungsdaten. Audio wird temporär konvertiert; Deck-Ergebnisse sind nicht im Konfigurationsbackup enthalten.
Unterstützt Gesang (BS-Roformer), Schlagzeug/Bass (Demucs FT), Gitarre/Klavier/Rest (Demucs 6s). Kein ClearVoice. Upload bis 256 MiB, maximal 20 Minuten. Native Worker laufen pro Auftrag; dauerhaftes Warmhalten ist noch nicht implementiert. Bei fremder GPU-Belegung wird abgelehnt, keine fremden Dienste werden beendet.
Modellquellen einschließlich aller Ensemble-Dateien und SHA-256 stehen in separator_sources.json. Paket-Downloads und Restore prüfen alle Hashes. Quelle darf GitHub/UVR oder Meta sein; es wird keine Hugging-Face-Herkunft erfunden. Lokale Übernahme erhält IDs und wird separat als Übernahme dargestellt. Unbekannte lokale Dateien benötigen weiterhin einen belegten Herkunftsabgleich.
Backup speichert Paketrezepte, Quellen und Container-Konfiguration. Der Docker-Helfer kann das hier hinterlegte Image-Rezept auf einer leeren Installation wieder bauen; Gewichte werden von den geprüften Quellen erneut bezogen.
+78
View File
@@ -0,0 +1,78 @@
"""Old Athena separation UI, remote Deck processing; no CUDA or models here."""
import io,json,os,secrets,subprocess,tempfile,time,urllib.request,urllib.error,zipfile
from pathlib import Path
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
from email import policy
from email.parser import BytesParser
BASE='http://127.0.0.1:8108'
CONFIG=Path('/data/deck-connection.json')
TARGETS={'vocals':'model_bs_roformer_ep_317_sdr_12.9755.ckpt','drums':'htdemucs_ft.yaml','bass':'htdemucs_ft.yaml','guitar':'htdemucs_6s.yaml','piano':'htdemucs_6s.yaml','other':'htdemucs_6s.yaml'}
def request(path,body=None,ctype='application/json',token=None):
if token is None:token=json.loads(CONFIG.read_text())['token'] if CONFIG.exists() else ''
if not token:raise ValueError('Zuerst die Deck-Verbindung einrichten.')
req=urllib.request.Request(BASE+path,data=body,headers={'Authorization':'Bearer '+token,'Content-Type':ctype})
try:
with urllib.request.urlopen(req,timeout=90) as response:return response.read()
except urllib.error.HTTPError as exc:
try:message=json.loads(exc.read()).get('error','Deck-Anfrage abgelehnt')
except Exception:message='Deck-Anfrage abgelehnt'
raise ValueError(message if isinstance(message,str) else message.get('message','Deck-Anfrage abgelehnt')) from None
class Handler(BaseHTTPRequestHandler):
def log_message(self,*args):pass
def send(self,body,status=200,mime='application/json'):
if isinstance(body,dict):body=json.dumps(body).encode()
self.send_response(status);self.send_header('Content-Type',mime);self.send_header('Content-Length',str(len(body)));self.send_header('Cache-Control','no-store');self.send_header('X-Content-Type-Options','nosniff');self.send_header('Content-Security-Policy',"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'");self.end_headers();self.wfile.write(body)
def do_GET(self):
if self.path=='/health':return self.send({'status':'ok','connected':CONFIG.exists()})
if self.path=='/':return self.send(Path('/app/index.html').read_bytes(),mime='text/html; charset=utf-8')
if self.path=='/api/status':
try:return self.send(request('/api/v1/separator-runtime'))
except ValueError as exc:return self.send({'detail':str(exc)},400)
return self.send({'detail':'Nicht gefunden'},404)
def do_POST(self):
origin=self.headers.get('Origin');host=self.headers.get('Host','')
if self.headers.get('X-Athena-UI')!='1' or origin and origin!='http://'+host:return self.send({'detail':'Same-origin erforderlich'},403)
try:
length=int(self.headers.get('Content-Length','0'))
if self.headers.get('Transfer-Encoding') or not 0<length<=256*1024**2:raise ValueError('Upload maximal 256 MiB.')
self.connection.settimeout(60);raw=self.rfile.read(length)
if len(raw)!=length:raise ValueError('Upload unvollständig.')
if self.path=='/api/connection':
if length>4096:raise ValueError('Verbindungsdaten zu groß.')
value=json.loads(raw);token=value.get('token','')
if not isinstance(token,str) or not 1<=len(token)<=256:raise ValueError('Ungültiger Token.')
request('/api/v1/separator-runtime',token=token)
CONFIG.parent.mkdir(exist_ok=True);tmp=CONFIG.with_suffix('.tmp');tmp.write_text(json.dumps({'token':token}));tmp.chmod(0o600);tmp.replace(CONFIG)
return self.send({'ok':True})
if self.path!='/v1/separate':return self.send({'detail':'Nicht gefunden'},404)
ctype=self.headers.get('Content-Type','')
if not ctype.startswith('multipart/form-data;'):raise ValueError('Audiodatei fehlt.')
msg=BytesParser(policy=policy.default).parsebytes(b'Content-Type: '+ctype.encode()+b'\r\n\r\n'+raw)
fields={}
if not msg.is_multipart() or msg.defects:raise ValueError('Ungültiger Upload.')
for part in msg.iter_parts():
name=part.get_param('name',header='content-disposition')
if name not in ('file','target') or name in fields:raise ValueError('Ungültiges Upload-Feld.')
fields[name]=part.get_payload(decode=True)
target=fields.get('target',b'').decode()
if target not in TARGETS:raise ValueError('Zielspur noch nicht angebunden.')
with tempfile.TemporaryDirectory() as work:
source=Path(work)/'input';source.write_bytes(fields['file']);wav=Path(work)/'input.wav'
subprocess.run(['ffmpeg','-nostdin','-hide_banner','-loglevel','error','-y','-i',str(source),'-t','1201','-ac','2','-ar','44100','-c:a','pcm_s16le',str(wav)],check=True,timeout=120,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
data=wav.read_bytes();boundary=secrets.token_hex(24)
body=b''
for name,value in [('model',TARGETS[target]),('target',target)]:body+=f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'.encode()
body+=f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="input.wav"\r\nContent-Type: audio/wav\r\n\r\n'.encode()+data+f'\r\n--{boundary}--\r\n'.encode()
job=json.loads(request('/api/v1/separator-jobs/start',body,'multipart/form-data; boundary='+boundary));ident=job['id'];deadline=time.monotonic()+7250
while time.monotonic()<deadline:
time.sleep(2);current=json.loads(request('/api/v1/separator-tests'))['job']
if not current or current['id']!=ident:raise ValueError('Auftrag wurde ersetzt.')
if current['state']=='running':continue
if current['state']!='complete':raise ValueError(current['phase'])
result=io.BytesIO()
with zipfile.ZipFile(result,'w',zipfile.ZIP_STORED) as bundle:
for f in current['files']:bundle.writestr(Path(f['name']).name,request(f"/api/v1/separator-tests/audio?id={ident}&index={f['index']}"))
return self.send(result.getvalue(),mime='application/zip')
raise ValueError('Trennauftrag hat das Zeitlimit überschritten.')
except Exception as exc:return self.send({'detail':str(exc) if isinstance(exc,ValueError) else 'Verarbeitung fehlgeschlagen; Verbindung und Audioformat prüfen.'},400)
if __name__=='__main__':ThreadingHTTPServer(('127.0.0.1',8128),Handler).serve_forever()
+19
View File
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Athena · Spuren herauslösen</title><style>
:root{color-scheme:dark;--bg:#07111c;--card:#101d2b;--line:#26384b;--cyan:#48d7f5;--mint:#63e6be;--text:#ecf5ff;--muted:#91a4b7}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#142a42 0,#07111c 42%);font:16px system-ui,sans-serif;color:var(--text);min-height:100vh;display:grid;place-items:center;padding:24px}.card{width:min(880px,100%);padding:32px;border:1px solid var(--line);border-radius:22px;background:rgba(16,29,43,.96);box-shadow:0 25px 70px #0008}.eyebrow{color:var(--cyan);font-weight:800;letter-spacing:.14em;text-transform:uppercase;font-size:12px}h1{font-size:clamp(30px,5vw,52px);margin:.3em 0 .15em}p{color:var(--muted);line-height:1.6}.targets{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin:24px 0}.target{display:block;border:1px solid var(--line);border-radius:14px;padding:14px 10px;text-align:center;cursor:pointer}.target:has(input:checked){border-color:var(--cyan);background:#48d7f510;box-shadow:0 0 0 1px #48d7f528}.target input{display:none}.target b,.target span{display:block}.target span{color:var(--muted);font-size:12px;margin-top:5px;line-height:1.35}.section{grid-column:1/-1;color:var(--cyan);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;margin-top:8px}.drop{display:block;margin:20px 0;padding:36px 24px;border:2px dashed #3f5a72;border-radius:18px;text-align:center;cursor:pointer;transition:.2s}.drop:hover,.drop.drag{border-color:var(--cyan);background:#48d7f50b}.drop input{display:none}.file{color:var(--mint);font-weight:700;margin-top:8px}button{width:100%;border:0;border-radius:13px;padding:15px;font-weight:800;font-size:16px;background:linear-gradient(90deg,var(--cyan),var(--mint));color:#05202a;cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}.status{min-height:28px;margin-top:18px;color:var(--muted)}.bar{height:7px;background:#07111c;border-radius:9px;overflow:hidden;margin-top:12px}.fill{height:100%;width:0;background:linear-gradient(90deg,var(--cyan),var(--mint));transition:.4s}.run .fill{width:85%;animation:pulse 1.5s infinite alternate}@keyframes pulse{to{opacity:.45}}small{display:block;color:#71879a;margin-top:20px}@media(max-width:760px){.targets{grid-template-columns:repeat(2,1fr)}}
</style></head><body><main class="card"><div class="eyebrow">Athena Audio Lab</div><h1>Was möchtest du herauslösen?</h1><p>Der Download enthält immer die gewählte Spur separat und zusätzlich den vollständigen Rest ohne diese Spur.</p>
<details><summary>Deck-Verbindung einrichten</summary><p>API-Token aus Athena Deck → Zugang &amp; API-Token. Er bleibt nur auf dem Server gespeichert.</p><input id="token" type="password" autocomplete="off" placeholder="Deck API-Token"><button id="connect">Verbinden</button></details><p id="packages">Modellpakete werden geprüft …</p><div class="targets">
<div class="section">Musik</div>
<label class="target"><input type="radio" name="target" value="vocals" checked><b>Gesang</b><span>BS‑RoFormer<br>Gesang / Instrumental</span></label>
<label class="target"><input type="radio" name="target" value="drums"><b>Schlagzeug</b><span>HTDemucs FT</span></label>
<label class="target"><input type="radio" name="target" value="bass"><b>Bass</b><span>HTDemucs FT</span></label>
<label class="target"><input type="radio" name="target" value="guitar"><b>Gitarre</b><span>HTDemucs 6s<br>experimentell</span></label>
<label class="target"><input type="radio" name="target" value="piano"><b>Piano</b><span>HTDemucs 6s<br>experimentell</span></label>
<label class="target"><input type="radio" name="target" value="other"><b>Sonstiges</b><span>Synths, Streicher etc.<br>gemischte Spur</span></label>
</div>
<label class="drop" id="drop">Audio auswählen oder hier ablegen<input id="file" type="file" accept="audio/*"><div class="file" id="name">Noch keine Datei gewählt</div></label><button id="start" disabled>Ausgewählte Spur und Rest erzeugen</button><div class="status" id="status">Bereit.</div><div class="bar" id="bar"><div class="fill"></div></div><small>Verarbeitung durch Athena Deck. Bis 20 Minuten und 256 MiB; Ausgabe als WAV-ZIP. Modell wird nach dem Auftrag entladen. Synthesizer, Streicher sowie elektrische und akustische Gitarre separat benötigen zusätzliche Spezialmodelle.</small></main><script>
const file=document.querySelector('#file'),drop=document.querySelector('#drop'),name=document.querySelector('#name'),start=document.querySelector('#start'),status=document.querySelector('#status'),bar=document.querySelector('#bar');let selected;async function packages(){let r=await fetch('/api/status'),s=await r.json();document.querySelector('#packages').textContent=r.ok?s.models.map(m=>`${m.name}: ${m.installed?'bereit':'fehlt – in Deck herunterladen'}`).join(' · '):s.detail}document.querySelector('#connect').onclick=async()=>{let token=document.querySelector('#token');let r=await fetch('/api/connection',{method:'POST',headers:{'Content-Type':'application/json','X-Athena-UI':'1'},body:JSON.stringify({token:token.value})});let s=await r.json();token.value='';status.textContent=r.ok?'Deck verbunden.':s.detail;packages()};packages();const names={vocals:'gesang',drums:'schlagzeug',bass:'bass',guitar:'gitarre',piano:'piano',other:'sonstiges',speech:'sprache-und-hintergrund'};function choose(f){selected=f;name.textContent=f?`${f.name} · ${(f.size/1048576).toFixed(1)} MiB`:'Noch keine Datei gewählt';start.disabled=!f}file.onchange=()=>choose(file.files[0]);drop.ondragover=e=>{e.preventDefault();drop.classList.add('drag')};drop.ondragleave=()=>drop.classList.remove('drag');drop.ondrop=e=>{e.preventDefault();drop.classList.remove('drag');choose(e.dataTransfer.files[0])};start.onclick=async()=>{const target=document.querySelector('input[name=target]:checked').value;start.disabled=true;bar.classList.add('run');status.textContent=target==='speech'?'MossFormer2 trennt Sprache und Hintergrund – das kann einige Minuten dauern …':'Modell löst die gewählte Spur heraus – das kann einige Minuten dauern …';let body=new FormData();body.append('file',selected);body.append('target',target);try{let r=await fetch('/v1/separate',{method:'POST',headers:{'X-Athena-UI':'1'},body});if(!r.ok)throw Error((await r.json()).detail||`HTTP ${r.status}`);let blob=await r.blob(),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`athena-${names[target]}-und-rest.zip`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),5000);status.textContent='Fertig – ZIP mit der ausgewählten Spur und dem Rest wurde geladen.'}catch(e){status.textContent=`Fehler: ${e.message}`}finally{bar.classList.remove('run');start.disabled=false}};
</script></body></html>
+3
View File
@@ -28,6 +28,9 @@ def main():
(base/'ladypoly').mkdir(exist_ok=True)
for name in ('Dockerfile','bridge.py'):
shutil.copyfile(Path(__file__).parent/'ladypoly'/name,base/'ladypoly'/name)
(base/'separator-web').mkdir(exist_ok=True)
for name in ('Dockerfile','app.py','index.html'):
shutil.copyfile(Path(__file__).parent/'separator-web'/name,base/'separator-web'/name)
if args.deck_state:
registered=Path(args.deck_state).resolve()
if not registered.is_dir():raise SystemExit('Deck-Zustandsverzeichnis fehlt.')
+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="/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>
<!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="#separation">Audio-Trennung</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>
+2 -2
View File
@@ -64,7 +64,7 @@ window.ProfilesUI=(()=>{
async function refreshLibrary(){
const c=await api('catalog');if(!box.isConnected)return;
const selected=select.value;entries=c.entries;projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));
select.innerHTML='<option value="">Kein Projektor zugeordnet · Vision aus</option>'+projectors.map(x=>`<option value="${e(x.id)}">${e(x.repo)} / ${e(x.file)}</option>`).join('');select.value=selected;
select.innerHTML='<option value="">Kein Projektor zugeordnet · Vision aus</option>'+projectors.map(x=>`<option value="${e(x.id)}">${e(x.download_source?.repo||x.repo)} / ${e(x.download_source?.file||x.file)}</option>`).join('');select.value=selected;
return c;
}
function showFiles(detail,target,n){
@@ -135,7 +135,7 @@ window.ProfilesUI=(()=>{
panelSequence++;
if(!models.length)return;const data=p||{id:null,revision:0,name:'',model_id:id||models[0].id,parameters:Object.fromEntries(Object.entries(schema).map(([k,v])=>[k,v[2]]))};
if(!p&&kind==='image'&&models.find(m=>m.id===data.model_id)?.repo==='RunningHubAI/rh-flux.2-klein-9b-fp16-unet-2067980602644717569'){data.parameters.steps=4;data.parameters.guidance=1;}
el('profile-editor').innerHTML=`<section class="card editor"><h2>${data.id?'Profil bearbeiten':'Profil anlegen'}</h2><form id="profile-form"><div class="form-grid"><label>API-Profilname<input name="name" required minlength="2" maxlength="64" placeholder="z. B. Qwen3.8 27B MEDIUM" value="${e(data.name)}"></label><label>Heruntergeladene Modelldatei<select name="model_id">${models.map(m=>`<option value="${m.id}" ${m.id===data.model_id?'selected':''}>${e(m.file)} · ${(m.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${Object.entries(schema).map(([k,[min,max,defaultValue]])=>`<label>${e(labels[k])}<input name="${k}" type="number" required min="${min}" max="${max}" step="${['guidance','speed','temperature','top_p','mtp_min_p'].includes(k)?'any':['width','height'].includes(k)?64:1}" value="${e(data.parameters[k]??defaultValue)}"></label>`).join('')}</div>${kind==='video'?videoDevices(data):''}${kind==='chat'?gpuEditor(data.parameters)+`<h3>KV-Cache</h3><div class="form-grid">${["k","v"].map(axis=>`<label>${axis.toUpperCase()}-Cache<select name="cache_type_${axis}">${["q4_0","q8_0","f16"].map(type=>`<option value="${type}" ${(data.parameters[`cache_type_${axis}`]||"q4_0")===type?"selected":""}>${type}</option>`).join("")}</select></label>`).join("")}</div><p class="small">q4_0 spart VRAM; q8_0 und f16 brauchen mehr Speicher. K und V werden getrennt eingestellt. MTP-Draft-KV bleibt separat f16.</p><h3>Vision-Projektor</h3><label>Projektor<select name="vision_projector"><option value="">Kein Projektor zugeordnet · Vision aus</option>${projectors.map(x=>`<option value="${e(x.id)}" ${data.parameters.vision_projector===x.id?'selected':''}>${e(x.repo)} / ${e(x.file)}</option>`).join('')}</select></label><section id="projector-discovery" class="card"></section><label>Projektor ausführen auf<select name="vision_device"><option value="cpu">CPU</option>${gpus.map(g=>`<option value="${e(g.uuid)}" ${data.parameters.vision_device===g.uuid?'selected':''}>${e(g.name)}</option>`).join('')}</select></label><p>Passende mmproj.gguf unter Entdecken herunterladen – auch aus einem anderen Repository als das quantisierte Modell. Modell und Projektor müssen zusammenpassen; der Dateiname allein garantiert keine Kompatibilität. Das Projektor-Gerät wird unabhängig von den Sprachmodell-GPUs gewählt. Der Sprachmodell-Split verteilt ausschließlich die Sprachmodell-Gewichte. GPU-Projektoren erhalten eine zusätzliche konservative Speicherreserve.</p><label><input name="mtp" type="checkbox" ${data.parameters.mtp?'checked':''}> MTP aktivieren (eingebautes Draft-Modul)</label><p class="small">Nur für GGUF-Modelle mit MTP-Gewichten und kompatible Builds. Zusätzlicher Speicherbedarf; Geschwindigkeit hängt vom Modell ab. Referenz Medium: 2 Draft-Token, Mindestwahrscheinlichkeit 0,05, Draft-KV f16.</p>`:''}<p class="note">Speichern startet kein Modell. Die Ausführung benötigt eine passende Laufzeit und gegebenenfalls zusätzliche Modelldateien. ${kind==='chat'?'Das Kontextbudget gilt insgesamt für alle Slots.':''}</p><button>Profil auf Athena speichern</button><button class="secondary" type="button" id="profile-close">Schließen</button><p id="profile-error" role="alert"></p></form></section>`;
el('profile-editor').innerHTML=`<section class="card editor"><h2>${data.id?'Profil bearbeiten':'Profil anlegen'}</h2><form id="profile-form"><div class="form-grid"><label>API-Profilname<input name="name" required minlength="2" maxlength="64" placeholder="z. B. Qwen3.8 27B MEDIUM" value="${e(data.name)}"></label><label>Heruntergeladene Modelldatei<select name="model_id">${models.map(m=>`<option value="${m.id}" ${m.id===data.model_id?'selected':''}>${e(m.file)} · ${(m.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${Object.entries(schema).map(([k,[min,max,defaultValue]])=>`<label>${e(labels[k])}<input name="${k}" type="number" required min="${min}" max="${max}" step="${['guidance','speed','temperature','top_p','mtp_min_p'].includes(k)?'any':['width','height'].includes(k)?64:1}" value="${e(data.parameters[k]??defaultValue)}"></label>`).join('')}</div>${kind==='video'?videoDevices(data):''}${kind==='chat'?gpuEditor(data.parameters)+`<h3>KV-Cache</h3><div class="form-grid">${["k","v"].map(axis=>`<label>${axis.toUpperCase()}-Cache<select name="cache_type_${axis}">${["q4_0","q8_0","f16"].map(type=>`<option value="${type}" ${(data.parameters[`cache_type_${axis}`]||"q4_0")===type?"selected":""}>${type}</option>`).join("")}</select></label>`).join("")}</div><p class="small">q4_0 spart VRAM; q8_0 und f16 brauchen mehr Speicher. K und V werden getrennt eingestellt. MTP-Draft-KV bleibt separat f16.</p><h3>Vision-Projektor</h3><label>Projektor<select name="vision_projector"><option value="">Kein Projektor zugeordnet · Vision aus</option>${projectors.map(x=>`<option value="${e(x.id)}" ${data.parameters.vision_projector===x.id?'selected':''}>${e(x.download_source?.repo||x.repo)} / ${e(x.download_source?.file||x.file)}</option>`).join('')}</select></label><section id="projector-discovery" class="card"></section><label>Projektor ausführen auf<select name="vision_device"><option value="cpu">CPU</option>${gpus.map(g=>`<option value="${e(g.uuid)}" ${data.parameters.vision_device===g.uuid?'selected':''}>${e(g.name)}</option>`).join('')}</select></label><p>Passende mmproj.gguf unter Entdecken herunterladen – auch aus einem anderen Repository als das quantisierte Modell. Modell und Projektor müssen zusammenpassen; der Dateiname allein garantiert keine Kompatibilität. Das Projektor-Gerät wird unabhängig von den Sprachmodell-GPUs gewählt. Der Sprachmodell-Split verteilt ausschließlich die Sprachmodell-Gewichte. GPU-Projektoren erhalten eine zusätzliche konservative Speicherreserve.</p><label><input name="mtp" type="checkbox" ${data.parameters.mtp?'checked':''}> MTP aktivieren (eingebautes Draft-Modul)</label><p class="small">Nur für GGUF-Modelle mit MTP-Gewichten und kompatible Builds. Zusätzlicher Speicherbedarf; Geschwindigkeit hängt vom Modell ab. Referenz Medium: 2 Draft-Token, Mindestwahrscheinlichkeit 0,05, Draft-KV f16.</p>`:''}<p class="note">Speichern startet kein Modell. Die Ausführung benötigt eine passende Laufzeit und gegebenenfalls zusätzliche Modelldateien. ${kind==='chat'?'Das Kontextbudget gilt insgesamt für alle Slots.':''}</p><button>Profil auf Athena speichern</button><button class="secondary" type="button" id="profile-close">Schließen</button><p id="profile-error" role="alert"></p></form></section>`;
if(kind==='chat'){const threads=el('profile-form').querySelector('[name=threads]').closest('label');const advanced=document.createElement('details');advanced.innerHTML='<summary>Erweitert: CPU-Threads</summary><p>CPU-Threads steuern CPU-Arbeit und Offloading, nicht die Anzahl der CUDA-Rechenkerne.</p>';advanced.append(threads);el('profile-form').insertBefore(advanced,el('profile-form').querySelector('.note'));}
if(kind==='chat'){bindProjectorDiscovery();const f=el('profile-form');const reserveFields=()=>{for(const n of ['reserve_first','reserve_second'])f.elements[n].disabled=f.elements.gpu_reserve_mode.value!=='manual';};f.elements.gpu_reserve_mode.onchange=reserveFields;reserveFields();}
if(kind==='video')bindVideoDevices(data);
+2 -2
View File
@@ -2,13 +2,13 @@ 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>`;
document.querySelector('#view').innerHTML=`<div id="separator-panel" data-test="${test}"><div class="kicker">${test?'KI-WERKZEUGE / AUDIO-TRENNUNG':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${test?'Audio-Trennung':'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>${test?'<p><a href="http://127.0.0.1:8128/" target="_blank" rel="noopener">Audio-Trennoberfläche öffnen →</a> · auf dem Client ist der SSH-Tunnel für Port 8128 erforderlich.</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}));}
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">Quelle: ${e(m.source?.publisher||'Nicht belegt')} · geprüftes Komplettpaket</span><details><summary>Originalquellen und Prüfsummen</summary>${(m.source?.files||[]).map(f=>`<p>${e(f.file)}<br>${f.urls.map(u=>`<a href="${e(u)}" target="_blank" rel="noopener">Downloadquelle</a>`).join(" · ")}<br><code>${e(f.sha256)}</code></p>`).join("")}</details><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(''):'';}
+10 -8
View File
@@ -8,17 +8,19 @@ class SeparatorTests:
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):
def start(self,model,audio,target=None,full=False):
targets={'vocals':'model_bs_roformer_ep_317_sdr_12.9755.ckpt','drums':'htdemucs_ft.yaml','bass':'htdemucs_ft.yaml','guitar':'htdemucs_6s.yaml','piano':'htdemucs_6s.yaml','other':'htdemucs_6s.yaml'}
if target is not None and targets.get(target)!=model:raise ValueError('Zielspur und Modellpaket passen nicht zusammen.')
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.')
if not isinstance(audio,bytes) or not 44<=len(audio)<=(256 if full else 8)*1024**2:raise ValueError('WAV überschreitet die erlaubte Upload-Größe.')
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 wav.getnchannels() not in (1,2) or wav.getsampwidth()!=2 or not 8000<=wav.getframerate()<=48000 or not 0<wav.getnframes()/wav.getframerate()<=(1200 if full else 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.target=target;self.full=full;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.'
@@ -30,12 +32,12 @@ class SeparatorTests:
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'))
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),getattr(self,'target',None) or ''],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
deadline=time.monotonic()+(7200 if getattr(self,'full',False) else 600)
while self.process.poll() is None:
if self.cancel.wait(.2):raise InterruptedError()
if time.monotonic()>deadline:raise ValueError('Trenntest überschreitet zehn Minuten.')
if time.monotonic()>deadline:raise ValueError('Trennauftrag überschreitet das Zeitlimit.')
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.')
@@ -67,5 +69,5 @@ class SeparatorTests:
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ß.')
if path.stat().st_size>512*1024**2:raise ValueError('Spur zu groß.')
return path.read_bytes()
+43 -6
View File
@@ -3,9 +3,22 @@ import hashlib,json,os,shutil,sys,time
from pathlib import Path
from ltx_original_runtime import LTXOriginalRuntime
VERSION='0.47.0'
SOURCES=json.loads(Path(__file__).with_name('separator_sources.json').read_text())
def package_files(name):return [x['file'] for x in SOURCES[name]['files']]
def verify_package(directory,name):
for item in SOURCES[name]['files']:
path=Path(directory)/item['file']
if not path.is_file() or path.is_symlink():return False
digest=hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda:stream.read(1024**2),b''):digest.update(chunk)
if digest.hexdigest()!=item['sha256']:return False
return True
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')}
'model_bs_roformer_ep_317_sdr_12.9755.ckpt':('BS-RoFormer · Gesang / Instrumental','model_bs_roformer_ep_317_sdr_12.9755.yaml'),
'htdemucs_ft.yaml':('Demucs FT · Gesang / Schlagzeug / Bass / Rest','htdemucs_ft.yaml'),
'htdemucs_6s.yaml':('Demucs 6s · zusätzlich Gitarre / Piano','htdemucs_6s.yaml')}
class SeparatorRuntime(LTXOriginalRuntime):
name='Audio Separator'
def __init__(self,root):super().__init__(root,None)
@@ -19,8 +32,15 @@ class SeparatorRuntime(LTXOriginalRuntime):
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]))
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,source=SOURCES[name],installed=self.model_ready(name)) for name,(label,config) in MODELS.items()])
def model_ready(self,name):
if name not in MODELS:return False
directory=self.root/'models';marker=directory/(name+'.ready')
try:
recorded=json.loads(marker.read_text())
return recorded=={x['file']:x['sha256'] for x in SOURCES[name]['files']} and all((directory/f).is_file() and not (directory/f).is_symlink() and (directory/f).stat().st_size>0 for f in package_files(name))
except (OSError,ValueError):return False
def start(self):
if not shutil.which('ffmpeg'):raise ValueError('FFmpeg fehlt. Deck-Systemvoraussetzungen installieren.')
with self.lock:
@@ -41,9 +61,26 @@ class SeparatorRuntime(LTXOriginalRuntime):
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.')
import urllib.request
for item in SOURCES[model]['files']:
self._phase('Modellpaket: '+item['file'])
succeeded=False
for url in item['urls']:
try:
digest=hashlib.sha256();size=0
with urllib.request.urlopen(url,timeout=60) as response,(directory/item['file']).open('wb') as out:
while chunk:=response.read(1024**2):
if self.cancel.is_set():raise InterruptedError()
size+=len(chunk)
if size>2*1024**3:raise ValueError('Quelldatei zu groß.')
digest.update(chunk);out.write(chunk)
if digest.hexdigest()!=item['sha256']:raise ValueError('Quelldatei stimmt nicht mit dem geprüften Rezept überein.')
succeeded=True;break
except InterruptedError:raise
except Exception:continue
if not succeeded:raise ValueError('Geprüfte Quelle nicht verfügbar: '+item['file'])
filenames=package_files(model)
if not verify_package(directory,model):raise ValueError('Modellpaket-Prüfsumme stimmt nicht.')
if self.cancel.is_set():raise InterruptedError()
destination=self.root/'models';destination.mkdir(exist_ok=True);hashes={}
for f in filenames:
+113
View File
@@ -0,0 +1,113 @@
{
"vocals_mel_band_roformer.ckpt": {
"recipe_version": 1,
"publisher": "Kimberley Jensen / UVR",
"homepage": "https://github.com/nomadkaraoke/python-audio-separator",
"files": [
{
"file": "vocals_mel_band_roformer.ckpt",
"sha256": "87201f4d31afb5bc79993230fc49446918425574db48c01c405e44f365c7559e",
"urls": [
"https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/vocals_mel_band_roformer.ckpt",
"https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/vocals_mel_band_roformer.ckpt"
]
},
{
"file": "vocals_mel_band_roformer.yaml",
"sha256": "b958b29c8f7195f0d86bee6759a33980db675c4ecaf2fcaa80fa125828e6cd38",
"urls": [
"https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/mdx_model_data/mdx_c_configs/vocals_mel_band_roformer.yaml",
"https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/vocals_mel_band_roformer.yaml"
]
}
]
},
"model_bs_roformer_ep_317_sdr_12.9755.ckpt": {
"recipe_version": 1,
"publisher": "Viperx / UVR",
"homepage": "https://github.com/nomadkaraoke/python-audio-separator",
"files": [
{
"file": "model_bs_roformer_ep_317_sdr_12.9755.ckpt",
"sha256": "5b84f37e8d444c8cb30c79d77f613a41c05868ff9c9ac6c7049c00aefae115aa",
"urls": [
"https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/model_bs_roformer_ep_317_sdr_12.9755.ckpt",
"https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/model_bs_roformer_ep_317_sdr_12.9755.ckpt"
]
},
{
"file": "model_bs_roformer_ep_317_sdr_12.9755.yaml",
"sha256": "2bfdd16c656bd9519aba757cc4f8834b7ede675eb1e00ec4772d74ae1c41af7f",
"urls": [
"https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/mdx_model_data/mdx_c_configs/model_bs_roformer_ep_317_sdr_12.9755.yaml",
"https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/model_bs_roformer_ep_317_sdr_12.9755.yaml"
]
}
]
},
"htdemucs_ft.yaml": {
"recipe_version": 1,
"publisher": "Meta / Demucs",
"homepage": "https://github.com/facebookresearch/demucs",
"files": [
{
"file": "htdemucs_ft.yaml",
"sha256": "69470b8c1bbd674437b51bc9fb491327a10ab0396b702c93389b9cf750016346",
"urls": [
"https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/htdemucs_ft.yaml",
"https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/htdemucs_ft.yaml"
]
},
{
"file": "f7e0c4bc-ba3fe64a.th",
"sha256": "ba3fe64ae8ef66ac9a4857222ce48efbdc5eb3ad375cb79dd13debee5aaa4066",
"urls": [
"https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/f7e0c4bc-ba3fe64a.th"
]
},
{
"file": "d12395a8-e57c48e6.th",
"sha256": "e57c48e6b0e38af4f7118d7bd08c49f0a0c0edf7d09143bdd902ea0d237303e6",
"urls": [
"https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/d12395a8-e57c48e6.th"
]
},
{
"file": "92cfc3b6-ef3bcb9c.th",
"sha256": "ef3bcb9c8b40d14ae5d51b6db2587339cc12c6b77c0be151ce6d69002e087bf2",
"urls": [
"https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/92cfc3b6-ef3bcb9c.th"
]
},
{
"file": "04573f0d-f3cf25b2.th",
"sha256": "f3cf25b222c4eed7cd49dd8b2c9597d50c18bd154090f7b919cfa5f93cf22c49",
"urls": [
"https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/04573f0d-f3cf25b2.th"
]
}
]
},
"htdemucs_6s.yaml": {
"recipe_version": 1,
"publisher": "Meta / Demucs",
"homepage": "https://github.com/facebookresearch/demucs",
"files": [
{
"file": "htdemucs_6s.yaml",
"sha256": "207405151270af8fd81c2373c25d27950916682ac91dca7884a11ce13dad6f58",
"urls": [
"https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/htdemucs_6s.yaml",
"https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/htdemucs_6s.yaml"
]
},
{
"file": "5c90dfd2-34c22ccb.th",
"sha256": "34c22ccb381c6f9fdbf324f04e1e2fe21aaaf293f5ded163a162697ff9a02ddd",
"urls": [
"https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/5c90dfd2-34c22ccb.th"
]
}
]
}
}
+16 -2
View File
@@ -3,9 +3,12 @@ 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:]
model_dir,model,input_file,output_dir=sys.argv[1:5]
target=sys.argv[5] if len(sys.argv)>5 else ''
from separator_runtime import verify_package
if not verify_package(model_dir,model):raise RuntimeError('Modellpaket-Prüfsumme stimmt nicht.')
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=Separator(model_file_dir=model_dir,output_dir=output_dir,output_format='WAV',use_soundfile=True,demucs_params={'segment_size':7,'shifts':2,'overlap':.25},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=[]
@@ -14,4 +17,15 @@ for name in files:
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.')
if target:
import subprocess
chosen=next((n for n in names if target in Path(n).stem.lower()),None)
if not chosen:raise RuntimeError('Zielspur fehlt.')
others=[root/n for n in names if n!=chosen]
if not others:raise RuntimeError('Restspuren fehlen.')
args=['ffmpeg','-hide_banner','-loglevel','error','-y']
for f in others:args+=['-i',str(f)]
args+=['-filter_complex',''.join(f'[{i}:a]' for i in range(len(others)))+f'amix=inputs={len(others)}:normalize=0:dropout_transition=0[rest]','-map','[rest]','-c:a','pcm_s16le',str(root/'rest.wav')]
subprocess.run(args,check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=180)
names=[chosen,'rest.wav']
(root/'results.json').write_text(json.dumps(names))
+5 -5
View File
@@ -187,7 +187,7 @@ class Handler(BaseHTTPRequestHandler):
return secrets.compare_digest(actual,record['api_token_hash'])
def token_route(self):
return self.command == 'GET' and self.path in ('/api/v1/status','/api/v1/hardware')
return (self.command == 'GET' and (self.path in ('/api/v1/status','/api/v1/hardware','/api/v1/separator-runtime','/api/v1/separator-tests') or urlsplit(self.path).path=='/api/v1/separator-tests/audio')) or (self.command=='POST' and self.path in ('/api/v1/separator-jobs/start','/api/v1/separator-tests/cancel'))
def session_token(self):
try:
@@ -547,15 +547,15 @@ class Handler(BaseHTTPRequestHandler):
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'):
if self.path in ('/api/v1/separator-tests/start','/api/v1/separator-jobs/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))
fields,audio=read_upload(self,validate=False,max_audio=256*1024**2 if self.path=='/api/v1/separator-jobs/start' else 8*1024**2)
if not {'model'}<=set(fields) or set(fields)-{'model','target'}:raise ValueError('Nur Modell, Zielspur und WAV-Datei erwartet.')
return self.respond(self.server.separator_tests.start(fields['model'],audio,target=fields.get('target'),full=self.path=='/api/v1/separator-jobs/start'))
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:
+3 -3
View File
@@ -22,13 +22,13 @@ 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,validate=True):
def read_upload(handler,validate=True,max_audio=MAX_AUDIO):
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'))
except ValueError:raise ValueError('Ungültige Upload-Länge.') from None
content_type=handler.headers.get('Content-Type','')
if not 0<length<=MAX_AUDIO+65536 or not content_type.lower().startswith('multipart/form-data;'):raise ValueError('Multipart-Upload bis 8 MiB erforderlich.')
if not 0<length<=max_audio+65536 or not content_type.lower().startswith('multipart/form-data;'):raise ValueError('Multipart-Upload bis 8 MiB erforderlich.')
raw=handler.rfile.read(length)
if len(raw)!=length:raise ValueError('Upload unvollständig.')
message=BytesParser(policy=policy.default).parsebytes(b'Content-Type: '+content_type.encode()+b'\r\nMIME-Version: 1.0\r\n\r\n'+raw)
@@ -41,7 +41,7 @@ def read_upload(handler,validate=True):
if audio is not None:raise ValueError('Nur eine Audiodatei erlaubt.')
audio=data
else:
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.')
if name not in ('model','profile_id','language','response_format','target') 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
if validate:validate_wav(audio)
+3 -1
View File
@@ -19,7 +19,9 @@ class Tests(unittest.TestCase):
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))
self.assertFalse(runtime.model_ready(name));(models/MODELS[name][1]).write_text('model: {}');(models/(name+'.ready')).write_text('{}');self.assertFalse(runtime.model_ready(name))
from separator_runtime import SOURCES
(models/(name+'.ready')).write_text(json.dumps({f['file']:f['sha256'] for f in SOURCES[name]['files']}));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):
+19
View File
@@ -0,0 +1,19 @@
import hashlib,json,tempfile,unittest
from pathlib import Path
from unittest.mock import patch
from separator_runtime import SOURCES,MODELS,verify_package,package_files
class Tests(unittest.TestCase):
def test_demucs_ensemble_complete_and_sources_https(self):
self.assertEqual(set(SOURCES),set(MODELS))
self.assertEqual(len(package_files('htdemucs_ft.yaml')),5)
self.assertEqual(len(package_files('htdemucs_6s.yaml')),2)
for recipe in SOURCES.values():
for f in recipe['files']:
self.assertEqual(len(f['sha256']),64)
self.assertTrue(all(u.startswith('https://') for u in f['urls']))
def test_wrong_or_missing_weight_rejected(self):
with tempfile.TemporaryDirectory() as d:
recipe={'x':{'files':[{'file':'weight','sha256':hashlib.sha256(b'valid').hexdigest()}]}}
with patch.dict(SOURCES,recipe):
self.assertFalse(verify_package(d,'x'));Path(d,'weight').write_bytes(b'wrong');self.assertFalse(verify_package(d,'x'))
Path(d,'weight').write_bytes(b'valid');self.assertTrue(verify_package(d,'x'))