From 67cf57a5b752ce0789eeb6e71dc5d2000a2e159e Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:58:36 +0200 Subject: [PATCH] Restore Video workspace and route native video API through shared endpoint --- README.md | 14 +++++++++--- deploy/Dockerfile | 2 +- deploy/docker_helper.py | 47 ++++++++++++++++++++++++++++++++++++++- deploy/install.py | 2 +- docs/DOCKER_SERVICES.md | 4 +++- endpoint-ui.js | 2 +- endpoint.py | 13 +++++++++++ index.html | 2 +- network/install_remote.py | 2 +- studio.js | 1 - test_endpoint.py | 12 ++++++++-- test_video_proxy.py | 41 ++++++++++++++++++++++++++++++++++ video-ui.js | 8 +++---- video.py | 6 ++--- video_proxy.py | 30 +++++++++++++++++++++++++ 15 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 test_video_proxy.py create mode 100644 video_proxy.py diff --git a/README.md b/README.md index 714cd02..eced015 100644 --- a/README.md +++ b/README.md @@ -151,13 +151,13 @@ Interne API (nur angemeldete Administratorsitzung): `GET /api/v1/huggingface` li ### Video: externe Dienste statt Deck-Profile -Deck verwaltet für Video ausschließlich den aktiven Dienst, Start/Stopp und die GPU-Freigabe. Unter **Weitere Dienste → Aktiver Videodienst** einen administrativ eingerichteten Dienst wählen; unter **Übersicht → Video** starten. **LLM** beendet den Videodienst. Modellauswahl, Komponenten, Auflösung, Dauer, Prompts und Generierung erfolgen ausschließlich über dessen Original-API und Oberfläche. Heute ist LTX Desktop angebunden; weitere containerisierte Videodienste lassen sich mit derselben Dienststeuerung registrieren. Keine Übersetzung zwischen APIs. +Deck verwaltet für Video ausschließlich den aktiven Dienst, Start/Stopp und die GPU-Freigabe. Unter **Video → Aktiver Videodienst** einen administrativ eingerichteten Dienst wählen; unter **Übersicht → Video** starten. **LLM** beendet den Videodienst. Modellauswahl, Komponenten, Auflösung, Dauer, Prompts und Generierung erfolgen ausschließlich über dessen Original-API und Oberfläche. Heute ist LTX Desktop angebunden; weitere containerisierte Videodienste lassen sich mit derselben Dienststeuerung registrieren. Keine Übersetzung zwischen APIs. -Videoprofile, Testformular, eigene Video-Laufzeitinstallation und `/v1/videos`-Generierung wurden entfernt. Alte Profil- und Modelldaten bleiben auf Platte erhalten, werden jedoch nicht mehr als aktive Videoprofile angeboten. `/v1/videos…` liefert HTTP 410 mit Verweis auf die Original-API. Der ursprüngliche Deck-Python-Video-Worker wird nicht mehr installiert oder gestartet. +Der Video-Bereich ist wieder Teil der KI-Werkzeuge. Generierungsparameter bleiben ausschließlich in der Video-Anwendung. Das frühere Testformular, die eigene Video-Laufzeitinstallation und `/v1/videos`-Generierung sind entfernt. Alte Profil- und Modelldaten bleiben auf Platte erhalten, werden jedoch nicht mehr als aktive Videoprofile angeboten. `/v1/videos…` liefert HTTP 410 mit Verweis auf die Original-API. Der ursprüngliche Deck-Python-Video-Worker wird nicht mehr installiert oder gestartet. Die Steuerung erfolgt über den beschränkten Docker-Systemhelfer, ohne Docker-Socket in Deck. Root registriert Container-IDs, API-Adresse und Healthcheck in `/var/lib/athena-deck-docker/video-services.json`. Nur registrierte Dienste können gestartet/gestoppt werden; beliebige Docker-Aktionen sind ausgeschlossen. Siehe [Videodienste](docs/DOCKER_SERVICES.md#videodienste). -Vor Videostart beendet Deck seine eigenen GPU-Aufträge, wartet auf Freigabe und verweigert den Start bei fremden GPU-Prozessen. Der alte Router bleibt unberührt. Während Video aktiv ist, sind Chat/Bild/TTS-GPU-Aufträge gesperrt. CPU-STT bleibt verfügbar. Beim Deck-Neustart wird ein laufender registrierter Videodienst erkannt und die GPU-Sperre wiederhergestellt; kein automatischer Dienststart und kein Abbruch externer Generierung beim bloßen Deck-Neustart. +Vor Videostart beendet Deck seine eigenen GPU-Aufträge, wartet auf Freigabe und verweigert den Start bei fremden GPU-Prozessen. Der alte Router bleibt unberührt. Im Videomodus wird der gemeinsame API-Port vollständig auf die Original-API des aktiven Videodienstes umgeschaltet. Chat/Bild/TTS/STT-Clients können diesen Port erst nach dem Rückwechsel auf LLM wieder verwenden. Beim Deck-Neustart wird ein laufender registrierter Videodienst erkannt und die GPU-Sperre wiederhergestellt; kein automatischer Dienststart und kein Abbruch externer Generierung beim bloßen Deck-Neustart. LTX auf Athena: originale API am Host `http://127.0.0.1:41955`, bestehender SSH-Zugang über LTX Athena. Alternativer Tunnel vom Mac: @@ -166,3 +166,11 @@ ssh -N -i /Users/mike_i386/.ssh/athena_key -o BatchMode=yes -o ExitOnForwardFail ``` Dann API unter `http://127.0.0.1:41956`; Authentifizierung mit dem bestehenden LTX-Token, nicht dem Deck-Token. Keine Zugangsdaten in URLs. Der Port stellt eine API bereit, keine Browser-Studio-Oberfläche. + +### Gemeinsamer API-Port: Protokollwechsel + +Im LLM-Modus bleibt der Deck-Endpunkt OpenAI-kompatibel. Im Videomodus werden HTTP-Pfad, Query, Methode, Inhalt, Antwortstatus und Antwortdaten unverändert an die registrierte Video-API weitergereicht. Es gibt keine Modell-/Parameterübersetzung. Beispiel auf Port 8120: `POST /api/generate`, `GET /api/generation/progress`, `POST /api/generate/cancel`. Kein `/v1`-Präfix für LTX. Während des Moduswechsels HTTP 503. + +Am gemeinsamen Port gilt weiterhin der Deck-Bearer-Token. Der root-eigene Helfer setzt intern den separaten LTX-Token; er gibt ihn niemals an Deck oder den Browser zurück. Der interne LTX-Port 41955 bleibt unverändert, sodass der bisherige LTX-Athena-Zugang weiter funktioniert. HTTP GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS und Range-Header werden transportiert. Request-Bodies benötigen Content-Length (maximal 256 MiB); kein WebSocket-Transport. Die geprüfte LTX-Desktop-API verwendet für Fortschritt HTTP-Polling. + +Ein separates LTX-Webfrontend gehört als Docker-Anwendung unter Weitere Dienste, nicht die Video-Laufzeit-Auswahl. Dieses Webfrontend ist noch nicht implementiert. Der vorhandene LTX-Athena-Client benötigt neben API-Zugriff weiterhin seine Dateiübertragung über SSH/SCP; ein Browser-Port muss Electron-Dateifunktionen separat ersetzen. Weiterleitung allein stellt keine neuen Datei-Upload-/Downloadrouten im LTX-Backend bereit. diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 0c7099b..005de18 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -14,7 +14,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins WORKDIR /app COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock -COPY 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 profiles.py capacity.py server.py runtime.py video.py catalog.py hub_auth.py auth.py collect_hardware.py /app/ +COPY 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 profiles.py capacity.py server.py runtime.py video.py video_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py /app/ COPY 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 index.html app.js studio.js runtime-ui.js catalog-ui.js style.css login.html login.js access-ui.js network-ui.js /app/ COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ diff --git a/deploy/docker_helper.py b/deploy/docker_helper.py index fd97e64..e2dc48f 100644 --- a/deploy/docker_helper.py +++ b/deploy/docker_helper.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Root-owned local helper. Only inventory and fresh Docker install; no generic RPC.""" import argparse +import http.client +from urllib.parse import urlsplit import re import urllib.request import urllib.error @@ -149,6 +151,46 @@ class Manager: if data['action']=='install':return self.install() raise ValueError('Aktion nicht erlaubt.') +def video_http(manager,data,source,target): + """Fixed registered upstream only; no model translation or secret returned to Deck.""" + sent=False;connection=None + try: + if set(data)!={'action','service','method','path','length','headers'}:raise ValueError('Ungültige Proxy-Anfrage.') + method=data['method'];path=data['path'];length=data['length'];headers=data['headers'] + if method not in ('GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS') or not isinstance(path,str) or not path.startswith('/') or path.startswith('//') or any(ord(c)<32 for c in path):raise ValueError('Ungültige HTTP-Anfrage.') + if type(length) is not int or not 0<=length<=256*1024*1024 or not isinstance(headers,dict):raise ValueError('Ungültige Anfragegröße.') + allowed={'content-type','accept','range','if-range','if-none-match','if-modified-since'} + if any(not isinstance(k,str) or k.lower() not in allowed or not isinstance(v,str) or '\r' in v or '\n' in v for k,v in headers.items()):raise ValueError('Ungültige Header.') + config=next((c for c in manager.video_configs() if c['id']==data['service']),None) + if config is None:raise ValueError('Videodienst nicht freigegeben.') + if manager.video_status(config)['running'] is not True:raise ValueError('Videodienst nicht gestartet.') + url=urlsplit(config['api_url']) + if url.scheme not in ('http','https') or not url.hostname or url.username or url.password:raise ValueError('Ungültiges Backend-Ziel.') + connection=(http.client.HTTPSConnection if url.scheme=='https' else http.client.HTTPConnection)(url.hostname,url.port,timeout=3600) + connection.putrequest(method,path,skip_accept_encoding=True) + for key,value in headers.items():connection.putheader(key,value) + if config.get('token_file'):connection.putheader('Authorization','Bearer '+Path(config['token_file']).read_text().strip()) + connection.putheader('Content-Length',str(length));connection.putheader('Connection','close');connection.endheaders() + remaining=length + while remaining: + chunk=source.read(min(1024*1024,remaining)) + if not chunk:raise ValueError('Unvollständige Anfrage.') + connection.send(chunk);remaining-=len(chunk) + response=connection.getresponse();sent=True + target.write(f'HTTP/1.1 {response.status} {response.reason}\r\n'.encode()) + hop={'connection','keep-alive','proxy-authenticate','proxy-authorization','te','trailer','transfer-encoding','upgrade'} + for key,value in response.getheaders(): + if key.lower() not in hop:target.write(f'{key}: {value}\r\n'.encode('latin-1')) + target.write(b'Connection: close\r\n\r\n');target.flush() + if method!='HEAD': + while chunk:=response.read(1024*1024):target.write(chunk);target.flush() + except Exception: + if not sent: + body=b'{"error":"Video-API nicht erreichbar oder Anfrage nicht erlaubt."}' + target.write(b'HTTP/1.1 502 Bad Gateway\r\nContent-Type: application/json\r\nContent-Length: '+str(len(body)).encode()+b'\r\nConnection: close\r\n\r\n'+body) + finally: + if connection:connection.close() + class Handler(socketserver.StreamRequestHandler): def handle(self): self.connection.settimeout(20) @@ -157,7 +199,10 @@ class Handler(socketserver.StreamRequestHandler): if uid not in (0,self.server.client_uid):raise ValueError('Client nicht erlaubt.') raw=self.rfile.readline(4097) if len(raw)>4096:raise ValueError('Anfrage zu groß.') - result=self.server.manager.dispatch(json.loads(raw)) + data=json.loads(raw) + if isinstance(data,dict) and data.get('action')=='video-http': + self.connection.settimeout(3600);video_http(self.server.manager,data,self.rfile,self.wfile);return + result=self.server.manager.dispatch(data) except Exception as exc:result={'error':str(exc) if isinstance(exc,ValueError) else 'Docker-Systemhelfer nicht verfügbar.'} self.wfile.write(json.dumps(result).encode()+b'\n') diff --git a/deploy/install.py b/deploy/install.py index 3aa487a..cdc0bf1 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 = ['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-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.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/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','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/docs/DOCKER_SERVICES.md b/docs/DOCKER_SERVICES.md index 941f8c9..b7c3cca 100644 --- a/docs/DOCKER_SERVICES.md +++ b/docs/DOCKER_SERVICES.md @@ -98,6 +98,8 @@ Zusätzlich zum labelgefilterten Inventar kann der root-eigene Helfer explizit r Keine Secrets werden an Deck übertragen. Der Helfer akzeptiert nur `video-status`, `video-start` und `video-stop` für registrierte IDs. Container-Neuerstellung erfordert bewusste Aktualisierung der ID. Healthchecks folgen keinen Redirects. Die GUI erlaubt nur die Auswahl registrierter Dienste, keine freien Containerbefehle, Zieladressen oder Secret-Dateipfade. -Deck speichert lediglich die aktive Dienst-ID. Ein Wechsel ist nur bei gestoppten Videodiensten möglich. API-Adresse und Protokoll gehören zum Dienst; beim Wechsel auf eine andere Engine ändert sich auch deren Original-API. Client-Kompatibilität wird nicht übersetzt. Die Integration setzt einen vorhandenen, eingerichteten Dienst voraus; sie installiert keine beliebige Video-Engine automatisch. +Der eigenständige KI-Bereich Video speichert lediglich die aktive Laufzeit-ID. Weitere Dienste ist für separate Webfrontends vorgesehen. Ein Wechsel ist nur bei gestoppten Videodiensten möglich. Der gemeinsame Deck-API-Port reicht im Videomodus die Original-API unverändert weiter; beim Wechsel auf eine andere Engine ändert sich deren API-Vertrag. Client-Kompatibilität wird nicht übersetzt. Die Integration setzt einen vorhandenen, eingerichteten Dienst voraus; sie installiert keine beliebige Video-Engine automatisch. Interne Verwaltungs-API: GET `/api/v1/video`, POST `/api/v1/video/service` mit `{ "id": "ltx" }`, POST `/api/v1/video/mode` mit `{ "mode": "video" }` oder `llm`. Bestehender Sitzungsschutz gilt. Start/Stop laufen asynchron und zeigen Phase sowie API-Bereitschaft. + +Der zusätzliche Transport `video-http` ist nur über das private Unix-Socket zugänglich und an registrierte Dienst-IDs gebunden. Zielhost und Upstream-Zugangsdaten stammen ausschließlich aus der root-eigenen Konfiguration, niemals aus einer Client-Anfrage. Keine Redirect-Verfolgung, kein freier HTTP-Proxy. Payloads werden gestreamt und nicht protokolliert. Der öffentliche Deck-Endpunkt prüft vorher seinen Bearer-Token und den aktiven Videomodus. diff --git a/endpoint-ui.js b/endpoint-ui.js index 6743bbd..3863829 100644 --- a/endpoint-ui.js +++ b/endpoint-ui.js @@ -16,7 +16,7 @@ window.EndpointUI=(()=>{ if(pending)return;const id=sequence; try{const s=await api();if(!root.isConnected||id!==sequence)return; put(root.querySelector('#endpoint-summary'),summary(s)); - if(s.video)VideoUI.mode(root.querySelector('#video-mode-panel'),s.video,s.profiles); + 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/endpoint.py b/endpoint.py index 3f18b64..1cc78ce 100644 --- a/endpoint.py +++ b/endpoint.py @@ -1,3 +1,4 @@ +from video_proxy import relay """Separate authenticated OpenAI-compatible API; only explicitly enabled Deck profiles.""" import base64 import hashlib @@ -155,6 +156,11 @@ class APIHandler(BaseHTTPRequestHandler): return False def do_GET(self):self.route() def do_POST(self):self.route() + def do_PUT(self):self.route() + def do_PATCH(self):self.route() + def do_DELETE(self):self.route() + def do_HEAD(self):self.route() + def do_OPTIONS(self):self.route() def route(self): ep=self.server.endpoint;admitted=False try: @@ -163,6 +169,13 @@ class APIHandler(BaseHTTPRequestHandler): with ep.lock: if not ep.allowed():raise APIError('Endpunkt wird gestoppt.',503,'endpoint_stopping') ep.inflight+=1;admitted=True + if ep.video and ep.scheduler.gpu_mode!='llm': + if ep.scheduler.gpu_mode=='switching':raise APIError('Moduswechsel läuft.',503,'mode_switching') + video=ep.video.status() + if not video.get('service',{}).get('ready'):raise APIError('Video-API noch nicht bereit.',503,'video_not_ready') + try:relay(self,video['selected']) + except ValueError as exc:raise APIError(str(exc)) from None + return model_routes={'/v1/models':'chat','/v1/images/models':'image','/v1/audio/speech/models':'audio','/v1/audio/transcriptions/models':'stt'} if self.command=='GET' and self.path in model_routes:return self.send(ep.model_list(model_routes[self.path])) if self.command=='GET' and self.path=='/health':return self.send({'status':'ok','service':'athena-deck-api'}) diff --git a/index.html b/index.html index 1dd6a67..f9bdfe8 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -
${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}
${audioDescriptions[page]&&!['audio','stt'].includes(page)?'Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.
':''}Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.
llama.cpp-Builds verwalten →${s.mode==='llm'?'LLM-Betrieb · Videodienst gestoppt':s.service?.ready?'Video-API bereit':s.service?.running?'Videodienst läuft · API noch nicht bereit':'Dienststatus nicht prüfbar · GPU-Aufträge gesperrt'}
`}${s.service?.api_url?`Original-API: ${e(s.service.api_url)}
${e(s.service.message)}
`:''}${s.error?`${e(s.error)}
`:''}Modelle, Auflösung, Dauer und Generierung werden in der jeweiligen Video-Anwendung verwaltet. Video beendet Deck-GPU-Aufträge; fremde Dienste werden nicht beendet.
Aktiven Videodienst unter Weitere Dienste wählenGemeinsamer API-Port: ${e(port||"siehe Endpunkt")} · LLM-Modus: OpenAI-API · Video-Modus: Original-API der Video-Laufzeit
${s.mode==='llm'?'LLM-Betrieb · Videodienst gestoppt':s.service?.ready?'Video-API bereit':s.service?.running?'Videodienst läuft · API noch nicht bereit':'Dienststatus nicht prüfbar · GPU-Aufträge gesperrt'}
`}${s.service?.api_url?`Interner Backend-Zugang: ${e(s.service.api_url)}
${e(s.service.message)}
`:''}${s.error?`${e(s.error)}
`:''}Modelle, Auflösung, Dauer und Generierung werden in der jeweiligen Video-Anwendung verwaltet. Video beendet Deck-GPU-Aufträge; fremde Dienste werden nicht beendet.
Aktiven Videodienst im Bereich Video wählenDie Übersicht startet den hier gewählten Dienst. Jeder Dienst behält seine eigene API; keine Übersetzung von Generierungsparametern.
${s.services.map(x=>``).join('')||'Noch kein Videodienst durch den Administrator zugeordnet.
'}Zum Wechseln zuerst in der Übersicht auf LLM schalten.
Deck verwaltet keine Videoprofile. Der aktive Dienst wird in der Übersicht gestartet und über seine Original-API bedient.
Zur ÜbersichtVideo ist ein KI-Bereich in Deck. Die Übersicht startet die gewählte Video-Laufzeit. Im Videomodus stellt der gemeinsame API-Port deren Original-API bereit. Das separate LTX-Webfrontend gehört unter Weitere Dienste.
Zur Übersicht