Add profile presets and min-p controls, embeddings and VoxCPM workers

This commit is contained in:
Mikei386 committed 2026-10-02 18:42:29 +02:00
1 parent 74dd2ed8ba
commit a7b30b62d2
32 files changed
+615 -64

No files matched your search

+39 -14
View File
@@ -40,7 +40,7 @@ def chat_upstream_error(status, raw):
class Endpoint:
def __init__(self,root,profiles,worker,scheduler,images,credentials,management_port):
self.root=Path(root);self.profiles=profiles;self.worker=worker;self.scheduler=scheduler;self.images=images;self.tts=None;self.stt=None;self.music=None;self.video=None;self.credentials=credentials
self.root=Path(root);self.profiles=profiles;self.worker=worker;self.scheduler=scheduler;self.images=images;self.tts=None;self.voxcpm=None;self.embeddings=None;self.stt=None;self.music=None;self.video=None;self.credentials=credentials
self.management_port=management_port;self.lock=threading.RLock();self.http=None;self.thread=None;self.state='stopped';self.error=None;self.inflight=0
self.allowed_ports=[int(p) for p in os.environ.get('DECK_API_PORTS','').split(',') if p]
self.config=dict(port=self.allowed_ports[0] if self.allowed_ports else 8120,enabled_profiles=[],autostart=False)
@@ -126,6 +126,7 @@ class Endpoint:
with self.lock:pending=self.inflight
if not pending and self.scheduler.unload_idle():break
time.sleep(.1)
if self.embeddings:self.embeddings.stop()
with self.lock:self.http=None;self.thread=None;self.state='stopped'
def close(self):
# Process shutdown does not change the user's autostart preference.
@@ -133,6 +134,8 @@ class Endpoint:
if http:http.shutdown();http.server_close()
if self.music:self.music.stop()
if self.tts:self.tts.stop()
if self.embeddings:self.embeddings.stop()
if self.voxcpm:self.voxcpm.stop()
if self.stt:self.stt.stop()
self.images.stop();self.worker.stop()
def allowed(self):
@@ -203,8 +206,8 @@ 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.scheduler.gpu_mode=='separator':raise APIError('Audio-Trennung aktiv. Für Chat, Bilder oder Musik die Betriebsart wechseln.',503,'separator_mode_active')
if ep.video and ep.scheduler.gpu_mode not in ('llm','music'):
if ep.scheduler.gpu_mode=='separator' and self.path not in ('/v1/embeddings','/v1/embeddings/models'):raise APIError('Audio-Trennung aktiv. Für Chat, Bilder oder Musik die Betriebsart wechseln.',503,'separator_mode_active')
if ep.video and ep.scheduler.gpu_mode not in ('llm','music') and self.path not in ('/v1/embeddings','/v1/embeddings/models'):
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')
@@ -214,14 +217,17 @@ class APIHandler(BaseHTTPRequestHandler):
if self.command=='GET' and self.path=='/v1/capabilities':
from api_compat import capabilities
return self.send(capabilities())
model_routes={'/v1/models':'chat','/v1/images/models':'image','/v1/audio/speech/models':'audio','/v1/audio/transcriptions/models':'stt','/v1/audio/music/models':'music'}
if self.command=='GET' and self.path in model_routes:return self.send(ep.model_list(model_routes[self.path]))
model_routes={'/v1/embeddings/models':'embeddings','/v1/models':'chat','/v1/images/models':'image','/v1/audio/speech/models':'audio','/v1/audio/transcriptions/models':'stt','/v1/audio/music/models':'music'}
if self.command=='GET' and self.path in model_routes:
listing=ep.model_list(model_routes[self.path])
if self.path=='/v1/audio/speech/models':listing['data']+=ep.model_list('voice')['data']
return self.send(listing)
if self.command=='GET' and self.path=='/health':return self.send({'status':'ok','service':'athena-deck-api'})
if self.video_route(ep):return
if self.command!='POST':raise APIError('Route nicht gefunden.',404)
if ep.scheduler.gpu_mode=='separator':raise APIError('Audio-Trennung aktiv. Für Chat, Bilder oder Musik die Betriebsart wechseln.',503,'separator_mode_active')
if ep.scheduler.gpu_mode=='music' and self.path!='/v1/audio/music':raise APIError('Musik-Modus aktiv. Für andere KI-Aufträge auf LLM wechseln.',503,'music_mode_active')
if ep.video and ep.scheduler.gpu_mode not in ('llm','music') and self.path!='/v1/audio/transcriptions':raise APIError('Video-Modus aktiv; Chat-, Bild- und TTS-Aufträge sind gesperrt. Auf LLM zurückschalten.',503,'video_mode_active')
if ep.scheduler.gpu_mode=='separator' and self.path not in ('/v1/embeddings','/v1/embeddings/models'):raise APIError('Audio-Trennung aktiv. Für Chat, Bilder oder Musik die Betriebsart wechseln.',503,'separator_mode_active')
if ep.scheduler.gpu_mode=='music' and self.path not in ('/v1/audio/music','/v1/embeddings'):raise APIError('Musik-Modus aktiv. Für andere KI-Aufträge auf LLM wechseln.',503,'music_mode_active')
if ep.video and ep.scheduler.gpu_mode not in ('llm','music') and self.path not in ('/v1/audio/transcriptions','/v1/embeddings'):raise APIError('Video-Modus aktiv; Chat-, Bild- und TTS-Aufträge sind gesperrt. Auf LLM zurückschalten.',503,'video_mode_active')
if self.path=='/v1/audio/transcriptions':return self.transcription(ep)
if self.path=='/v1/images/edits':
from image_upload import read_image_upload
@@ -232,7 +238,7 @@ class APIHandler(BaseHTTPRequestHandler):
fields['n']=int(fields.get('n','1'))
return self.image(ep,fields,images)
except ValueError as exc:raise APIError(str(exc)) from None
if self.path not in ('/v1/chat/completions','/v1/images/generations','/v1/audio/speech','/v1/audio/music'):raise APIError('Route nicht implementiert.',404)
if self.path not in ('/v1/embeddings','/v1/chat/completions','/v1/images/generations','/v1/audio/speech','/v1/audio/music'):raise APIError('Route nicht implementiert.',404)
if self.headers.get('Transfer-Encoding'):raise APIError('Chunked Upload wird nicht unterstützt.')
try:length=int(self.headers.get('Content-Length','0'))
except ValueError:raise APIError('Ungültige Content-Length.') from None
@@ -240,6 +246,7 @@ class APIHandler(BaseHTTPRequestHandler):
try:data=json.loads(self.rfile.read(length),parse_constant=lambda _:(_ for _ in ()).throw(ValueError()))
except (ValueError,UnicodeError):raise APIError('Ungültiges JSON.') from None
if not isinstance(data,dict):raise APIError('JSON-Objekt erforderlich.')
if self.path=='/v1/embeddings':return self.embedding(ep,data)
if self.path=='/v1/chat/completions':return self.chat(ep,data)
if self.path=='/v1/audio/music':return self.music_generation(ep,data)
if self.path=='/v1/audio/speech':return self.speech(ep,data)
@@ -275,17 +282,35 @@ class APIHandler(BaseHTTPRequestHandler):
if not ep.tts:raise APIError('TTS-Laufzeit nicht eingerichtet.',501)
if set(data)-{'model','input','voice','response_format','speed','language'}:raise APIError('Nicht unterstützte TTS-Felder.')
if data.get('response_format','wav')!='wav':raise APIError('Derzeit wird nur WAV unterstützt; response_format=wav setzen.')
profile=ep.find_profile(data.get('model'),'audio')
try:job=ep.tts.start(profile['id'],data.get('input'),speaker=data.get('voice','Ryan'),language=data.get('language','Auto'),speed=data.get('speed'),wait=True)
except ValueError as exc:raise APIError(str(exc)) from None
from profiles import voice_recipe
voice=next((p for p in ep.rows() if p.get('model') and voice_recipe(p['model']) and p['name']==data.get('model') and p['enabled']),None)
if voice:
profile=ep.find_profile(data.get('model'),voice['kind']);backend=ep.voxcpm
if data.get('voice','default')!='default' or data.get('speed',1)!=1:raise APIError('VoxCPM2: voice=default und speed=1 verwenden; Referenzstimme im Deck-Testbereich.')
try:job=backend.start(profile['id'],data.get('input'),wait=True)
except ValueError as exc:raise APIError(str(exc)) from None
else:
profile=ep.find_profile(data.get('model'),'audio');backend=ep.tts
try:job=backend.start(profile['id'],data.get('input'),speaker=data.get('voice','Ryan'),language=data.get('language','Auto'),speed=data.get('speed'),wait=True)
except ValueError as exc:raise APIError(str(exc)) from None
end=time.monotonic()+620
while time.monotonic()<end:
current=ep.tts.status()['job']
current=backend.status()['job']
if current and current['id']==job['id'] and current['state'] in ('failed','cancelled'):raise APIError(current['phase'],503)
try:body=ep.tts.audio(job['id'])
try:body=backend.audio(job['id'])
except (ValueError,OSError):time.sleep(.25);continue
self.sent=True;self.send_response(200);self.send_header('Content-Type','audio/wav');self.send_header('Content-Length',str(len(body)));self.send_header('Cache-Control','no-store');self.send_header('Connection','close');self.end_headers();self.wfile.write(body);return
raise APIError('TTS-Zeitlimit überschritten.',504)
def embedding(self,ep,data):
if not ep.embeddings:raise APIError('Embedding-Worker nicht eingerichtet.',501)
if set(data)-{'model','input','encoding_format','user'}:raise APIError('Nicht unterstützte Embedding-Felder.')
if data.get('encoding_format','float')!='float':raise APIError('Nur encoding_format=float unterstützt.')
value=data.get('input');texts=[value] if isinstance(value,str) else value
if not isinstance(texts,list) or not 1<=len(texts)<=32 or any(not isinstance(t,str) or not t.strip() or len(t)>16000 for t in texts):raise APIError('input: 1–32 nichtleere Texte, jeweils maximal 16000 Zeichen. Kontextgrenze wird zusätzlich von llama.cpp geprüft.')
profile=ep.find_profile(data.get('model'),'embeddings')
try:return self.send(ep.embeddings.generate(profile,{k:v for k,v in data.items() if k!='user'}))
except ValueError as exc:raise APIError(str(exc)) from None
def music_generation(self,ep,data):
if ep.scheduler.gpu_mode!='music':raise APIError('Bitte in der Übersicht auf Musik umschalten.',503,'music_mode_required')
if not ep.music:raise APIError('Musikworker nicht eingerichtet.',501)