Spaces:
Running
Running
feat: redesign custom GGUF loader with native HF search
Browse files- Add gradio-huggingfacehub-search dependency for native HF search
- Replace manual textbox+dropdown with HuggingfaceHubSearch component
- Remove 150+ lines of custom search logic (get_popular_gguf_models, search_gguf_models)
- Simplify UI: 1 search component + 1 file dropdown + load button
- Auto-discover GGUF files when model selected (no manual button needed)
- Update event handlers for new auto-discovery flow
- Better UX matching gguf-my-repo space design
- app.py +31 -218
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -18,6 +18,7 @@ from llama_cpp import Llama
|
|
| 18 |
from opencc import OpenCC
|
| 19 |
import logging
|
| 20 |
from huggingface_hub import list_repo_files, hf_hub_download
|
|
|
|
| 21 |
|
| 22 |
# Configure logging
|
| 23 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -28,136 +29,6 @@ llm = None
|
|
| 28 |
converter = None
|
| 29 |
current_model_key = None
|
| 30 |
|
| 31 |
-
# Global cache for popular GGUF models (populated on first use)
|
| 32 |
-
_popular_gguf_cache: List[Dict[str, Any]] = []
|
| 33 |
-
_popular_gguf_cache_time: float = 0
|
| 34 |
-
_POPULAR_CACHE_TTL = 3600 # 1 hour cache
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def get_popular_gguf_models(limit: int = 20) -> List[Dict[str, Any]]:
|
| 38 |
-
"""Dynamically fetch popular GGUF models from HuggingFace Hub.
|
| 39 |
-
|
| 40 |
-
Uses HF Hub API to search for models with 'gguf' tag, sorted by downloads.
|
| 41 |
-
Cached for 1 hour to avoid repeated API calls.
|
| 42 |
-
|
| 43 |
-
Args:
|
| 44 |
-
limit: Maximum number of models to return
|
| 45 |
-
|
| 46 |
-
Returns:
|
| 47 |
-
List of model dicts with repo_id, downloads, tags
|
| 48 |
-
"""
|
| 49 |
-
global _popular_gguf_cache, _popular_gguf_cache_time
|
| 50 |
-
|
| 51 |
-
# Check cache
|
| 52 |
-
current_time = time.time()
|
| 53 |
-
if _popular_gguf_cache and (current_time - _popular_gguf_cache_time) < _POPULAR_CACHE_TTL:
|
| 54 |
-
return _popular_gguf_cache[:limit]
|
| 55 |
-
|
| 56 |
-
try:
|
| 57 |
-
from huggingface_hub import list_models
|
| 58 |
-
|
| 59 |
-
# Search for models with 'gguf' tag, sorted by downloads (most popular first)
|
| 60 |
-
models = list_models(
|
| 61 |
-
filter="gguf",
|
| 62 |
-
sort="downloads",
|
| 63 |
-
direction=-1, # Descending
|
| 64 |
-
limit=limit * 2, # Fetch more to filter
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
# Process and cache results
|
| 68 |
-
_popular_gguf_cache = []
|
| 69 |
-
for model in models:
|
| 70 |
-
# Skip if no GGUF files (just tagged)
|
| 71 |
-
if not model.tags or "gguf" not in model.tags:
|
| 72 |
-
continue
|
| 73 |
-
|
| 74 |
-
# Extract parameter count from tags if available
|
| 75 |
-
params = "Unknown"
|
| 76 |
-
for tag in model.tags:
|
| 77 |
-
if "b" in tag.lower() and any(c.isdigit() for c in tag):
|
| 78 |
-
params = tag
|
| 79 |
-
break
|
| 80 |
-
|
| 81 |
-
_popular_gguf_cache.append({
|
| 82 |
-
"repo_id": model.id,
|
| 83 |
-
"downloads": model.downloads,
|
| 84 |
-
"tags": [t for t in model.tags if t != "gguf"][:5], # Top 5 non-gguf tags
|
| 85 |
-
"params": params,
|
| 86 |
-
})
|
| 87 |
-
|
| 88 |
-
if len(_popular_gguf_cache) >= limit:
|
| 89 |
-
break
|
| 90 |
-
|
| 91 |
-
_popular_gguf_cache_time = current_time
|
| 92 |
-
logger.info(f"Cached {len(_popular_gguf_cache)} popular GGUF models from HF Hub")
|
| 93 |
-
return _popular_gguf_cache
|
| 94 |
-
|
| 95 |
-
except Exception as e:
|
| 96 |
-
logger.error(f"Failed to fetch popular GGUF models: {e}")
|
| 97 |
-
# Return empty list on error
|
| 98 |
-
return []
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def search_gguf_models(query: str, limit: int = 10) -> List[Dict[str, Any]]:
|
| 102 |
-
"""Search for GGUF models by query string.
|
| 103 |
-
|
| 104 |
-
Searches popular cached models first, then falls back to HF Hub API.
|
| 105 |
-
|
| 106 |
-
Args:
|
| 107 |
-
query: Search query (partial repo_id or keywords)
|
| 108 |
-
limit: Maximum results
|
| 109 |
-
|
| 110 |
-
Returns:
|
| 111 |
-
List of matching model dicts
|
| 112 |
-
"""
|
| 113 |
-
if not query or len(query) < 2:
|
| 114 |
-
return []
|
| 115 |
-
|
| 116 |
-
query_lower = query.lower()
|
| 117 |
-
|
| 118 |
-
# First, search in popular models cache
|
| 119 |
-
popular = get_popular_gguf_models(limit=50)
|
| 120 |
-
matches = [m for m in popular if query_lower in m["repo_id"].lower()]
|
| 121 |
-
|
| 122 |
-
# If we have enough matches from cache, return them
|
| 123 |
-
if len(matches) >= limit:
|
| 124 |
-
return matches[:limit]
|
| 125 |
-
|
| 126 |
-
# Otherwise, try HF Hub API search
|
| 127 |
-
try:
|
| 128 |
-
from huggingface_hub import list_models
|
| 129 |
-
|
| 130 |
-
api_models = list_models(
|
| 131 |
-
search=query,
|
| 132 |
-
filter="gguf",
|
| 133 |
-
sort="downloads",
|
| 134 |
-
direction=-1,
|
| 135 |
-
limit=limit,
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
for model in api_models:
|
| 139 |
-
if model.id not in [m["repo_id"] for m in matches]:
|
| 140 |
-
params = "Unknown"
|
| 141 |
-
for tag in model.tags or []:
|
| 142 |
-
if "b" in tag.lower() and any(c.isdigit() for c in tag):
|
| 143 |
-
params = tag
|
| 144 |
-
break
|
| 145 |
-
|
| 146 |
-
matches.append({
|
| 147 |
-
"repo_id": model.id,
|
| 148 |
-
"downloads": model.downloads,
|
| 149 |
-
"tags": [t for t in (model.tags or []) if t != "gguf"][:5],
|
| 150 |
-
"params": params,
|
| 151 |
-
})
|
| 152 |
-
|
| 153 |
-
if len(matches) >= limit:
|
| 154 |
-
break
|
| 155 |
-
|
| 156 |
-
except Exception as e:
|
| 157 |
-
logger.error(f"HF Hub search failed: {e}")
|
| 158 |
-
|
| 159 |
-
return matches[:limit]
|
| 160 |
-
|
| 161 |
|
| 162 |
def parse_quantization(filename: str) -> Optional[str]:
|
| 163 |
"""Extract quantization level from GGUF filename.
|
|
@@ -1669,23 +1540,13 @@ def create_interface():
|
|
| 1669 |
|
| 1670 |
# Custom Model UI (hidden by default, shown when custom_hf selected)
|
| 1671 |
with gr.Group(visible=False) as custom_model_group:
|
| 1672 |
-
gr.HTML('<div class="section-header" style="margin-top: 20px;"><span class="section-icon">π§</span> Custom
|
| 1673 |
-
|
| 1674 |
-
custom_repo_id = gr.Textbox(
|
| 1675 |
-
label="HuggingFace Repo ID",
|
| 1676 |
-
placeholder="e.g., unsloth/DeepSeek-R1-Distill-Qwen-7B-GGUF or type to search...",
|
| 1677 |
-
info="Type model name to search GGUF models on HF, or paste full repo ID",
|
| 1678 |
-
interactive=True,
|
| 1679 |
-
)
|
| 1680 |
|
| 1681 |
-
#
|
| 1682 |
-
|
| 1683 |
-
label="π Search
|
| 1684 |
-
|
| 1685 |
-
|
| 1686 |
-
info="Matching GGUF models from HuggingFace Hub will appear here as you type",
|
| 1687 |
-
interactive=True,
|
| 1688 |
-
visible=True,
|
| 1689 |
)
|
| 1690 |
|
| 1691 |
# Hidden fields to store discovered file data
|
|
@@ -1693,17 +1554,16 @@ def create_interface():
|
|
| 1693 |
|
| 1694 |
# File dropdown (populated after repo discovery)
|
| 1695 |
custom_file_dropdown = gr.Dropdown(
|
| 1696 |
-
label="π¦
|
| 1697 |
choices=[],
|
| 1698 |
value=None,
|
| 1699 |
-
info="GGUF files
|
| 1700 |
interactive=True,
|
| 1701 |
visible=True,
|
| 1702 |
)
|
| 1703 |
|
| 1704 |
# Action buttons
|
| 1705 |
with gr.Row():
|
| 1706 |
-
discover_btn = gr.Button("π Discover Files", variant="secondary", size="sm")
|
| 1707 |
load_btn = gr.Button("β¬οΈ Load Selected Model", variant="primary", size="sm")
|
| 1708 |
|
| 1709 |
# Status message
|
|
@@ -2010,103 +1870,56 @@ def create_interface():
|
|
| 2010 |
)
|
| 2011 |
|
| 2012 |
# ==========================================
|
| 2013 |
-
#
|
| 2014 |
# ==========================================
|
| 2015 |
|
| 2016 |
-
def
|
| 2017 |
-
"""
|
| 2018 |
-
if not query or len(query) < 2:
|
| 2019 |
-
# Clear search results if query too short
|
| 2020 |
-
return gr.update(choices=[], value=None)
|
| 2021 |
-
|
| 2022 |
-
# Search popular GGUF models from cache
|
| 2023 |
-
matches = search_gguf_models(query, limit=10)
|
| 2024 |
|
| 2025 |
-
|
| 2026 |
-
|
| 2027 |
-
|
| 2028 |
-
|
| 2029 |
-
# Format choices with metadata
|
| 2030 |
-
choices = []
|
| 2031 |
-
for m in matches:
|
| 2032 |
-
repo_id = m["repo_id"]
|
| 2033 |
-
params = m.get("params", "Unknown")
|
| 2034 |
-
downloads = m.get("downloads", 0)
|
| 2035 |
-
# Format downloads
|
| 2036 |
-
if downloads >= 1000000:
|
| 2037 |
-
dl_str = f"{downloads/1000000:.1f}M"
|
| 2038 |
-
elif downloads >= 1000:
|
| 2039 |
-
dl_str = f"{downloads/1000:.1f}K"
|
| 2040 |
-
else:
|
| 2041 |
-
dl_str = str(downloads)
|
| 2042 |
-
|
| 2043 |
-
display = f"{repo_id} | {params} params | β¬οΈ {dl_str}"
|
| 2044 |
-
choices.append((display, repo_id))
|
| 2045 |
-
|
| 2046 |
-
return gr.update(choices=choices, value=None)
|
| 2047 |
-
|
| 2048 |
-
# Auto-search as user types (with small delay via change event)
|
| 2049 |
-
custom_repo_id.change(
|
| 2050 |
-
fn=search_models_dynamic,
|
| 2051 |
-
inputs=[custom_repo_id],
|
| 2052 |
-
outputs=[model_search_results],
|
| 2053 |
-
)
|
| 2054 |
-
|
| 2055 |
-
def on_model_selected_from_search(selected_repo):
|
| 2056 |
-
"""Handle when user selects a model from search results."""
|
| 2057 |
-
if not selected_repo or selected_repo == "No matching models found":
|
| 2058 |
return (
|
| 2059 |
-
gr.update(value=""),
|
| 2060 |
gr.update(choices=[], value=None),
|
| 2061 |
-
gr.update(visible=True, value="Please select a model from search results"),
|
| 2062 |
[],
|
|
|
|
| 2063 |
)
|
| 2064 |
|
| 2065 |
-
#
|
| 2066 |
-
# First show searching status
|
| 2067 |
yield (
|
| 2068 |
-
gr.update(value=
|
| 2069 |
-
gr.update(choices=["Searching..."], value=None, interactive=False),
|
| 2070 |
-
gr.update(visible=True, value="π Discovering GGUF files..."),
|
| 2071 |
[],
|
|
|
|
| 2072 |
)
|
| 2073 |
|
| 2074 |
-
|
|
|
|
| 2075 |
|
| 2076 |
if error:
|
| 2077 |
yield (
|
| 2078 |
-
gr.update(value=selected_repo),
|
| 2079 |
gr.update(choices=[], value=None, interactive=True),
|
| 2080 |
-
gr.update(visible=True, value=f"β {error}"),
|
| 2081 |
[],
|
|
|
|
| 2082 |
)
|
| 2083 |
elif not files:
|
| 2084 |
yield (
|
| 2085 |
-
gr.update(value=selected_repo),
|
| 2086 |
gr.update(choices=[], value=None, interactive=True),
|
| 2087 |
-
gr.update(visible=True, value="β No GGUF files found in this repository"),
|
| 2088 |
[],
|
|
|
|
| 2089 |
)
|
| 2090 |
else:
|
|
|
|
| 2091 |
choices = [format_file_choice(f) for f in files]
|
| 2092 |
yield (
|
| 2093 |
-
gr.update(value=selected_repo),
|
| 2094 |
gr.update(choices=choices, value=choices[0] if choices else None, interactive=True),
|
| 2095 |
-
gr.update(visible=True, value=f"β
Found {len(files)} GGUF files! Select precision and click 'Load Selected Model'"),
|
| 2096 |
files,
|
|
|
|
| 2097 |
)
|
| 2098 |
|
| 2099 |
-
# When user selects from search
|
| 2100 |
-
|
| 2101 |
-
fn=
|
| 2102 |
-
inputs=[
|
| 2103 |
-
outputs=[custom_repo_id, custom_file_dropdown, custom_status, custom_repo_files],
|
| 2104 |
-
)
|
| 2105 |
-
|
| 2106 |
-
# Manual discover button (kept as backup)
|
| 2107 |
-
discover_btn.click(
|
| 2108 |
-
fn=discover_custom_files,
|
| 2109 |
-
inputs=[custom_repo_id],
|
| 2110 |
outputs=[custom_file_dropdown, custom_repo_files, custom_status],
|
| 2111 |
)
|
| 2112 |
|
|
@@ -2144,14 +1957,14 @@ def create_interface():
|
|
| 2144 |
|
| 2145 |
load_btn.click(
|
| 2146 |
fn=load_custom_model_selected,
|
| 2147 |
-
inputs=[
|
| 2148 |
outputs=[custom_status, retry_btn, custom_model_state],
|
| 2149 |
)
|
| 2150 |
|
| 2151 |
# Retry button - same as load
|
| 2152 |
retry_btn.click(
|
| 2153 |
fn=load_custom_model_selected,
|
| 2154 |
-
inputs=[
|
| 2155 |
outputs=[custom_status, retry_btn, custom_model_state],
|
| 2156 |
)
|
| 2157 |
|
|
|
|
| 18 |
from opencc import OpenCC
|
| 19 |
import logging
|
| 20 |
from huggingface_hub import list_repo_files, hf_hub_download
|
| 21 |
+
from gradio_huggingfacehub_search import HuggingfaceHubSearch
|
| 22 |
|
| 23 |
# Configure logging
|
| 24 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 29 |
converter = None
|
| 30 |
current_model_key = None
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
def parse_quantization(filename: str) -> Optional[str]:
|
| 34 |
"""Extract quantization level from GGUF filename.
|
|
|
|
| 1540 |
|
| 1541 |
# Custom Model UI (hidden by default, shown when custom_hf selected)
|
| 1542 |
with gr.Group(visible=False) as custom_model_group:
|
| 1543 |
+
gr.HTML('<div class="section-header" style="margin-top: 20px;"><span class="section-icon">π§</span> Load Custom GGUF Model</div>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1544 |
|
| 1545 |
+
# NEW: Native HF Hub Search Component
|
| 1546 |
+
model_search_input = HuggingfaceHubSearch(
|
| 1547 |
+
label="π Search HuggingFace Models",
|
| 1548 |
+
placeholder="Type model name to search (e.g., 'llama', 'qwen', 'phi')",
|
| 1549 |
+
search_type="model",
|
|
|
|
|
|
|
|
|
|
| 1550 |
)
|
| 1551 |
|
| 1552 |
# Hidden fields to store discovered file data
|
|
|
|
| 1554 |
|
| 1555 |
# File dropdown (populated after repo discovery)
|
| 1556 |
custom_file_dropdown = gr.Dropdown(
|
| 1557 |
+
label="π¦ Select GGUF File (Precision)",
|
| 1558 |
choices=[],
|
| 1559 |
value=None,
|
| 1560 |
+
info="Available GGUF files will appear after selecting a model above",
|
| 1561 |
interactive=True,
|
| 1562 |
visible=True,
|
| 1563 |
)
|
| 1564 |
|
| 1565 |
# Action buttons
|
| 1566 |
with gr.Row():
|
|
|
|
| 1567 |
load_btn = gr.Button("β¬οΈ Load Selected Model", variant="primary", size="sm")
|
| 1568 |
|
| 1569 |
# Status message
|
|
|
|
| 1870 |
)
|
| 1871 |
|
| 1872 |
# ==========================================
|
| 1873 |
+
# NEW: Auto-Discovery Flow with HuggingfaceHubSearch
|
| 1874 |
# ==========================================
|
| 1875 |
|
| 1876 |
+
def on_model_selected(repo_id):
|
| 1877 |
+
"""Handle model selection from HuggingfaceHubSearch.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1878 |
|
| 1879 |
+
Automatically discovers GGUF files in the selected repo.
|
| 1880 |
+
"""
|
| 1881 |
+
if not repo_id:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1882 |
return (
|
|
|
|
| 1883 |
gr.update(choices=[], value=None),
|
|
|
|
| 1884 |
[],
|
| 1885 |
+
gr.update(visible=False),
|
| 1886 |
)
|
| 1887 |
|
| 1888 |
+
# Show searching status
|
|
|
|
| 1889 |
yield (
|
| 1890 |
+
gr.update(choices=["π Searching for GGUF files..."], value=None, interactive=False),
|
|
|
|
|
|
|
| 1891 |
[],
|
| 1892 |
+
gr.update(visible=True, value=f"Discovering GGUF files in {repo_id}..."),
|
| 1893 |
)
|
| 1894 |
|
| 1895 |
+
# Discover files
|
| 1896 |
+
files, error = list_repo_gguf_files(repo_id)
|
| 1897 |
|
| 1898 |
if error:
|
| 1899 |
yield (
|
|
|
|
| 1900 |
gr.update(choices=[], value=None, interactive=True),
|
|
|
|
| 1901 |
[],
|
| 1902 |
+
gr.update(visible=True, value=f"β {error}"),
|
| 1903 |
)
|
| 1904 |
elif not files:
|
| 1905 |
yield (
|
|
|
|
| 1906 |
gr.update(choices=[], value=None, interactive=True),
|
|
|
|
| 1907 |
[],
|
| 1908 |
+
gr.update(visible=True, value=f"β No GGUF files found in {repo_id}"),
|
| 1909 |
)
|
| 1910 |
else:
|
| 1911 |
+
# Format and show files
|
| 1912 |
choices = [format_file_choice(f) for f in files]
|
| 1913 |
yield (
|
|
|
|
| 1914 |
gr.update(choices=choices, value=choices[0] if choices else None, interactive=True),
|
|
|
|
| 1915 |
files,
|
| 1916 |
+
gr.update(visible=True, value=f"β
Found {len(files)} GGUF files! Select precision and click 'Load Model'"),
|
| 1917 |
)
|
| 1918 |
|
| 1919 |
+
# When user selects from search, auto-discover files
|
| 1920 |
+
model_search_input.change(
|
| 1921 |
+
fn=on_model_selected,
|
| 1922 |
+
inputs=[model_search_input],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1923 |
outputs=[custom_file_dropdown, custom_repo_files, custom_status],
|
| 1924 |
)
|
| 1925 |
|
|
|
|
| 1957 |
|
| 1958 |
load_btn.click(
|
| 1959 |
fn=load_custom_model_selected,
|
| 1960 |
+
inputs=[model_search_input, custom_file_dropdown, custom_repo_files],
|
| 1961 |
outputs=[custom_status, retry_btn, custom_model_state],
|
| 1962 |
)
|
| 1963 |
|
| 1964 |
# Retry button - same as load
|
| 1965 |
retry_btn.click(
|
| 1966 |
fn=load_custom_model_selected,
|
| 1967 |
+
inputs=[model_search_input, custom_file_dropdown, custom_repo_files],
|
| 1968 |
outputs=[custom_status, retry_btn, custom_model_state],
|
| 1969 |
)
|
| 1970 |
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
gradio>=5.0.0
|
|
|
|
| 2 |
opencc-python-reimplemented>=0.1.7
|
| 3 |
huggingface-hub>=0.23.0
|
|
|
|
| 1 |
gradio>=5.0.0
|
| 2 |
+
gradio-huggingfacehub-search>=0.1.0
|
| 3 |
opencc-python-reimplemented>=0.1.7
|
| 4 |
huggingface-hub>=0.23.0
|