Add isolated Qwen3 TTS runtime, GUI setup and speech endpoint

This commit is contained in:
Mikei386
2026-09-28 23:51:56 +02:00
parent 52b1116bdb
commit 88e0edcc59
22 changed files with 413 additions and 24 deletions
+28 -2
View File
@@ -7,6 +7,8 @@ import secrets
from http.cookies import SimpleCookie, CookieError
from auth import CredentialStore, verify_password
from catalog import Catalog
from tts_runtime import TTSRuntime
from tts_test import TTSTests
from profiles import Profiles
from capacity import assess, overview
from runtime import Runtime
@@ -70,6 +72,9 @@ class Server(ThreadingHTTPServer):
self.image_runtime = ImageRuntime(self.catalog.root.parent/"image-runtime")
self.image_tests.runtime = self.image_runtime
self.profiles.image_runtime_ready=lambda:self.image_tests.status()["runtime_installed"]
self.tts_runtime=TTSRuntime(self.catalog.root.parent/'tts-runtime')
self.tts_tests=TTSTests(self.catalog.root.parent/'tts-tests',self.profiles,self.tts_runtime)
self.profiles.tts_blockers=self.tts_tests.blockers
self.docker = DockerSupport()
self.hardware = HardwareProvider()
self.started = time.time()
@@ -86,7 +91,9 @@ class Server(ThreadingHTTPServer):
self.scheduler=Scheduler(self.worker)
self.profiles.chat_blockers=self.worker.blockers
self.image_tests.acquire=self.scheduler.image_reservation
self.tts_tests.acquire=self.scheduler.image_reservation
self.endpoint=Endpoint(self.catalog.root.parent,self.profiles,self.worker,self.scheduler,self.image_tests,self.credentials,self.server_port)
self.endpoint.tts=self.tts_tests
if self.endpoint.config['autostart']:
try:self.endpoint.start()
except ValueError as exc:self.endpoint.error=str(exc)
@@ -243,7 +250,7 @@ class Handler(BaseHTTPRequestHandler):
return self.respond({'error':'Anmeldung erforderlich.'},401)
if not self.authenticated() and self.path == '/':
return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8')
routes = {'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')}
routes = {'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')}
if self.path in routes:
name, mime = routes[self.path]
return self.respond((ROOT/name).read_bytes(), mime=mime)
@@ -255,6 +262,10 @@ class Handler(BaseHTTPRequestHandler):
if self.path == '/api/v1/chat-tests':return self.respond(self.server.chat_tests.status())
if self.path == '/api/v1/endpoint':return self.respond(self.server.endpoint.status())
if self.path == '/api/v1/docker':return self.respond(self.server.docker.status())
if self.path == '/api/v1/tts':return self.respond(self.server.tts_tests.status())
if urlsplit(self.path).path == '/api/v1/tts/audio':
try:return self.respond(self.server.tts_tests.audio(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='audio/wav')
except (OSError,ValueError):return self.respond({'error':'Audio nicht verfügbar.'},404)
if self.path == '/api/v1/image-runtime':return self.respond(self.server.image_runtime.status())
if self.path == '/api/v1/image-tests':return self.respond(self.server.image_tests.status())
if urlsplit(self.path).path == '/api/v1/image-tests/image':
@@ -364,6 +375,17 @@ class Handler(BaseHTTPRequestHandler):
return self.respond(ep.start() if self.path.endswith('/start') else ep.stop())
except ValueError as exc:return self.respond({'error':str(exc)},400)
except OSError:return self.respond({'error':'Endpunkt konnte nicht konfiguriert werden.'},503)
if self.path in ('/api/v1/tts/install','/api/v1/tts/install-cancel','/api/v1/tts/assign','/api/v1/tts/start','/api/v1/tts/cancel'):
try:
data=self.read_json();t=self.server.tts_tests
if self.path.endswith('/install') and not data:return self.respond(self.server.tts_runtime.start())
if self.path.endswith('/install-cancel') and not data:return self.respond(self.server.tts_runtime.stop())
if self.path.endswith('/assign') and set(data)=={'id','revision'}:return self.respond(t.assign(data['id'],data['revision']))
if self.path.endswith('/start') and set(data)=={'profile_id','text','speaker','language'}:return self.respond(t.start(**data))
if self.path.endswith('/cancel') and not data:return self.respond(t.stop())
raise ValueError('Ungültige TTS-Anfrage.')
except ValueError as exc:return self.respond({'error':str(exc)},400)
except Exception:return self.respond({'error':'TTS-Aktion fehlgeschlagen.'},503)
if self.path in ('/api/v1/image-runtime/install','/api/v1/image-runtime/cancel'):
try:
if self.read_json():raise ValueError('Keine Parameter erwartet.')
@@ -389,7 +411,9 @@ class Handler(BaseHTTPRequestHandler):
if self.path == '/api/v1/profiles/delete':
try:
data=self.read_json()
with self.server.image_tests.lock:
with self.server.image_tests.lock, self.server.tts_tests.lock:
tts_job=self.server.tts_tests.job
if tts_job and tts_job.get("state")=="running" and tts_job.get("profile_id")==data.get("id"):raise ValueError("Dieses TTS-Profil wird gerade ausgeführt. Zuerst den Auftrag beenden.")
job=self.server.image_tests.job
if job and job.get('state')=='running' and job.get('profile_id')==data.get('id'):raise ValueError('Dieses Profil wird gerade ausgeführt. Zuerst den Auftrag beenden.')
return self.respond(self.server.profiles.delete(data))
@@ -447,6 +471,8 @@ def main():
server.auto_tests.stop()
server.chat_tests.stop()
server.endpoint.close()
server.tts_tests.stop()
server.tts_runtime.stop()
server.image_runtime.stop()
server.image_tests.stop()
server.runtime.stop()