Separate stable API profile IDs from display names with legacy aliases
This commit is contained in:
1 parent
0cffcf4295
commit
a908313c8f
7 files changed
+88
-11
No files matched your search
+27
-2
@@ -1,3 +1,4 @@
|
||||
import unicodedata
|
||||
"""Server-persisted library-backed profiles with validated, typed parameters."""
|
||||
import json
|
||||
import re
|
||||
@@ -137,6 +138,20 @@ def audio_package_recipe(model,source):
|
||||
return {f'runtime_file_{i}':dict(label='Laufzeit-Paket: '+f,files=[f]) for i,f in enumerate(extras)} or None
|
||||
return None
|
||||
|
||||
def api_slug(name):
|
||||
text=unicodedata.normalize('NFKD',name.replace('ß','ss')).encode('ascii','ignore').decode().lower()
|
||||
return re.sub(r'[^a-z0-9]+','-',text).strip('-')[:64].rstrip('-') or 'model'
|
||||
|
||||
def migrate_api_ids(rows):
|
||||
used={r['api_id'] for r in rows if r.get('api_id')}
|
||||
for row in rows:
|
||||
if not row.get('api_id'):
|
||||
base=api_slug(row['name']);candidate=base;i=2
|
||||
while candidate in used or any(candidate in [r['name'],*r.get('api_aliases',[])] for r in rows if r is not row):
|
||||
suffix='-'+str(i);candidate=base[:64-len(suffix)]+suffix;i+=1
|
||||
row['api_id']=candidate;used.add(candidate)
|
||||
row.setdefault('api_aliases',[row['name']])
|
||||
|
||||
class Profiles:
|
||||
def _recipe(self,model):return component_recipe(model) or getattr(self,'runtime_recipe',lambda m:None)(model)
|
||||
def __init__(self,path,catalog):
|
||||
@@ -145,6 +160,10 @@ class Profiles:
|
||||
self.prompt_enhancer_ready=lambda task:False
|
||||
self.video_blockers=None;self.chat_blockers=None;self.tts_blockers=None;self.stt_blockers=None
|
||||
self.rows=json.loads(self.path.read_text()) if self.path.exists() else []
|
||||
original=json.dumps(self.rows)
|
||||
migrate_api_ids(self.rows)
|
||||
if self.path.exists() and json.dumps(self.rows)!=original:
|
||||
temp=self.path.with_suffix('.identity.tmp');temp.write_text(json.dumps(self.rows));temp.replace(self.path)
|
||||
# First baselines may be captured while a pre-Preset server still runs.
|
||||
# Merge on startup so its old in-memory saves cannot discard that baseline.
|
||||
pending=self.path.with_name('profile-presets-pending.json')
|
||||
@@ -203,7 +222,7 @@ class Profiles:
|
||||
p['runnable']=not p['blockers'];p['state']='ready' if p['runnable'] else 'configured'
|
||||
return {'profiles':[p for p in rows if p['kind']!='video'],'schemas':{k:v for k,v in SCHEMAS.items() if k!='video'}}
|
||||
def save(self,data):
|
||||
if set(data)!={'id','revision','name','kind','model_id','parameters'}:raise ValueError('Ungültige Profilfelder.')
|
||||
if set(data)-{'api_id'}!={'id','revision','name','kind','model_id','parameters'}:raise ValueError('Ungültige Profilfelder.')
|
||||
kind=data['kind'];name=data['name'];params=data['parameters']
|
||||
if kind=='video':raise ValueError('Videoprofile werden nicht mehr in Deck verwaltet. Originale LTX-Oberfläche verwenden.')
|
||||
if not isinstance(kind,str) or kind not in SCHEMAS or not isinstance(name,str) or not (2<=len(name)<=64 and name[0].isalnum() and name[-1].isalnum() and re.fullmatch(r'[\w .-]+',name)):raise ValueError('API-Name: 2–64 Zeichen; Buchstaben, Ziffern, Leerzeichen, Punkt, Bindestrich oder Unterstrich. Anfang und Ende müssen Buchstabe oder Ziffer sein.')
|
||||
@@ -231,8 +250,14 @@ class Profiles:
|
||||
if data['id'] is not None and not existing:raise ValueError('Profil nicht gefunden.')
|
||||
if data['revision']!=(existing['revision'] if existing else 0):raise ValueError('Profil wurde zwischenzeitlich geändert. Ansicht neu laden.')
|
||||
if any(p['name']==name and p is not existing for p in self.rows):raise ValueError('API-Profilname bereits vergeben.')
|
||||
api_id=data.get('api_id') or (existing.get('api_id') if existing else api_slug(name))
|
||||
if not isinstance(api_id,str) or not re.fullmatch(r'[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}',api_id):raise ValueError('API-ID: 1–64 Buchstaben, Ziffern, Bindestrich oder Unterstrich, ohne Leerzeichen.')
|
||||
aliases=list(existing.get('api_aliases',[]) if existing else [])
|
||||
if existing:
|
||||
aliases=list(dict.fromkeys(aliases+[existing['name'],existing['api_id']]))
|
||||
if any(set([api_id,name,*aliases]) & set([p.get('api_id',p['name']),p['name'],*p.get('api_aliases',[])]) for p in self.rows if p is not existing):raise ValueError('API-ID oder Alias bereits vergeben.')
|
||||
components=existing.get('components',{}) if existing and existing['model_id']==model['id'] else {}
|
||||
row=dict(presets=existing.get('presets',[]) if existing else [],components=components,prompt_enhancer=existing.get('prompt_enhancer',dict(PROMPT_ENHANCER_DEFAULTS)) if existing and existing['model_id']==model['id'] and kind=='image' else dict(PROMPT_ENHANCER_DEFAULTS) if kind=='image' else None,id=existing['id'] if existing else uuid.uuid4().hex,revision=data['revision']+1,name=name,kind=kind,model_id=model['id'],parameters=params,updated_at=time.time())
|
||||
row=dict(api_id=api_id,api_aliases=aliases,presets=existing.get('presets',[]) if existing else [],components=components,prompt_enhancer=existing.get('prompt_enhancer',dict(PROMPT_ENHANCER_DEFAULTS)) if existing and existing['model_id']==model['id'] and kind=='image' else dict(PROMPT_ENHANCER_DEFAULTS) if kind=='image' else None,id=existing['id'] if existing else uuid.uuid4().hex,revision=data['revision']+1,name=name,kind=kind,model_id=model['id'],parameters=params,updated_at=time.time())
|
||||
rows=[row if p is existing else p for p in self.rows] if existing else self.rows+[row]
|
||||
self.path.parent.mkdir(parents=True,exist_ok=True,mode=0o700)
|
||||
temp=self.path.with_suffix('.tmp');temp.write_text(json.dumps(rows));temp.replace(self.path);self.rows=rows
|
||||
|
||||
Reference in new issue
Block a user