"""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' or self.scheduler.gpu_mode=='restoring':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()