lainlives commited on
Commit
85d4692
·
verified ·
1 Parent(s): 1553273

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. Dockerfile +102 -0
  2. README.md +7 -9
  3. app.py +249 -0
  4. theme.py +96 -0
Dockerfile ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:12.8.0-cudnn-devel-ubuntu24.04
2
+ ENV DEBIAN_FRONTEND=noninteractive
3
+
4
+ ARG HF_TOKEN
5
+
6
+ ENV HF_TOKEN=$HF_TOKEN
7
+ RUN rm -rf /usr/local/bin /usr/local/lib* || true
8
+ RUN ln -s /usr/bin /usr/local/bin && ln -s /usr/lib /usr/local/lib && ln -s /usr/lib /usr/local/lib64
9
+ RUN apt-get update && apt-get install -y
10
+ RUN apt-get update && \
11
+ apt-get upgrade -y
12
+ RUN apt-get install -y --no-install-recommends --fix-missing \
13
+ git \
14
+ git-lfs \
15
+ wget \
16
+ curl \
17
+ cmake \
18
+ build-essential \
19
+ libssl-dev \
20
+ zlib1g-dev \
21
+ libbz2-dev \
22
+ libreadline-dev \
23
+ libsqlite3-dev \
24
+ libncursesw5-dev \
25
+ xz-utils \
26
+ tk-dev \
27
+ libxml2-dev \
28
+ libxmlsec1-dev \
29
+ libffi-dev \
30
+ golang-go \
31
+ python3 \
32
+ liblzma-dev \
33
+ ffmpeg \
34
+ nvidia-driver-570 \
35
+ python3 \
36
+ python3-pip unzip curl original-awk grep sed zstd
37
+
38
+ WORKDIR /app
39
+ COPY --chown=1000 . /app
40
+ RUN mkdir /app -p && chmod 777 /app
41
+
42
+ # RUN bash instollama.sh # Currently all model types are supported no need to build
43
+
44
+ RUN curl -fsSL https://ollama.com/install.sh | sh
45
+
46
+ # RUN cd /app && \
47
+ # git clone --recursive https://github.com/ollama/ollama.git && \
48
+ # cd ollama && \
49
+ # go generate ./... && \
50
+ # go build . && \
51
+ # ln -s $PWD/ollama /usr/bin/ollama && \
52
+ # chmod +x ollama && \
53
+ # cd ..
54
+
55
+ # RUN cd /app && \
56
+ # git clone --recursive https://github.com/ggerganov/llama.cpp && \
57
+ # cd llama.cpp && \
58
+ # cmake -B build -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=OFF -DLLAMA_CURL=OFF && \
59
+ # cmake --build build --config Release -j --target llama-quantize --parallel 12 && \
60
+ # cp ./build/bin/llama-* /usr/bin/ && \
61
+ # cp convert_hf_to_gguf.py /usr/bin/convert_hf_to_gguf && \
62
+ # rm -rf build && \
63
+ # cd ..
64
+ # RUN id -u 1000 &>/dev/null || useradd -m -u 1000 user
65
+ # USER 1000
66
+ # ENV HOME=/home/user \
67
+ # PATH=/home/user/.local/bin:${PATH}
68
+ WORKDIR /app
69
+ # RUN curl https://pyenv.run | bash
70
+ # ENV PATH=${HOME}/.pyenv/shims:${HOME}/.pyenv/bin:${PATH}
71
+ # ARG PYTHON_VERSION=3.13
72
+ # RUN pyenv install ${PYTHON_VERSION} && \
73
+ # pyenv global ${PYTHON_VERSION} && \
74
+ # pyenv rehash
75
+ RUN pip install --no-cache-dir -U pip setuptools wheel --break-system-packages --ignore-installed
76
+ RUN pip install "huggingface-hub" "hf-transfer" "gradio[oauth]>=6.5.1" "APScheduler" "protobuf>=4.21.0,<5.0.0" "sentencepiece>=0.1.98,<0.3.0" "numpy~=1.26.4" "gguf>=0.1.0" "fastapi" --break-system-packages --ignore-installed
77
+
78
+ RUN pip install "torch>=2.8.0" --break-system-packages --ignore-installed
79
+ RUN pip install git+https://github.com/huggingface/transformers.git --break-system-packages --ignore-installed
80
+
81
+
82
+
83
+ RUN mkdir /tmp/llama && hf download lainlives/llama.cpp --local-dir /tmp/llama && chmod +x /tmp/llama/* && cp /tmp/llama/convert* /app/convert_hf_to_gguf.py && mv /tmp/llama/* /usr/bin/
84
+
85
+ ENV PYTHONPATH=${HOME}/app \
86
+ PYTHONUNBUFFERED=1 \
87
+ HF_HUB_ENABLE_HF_TRANSFER=1 \
88
+ GRADIO_ALLOW_FLAGGING=never \
89
+ GRADIO_NUM_PORTS=1 \
90
+ GRADIO_SERVER_NAME=0.0.0.0 \
91
+ GRADIO_ANALYTICS_ENABLED=False \
92
+ TQDM_POSITION=-1 \
93
+ TQDM_MININTERVAL=1 \
94
+ SYSTEM=spaces \
95
+ LD_LIBRARY_PATH=/usr/local/cuda/lib64:${LD_LIBRARY_PATH} \
96
+ PATH=/usr/local/nvidia/bin:${PATH}
97
+
98
+
99
+
100
+
101
+ EXPOSE 7860
102
+ ENTRYPOINT python3 /app/app.py
README.md CHANGED
@@ -1,12 +1,10 @@
1
  ---
2
- title: Safetensors2ollama Repo
3
- emoji: 🌍
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 6.9.0
8
- app_file: app.py
9
  pinned: false
 
 
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: HF to Ollama-currently indev
3
+ emoji: 📈
4
+ colorFrom: gray
5
+ colorTo: pink
6
+ sdk: docker
 
 
7
  pinned: false
8
+ suggested_hardware: "a10g-large"
9
+ disable_embedding: true
10
  ---
 
 
app.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ from sys import argv
4
+ from time import strftime, sleep
5
+ import shutil
6
+ from pathlib import Path
7
+ import gradio as gr
8
+ import signal
9
+ from huggingface_hub import snapshot_download, HfApi
10
+ from apscheduler.schedulers.background import BackgroundScheduler
11
+ from theme import blurple
12
+
13
+ # Used for restarting the space
14
+ HF_TOKEN = os.environ.get("HF_TOKEN")
15
+ TEST_OKEY = os.environ.get("TEST_OKEY")
16
+ TEST_TOKEN = os.environ.get("HF_TOKEN")
17
+ HOST_REPO = "lainlives/ztestzz"
18
+ LLAMACPP_DIR = Path("./llama.cpp")
19
+ CONVERT_SCRIPT = "/app/convert_hf_to_gguf.py"
20
+ QUANTIZE_BIN = "llama-quantize"
21
+ TARGET_QUANTS = ["Q8_0", "Q6_K", "Q5_K_M", "Q5_K_S", "Q5_0", "Q4_K_M", "Q4_K_S", "Q4_0"]
22
+
23
+
24
+ def format_log(msg):
25
+ return f"[{strftime('%H:%M:%S')}] {msg}"
26
+
27
+
28
+ def setup_ollama_keys(private_key_content):
29
+ """
30
+ Writes the user's private key to ~/.ollama/id_ed25519
31
+ """
32
+ if not private_key_content:
33
+ return False, "⚠️ No Private Key provided. Pushing will fail."
34
+
35
+ ollama_dir = Path(os.path.expanduser("~/.ollama"))
36
+ ollama_dir.mkdir(parents=True, exist_ok=True)
37
+
38
+ key_path = ollama_dir / "id_ed25519"
39
+ os.remove(key_path)
40
+
41
+ try:
42
+ # Write the key
43
+ with open(key_path, "w") as f:
44
+ f.write(private_key_content.strip())
45
+ os.chmod(key_path, 0o600)
46
+
47
+ return True, "🔑 Private Key installed successfully."
48
+ except Exception as e:
49
+ return False, f"❌ Failed to install keys: {e}"
50
+
51
+
52
+ def push_to_ollama(gguf_path, ollama_repo, tag_suffix):
53
+ ollama_tag = f"{ollama_repo}:{tag_suffix.lower()}"
54
+
55
+ # 1. Write the Modelfile to disk
56
+ # The CLI needs a physical file to point to with the '-f' flag
57
+ modelfile_path = gguf_path.parent / "Modelfile"
58
+ with open(modelfile_path, "w") as f:
59
+ f.write(f"FROM {gguf_path.resolve()}")
60
+
61
+ logs = []
62
+ logs.append(format_log(f"🐳 Creating Ollama build: {ollama_tag}"))
63
+
64
+ try:
65
+ create_cmd = ["ollama", "create", ollama_tag, "-f", str(modelfile_path)]
66
+ subprocess.run(create_cmd, check=True, capture_output=True)
67
+
68
+ if modelfile_path.exists():
69
+ os.remove(modelfile_path)
70
+
71
+ logs.append(format_log(f"⬆️ Pushing to registry: {ollama_tag}..."))
72
+
73
+ push_cmd = ["ollama", "push", ollama_tag]
74
+ push_result = subprocess.run(push_cmd, capture_output=True, text=True)
75
+
76
+ if push_result.returncode == 0:
77
+ logs.append(format_log(f"✅ Successfully pushed {ollama_tag}"))
78
+ else:
79
+ logs.append(format_log(f"❌ Push failed: {push_result.stderr}"))
80
+
81
+ # Remove the local tag to save disk space in the container
82
+ subprocess.run(["ollama", "rm", ollama_tag]) # stdout=subprocess.DEVNULL
83
+
84
+ except subprocess.CalledProcessError as e:
85
+ # Captures errors from the 'check=True' on create_cmd
86
+ logs.append(format_log(f"❌ Ollama Create Error: {e}"))
87
+ except Exception as e:
88
+ logs.append(format_log(f"❌ Error on {tag_suffix}: {str(e)}"))
89
+
90
+ return logs
91
+
92
+
93
+ def start_ollama_daemon(ollama_key):
94
+ print("⏳ Starting Ollama daemon in background...")
95
+ logs.append(format_log(f"⏳ Starting Ollama daemon in background...\n"))
96
+ env = os.environ.copy()
97
+ # Auth
98
+ success, auth_msg = setup_ollama_keys(ollama_key)
99
+ logs.append(format_log(auth_msg))
100
+ yield "\n".join(logs)
101
+ if not success:
102
+ logs.append(format_log("❌ Stopping: Authentication setup failed."))
103
+ yield "\n".join(logs)
104
+ return
105
+ process = subprocess.Popen(["ollama", "serve"], env=env) # stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
106
+ pid = process.pid
107
+ logs.append(format_log("⏳ Starting Ollama daemon in background..."))
108
+ sleep(2)
109
+ return pid, logs
110
+
111
+
112
+ def stop_ollama_daemon(pid):
113
+ print("⏳ Stopping Ollama daemon...")
114
+ os.kill(pid, signal.SIGQUIT)
115
+ subprocess.Popen(["pkill", "ollama"]) # , stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
116
+ return logs
117
+
118
+
119
+ def run_conversion(hf_repo, ollama_repo, hf_token, progress=gr.Progress()):
120
+ logs = []
121
+
122
+ work_dir = Path("conversion_work_dir")
123
+ download_dir = work_dir / "downloads"
124
+ output_dir = work_dir / "output"
125
+
126
+ if work_dir.exists():
127
+ shutil.rmtree(work_dir)
128
+ os.makedirs(download_dir, exist_ok=True)
129
+ os.makedirs(output_dir, exist_ok=True)
130
+
131
+ try:
132
+ # Download
133
+ logs.append(format_log(f"⬇️ Downloading {hf_repo}..."))
134
+ yield "\n".join(logs)
135
+
136
+ model_path = snapshot_download(
137
+ repo_id=hf_repo,
138
+ local_dir=download_dir,
139
+ token=hf_token if hf_token else None
140
+ )
141
+ logs.append(format_log("✅ Download complete."))
142
+ yield "\n".join(logs)
143
+
144
+ # BF16
145
+ bf16_path = output_dir / "model-bf16.gguf"
146
+ logs.append(format_log("⚙️ Converting to BF16..."))
147
+ yield "\n".join(logs)
148
+
149
+ cmd = ["python3", str(CONVERT_SCRIPT), str(model_path), "--outtype", "bf16", "--outfile", str(bf16_path)]
150
+ result = subprocess.run(cmd, capture_output=True, text=True)
151
+
152
+ if result.returncode == 0:
153
+ logs.extend(push_to_ollama(bf16_path, ollama_repo, "bf16"))
154
+ os.remove(bf16_path)
155
+ logs.append(format_log("🧹 Cleaned up BF16"))
156
+ else:
157
+ logs.append(format_log(f"⚠️ BF16 Conversion failed: {result.stderr}"))
158
+ yield "\n".join(logs)
159
+
160
+ # FP16
161
+ fp16_path = output_dir / "model-f16.gguf"
162
+ logs.append(format_log("⚙️ Converting to FP16 (Master)..."))
163
+ yield "\n".join(logs)
164
+
165
+ cmd = ["python3", str(CONVERT_SCRIPT), str(model_path), "--outtype", "f16", "--outfile", str(fp16_path)]
166
+ subprocess.run(cmd, check=True, capture_output=True)
167
+
168
+ logs.extend(push_to_ollama(fp16_path, ollama_repo, "f16"))
169
+ yield "\n".join(logs)
170
+
171
+ # Quant Loop
172
+ for quant in TARGET_QUANTS:
173
+ logs.append(format_log(f"--- {quant} ---"))
174
+ yield "\n".join(logs)
175
+
176
+ final_gguf = output_dir / f"model-{quant}.gguf"
177
+ q_cmd = [str(QUANTIZE_BIN), str(fp16_path), str(final_gguf), quant]
178
+ q_result = subprocess.run(q_cmd, capture_output=True, text=True)
179
+
180
+ if q_result.returncode != 0:
181
+ logs.append(format_log(f"❌ Quantize failed: {q_result.stderr}"))
182
+ continue
183
+
184
+ logs.extend(push_to_ollama(final_gguf, ollama_repo, quant))
185
+ os.remove(final_gguf)
186
+ logs.append(format_log(f"🧹 Cleaned up {quant}"))
187
+ yield "\n".join(logs)
188
+
189
+ if fp16_path.exists():
190
+ os.remove(fp16_path)
191
+ logs.append(format_log("🧹 Cleaned up f16"))
192
+
193
+ except Exception as e:
194
+ logs.append(format_log(f"❌ CRITICAL ERROR: {str(e)}"))
195
+
196
+ finally:
197
+ if work_dir.exists():
198
+ shutil.rmtree(work_dir)
199
+ logs.append(format_log("🏁 Job Done. Workspace cleared."))
200
+ yield "\n".join(logs)
201
+
202
+
203
+ def run_pipeline(hf_repo, ollama_repo, hf_token, ollama_key, progress=gr.Progress()):
204
+ pid = start_ollama_daemon(ollama_key)
205
+ # We yield from the generator
206
+ for update in run_conversion(hf_repo, ollama_repo, hf_token, progress):
207
+ yield update
208
+ sleep(10)
209
+ stop_ollama_daemon(pid)
210
+
211
+
212
+ # --- UI ---
213
+ with gr.Blocks(title="HF to Ollama") as demo:
214
+ target_quants_str = ', '.join(str(item) for item in TARGET_QUANTS)
215
+ gr.Markdown("## Convert a safetensor HF repo to an Ollama repo.")
216
+ gr.Markdown(f"This space will generate F16, BF16, {target_quants_str} GGUFs from safetensors.")
217
+ gr.Markdown("And pushes them to your Ollama repo. You will need an Ollama ssh key, not an API key to push.")
218
+ gr.Markdown("Temporarily move yours from ~/.ollama/id_ed25519 This will cause Ollama to generate a new one")
219
+ gr.Markdown("After logging in set it aside, that ssh key can be used, or the old one, whichever, for spaces")
220
+
221
+ with gr.Row():
222
+ with gr.Column():
223
+ hf_input = gr.Textbox(label="Source HF Repo", placeholder="unsloth/Qwen3.5-9B", value="unsloth/Qwen3.5-0.8B")
224
+ hf_token_input = gr.Textbox(label="HF Token (for gated models and faster download)", type="password", value=TEST_TOKEN)
225
+
226
+ with gr.Column():
227
+ ollama_input = gr.Textbox(label="Destination Ollama Repo", placeholder="user/model", value="fervent_mcclintock/Qwen3.5-9B")
228
+ ollama_key_input = gr.Textbox(label="Ollama Private (ssh) Key", lines=7, type="password", placeholder="-----BEGIN OPENSSH PRIVATE KEY-----...", value=TEST_OKEY)
229
+ btn = gr.Button("Start", variant="primary")
230
+
231
+ logs = gr.TextArea(label="Logs", interactive=False, lines=10, autoscroll=True)
232
+
233
+ btn.click(
234
+ fn=run_pipeline,
235
+ inputs=[hf_input, ollama_input, hf_token_input, ollama_key_input],
236
+ outputs=logs
237
+ )
238
+
239
+
240
+ def restart_space():
241
+ HfApi().restart_space(repo_id=HOST_REPO, token=HF_TOKEN, factory_reboot=True)
242
+
243
+
244
+ scheduler = BackgroundScheduler()
245
+ scheduler.add_job(restart_space, "interval", seconds=21600)
246
+ scheduler.start()
247
+
248
+ if __name__ == "__main__":
249
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860, theme=blurple)
theme.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ blurple = gr.themes.Glass(
4
+ primary_hue="purple",
5
+ secondary_hue=gr.themes.Color(
6
+ c100="#f3f4f6",
7
+ c200="#e5e7eb",
8
+ c300="#d1d5db",
9
+ c400="#9ca3af",
10
+ c50="#f9fafb",
11
+ c500="#6b7280",
12
+ c600="#4b5563",
13
+ c700="#374151",
14
+ c800="rgba(28.422987236857917, 2.1975645867375784, 39.326663208007815, 1)",
15
+ c900="#1c0227",
16
+ c950="#1c0227",
17
+ ),
18
+ neutral_hue=gr.themes.Color(
19
+ c100="#f3e8ff",
20
+ c200="#e9d5ff",
21
+ c300="#d8b4fe",
22
+ c400="#c084fc",
23
+ c50="#faf5ff",
24
+ c500="#a855f7",
25
+ c600="rgba(83.78266724809674, 29.540070278324272, 132.9400207519531, 1)",
26
+ c700="rgba(48.28126126334004, 17.30792685680411, 76.3866943359375, 1)",
27
+ c800="rgba(46.03751121625044, 13.894996526550633, 72.53336791992187, 1)",
28
+ c900="#2e0e49",
29
+ c950="#2e0e49",
30
+ ),
31
+ radius_size="none",
32
+ ).set(
33
+ background_fill_primary="*neutral_700",
34
+ background_fill_secondary="*secondary_800",
35
+ border_color_accent="*neutral_600",
36
+ border_color_primary="*secondary_600",
37
+ color_accent_soft="*neutral_700",
38
+ link_text_color="*secondary_500",
39
+ link_text_color_active="*secondary_500",
40
+ link_text_color_hover="*secondary_400",
41
+ link_text_color_visited="*secondary_600",
42
+ code_background_fill="*neutral_800",
43
+ shadow_spread_dark="0px",
44
+ block_background_fill="*secondary_800",
45
+ block_border_color="*secondary_600",
46
+ block_border_width="1px",
47
+ block_label_background_fill="*secondary_700",
48
+ block_label_border_color="*secondary_600",
49
+ block_label_text_color="*neutral_200",
50
+ block_label_text_size="*text_sm",
51
+ block_title_text_color="*neutral_200",
52
+ checkbox_background_color="*secondary_400",
53
+ checkbox_border_color="*neutral_700",
54
+ checkbox_border_color_hover="*neutral_600",
55
+ checkbox_label_border_color="*secondary_700",
56
+ checkbox_label_gap="*form_gap_width",
57
+ error_background_fill="*background_fill_primary",
58
+ error_border_color="#ef4444",
59
+ error_text_color="#fef2f2",
60
+ error_icon_color="#ef4444",
61
+ input_background_fill="*secondary_600",
62
+ input_border_color="*secondary_600",
63
+ input_border_color_focus="*secondary_500",
64
+ input_placeholder_color="*neutral_500",
65
+ input_radius="*radius_xxs",
66
+ stat_background_fill="*primary_500",
67
+ table_border_color="*neutral_700",
68
+ table_even_background_fill="*neutral_700",
69
+ table_odd_background_fill="*neutral_700",
70
+ button_border_width="0px",
71
+ button_border_width_dark="0px",
72
+ button_transition="all 0.5s ease",
73
+ button_large_text_weight="500",
74
+ button_medium_text_weight="500",
75
+ button_primary_background_fill="linear-gradient(30deg, *primary_800 0%, *primary_950 50%)",
76
+ button_primary_background_fill_dark="linear-gradient(30deg, *primary_800 0%, *primary_950 50%)",
77
+ button_primary_background_fill_hover="linear-gradient(90deg, *primary_950 0%, *primary_700 60%)",
78
+ button_primary_background_fill_hover_dark="linear-gradient(90deg, *primary_950 0%, *primary_700 60%)",
79
+ button_primary_border_color="*primary_600",
80
+ button_primary_border_color_hover="*primary_500",
81
+ button_primary_text_color="white",
82
+ button_primary_text_color_hover="*code_background_fill",
83
+ button_primary_text_color_hover_dark="*code_background_fill",
84
+ button_primary_shadow="*button_primary_shadow",
85
+ button_primary_shadow_active="*button_primary_shadow",
86
+ button_secondary_background_fill="linear-gradient(100deg, *primary_950 0%, *primary_600 70%)",
87
+ button_secondary_background_fill_dark="linear-gradient(100deg, *primary_950 0%, *primary_600 70%)",
88
+ button_secondary_background_fill_hover="linear-gradient(90deg, *primary_700 0%, *primary_950 60%)",
89
+ button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_700 0%, *primary_950 60%)",
90
+ button_secondary_border_color="*neutral_600",
91
+ button_secondary_border_color_hover="*neutral_500",
92
+ button_secondary_text_color="white",
93
+ button_secondary_text_color_hover="*table_even_background_fill",
94
+ button_secondary_text_color_hover_dark="*table_even_background_fill",
95
+ button_cancel_text_color_hover="white",
96
+ )