From 7b96d4b3bc32a428f37f67e43e259b2de8aa8e3c Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:59:32 +0200 Subject: [PATCH] Simplify control into LLM video and music modes --- STUDIO.md | 13 +++++++++++++ endpoint-ui.js | 47 +++++++++++++++++++++++++++++++++++++++++++---- style.css | 3 +++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/STUDIO.md b/STUDIO.md index 0537140..7355fba 100644 --- a/STUDIO.md +++ b/STUDIO.md @@ -264,3 +264,16 @@ aktuellen Browserseite. Beim erneuten Öffnen einer Bibliothek erscheinen deren zuletzt geladene Dateien sofort mit Aktualisierungshinweis. Aktionen bleiben bis zur frischen Serverantwort gesperrt; Löschschutz und Profilzuordnungen werden weiterhin serverseitig geprüft. Ein Neuladen der Browserseite leert diesen Cache. + +### Vereinfachte Steuerung + +Unter Steuerung zuerst LLM, Video oder Musik auswählen und anschließend starten. +Die Auswahl allein verändert den laufenden Betrieb nicht. Start wechselt bei +Bedarf die Betriebsart und öffnet den gemeinsamen API-Listener. Stoppen schließt +den Listener und beendet bei Video/Musik den exklusiven Betrieb durch Rückkehr +auf LLM; die bisherigen Schutzprüfungen und Wartebedingungen bleiben bestehen. +Während eines Wechsels erscheint ein unbestimmter Ladebalken mit der echten +Wechselphase, ohne erfundene Prozentangabe. Bereit bedeutet API-erreichbar; der +Hinweis zu Gewichten unterscheidet geladene Modelle vom Laden bei Anfrage. +API-Adressen und der Port werden für die ausgewählte Betriebsart angezeigt. +Technische Konfiguration und Profilfreigaben bleiben unter Einstellungen → Endpunkt. diff --git a/endpoint-ui.js b/endpoint-ui.js index 2a46b1a..772e080 100644 --- a/endpoint-ui.js +++ b/endpoint-ui.js @@ -1,22 +1,61 @@ window.EndpointUI=(()=>{ const e=v=>String(v??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const stateName={running:'Läuft',stopped:'Gestoppt',stopping:'Wird gestoppt · laufende Aufträge werden beendet',loading:'Modell lädt',ready:'Modell bereit',failed:'Fehlgeschlagen'}; - let pending=false,sequence=0; + let pending=false,sequence=0,selectedMode=null,controlTimer=null; async function api(path='',data){const r=await fetch('/api/v1/endpoint'+path,data!==undefined?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Endpunkt nicht erreichbar');return v;} function put(el,html){if(el._deckHtml!==html){el.innerHTML=html;el._deckHtml=html;}} function summary(s){return `

OpenAI-kompatibler Endpunkt

${e(stateName[s.state])}
Erreichbarkeit${s.reachable?'Listener bereit':'Nicht erreichbar'}
Lokaler API-Port${s.port}
Laufende / wartende Aufträge${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}

${e(s.base_url)} · Port und Profile einstellen →

Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}

${s.worker.error||s.error?`

${e(s.worker.error||s.error)}

`:''}

Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.

${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)'],['music','Musik']].map(([key,label])=>{const c=s.counts[key];return `

${label}

${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}

${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}
`}).join('')}
`;} + const modeLabels={llm:'LLM',video:'Video',music:'Musik'}; + function control(s,root){ + const v=s.video||{},active=v.mode||'llm'; + if(!selectedMode)selectedMode=active==='switching'?(v.target_mode||'llm'):active; + const selected=selectedMode,label=modeLabels[selected],switching=v.state==='switching',busy=pending||switching||s.state==='stopping'; + const current=selected===active,available=selected==='video'?Boolean(v.service?.id):Boolean(s.counts[selected]?.available); + const ready=current&&s.reachable&&!switching&&(selected!=='video'||v.service?.ready); + const loading=current&&selected==='llm'&&s.worker.state==='loading'; + const address=location.protocol+'//'+location.hostname+':'+s.port; + const routes=selected==='llm'?[['Chat',address+'/v1/chat/completions'],['Modelle',address+'/v1/models']]:selected==='music'?[['Musik erzeugen',address+'/v1/audio/music'],['Musikmodelle',address+'/v1/audio/music/models']]:[['Video-API',address+'/']]; + const reason=available?'':selected==='video'?'Wähle zuerst ein eingerichtetes Videomodell.':'Gib zuerst ein ausführbares Profil am API-Endpunkt frei.'; + const state=switching?'Wechsel läuft':loading?'Modell wird geladen':ready?'Bereit':current&&s.state==='stopping'?'Wird gestoppt':current&&s.reachable?'Startet':current?'Gestoppt':'Nicht aktiv'; + put(root.querySelector('#control-modes'),`
${Object.entries(modeLabels).map(([id,name])=>``).join('')}
`); + put(root.querySelector('#endpoint-summary'),`

${label}

${e(state)}

${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:s.reachable?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}

${switching||loading||pending||s.state==='stopping'?`
${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}
`:''}${reason?`

${e(reason)} Einrichten →

`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?``:''}

API-Adresse

${routes.map(([name,url])=>`
${name}${e(url)}
`).join('')}

${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${s.port}

${selected==='llm'?`

${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}

`:selected==='music'?`

${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage

`:`

${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung

${ready?`${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →`:''}`}
Weitere Dienste & Einstellungen

Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.

Endpunkt und Port einstellen →

${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge

`); + root.querySelectorAll('[data-control-mode]').forEach(b=>b.onclick=()=>{selectedMode=b.dataset.controlMode;control(s,root);}); + root.querySelector('[data-control-start]').onclick=()=>controlAction('start',selected); + root.querySelector('[data-control-stop]').onclick=()=>controlAction('stop',selected); + } + async function switchMode(mode){ + const r=await fetch('/api/v1/video/mode',{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify({mode})}); + const result=await r.json();if(!r.ok)throw Error(result.error||'Wechsel fehlgeschlagen');return result; + } + async function controlAction(action,mode){ + if(pending)return;pending=true;const root=document.querySelector('#endpoint-page'); + root.querySelectorAll('button').forEach(b=>b.disabled=true); + root.querySelector('#endpoint-message').innerHTML=`
${action==='start'?e(modeLabels[mode])+' wird gestartet':'Betrieb wird gestoppt'}
`; + try{ + const s=await api(); + if(action==='start'){ + if((s.video?.mode||'llm')!==mode||(mode==='video'&&!s.video?.service?.ready))await switchMode(mode); + if(s.state==='stopped')await api('/start',{}); + }else{ + await api('/stop',{}); + if(s.video&&s.video.mode!=='llm')await switchMode('llm'); + } + if(root.isConnected)root.querySelector('#endpoint-message').textContent=''; + }catch(error){if(root.isConnected)root.querySelector('#endpoint-message').textContent=error.message;} + finally{pending=false;if(root.isConnected){root.querySelector('#control-modes')._deckHtml=null;root.querySelector('#endpoint-summary')._deckHtml=null;render(false);}} + } async function render(settings=false){ const view=document.querySelector('#view');let root=view.querySelector('#endpoint-page'); if(!root||root.dataset.settings!==String(settings)){ - sequence++;view.innerHTML=`
${settings?'EINSTELLUNGEN / API':'ATHENA DECK / ROUTER'}

${settings?'Endpunkt':'Steuerung'}

Eigener Router für Deck-Profile. Bestehende Athena-Dienste bleiben unabhängig.

${settings?'

Lokaler API-Port

Der Zugriff verwendet den API-Token aus Zugang & API-Token. Verwaltungsoberfläche und API haben getrennte Ports.

Am Endpunkt angebotene Profile

Nur explizit aktivierte, ausführbare Profile erscheinen in GET /v1/models. Aktivieren lädt noch kein Modell.

API-Routen

POST /v1/chat/completions · Text und Streaming
POST /v1/images/generations · Qwen-Image-Rezept, ein Bild als b64_json
POST /v1/audio/speech und POST /v1/audio/transcriptions · noch nicht eingerichtet (501)

Alle Routen nutzen denselben Port und Bearer-Token. Bei Bildaufträgen bestimmt das Profil Auflösung, Schritte, Guidance und Seed. Noch keine Bildbearbeitung oder Vision-Eingaben.

':''}
`; + sequence++;clearTimeout(controlTimer);selectedMode=null;view.innerHTML=`
${settings?'EINSTELLUNGEN / API':'ATHENA DECK / ROUTER'}

${settings?'Endpunkt':'Steuerung'}

${settings?'

API-Port und freigegebene Profile verwalten.

':'

Betriebsart wählen und starten.

'}

${settings?'

Lokaler API-Port

Der Zugriff verwendet den API-Token aus Zugang & API-Token. Verwaltungsoberfläche und API haben getrennte Ports.

Am Endpunkt angebotene Profile

Nur explizit aktivierte, ausführbare Profile erscheinen in GET /v1/models. Aktivieren lädt noch kein Modell.

API-Routen

POST /v1/chat/completions · Text und Streaming
POST /v1/images/generations · Qwen-Image-Rezept, ein Bild als b64_json
POST /v1/audio/speech und POST /v1/audio/transcriptions · noch nicht eingerichtet (501)

Alle Routen nutzen denselben Port und Bearer-Token. Bei Bildaufträgen bestimmt das Profil Auflösung, Schritte, Guidance und Seed. Noch keine Bildbearbeitung oder Vision-Eingaben.

':''}
`; root=view.querySelector('#endpoint-page'); - for(const action of ['start','stop'])root.querySelector('#endpoint-'+action).onclick=()=>mutate('/'+action,{}); + if(settings)for(const action of ['start','stop'])root.querySelector('#endpoint-'+action).onclick=()=>mutate('/'+action,{}); root.querySelector('#endpoint-port-form')?.addEventListener('submit',event=>{event.preventDefault();mutate('/config',{port:Number(root.querySelector('#endpoint-port').value)});}); } if(pending)return;const id=sequence; try{const s=await api();if(!root.isConnected||id!==sequence)return; + if(!settings){control(s,root);clearTimeout(controlTimer);if(s.video?.state==='switching'||s.worker.state==='loading'||s.state==='stopping')controlTimer=setTimeout(()=>{if(root.isConnected)render(false);},1000);return;} put(root.querySelector('#endpoint-summary'),summary(s)); - if(s.video)VideoUI.mode(root.querySelector('#video-mode-panel'),s.video,s.profiles,s.port); root.querySelector('#endpoint-start').disabled=s.state!=='stopped';root.querySelector('#endpoint-stop').disabled=s.state!=='running'; if(settings){const port=root.querySelector('#endpoint-port');if(document.activeElement!==port)port.value=s.port;port.disabled=s.state!=='stopped';root.querySelector('#endpoint-port-save').disabled=s.state!=='stopped'; root.querySelector('#endpoint-port-help').textContent=s.allowed_ports.length?'Docker-Testinstallation: veröffentlichte Loopback-Ports '+s.allowed_ports.join(', ')+'. Portwechsel nur bei gestopptem Endpunkt. Der SSH-Tunnel muss denselben Port weiterleiten.':'Nativer Dienst: freie Ports zwischen 1024 und 65535, nur bei gestopptem Endpunkt.'; diff --git a/style.css b/style.css index 451ea74..616d982 100644 --- a/style.css +++ b/style.css @@ -42,3 +42,6 @@ nav a.nav-sub{padding-left:28px;font-size:12px;color:var(--deck-tone-a4bda8, #a4 /* Compact, expandable detail lists share one interaction across model categories. */ .detail-list{display:grid;gap:8px;margin-top:20px}.detail-row{border:1px solid var(--deck-tone-303a37,#303a37);border-radius:9px;background:var(--deck-tone-181f21,#181f21);overflow:hidden}.detail-summary{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1.3fr) minmax(110px,.7fr) minmax(140px,1fr);gap:18px;align-items:center;padding:18px 18px 18px 38px;position:relative;list-style:none;line-height:1.5}.detail-summary::-webkit-details-marker{display:none}.detail-summary:before{content:'▸';position:absolute;left:16px;top:19px;color:var(--deck-tone-c5edaf,#c5edaf)}.detail-row[open]>.detail-summary:before{content:'▾'}.detail-summary:hover{background:var(--deck-tone-20292a,#20292a)}.detail-summary:focus-visible{outline:2px solid var(--deck-tone-c5edaf,#c5edaf);outline-offset:-3px}.detail-summary strong{font-size:14px;overflow-wrap:anywhere}.detail-summary span{font-size:12px;color:var(--deck-tone-97a79e,#97a79e);overflow-wrap:anywhere}.detail-summary small{display:block;margin-top:4px}.detail-body{padding:8px 22px 22px;border-top:1px solid var(--deck-tone-303a37,#303a37)}.detail-body>.card-actions{margin-top:16px}.detail-body .execution-setup{margin-top:12px;padding-top:12px}.detail-row .endpoint-choice{margin:0 8px 8px 0;font-size:12px}#studio[data-page=runtimes] .detail-summary{grid-template-columns:1fr 1fr 1fr}.download-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 20px;padding:18px;margin-top:8px;border-radius:9px}.download-row .card-top,.download-row progress,.download-row .warning{grid-column:1/-1}.download-row h3,.download-row p{margin:0}.download-row h3{font-size:14px;overflow-wrap:anywhere}.download-row .hub-filename{grid-column:1/-1}.download-row button{margin:0;font-size:12px}.download-row .card-top{display:flex;justify-content:space-between}.download-row .card-top strong{font-size:14px}.nav-heading{margin:16px 12px 4px}.eyebrow{margin-top:24px}aside{padding-top:24px}nav a{padding:9px 12px}nav .nav-heading:first-child{margin-top:0}.studio-tabs{flex-wrap:wrap} @media(max-width:1000px){.detail-summary{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px 18px}}@media(max-width:600px){.detail-summary,#studio[data-page=runtimes] .detail-summary{grid-template-columns:1fr;gap:5px}.detail-body{padding:8px 16px 16px}.download-row{grid-template-columns:1fr}.download-row>*{grid-column:1!important}nav{grid-template-columns:1fr 1fr}nav .nav-heading{grid-column:1/-1}.card-actions{gap:6px}} + +/* Control is a focused mode chooser; detailed API configuration lives in settings. */ +.control-modes{display:flex;gap:12px;margin:24px 0 20px}.control-modes button{min-width:110px;margin:0;font-size:15px}.control-card{max-width:900px}.control-heading{display:flex;align-items:center;justify-content:space-between;gap:16px}.control-heading h2{margin:0;font-size:25px}.control-card>.card-actions{margin-top:18px}.control-api{border-top:1px solid var(--deck-tone-303a37,#303a37);margin-top:24px;padding-top:8px}.control-api h3{font-size:14px}.control-api>div{display:grid;grid-template-columns:130px minmax(0,1fr);gap:14px;margin:12px 0;font-size:13px}.control-api span{color:var(--deck-tone-97a79e,#97a79e)}.control-api code{overflow-wrap:anywhere}.control-details{border-top:1px solid var(--deck-tone-303a37,#303a37);padding-top:16px;margin-top:20px}.control-card .small{font-size:12px}@media(max-width:600px){.control-modes{gap:8px}.control-modes button{min-width:0;flex:1;padding:12px}.control-api>div{grid-template-columns:1fr;gap:4px}}