Luigi commited on
Commit
c997232
Β·
1 Parent(s): 68d2790

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

Files changed (2) hide show
  1. app.py +31 -218
  2. 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 HuggingFace Model</div>')
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
- # Search results dropdown (shows matching models from HF)
1682
- model_search_results = gr.Dropdown(
1683
- label="πŸ” Search Results - Select a Model",
1684
- choices=[],
1685
- value=None,
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="πŸ“¦ Available GGUF Files - Select Precision",
1697
  choices=[],
1698
  value=None,
1699
- info="GGUF files from selected repo (alphabetically sorted)",
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
- # DYNAMIC MODEL SEARCH - New Feature
2014
  # ==========================================
2015
 
2016
- def search_models_dynamic(query):
2017
- """Search for GGUF models as user types."""
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
- if not matches:
2026
- # No matches found
2027
- return gr.update(choices=["No matching models found"], value=None)
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
- # Auto-discover files for selected repo
2066
- # First show searching status
2067
  yield (
2068
- gr.update(value=selected_repo),
2069
- gr.update(choices=["Searching..."], value=None, interactive=False),
2070
- gr.update(visible=True, value="πŸ” Discovering GGUF files..."),
2071
  [],
 
2072
  )
2073
 
2074
- files, error = list_repo_gguf_files(selected_repo)
 
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 results, auto-fill repo and discover files
2100
- model_search_results.change(
2101
- fn=on_model_selected_from_search,
2102
- inputs=[model_search_results],
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=[custom_repo_id, custom_file_dropdown, custom_repo_files],
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=[custom_repo_id, custom_file_dropdown, custom_repo_files],
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