From cf3e354434fd17cc613e40c85485aba1b6569fc7 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:32:17 +0200 Subject: [PATCH] Add managed LadyPoly WebUI using Deck music API --- README.md | 6 +++ deploy/docker_backup.py | 22 ++++++---- deploy/install.py | 2 +- deploy/ladypoly/Dockerfile | 14 +++++++ deploy/ladypoly/README.md | 52 ++++++++++++++++++++++++ deploy/ladypoly/bridge.py | 73 ++++++++++++++++++++++++++++++++++ deploy/ladypoly/install.py | 15 +++++++ deploy/ladypoly/test_bridge.py | 30 ++++++++++++++ deploy/setup_docker_helper.py | 3 ++ docker-ui.js | 2 +- endpoint.py | 2 +- test_backup.py | 5 +++ test_endpoint.py | 2 + 13 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 deploy/ladypoly/Dockerfile create mode 100644 deploy/ladypoly/README.md create mode 100644 deploy/ladypoly/bridge.py create mode 100644 deploy/ladypoly/install.py create mode 100644 deploy/ladypoly/test_bridge.py diff --git a/README.md b/README.md index 9b777f1..15489ca 100644 --- a/README.md +++ b/README.md @@ -200,3 +200,9 @@ Für reine App-Updates kann der Debian-Installer mit `--update --reuse-runtime` Optionales SwarmUI mit vorhandener ComfyUI-Laufzeit: [Installation und Betrieb](deploy/swarm-ui/README.md). Backup und Wiederherstellung unter **Einstellungen → Backup & Restore**: [Umfang, Bedienung, API und Grenzen](BACKUP.md). + +## LadyPoly YuE2 + +Die optionale LadyPoly-WebUI läuft als markierter Anwendungscontainer unter +Weitere Dienste und nutzt Decks Musikworker statt einer eigenen GPU-Laufzeit. +Installation, Verbindung und Einschränkungen: [LadyPoly](deploy/ladypoly/README.md). diff --git a/deploy/docker_backup.py b/deploy/docker_backup.py index 658bbfe..645d580 100644 --- a/deploy/docker_backup.py +++ b/deploy/docker_backup.py @@ -20,6 +20,8 @@ def config_files(source,destination): p=Path(source) if destination.startswith('/run/secrets/'): return {'file':blob(p)} if p.exists() else {} + if destination=='/data' and p.is_dir(): + return {name:blob(p/name) for name in ('settings.json','deck-connection.json') if (p/name).is_file()} if destination.endswith('/Data') and p.is_dir(): files={} for name in ('Settings.fds','Backends.fds'): @@ -43,7 +45,7 @@ def export(manager): for row in manager.inventory(): x=json.loads(command(['inspect',row['id']]))[0];c=x['Config'];h=x['HostConfig'];image=json.loads(command(['image','inspect',x['Image']]))[0] registry=next((d for d in image.get('RepoDigests',[]) if '/' in d.split('@')[0] and ('.' in d.split('/')[0] or ':' in d.split('/')[0])),None) - if not registry and row['name']!='athena-swarm-ui':registry=next(iter(image.get('RepoDigests',[])),None) + if not registry and row['name'] not in ('athena-swarm-ui','athena-ladypoly'):registry=next(iter(image.get('RepoDigests',[])),None) mounts=[] for i,m in enumerate(x['Mounts']): source=m['Source'];relative=None @@ -51,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 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 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): @@ -59,8 +61,9 @@ 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'):raise ValueError('Host-Netzwerk nur für bekannte Deck-Oberflächen erlaubt.') - if c.get('build_recipe') not in (None,'swarm-ui') or c.get('build_recipe')=='swarm-ui' and (c['name']!='athena-swarm-ui' or c.get('image')!='athena-swarm-ui:de7b834'):raise ValueError('Unbekanntes Build-Rezept.') + 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('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.') if c.get('restart') not in ('no','always','unless-stopped','on-failure'):raise ValueError('Ungültige Neustartregel.') @@ -93,7 +96,9 @@ def validate(c): native_media=rel in media and c['name']=='ltx-deskweb' and (target,m['read_only'])==media[rel] and not m['was_file'] 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(): - if name not in ('file','Settings.fds','Backends.fds'):raise ValueError('Unbekannte Konfigurationsdatei.') + allowed=('file','Settings.fds','Backends.fds') + if c['name']=='athena-ladypoly' 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 @@ -110,8 +115,10 @@ 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.get('build_recipe')=='swarm-ui': - source=Path(__file__).parent/'swarm-ui' + 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'): + 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) else: @@ -135,6 +142,7 @@ def restore(manager,c): if base.is_symlink():raise ValueError('Unsicheres Dienstverzeichnis.') base.mkdir(parents=True,exist_ok=True,mode=0o700) args=['create','--name',name,'--network',c['network'],'--restart',c['restart'],'--cap-drop','ALL','--security-opt','no-new-privileges:true','--pids-limit','256'] + if c['name']=='athena-ladypoly':args+=['--read-only','--tmpfs','/tmp:rw,noexec,nosuid,size=128m,uid=65534,gid=65534'] if c['memory']:args+=['--memory',str(c['memory'])] if c['nanocpus']:args+=['--cpus',str(c['nanocpus']/1e9)] for k,v in c['labels'].items(): diff --git a/deploy/install.py b/deploy/install.py index 65ec1d9..ae3c521 100644 --- a/deploy/install.py +++ b/deploy/install.py @@ -14,7 +14,7 @@ import urllib.request ROOT = Path(__file__).resolve().parent.parent LABEL = 'de.casaderoll.athena-deck.standalone' -FILES = ['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 = ['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): diff --git a/deploy/ladypoly/Dockerfile b/deploy/ladypoly/Dockerfile new file mode 100644 index 0000000..39f6e4a --- /dev/null +++ b/deploy/ladypoly/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim +ARG UPSTREAM_COMMIT=3960afc309efcef5f4f570209cf01160b96b1ea3 +RUN apt-get update && apt-get install -y --no-install-recommends git libsndfile1 && rm -rf /var/lib/apt/lists/* \ + && git init /app && cd /app && git remote add origin https://github.com/Ladypoly/YuE2_WebUI.git \ + && git fetch --depth 1 origin ${UPSTREAM_COMMIT} && git checkout --detach FETCH_HEAD \ + && rm -rf /app/.git \ + && pip install --no-cache-dir fastapi==0.115.12 uvicorn==0.34.2 python-multipart==0.0.20 numpy==2.2.6 soundfile==0.13.1 +COPY bridge.py /app/webui/deck_bridge.py +RUN python -c "from pathlib import Path; p=Path('/app/webui/server.py'); s=p.read_text(); s=s.replace('SETTINGS_FILE = Path(__file__).resolve().parent / \"settings.json\"','SETTINGS_FILE = Path(\"/data/settings.json\")'); s=s.replace('from loras import Loras', 'from deck_bridge import Loras'); s=s.replace('if __name__ == \"__main__\":','from deck_bridge import install\ninstall(globals())\n\nif __name__ == \"__main__\":'); p.write_text(s)" +RUN python -c "from pathlib import Path; p=Path('/app/webui/static/index.html'); s=p.read_text().replace('
', 'In Deck das YuE2-Profil am API-Endpunkt freigeben und den Musik-Modus aktivieren. Hier den API-Token aus „Zugang & API“ und den genauen Profilnamen eingeben.
LadyPoly öffnen →Diese Anbindung unterstützt Liedtext, Stil, Seed und Syntheseschritte. Score, LoRA, eigener Writer und Cover-Generator sind hier noch nicht angebunden. Deck lädt und entlädt die Gewichte.
''' + +def install(g): + app=g['app'];g['SETTINGS'].song_engine='gguf';g['SETTINGS'].art_auto=False;g['SETTINGS'].stall_timeout=0 + @app.get('/deck-connection', response_class=HTMLResponse) + def connection_page():return PAGE + @app.post('/api/deck/connection') + def connection_save(body: Connection): + c=body.model_dump() + if not 1<=len(body.token)<=512:raise HTTPException(400,'Ungültiger Token.') + try: + models=json.loads(request('/v1/audio/music/models',connection=c))['data'] + if body.model not in [x['id'] for x in models]:raise RuntimeError('Profil nicht freigegeben oder nicht ausführbar. Bitte in Deck prüfen.') + except Exception as e:raise HTTPException(400,str(e)) from None + CONNECTION.write_text(json.dumps(c));CONNECTION.chmod(0o600) + return {'ok':True} + def render(job,spec): + if spec.get('abc') or spec.get('cfg_scale') is not None or spec.get('abc_sampling') or spec.get('semantic_sampling') or g['SETTINGS'].loras: + raise RuntimeError('Score, Sampling-Overrides und LoRA sind in der Deck-Anbindung noch nicht verfügbar.') + c=json.loads(CONNECTION.read_text()) if CONNECTION.exists() else {} + spec['cot']='off' + job.setup='Deck lädt YuE2 und erzeugt Musik · Musik-Modus erforderlich';job.setup_at=time.time();job.push(force=True) + start=time.monotonic() + audio=request('/v1/audio/music',dict(model=c.get('model',''),lyrics=spec['lyrics'],style=spec['style'],seed=spec['seed'],steps=g['SETTINGS'].ode_steps,max_tokens=9000)) + if job.cancel_flag.is_set():raise InterruptedError('UI-Auftrag abgebrochen. Laufenden Deck-Job in Deck stoppen.') + work=g['OUTPUTS']/('.pending-'+job.id);work.mkdir(parents=True,exist_ok=True) + wav=work/'audio.wav';wav.write_bytes(audio) + return g['GgufSong'](spec,wav,{'e2e_seconds':time.monotonic()-start},'',{'runtime':'Athena Deck audio.cpp','profile':c.get('model')}) + g['render_gguf']=render;g['render_torch']=render + g['ENGINE'].detail='Remote-Laufzeit in Athena Deck · Musik-Modus erforderlich' + @app.middleware('http') + async def deck_guard(req,call_next): + path=req.url.path + origin=req.headers.get('Origin') + if req.method!='GET' and origin and origin not in ('http://'+req.headers.get('Host',''),'https://'+req.headers.get('Host','')): + return g['JSONResponse']({'detail':'Cross-Origin-Zugriff nicht erlaubt.'},status_code=403) + if req.method!='GET' and not (path in {'/api/generate','/api/deck/connection','/api/unlock'} or (req.method=='DELETE' and path.startswith('/api/library/') and path.count('/')==3)): + return g['JSONResponse']({'detail':'Laufzeiten, Modelle und Einstellungen werden in Athena Deck verwaltet.'},status_code=409) + response=await call_next(req) + response.headers['Cache-Control']='no-store' + return response diff --git a/deploy/ladypoly/install.py b/deploy/ladypoly/install.py new file mode 100644 index 0000000..cb31139 --- /dev/null +++ b/deploy/ladypoly/install.py @@ -0,0 +1,15 @@ +"""Run as root on Athena. Only creates the explicit optional LadyPoly UI.""" +import os, socket, subprocess +from pathlib import Path +base=Path('/opt/athena-ladypoly/data');name='athena-ladypoly';image='athena-ladypoly:3960afc-deck1' +if subprocess.run(['docker','inspect',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode==0: + raise SystemExit('LadyPoly container already exists; no automatic replacement.') +with socket.socket() as s:s.bind(('127.0.0.1',8127)) +base.mkdir(parents=True,exist_ok=True);os.chown(base,65534,65534);base.chmod(0o700) +subprocess.check_call(['docker','run','-d','--name',name,'--network','host','--restart','unless-stopped','--read-only', + '--cap-drop','ALL','--security-opt','no-new-privileges:true','--memory','1g','--cpus','2', + '--tmpfs','/tmp:rw,noexec,nosuid,size=128m,uid=65534,gid=65534', + '--label','io.athena-deck.managed=true','--label','io.athena-deck.role=application', + '--label','io.athena-deck.application=ladypoly','--label','io.athena-deck.port=8127', + '-v',str(base)+':/data:rw',image]) +print('LadyPoly UI: http://127.0.0.1:8127/deck-connection') diff --git a/deploy/ladypoly/test_bridge.py b/deploy/ladypoly/test_bridge.py new file mode 100644 index 0000000..e129fa8 --- /dev/null +++ b/deploy/ladypoly/test_bridge.py @@ -0,0 +1,30 @@ +"""Synthetic bridge contract; no real API token or GPU job.""" +import io,json,tempfile,unittest,wave +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch +import server +import deck_bridge as bridge + +class BridgeTest(unittest.TestCase): + def test_forward_and_audio_library(self): + with tempfile.TemporaryDirectory() as temp: + root=Path(temp);connection=root/'connection.json';connection.write_text(json.dumps({'model':'Synthetic Music','token':'synthetic'})) + raw=io.BytesIO() + with wave.open(raw,'wb') as wav:wav.setnchannels(2);wav.setsampwidth(2);wav.setframerate(48000);wav.writeframes(b'\0'*192000) + job=SimpleNamespace(id='synthetic',cancel_flag=SimpleNamespace(is_set=lambda:False),push=lambda **k:None) + spec={'style':'synthetic piano','lyrics':'synthetic test','seed':123,'id':'synthetic','cot':'off'} + with patch.object(bridge,'CONNECTION',connection),patch.object(server,'OUTPUTS',root),patch.object(bridge,'request',return_value=raw.getvalue()) as request: + result=server.render_gguf(job,spec) + payload=request.call_args.args[1] + self.assertEqual(payload['model'],'Synthetic Music');self.assertEqual(payload['steps'],6) + self.assertEqual(payload['max_tokens'],9000);self.assertNotIn('abc',payload) + saved=result.save_artifacts(root/'take');self.assertEqual(saved['audio_seconds'],1) + self.assertTrue((root/'take/audio.flac').is_file()) + def test_unsupported_score_does_not_start_deck(self): + with patch.object(bridge,'request') as request: + with self.assertRaisesRegex(RuntimeError,'Score'): + server.render_gguf(None,{'abc':'synthetic'}) + request.assert_not_called() + +if __name__=='__main__':unittest.main() diff --git a/deploy/setup_docker_helper.py b/deploy/setup_docker_helper.py index a8d85e9..53674d2 100644 --- a/deploy/setup_docker_helper.py +++ b/deploy/setup_docker_helper.py @@ -25,6 +25,9 @@ def main(): (base/'swarm-ui').mkdir(exist_ok=True) for name in ('Dockerfile','patch-source.py','proxy.py','entrypoint.py'): shutil.copyfile(Path(__file__).parent/'swarm-ui'/name,base/'swarm-ui'/name) + (base/'ladypoly').mkdir(exist_ok=True) + for name in ('Dockerfile','bridge.py'): + shutil.copyfile(Path(__file__).parent/'ladypoly'/name,base/'ladypoly'/name) if args.deck_state: registered=Path(args.deck_state).resolve() if not registered.is_dir():raise SystemExit('Deck-Zustandsverzeichnis fehlt.') diff --git a/docker-ui.js b/docker-ui.js index d22342c..0c13499 100644 --- a/docker-ui.js +++ b/docker-ui.js @@ -4,7 +4,7 @@ window.DockerUI=(()=>{ function html(services=false){return `${services?'Zusätzliche Oberflächen werden getrennt von Modellen und Laufzeiten verwaltet. Hier erscheinen ausschließlich ausdrücklich für Deck markierte Anwendungscontainer.':'Optionale Umgebung für zusätzliche Studios. Native Modelllaufzeiten bleiben unabhängig davon.'}
`;} function bind(services=false){const panel=document.querySelector('#docker-panel'),message=document.querySelector('#docker-message');let signature='',pending=false; async function refresh(){try{const s=await api();if(!panel.isConnected)return;const next=JSON.stringify(s);if(signature!==next){signature=next;const running=s.job?.state==='running'; - if(services){panel.innerHTML=`Beide Container-Labels müssen gesetzt sein:
io.athena-deck.managed=true\nio.athena-deck.role=applicationKeine automatische Übernahme vorhandener Container. Labels werden beim Erstellen in Docker Compose gesetzt. Diese Ansicht verändert keine Container oder Modelldateien.
Docker-Verbindung prüfen →'+esc(s.message)+'
${esc(c.image)}
${esc(c.state)} · ${esc(c.status)}
${c.name==='athena-swarm-ui'?`API By URL · Deck steuert die Video-Laufzeit
SwarmUI öffnen →In Deck zuerst Video aktivieren. Zugriff über den SSH-Tunnel auf Port 8125.
`:''}Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.
Beide Container-Labels müssen gesetzt sein:
io.athena-deck.managed=true\nio.athena-deck.role=applicationKeine automatische Übernahme vorhandener Container. Labels werden beim Erstellen in Docker Compose gesetzt. Diese Ansicht verändert keine Container oder Modelldateien.
Docker-Verbindung prüfen →'+esc(s.message)+'
${esc(c.image)}
${esc(c.state)} · ${esc(c.status)}
${c.name==='athena-ladypoly'?`LadyPoly YuE2 · Oberfläche für Decks Musiklaufzeit
LadyPoly öffnen →Zuerst Deck-Verbindung einrichten und in der Übersicht Musik aktivieren. SSH-Tunnel: Port 8127.
`:''}${c.name==='athena-swarm-ui'?`API By URL · Deck steuert die Video-Laufzeit
SwarmUI öffnen →In Deck zuerst Video aktivieren. Zugriff über den SSH-Tunnel auf Port 8125.
`:''}Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.
${s.reachable?'Daemon erreichbar · Verbindung über begrenzten Systemhelfer':s.helper_available?'Systemhelfer erreichbar · Docker-Daemon nicht erreichbar':'Systemhelfer nicht verbunden'}
${esc(s.message)}
${s.job?`${esc(s.job.state)} · ${esc(s.job.phase)}
`:''}${!s.installed&&s.install_supported&&!running?'':``}Keine automatische Aktualisierung vorhandener Docker-Installationen. NVIDIA Container Toolkit und Treiber sind separate Voraussetzungen für GPU-Container. Die Paketinstallation wird nicht mitten im Vorgang abgebrochen.
Weitere Dienste öffnen →