64 lines
3.5 KiB
Python
64 lines
3.5 KiB
Python
"""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
|