File size: 6,913 Bytes
5ecb30e
 
 
 
 
 
9d07534
5ecb30e
 
 
 
9d07534
 
 
 
 
 
 
 
 
 
5ecb30e
9d07534
 
 
 
 
 
 
 
 
 
 
5ecb30e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d07534
 
 
 
5ecb30e
9d07534
 
5ecb30e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Hugging Face loading screen, keeping SDK download work off the UI thread."""
from concurrent.futures import ThreadPoolExecutor
from queue import Empty, Queue

import streamlit as st

from hub_models import build_plan, discover_models, download_model, format_size, friendly_hub_error, check_hosted_download
from hub_models import CACHE_ROOT
from app_environment import hosted_mode, hosted_download_limit, new_session_directory, PRIVATE_CACHE_ROOT


MODEL_PRESETS = {
    'SmolLM2 135M · small & quick': ('HuggingFaceTB/SmolLM2-135M', 'About 271 MB · English · safetensors'),
    'GPT-2 · classic English model': ('openai-community/gpt2', 'About 549 MB · English · safetensors'),
    'Gemma 3 1B · Q4 GGUF': (
        'https://huggingface.co/lmstudio-community/gemma-3-1B-it-qat-GGUF/blob/main/gemma-3-1B-it-QAT-Q4_0.gguf',
        'About 720 MB · quantized GGUF'),
    'Qwen3 0.6B · multilingual': ('Qwen/Qwen3-0.6B', 'About 1.52 GB · multilingual · safetensors'),
}


def render_hub_loader(activate_model):
    st.caption('Choose a suggested model or paste a Hugging Face link. Weights download directly to this Space.'
               if hosted_mode() else 'Choose a suggested model or paste a Hugging Face link. Weights download to this computer.')
    source = st.radio('Model source', ['Choose from list', 'Paste URL or repository ID'], horizontal=True, key='hf_source')
    if source == 'Choose from list':
        preset = st.selectbox('Suggested model', list(MODEL_PRESETS), key='hf_preset')
        repo, description = MODEL_PRESETS[preset]
        st.caption(description + ' · downloaded on first use; cached copies are reused')
    else:
        repo = st.text_input('Hugging Face repository or URL', placeholder='owner/model or https://huggingface.co/owner/model', key='hf_repo')
    if hosted_mode():
        st.caption(f'Up to {format_size(hosted_download_limit())} per model, subject to free storage. Larger vocabularies take longer to search.')
    with st.expander('Revision & access token (optional)'):
        revision = st.text_input('Revision', placeholder='main, branch, tag or commit', key='hf_revision')
        token = st.text_input('Hugging Face read token', type='password', key='hf_token',
                              help='For private or gated models. Sent to this app server and Hugging Face for this session; it is not saved as a login.')
    identity = (repo.strip(), revision.strip())
    if st.button('Find model files', key='hf_find_files', type='primary', disabled=not repo.strip(), use_container_width=True):
        st.session_state.pop('hf_catalog', None)
        try:
            with st.spinner('Reading Hugging Face repository…'):
                st.session_state.hf_catalog = discover_models(repo, revision, token)
                st.session_state.hf_catalog_identity = identity
        except Exception as error:
            st.error(friendly_hub_error(error, token))
    catalog = st.session_state.get('hf_catalog')
    if catalog is None or st.session_state.get('hf_catalog_identity') != identity:
        return
    st.link_button('View repository on Hugging Face', f'https://huggingface.co/{catalog.repo_id}')
    if not catalog.choices:
        st.info('No supported weights found. Choose a single-file GGUF, standalone safetensors, or a safetensors index. Split GGUF and PyTorch .bin files are not supported.')
        return
    preferred = catalog.choices.index(catalog.preferred) if catalog.preferred else 0
    selection = st.selectbox('Model weights', catalog.choices, index=preferred,
                             key=f'hf_weights:{catalog.repo_id}:{catalog.commit}:{catalog.preferred}')
    private = hosted_mode() and bool(token)
    if private and 'private_hub_cache' not in st.session_state:
        st.session_state.private_hub_cache = new_session_directory(PRIVATE_CACHE_ROOT)
    cache_root = st.session_state.private_hub_cache if private else CACHE_ROOT
    plan_key = (catalog.repo_id, catalog.commit, selection, str(cache_root))
    plans = st.session_state.setdefault('hf_plans', {})
    try:
        if plan_key not in plans:
            with st.spinner('Checking required model files…'):
                plans[plan_key] = build_plan(catalog, selection, token, cache_root=cache_root)
        plan = plans[plan_key]
    except Exception as error:
        st.error(friendly_hub_error(error, token))
        return
    st.caption(f'{len(plan.weight_files)} weights file(s) · {format_size(plan.size)} total · revision {catalog.commit[:12]}')
    if plan.embeddings_only:
        st.caption('Only shards containing recognised token embeddings will be downloaded.')
    elif selection.endswith('.index.json'):
        st.caption('Embedding names were not recognised in this index; all listed weight shards will be downloaded.')
    with st.expander('Files to download'):
        for filename in plan.files:
            st.text(f'{filename} · {format_size(catalog.files.get(filename))}')
    try:
        check_hosted_download(plan)
    except (ValueError, OSError) as error:
        st.info(str(error))
        return
    load_label = 'Load into Space' if hosted_mode() else 'Download & load'
    if st.button(load_label, key='hf_download', type='primary', use_container_width=True):
        events = Queue()
        progress = st.progress(0, text='Preparing download…')
        try:
            with ThreadPoolExecutor(max_workers=1) as executor:
                future = executor.submit(download_model, plan, token, lambda done, text: events.put((done, text)))
                while not future.done():
                    try:
                        done, text = events.get(timeout=.15)
                        # Drop outdated progress events without touching Streamlit on a worker.
                        while not events.empty():
                            done, text = events.get_nowait()
                        progress.progress(done, text=text)
                    except Empty:
                        pass
                model_path, tokenizer_path = future.result()
            progress.progress(1., text='Download complete. Reading embeddings…')
            activate_model(model_path, tokenizer_path, f'{catalog.repo_id} · {selection}')
            st.session_state.clear_hub_token = True
            st.rerun()
        except Exception as error:
            st.error(friendly_hub_error(error, token))
        finally:
            progress.empty()
    if private:
        st.caption('Token-authenticated downloads use a private session folder, removed when you clear the model. The Space owner operates this server.')
    else:
        st.caption('Downloads are cached and reused. Clearing a model keeps public downloaded files. Space restarts may clear the cache.' if hosted_mode() else 'Downloads are cached in this project’s .model-cache folder and reused. Clearing a model keeps its downloaded files.')