From e2b521fcaa2f74ece6025dd38b9797c3fdbb50a8 Mon Sep 17 00:00:00 2001
From: Mikei386 <44135113+Mikei386@users.noreply.github.com>
Date: Mon, 28 Sep 2026 19:37:13 +0200
Subject: [PATCH] Find direct Hub repositories and untagged conversational GGUF
models
---
README.md | 7 +++++++
catalog-ui.js | 4 ++--
catalog.py | 18 +++++++++++++++++-
test_catalog.py | 11 +++++++++++
4 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index c9b4e5c..a861ca7 100644
--- a/README.md
+++ b/README.md
@@ -155,3 +155,10 @@ Die Einstellung bleibt im Browser über Kategorien hinweg erhalten. Fehlende
Herausgeber-Metadaten können dazu führen, dass Ableitungen sichtbar bleiben;
es handelt sich nicht um eine verifizierte Liste offizieller Herausgeber.
API-Parameter: `GET /api/v1/catalog/search?...&base_only=true`.
+
+Die Suche akzeptiert auch `owner/repository` und direkte Hugging-Face-Repository-
+URLs. Eine solche Direktauswahl umgeht Kategorie- und Basismodellfilter mit einem
+sichtbaren Hinweis. Bei GGUF-Dateinamen wird die Endung für die Repositorysuche
+entfernt. Findet die Chat-Suche nichts, werden zusätzlich als `conversational`
+gekennzeichnete GGUF-Repositories ohne `text-generation`-Tag geprüft. Das ist
+keine globale Suche nach beliebigen Dateinamen im Hub.
diff --git a/catalog-ui.js b/catalog-ui.js
index 7e4eeef..2a548c3 100644
--- a/catalog-ui.js
+++ b/catalog-ui.js
@@ -10,7 +10,7 @@ window.CatalogUI = (() => {
function html(library, downloads=false) {
return `
HUGGING FACE · LIVE-KATALOG
${downloads ? 'Downloads' : library ? 'Deine Dateien auf Athena' : 'Das nächste Modell entdecken'}
${downloads ? 'Abgeschlossene Einträge ausblenden. Die Dateien bleiben in deiner Bibliothek.' : library ? 'Heruntergeladene Dateien mit festgehaltener Version und Integritätsprüfung.' : 'Modell suchen, Variante vergleichen und gezielt auf Athena herunterladen.'}
- ${downloads ? '' : library ? '' : `
Blendet anhand der Hub-Metadaten Finetunes, LoRAs, Merges und separate Quantisierungen aus. Unvollständig gekennzeichnete Ableitungen können sichtbar bleiben.
Beliebte Modelle
01 ENTDECKEN → 02 AUSWÄHLEN → 03 LADEN
Welches Modell interessiert dich?
Wähle ein Ergebnis, um Dateien, Varianten und Lizenz zu prüfen.
Downloads belegen Festplattenspeicher. Sie starten kein Modell. Für Bild, Audio und Video sind häufig mehrere Dateien und ein passender Worker erforderlich.
`}`;
+ ${downloads ? '' : library ? '
' : `
Blendet anhand der Hub-Metadaten Finetunes, LoRAs, Merges und separate Quantisierungen aus. Unvollständig gekennzeichnete Ableitungen können sichtbar bleiben.
Beliebte Modelle
01 ENTDECKEN → 02 AUSWÄHLEN → 03 LADEN
Welches Modell interessiert dich?
Wähle ein Ergebnis, um Dateien, Varianten und Lizenz zu prüfen.
Downloads belegen Festplattenspeicher. Sie starten kein Modell. Für Bild, Audio und Video sind häufig mehrere Dateien und ein passender Worker erforderlich.
`}`;
}
function bind(kind, library, downloads=false) {
const root = document.querySelector('#live-catalog'); if (!root) return;
@@ -45,7 +45,7 @@ window.CatalogUI = (() => {
}
async function search(q) {
const sequence = ++searchSequence; message('Modelle werden gesucht …');
- try { const result = await api('/search?'+new URLSearchParams({q,kind,sort:el('hub-sort').value,base_only:String(el('hub-base-only').checked)})); if (!root.isConnected || sequence !== searchSequence) return; models = result.models; renderModels(); enrich(sequence); message(result.base_only ? `Basismodellfilter aktiv · ${result.scanned} Hub-Treffer geprüft, höchstens 20 angezeigt. Bei fehlenden Treffern Suche eingrenzen oder Filter ausschalten.` : kind === 'audio' ? 'Audio-Suche: derzeit Text-to-Speech-Modelle.' : el('hub-sort').value==='newest'?'Neueste Hub-Repositories zuerst (Erstellungsdatum, nicht letztes Update).':'Wähle ein Modell für die Dateiauswahl. Name A–Z sortiert die angezeigten Treffer.'); }
+ try { const result = await api('/search?'+new URLSearchParams({q,kind,sort:el('hub-sort').value,base_only:String(el('hub-base-only').checked)})); if (!root.isConnected || sequence !== searchSequence) return; models = result.models; renderModels(); enrich(sequence); message(result.notice || (result.base_only ? `Basismodellfilter aktiv · ${result.scanned} Hub-Treffer geprüft, höchstens 20 angezeigt. Bei fehlenden Treffern Suche eingrenzen oder Filter ausschalten.` : kind === 'audio' ? 'Audio-Suche: derzeit Text-to-Speech-Modelle.' : el('hub-sort').value==='newest'?'Neueste Hub-Repositories zuerst (Erstellungsdatum, nicht letztes Update).':'Wähle ein Modell für die Dateiauswahl. Name A–Z sortiert die angezeigten Treffer.')); }
catch(error) { if (sequence === searchSequence) message(error.message); }
}
async function enrich(sequence){
diff --git a/catalog.py b/catalog.py
index 78bdcec..dcd6074 100644
--- a/catalog.py
+++ b/catalog.py
@@ -97,12 +97,28 @@ class Catalog:
if kind not in KINDS or not isinstance(q,str) or len(q)>120:raise ValueError('Ungültige Suche.')
if sort not in ('downloads','name','newest'):raise ValueError('Ungültige Sortierung.')
if not isinstance(base_only,bool):raise ValueError('Ungültiger Basismodellfilter.')
+ q=q.strip();direct=None;notice=None
+ if q.startswith('https://huggingface.co/'):
+ url=urllib.parse.urlsplit(q)
+ if url.netloc!='huggingface.co' or url.query or url.fragment:raise ValueError('Bitte einen direkten Hugging-Face-Repository-Link eingeben.')
+ direct=repo_id(url.path.strip('/'))
+ elif '/' in q:direct=repo_id(q)
+ if direct:
+ item=metadata('/api/models/'+direct)
+ return dict(models=[dict(repo=item['id'],downloads=item.get('downloads'),gated=item.get('gated',False),created_at=item.get('createdAt'))],base_only=base_only,scanned=1,notice='Direkt ausgewähltes Repository: Kategorie- und Basismodellfilter werden für diesen Treffer nicht angewendet.')
+ if q.lower().endswith('.gguf'):
+ q=q[:-5];notice='GGUF-Dateiname als Repository-Suchbegriff verwendet. Die Datei wählst du anschließend in den Modelldetails.'
query=dict(search=q,filter=KINDS[kind],limit=100 if base_only else 20,sort='createdAt' if sort=='newest' else 'downloads',direction=-1)
if base_only:query['expand']=['downloads','createdAt','gated','cardData','tags']
rows=metadata('/api/models?'+urllib.parse.urlencode(query,doseq=True))
+ if not rows and q and kind=='chat':
+ fallback=dict(query,filter='gguf',expand=['downloads','createdAt','gated','cardData','tags','pipeline_tag'])
+ candidates=metadata('/api/models?'+urllib.parse.urlencode(fallback,doseq=True))
+ rows=[x for x in candidates if x.get('pipeline_tag')=='text-generation' or 'conversational' in (x.get('tags') or [])]
+ if rows:notice='GGUF-Sprachmodelle ohne reguläres Kategorie-Tag gefunden. Bei aktiver Basismodellauswahl bleiben Quantisierungen ausgeblendet.'
scanned=len(rows)
if base_only:rows=[x for x in rows if not is_derived_model(x)]
- return {'models':[dict(repo=x['id'],downloads=x.get('downloads'),gated=x.get('gated',False),created_at=x.get('createdAt')) for x in rows[:20]],'base_only':base_only,'scanned':scanned}
+ return {'models':[dict(repo=x['id'],downloads=x.get('downloads'),gated=x.get('gated',False),created_at=x.get('createdAt')) for x in rows[:20]],'base_only':base_only,'scanned':scanned,'notice':notice}
def files(self,repo):
repo=repo_id(repo)
with self.cache_lock:
diff --git a/test_catalog.py b/test_catalog.py
index bf5a199..146c506 100644
--- a/test_catalog.py
+++ b/test_catalog.py
@@ -30,6 +30,17 @@ class CatalogTests(unittest.TestCase):
self.assertEqual([x['repo'] for x in value['models']],['maker/base']);self.assertEqual(value['scanned'],21)
self.assertIn('limit=100',fetch.call_args.args[0]);self.assertIn('expand=cardData',fetch.call_args.args[0])
with self.assertRaises(ValueError):Catalog(d).search('','video','downloads','true')
+ def test_direct_repository_bypasses_discovery_filters_explicitly(self):
+ item={'id':'jpetrina/example','tags':['gguf']}
+ with tempfile.TemporaryDirectory() as d,patch('catalog.metadata',return_value=item):
+ for q in ('jpetrina/example','https://huggingface.co/jpetrina/example'):
+ result=Catalog(d).search(q,'chat','downloads',True)
+ self.assertEqual(result['models'][0]['repo'],item['id']);self.assertIn('nicht angewendet',result['notice'])
+ def test_gguf_filename_fallback_finds_conversational_without_pipeline_tag(self):
+ item={'id':'maker/model-GGUF','tags':['gguf','conversational']}
+ with tempfile.TemporaryDirectory() as d,patch('catalog.metadata',side_effect=[[],[item]]) as fetch:
+ result=Catalog(d).search('model.gguf','chat')
+ self.assertEqual(result['models'][0]['repo'],item['id']);self.assertIn('search=model&',fetch.call_args_list[0].args[0]);self.assertIn('filter=gguf',fetch.call_args.args[0])
def test_download_integrity_and_library(self):
for valid in (True,False):
with self.subTest(valid=valid),tempfile.TemporaryDirectory() as directory: