Add configurable TTS and STT residency policies

This commit is contained in:
Mikei386
2026-09-29 20:47:47 +02:00
parent 90a97567f3
commit b2256647e9
9 changed files with 209 additions and 40 deletions
+63
View File
@@ -0,0 +1,63 @@
"""Persisted residency preferences for Deck-owned speech workers."""
import json
import threading
import time
from pathlib import Path
MODES=('auto','warm','per_request')
class AudioPolicy:
def __init__(self,path,profiles,tts,stt,scheduler,endpoint):
self.path=Path(path);self.profiles=profiles;self.tts=tts;self.stt=stt;self.scheduler=scheduler;self.endpoint=endpoint
self.lock=threading.RLock();self.settings={kind:{'mode':'auto','profile_id':None} for kind in ('tts','stt')};self.errors={};self.retry_after={}
if self.path.exists():
try:
stored=json.loads(self.path.read_text())
for kind in self.settings:
value=stored.get(kind,{})
if value.get('mode') in MODES and (value.get('profile_id') is None or isinstance(value.get('profile_id'),str)):
self.settings[kind]=dict(mode=value['mode'],profile_id=value.get('profile_id'))
except (OSError,ValueError,AttributeError,TypeError):pass
self.tts.policy=self.settings['tts']['mode'];self.stt.policy=self.settings['stt']['mode']
self.thread=threading.Thread(target=self._loop,daemon=True);self.thread.start()
def status(self):
with self.lock:return dict(settings={k:dict(v) for k,v in self.settings.items()},errors=dict(self.errors))
def configure(self,data):
if not isinstance(data,dict) or set(data)!={'kind','mode','profile_id'}:raise ValueError('Art, Modus und Profil-ID erforderlich.')
kind=data['kind'];mode=data['mode'];ident=data['profile_id']
if kind not in self.settings or mode not in MODES:raise ValueError('Ungültiger Audio-Modus.')
if ident is not None and (not isinstance(ident,str) or len(ident)>128):raise ValueError('Ungültige Profil-ID.')
if mode=='warm':
profile_kind='audio' if kind=='tts' else 'stt'
if not ident or not any(p['id']==ident and p['kind']==profile_kind and p['runnable'] for p in self.profiles.status()['profiles']):raise ValueError('Für „Immer bereit“ ein ausführbares Profil auswählen.')
with self.lock:
updated={k:dict(v) for k,v in self.settings.items()};updated[kind]=dict(mode=mode,profile_id=ident)
self.path.parent.mkdir(parents=True,exist_ok=True);temp=self.path.with_suffix('.tmp');temp.write_text(json.dumps(updated));temp.replace(self.path)
self.settings=updated;self.errors.pop(kind,None);self.retry_after.pop(kind,None)
worker=self.tts if kind=='tts' else self.stt;worker.policy=mode
if mode=='per_request':
with worker.lock:active=bool(worker.job and worker.job.get('state')=='running')
if not active:worker.unload_idle()
return self.status()
def _loop(self):
while True:
time.sleep(8)
with self.lock:settings={k:dict(v) for k,v in self.settings.items()}
if self.endpoint.state!='running':continue
for kind,worker in (('stt',self.stt),('tts',self.tts)):
item=settings[kind]
if item['mode']!='warm':continue
with self.lock:
if time.monotonic()<self.retry_after.get(kind,0):continue
if kind=='tts':
with self.scheduler.cv:
if self.scheduler.gpu_mode!='llm' or self.scheduler.active or self.scheduler.transition:continue
with worker.lock:
if worker.warming or worker.process and worker.process.poll() is None or worker.job and worker.job.get('state')=='running':continue
try:
worker.warm(item['profile_id'])
with self.lock:self.errors.pop(kind,None)
except Exception as exc:
with self.lock:
self.errors[kind]=str(exc) if isinstance(exc,ValueError) else 'Vorladen fehlgeschlagen. Ressourcen und Laufzeit prüfen.'
self.retry_after[kind]=time.monotonic()+60