Unter „Entdecken“ kannst du Dateien für diesen Bereich auswählen und herunterladen.
';
}
function renderJob() {
el('hub-capacity').textContent = `Athena · ${size(state.free_bytes)} freier Speicher · 10 GiB bleiben als Reserve frei`;
diff --git a/catalog.py b/catalog.py
index f8f2718..9c25858 100644
--- a/catalog.py
+++ b/catalog.py
@@ -36,6 +36,21 @@ def repo_id(value):
if not isinstance(value,str) or not re.fullmatch(r'[A-Za-z0-9_-][A-Za-z0-9_.-]{0,95}/[A-Za-z0-9_-][A-Za-z0-9_.-]{0,95}',value):raise ValueError('Ungültige Repository-ID.')
return value
+def file_role(filename):
+ """One classification shared by library UI and profile validation."""
+ name=filename.lower();parts=PurePosixPath(name).parts
+ if any(p in ('text_encoder','text_encoders') for p in parts) or re.search(r'(?:^|[/_-])(?:text.encoder|qwen3vl|t5xxl|clip_l|clip_g)(?:[/_.-]|$)',name):
+ role,label='text_encoder','Textencoder'
+ elif 'vae' in parts or re.search(r'(?:^|[/_-])vae(?:[/_.-]|$)',name):
+ role,label='vae','VAE'
+ elif re.search(r'(?:^|[/_-])(?:mmproj|lora|adapter)(?:[/_.-]|$)',name):
+ role,label='auxiliary','Zusatzkomponente'
+ elif name.endswith(('.gguf','.safetensors')):
+ role,label='model','Modelldatei'
+ else:
+ role,label='configuration','Konfiguration'
+ return dict(role=role,role_label=label,profile_eligible=role=='model')
+
class Catalog:
def __init__(self,root):
self.root=Path(root);self.lock=threading.RLock();self.job=None;self.cancel=threading.Event()
@@ -101,7 +116,7 @@ class Catalog:
if self.root.exists():
for p in self.root.glob('*/entry.json'):
try:
- item=json.loads(p.read_text());item['id']=p.parent.name;entries.append(item)
+ item=json.loads(p.read_text());item['id']=p.parent.name;item.update(file_role(item['file']));entries.append(item)
except (OSError,ValueError):pass
return dict(entries=entries,downloads=[dict(x) for x in reversed(self.history) if not x.get("dismissed")],job=dict(self.job) if self.job else None,free_bytes=shutil.disk_usage(self.root if self.root.exists() else self.root.parent).free)
def start(self,repo,filename,revision,kind):
diff --git a/profiles-ui.js b/profiles-ui.js
index 272ddbb..f78ef0c 100644
--- a/profiles-ui.js
+++ b/profiles-ui.js
@@ -6,7 +6,7 @@ window.ProfilesUI=(()=>{
function bind(kind,modelId){
const root=document.querySelector('#live-profiles'),el=id=>root.querySelector('#'+id);let rows=[],models=[],schema={},panelSequence=0;
const message=t=>{if(root.isConnected)el('profile-message').textContent=t;};
- async function load(){try{const [p,c]=await Promise.all([api('profiles'),api('catalog')]);if(!root.isConnected)return;rows=p.profiles.filter(x=>x.kind===kind);models=c.entries.filter(x=>x.kind===kind&&/\.(gguf|safetensors)$/i.test(x.file)&&! /^(text_encoders|vae)\//i.test(x.file));schema=p.schemas[kind];el('profile-new').disabled=!models.length;
+ async function load(){try{const [p,c]=await Promise.all([api('profiles'),api('catalog')]);if(!root.isConnected)return;rows=p.profiles.filter(x=>x.kind===kind);models=c.entries.filter(x=>x.kind===kind&&x.profile_eligible===true);schema=p.schemas[kind];el('profile-new').disabled=!models.length;
el('profile-list').innerHTML=rows.map(p=>`GESPEICHERT AUF ATHENA
${e(p.name)}
${e(p.model?.file||'Modelldatei nicht verfügbar')}
${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}
`;
root.querySelectorAll('[data-components]').forEach(b=>b.onclick=()=>components(rows.find(p=>p.id===b.dataset.components)));
root.querySelectorAll('[data-edit]').forEach(b=>b.onclick=()=>editor(rows.find(p=>p.id===b.dataset.edit)));
diff --git a/profiles.py b/profiles.py
index 922e626..a03dec9 100644
--- a/profiles.py
+++ b/profiles.py
@@ -43,7 +43,7 @@ class Profiles:
if not isinstance(kind,str) or kind not in SCHEMAS or not isinstance(name,str) or not re.fullmatch('[a-zA-Z0-9][a-zA-Z0-9_-]{1,63}',name):raise ValueError('API-Name: 2–64 Buchstaben, Ziffern, Bindestrich oder Unterstrich.')
model=self.catalog.entry(data['model_id'])
if model['repo']==QWEN_REPO and any(model['file'] in r['files'] for r in QWEN_COMPONENTS.values()):raise ValueError('Textencoder und VAE werden über Komponenten zugeordnet, nicht als Hauptmodell.')
- if model['kind']!=kind or not model['file'].lower().endswith(('.gguf','.safetensors')):raise ValueError('Eine Gewichtsdatei dieses Bereichs auswählen, keine Konfiguration.')
+ if model['kind']!=kind or not model['profile_eligible']:raise ValueError('Eine Gewichtsdatei dieses Bereichs auswählen, keine Konfiguration.')
if not isinstance(params,dict) or set(params)!=set(SCHEMAS[kind]):raise ValueError('Unvollständige oder unbekannte Profilparameter.')
for key,(lo,hi,_) in SCHEMAS[kind].items():
value=params[key]
diff --git a/test_model_management.py b/test_model_management.py
index 3214524..5ad74fc 100644
--- a/test_model_management.py
+++ b/test_model_management.py
@@ -73,3 +73,13 @@ class ManagementTests(unittest.TestCase):
with self.assertRaises(ValueError):restarted.assign(dict(id=saved['id'],revision=2,components=ids))
request=self.request();request['model_id']=ids['vae']
with self.assertRaises(ValueError):restarted.save(request)
+ def test_auxiliary_files_cannot_be_profile_models(self):
+ from catalog import file_role
+ for name,role in [('text_encoders/qwen3vl_8b_int8_convrot.safetensors','text_encoder'),('vae/qwen_image_2.1_vae_bf16.safetensors','vae'),('qwen_image_vae.safetensors','vae'),('mmproj-model.gguf','auxiliary')]:
+ with self.subTest(name=name):
+ self.assertEqual(file_role(name)['role'],role)
+ path=self.root/'models'/self.model_id/'entry.json';data=json.loads(path.read_text());data['file']=name;path.write_text(json.dumps(data))
+ suffix=Path(name).suffix;(path.parent/('model'+suffix)).write_bytes(b'GGUFtest')
+ self.assertFalse(self.catalog.status()['entries'][0]['profile_eligible'])
+ with self.assertRaises(ValueError):self.profiles.save(self.request())
+ self.assertTrue(file_role('qwen-image-2.1-UC-Q8_0.gguf')['profile_eligible'])