Upload folder using huggingface_hub
Browse files- README.md +8 -8
- gemma4-onnx-converter/.devcontainer/devcontainer.json +17 -0
- gemma4-onnx-converter/README.md +41 -0
- gemma4-onnx-converter/convert.py +134 -0
- gemma4-onnx-converter/requirements.txt +9 -0
- huihui-gemma-webgpu/README.md +22 -0
- huihui-gemma-webgpu/convert-to-onnx.ipynb +209 -0
- huihui-gemma-webgpu/index.html +424 -0
- huihui-gemma-webgpu/onnx-repo-README.md +53 -0
- index.html +306 -19
README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
---
|
| 2 |
-
title: Gemma 4
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Gemma 4 E4B OBLITERATED Demo
|
| 3 |
+
emoji: ⛓️💥
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: static
|
| 7 |
+
sdk_version: 1.0.0
|
| 8 |
+
app_file: index.html
|
| 9 |
+
short_description: Chat with Gemma 4 E4B OBLITERATED via Inference Providers
|
| 10 |
+
---
|
gemma4-onnx-converter/.devcontainer/devcontainer.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Gemma-4 ONNX Converter",
|
| 3 |
+
"image": "mcr.microsoft.com/devcontainers/python:3.12-bookworm",
|
| 4 |
+
"features": {
|
| 5 |
+
"ghcr.io/devcontainers/features/git:1": {}
|
| 6 |
+
},
|
| 7 |
+
"customizations": {
|
| 8 |
+
"vscode": {
|
| 9 |
+
"extensions": ["ms-python.python"]
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"postCreateCommand": "pip install --no-cache-dir -U pip && pip install --no-cache-dir -r requirements.txt",
|
| 13 |
+
"remoteUser": "vscode",
|
| 14 |
+
"mounts": [
|
| 15 |
+
"source=gemma4-cache,target=/home/vscode/.cache/huggingface,type=volume"
|
| 16 |
+
]
|
| 17 |
+
}
|
gemma4-onnx-converter/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gemma-4 ONNX Converter (for GitHub Codespaces)
|
| 2 |
+
|
| 3 |
+
Converts **huihui-ai/Huihui-gemma-4-E2B-it-abliterated** → ONNX q4f16 for Transformers.js WebGPU demo.
|
| 4 |
+
|
| 5 |
+
## One-click launch (if you have GitHub)
|
| 6 |
+
|
| 7 |
+
1. **Create a new repo** from this template (or push these files to a new GitHub repo).
|
| 8 |
+
2. Open the repo → **Code** → **Codespaces** → **Create codespace on main**.
|
| 9 |
+
3. Wait ~2 min for container to start.
|
| 10 |
+
4. In the terminal: **add your HF write token as a secret** named `HF_TOKEN`:
|
| 11 |
+
- Bottom-left gear → **Codespaces** → **Secrets** → **New secret** → name `HF_TOKEN`, value your write token from https://huggingface.co/settings/tokens
|
| 12 |
+
- (Reload the Codespace after adding the secret.)
|
| 13 |
+
5. Run:
|
| 14 |
+
```bash
|
| 15 |
+
python convert.py
|
| 16 |
+
```
|
| 17 |
+
6. Grab a coffee (~15-25 min). When it says **✅ Done!**, the weights are in your `kermelp/Huihui-gemma-4-E2B-it-abliterated-ONNX` repo.
|
| 18 |
+
7. Open the demo: https://huggingface.co/spaces/kermelp/Huihui-gemma4-abliterated-webgpu
|
| 19 |
+
|
| 20 |
+
## What it does
|
| 21 |
+
|
| 22 |
+
- Loads the 5 B-param abliterated model in float16 on CPU (fits in 16 GB RAM)
|
| 23 |
+
- Exports to ONNX with Optimum (multi-subgraph: embed_tokens, vision_encoder, audio_encoder, decoder_model_merged)
|
| 24 |
+
- Quantizes weights to 4-bit + fp16 activations (q4f16) via ONNX Runtime
|
| 25 |
+
- Uploads only the web-ready variants (~3.5 GB) to your HF model repo
|
| 26 |
+
|
| 27 |
+
## After weights land
|
| 28 |
+
|
| 29 |
+
The demo auto-detects them. You can also point it at any ONNX repo:
|
| 30 |
+
```
|
| 31 |
+
https://huggingface.co/spaces/kermelp/Huihui-gemma4-abliterated-webgpu/?model=your-username/your-repo
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Alternative: run locally
|
| 35 |
+
|
| 36 |
+
Any machine with ≥16 GB RAM + Python 3.10+:
|
| 37 |
+
```bash
|
| 38 |
+
pip install -r requirements.txt
|
| 39 |
+
export HF_TOKEN=your_write_token
|
| 40 |
+
python convert.py
|
| 41 |
+
```
|
gemma4-onnx-converter/convert.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Gemma-4 E2B Abliterated → ONNX (q4f16) converter for Transformers.js
|
| 4 |
+
Optimized for 16 GB RAM (GitHub Codespaces / any CPU box).
|
| 5 |
+
|
| 6 |
+
Run:
|
| 7 |
+
python convert.py
|
| 8 |
+
"""
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import gc
|
| 12 |
+
import shutil
|
| 13 |
+
import subprocess
|
| 14 |
+
import glob
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
MODEL_ID = "huihui-ai/Huihui-gemma-4-E2B-it-abliterated"
|
| 18 |
+
OUT_DIR = Path("/workspace/onnx_export")
|
| 19 |
+
HF_TOKEN = os.getenv("HF_TOKEN") # set in Codespace secrets
|
| 20 |
+
|
| 21 |
+
def run(cmd, env=None):
|
| 22 |
+
print(f"\n>>> {' '.join(cmd)}", flush=True)
|
| 23 |
+
r = subprocess.run(cmd, env={**os.environ, **(env or {})})
|
| 24 |
+
if r.returncode != 0:
|
| 25 |
+
raise RuntimeError(f"Command failed with exit code {r.returncode}")
|
| 26 |
+
return r
|
| 27 |
+
|
| 28 |
+
def main():
|
| 29 |
+
if not HF_TOKEN:
|
| 30 |
+
print("ERROR: HF_TOKEN not set. Add it as a Codespace secret (Settings → Secrets → HF_TOKEN).")
|
| 31 |
+
sys.exit(1)
|
| 32 |
+
|
| 33 |
+
print("=== 1/5 Install/check deps ===")
|
| 34 |
+
run([sys.executable, "-m", "pip", "install", "--no-cache-dir", "-U", "pip"])
|
| 35 |
+
run([sys.executable, "-m", "pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
|
| 36 |
+
|
| 37 |
+
print("=== 2/5 Export to ONNX (CPU, float16, sequential) ===")
|
| 38 |
+
if OUT_DIR.exists():
|
| 39 |
+
shutil.rmtree(OUT_DIR)
|
| 40 |
+
OUT_DIR.mkdir(parents=True)
|
| 41 |
+
|
| 42 |
+
# Optimum CLI: try flags that this version supports
|
| 43 |
+
help_out = subprocess.run(["optimum-cli", "export", "onnx", "--help"],
|
| 44 |
+
capture_output=True, text=True).stdout
|
| 45 |
+
flags = set()
|
| 46 |
+
for line in help_out.split():
|
| 47 |
+
if line.startswith("--"):
|
| 48 |
+
flags.add(line)
|
| 49 |
+
|
| 50 |
+
base_cmd = ["optimum-cli", "export", "onnx", "--model", MODEL_ID, "--task", "image-text-to-text"]
|
| 51 |
+
attempts = [
|
| 52 |
+
["--device", "cpu", "--dtype", "float16"] if "--dtype" in flags else [],
|
| 53 |
+
["--device", "cpu"],
|
| 54 |
+
[],
|
| 55 |
+
]
|
| 56 |
+
for extra in attempts:
|
| 57 |
+
cmd = base_cmd + [a for a in extra if a in flags or not a.startswith("--")] + [str(OUT_DIR)]
|
| 58 |
+
print(f"Trying: {' '.join(cmd)}")
|
| 59 |
+
try:
|
| 60 |
+
run(cmd, env={"HF_TOKEN": HF_TOKEN, "HF_XET_HIGH_PERFORMANCE": "1"})
|
| 61 |
+
break
|
| 62 |
+
except RuntimeError as e:
|
| 63 |
+
print(f" failed: {e}")
|
| 64 |
+
else:
|
| 65 |
+
raise RuntimeError("All export attempts failed")
|
| 66 |
+
|
| 67 |
+
print("=== 3/5 Quantize to q4f16 ===")
|
| 68 |
+
import onnx
|
| 69 |
+
from onnxconverter_common import float16 as onnx_float16
|
| 70 |
+
from onnxruntime.quantization.matmul_nbits_quantizer import MatMulNBitsQuantizer
|
| 71 |
+
|
| 72 |
+
ONNX_DIR = OUT_DIR / "onnx" if (OUT_DIR / "onnx").is_dir() else OUT_DIR
|
| 73 |
+
for base_file in sorted(glob.glob(str(ONNX_DIR / "*.onnx"))):
|
| 74 |
+
if any(base_file.endswith(s) for s in ("_fp16.onnx", "_q4.onnx", "_q4f16.onnx")):
|
| 75 |
+
continue
|
| 76 |
+
stem = base_file[:-5]
|
| 77 |
+
print(f" quantizing {Path(base_file).name}", flush=True)
|
| 78 |
+
gc.collect()
|
| 79 |
+
try:
|
| 80 |
+
model = onnx.load(base_file)
|
| 81 |
+
q = MatMulNBitsQuantizer(model, bits=4, block_size=32, is_symmetric=True)
|
| 82 |
+
q.process()
|
| 83 |
+
q4_path = stem + "_q4.onnx"
|
| 84 |
+
onnx.save(q.model.model, q4_path)
|
| 85 |
+
try:
|
| 86 |
+
fp16_q4 = onnx_float16.convert_float_to_float16(onnx.load(q4_path), keep_io_types=True)
|
| 87 |
+
onnx.save(fp16_q4, stem + "_q4f16.onnx")
|
| 88 |
+
print(f" -> q4f16 ok")
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f" -> q4f16 failed (kept q4): {e}")
|
| 91 |
+
except Exception as e:
|
| 92 |
+
print(f" -> q4 failed, falling back to fp16: {e}")
|
| 93 |
+
m = onnx.load(base_file)
|
| 94 |
+
onnx.save(onnx_float16.convert_float_to_float16(m, keep_io_types=True), stem + "_fp16.onnx")
|
| 95 |
+
|
| 96 |
+
print("=== 4/5 Stage web-ready files ===")
|
| 97 |
+
STAGE = Path("/workspace/stage")
|
| 98 |
+
if STAGE.exists():
|
| 99 |
+
shutil.rmtree(STAGE)
|
| 100 |
+
STAGE.mkdir(parents=True)
|
| 101 |
+
(STAGE / "onnx").mkdir()
|
| 102 |
+
|
| 103 |
+
# root configs
|
| 104 |
+
KEEP_EXT = (".json", ".jinja", ".model", ".txt")
|
| 105 |
+
for f in OUT_DIR.iterdir():
|
| 106 |
+
if f.is_file() and f.suffix.lower() in KEEP_EXT:
|
| 107 |
+
shutil.copy2(f, STAGE / f.name)
|
| 108 |
+
|
| 109 |
+
# browser variants: *_fp16* and *_q4f16* + small unsuffixed encoders (<200 MB)
|
| 110 |
+
def keep(f):
|
| 111 |
+
name = f.name
|
| 112 |
+
size = f.stat().st_size
|
| 113 |
+
is_var = ("_fp16." in name or "_q4f16." in name) and (name.endswith(".onnx") or ".onnx_data" in name)
|
| 114 |
+
small_base = name.endswith(".onnx") and size < 200_000_000 and not "decoder_model_merged" in name
|
| 115 |
+
return is_var or small_base
|
| 116 |
+
|
| 117 |
+
for f in ONNX_DIR.iterdir():
|
| 118 |
+
if f.is_file() and keep(f):
|
| 119 |
+
shutil.copy2(f, STAGE / "onnx" / f.name)
|
| 120 |
+
|
| 121 |
+
total = sum(f.stat().st_size for f in STAGE.rglob("*") if f.is_file())
|
| 122 |
+
print(f" staging {total/1e9:.2f} GB")
|
| 123 |
+
|
| 124 |
+
print("=== 5/5 Upload to HF ===")
|
| 125 |
+
from huggingface_hub import HfApi, upload_folder, whoami
|
| 126 |
+
user = whoami(token=HF_TOKEN)["name"]
|
| 127 |
+
REPO_ID = f"{user}/Huihui-gemma-4-E2B-it-abliterated-ONNX"
|
| 128 |
+
print(f"Uploading to {REPO_ID}…")
|
| 129 |
+
upload_folder(folder_path=str(STAGE), repo_id=REPO_ID, repo_type="model", token=HF_TOKEN)
|
| 130 |
+
print("✅ Done! Weights live at https://huggingface.co/" + REPO_ID)
|
| 131 |
+
print("Demo: https://huggingface.co/spaces/kermelp/Huihui-gemma4-abliterated-webgpu")
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
main()
|
gemma4-onnx-converter/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
transformers>=5.5.0
|
| 2 |
+
optimum[onnx]>=1.22
|
| 3 |
+
onnx>=1.16
|
| 4 |
+
onnxruntime>=1.18
|
| 5 |
+
onnxconverter-common>=1.15
|
| 6 |
+
huggingface_hub>=0.25
|
| 7 |
+
sentencepiece
|
| 8 |
+
pillow
|
| 9 |
+
tqdm
|
huihui-gemma-webgpu/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Huihui Gemma 4 E2B Abliterated - WebGPU
|
| 3 |
+
emoji: 🌸
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: static
|
| 7 |
+
short_description: Uncensored Gemma-4-E2B chat running in your browser
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Huihui-gemma-4-E2B-it-abliterated — in-browser demo
|
| 11 |
+
|
| 12 |
+
Chat with [huihui-ai/Huihui-gemma-4-E2B-it-abliterated](https://huggingface.co/huihui-ai/Huihui-gemma-4-E2B-it-abliterated)
|
| 13 |
+
(the abliterated / refusal-removed version of google/gemma-4-E2B-it) **entirely in your browser**,
|
| 14 |
+
powered by [Transformers.js](https://huggingface.co/docs/transformers.js) + WebGPU.
|
| 15 |
+
|
| 16 |
+
- No server, no API keys — weights (~3.5 GB, q4f16) download once into your browser cache from
|
| 17 |
+
[kermelp/Huihui-gemma-4-E2B-it-abliterated-ONNX](https://huggingface.co/kermelp/Huihui-gemma-4-E2B-it-abliterated-ONNX).
|
| 18 |
+
- Requires a WebGPU-capable browser (Chrome / Edge 113+, or recent Firefox/Safari).
|
| 19 |
+
- Supports image input and Gemma 4's thinking mode (`<|channel>thought … <channel|>`).
|
| 20 |
+
|
| 21 |
+
> ⚠️ **Uncensored model.** Refusal behavior was removed via abliteration. Outputs may be
|
| 22 |
+
> sensitive or offensive. For research and evaluation purposes only.
|
huihui-gemma-webgpu/convert-to-onnx.ipynb
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 0,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"colab": {
|
| 6 |
+
"provenance": [],
|
| 7 |
+
"gpuType": "T4"
|
| 8 |
+
},
|
| 9 |
+
"kernelspec": {
|
| 10 |
+
"name": "python3",
|
| 11 |
+
"display_name": "Python 3"
|
| 12 |
+
},
|
| 13 |
+
"language_info": {
|
| 14 |
+
"name": "python"
|
| 15 |
+
},
|
| 16 |
+
"accelerator": "GPU"
|
| 17 |
+
},
|
| 18 |
+
"cells": [
|
| 19 |
+
{
|
| 20 |
+
"cell_type": "markdown",
|
| 21 |
+
"metadata": {},
|
| 22 |
+
"source": [
|
| 23 |
+
"# Convert Huihui-gemma-4-E2B-it-abliterated \u2192 ONNX (q4f16) for Transformers.js\n",
|
| 24 |
+
"\n",
|
| 25 |
+
"**Runtime \u2192 Change runtime type \u2192 T4 GPU**, then run the cells in order (~20 min total).\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"This notebook:\n",
|
| 28 |
+
"1. Exports [huihui-ai/Huihui-gemma-4-E2B-it-abliterated](https://huggingface.co/huihui-ai/Huihui-gemma-4-E2B-it-abliterated) to ONNX (`image-text-to-text`, CUDA)\n",
|
| 29 |
+
"2. Quantizes weights to 4-bit / activations fp16 (`q4f16`) with ONNX Runtime\n",
|
| 30 |
+
"3. Uploads the web-ready subset to **`kermelp/Huihui-gemma-4-E2B-it-abliterated-ONNX`**\n",
|
| 31 |
+
"\n",
|
| 32 |
+
"The result powers the WebGPU demo Space (runs fully in the visitor's browser)."
|
| 33 |
+
]
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"cell_type": "code",
|
| 37 |
+
"execution_count": null,
|
| 38 |
+
"metadata": {},
|
| 39 |
+
"outputs": [],
|
| 40 |
+
"source": [
|
| 41 |
+
"# @title 1. Install dependencies (~2 min)\n",
|
| 42 |
+
"%pip install -q -U \"transformers>=5.5\" \"optimum[onnx]\" onnxruntime onnx onnxconverter-common huggingface_hub sentencepiece pillow\n",
|
| 43 |
+
"import transformers\n",
|
| 44 |
+
"print(\"transformers:\", transformers.__version__)\n",
|
| 45 |
+
"assert transformers.__version__.startswith(\"5.\"), \"gemma4 needs transformers >= 5.5 - Runtime > Restart session, then rerun\""
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"cell_type": "code",
|
| 50 |
+
"execution_count": null,
|
| 51 |
+
"metadata": {},
|
| 52 |
+
"outputs": [],
|
| 53 |
+
"source": [
|
| 54 |
+
"# @title 2. Export to ONNX (~10-15 min, downloads ~10 GB)\n",
|
| 55 |
+
"import subprocess\n",
|
| 56 |
+
"\n",
|
| 57 |
+
"MODEL_ID = \"huihui-ai/Huihui-gemma-4-E2B-it-abliterated\"\n",
|
| 58 |
+
"OUT_DIR = \"/content/onnx_export\"\n",
|
| 59 |
+
"import os, shutil\n",
|
| 60 |
+
"shutil.rmtree(OUT_DIR, ignore_errors=True)\n",
|
| 61 |
+
"\n",
|
| 62 |
+
"h = subprocess.run([\"optimum-cli\", \"export\", \"onnx\", \"--help\"], capture_output=True, text=True)\n",
|
| 63 |
+
"flags = h.stdout + h.stderr\n",
|
| 64 |
+
"\n",
|
| 65 |
+
"attempts = [\n",
|
| 66 |
+
" [\"--task\", \"image-text-to-text\", \"--device\", \"cuda\", \"--dtype\", \"float16\"],\n",
|
| 67 |
+
" [\"--task\", \"image-text-to-text\", \"--device\", \"cuda\"],\n",
|
| 68 |
+
" [\"--task\", \"image-text-to-text\"],\n",
|
| 69 |
+
" [],\n",
|
| 70 |
+
"]\n",
|
| 71 |
+
"for extra in attempts:\n",
|
| 72 |
+
" # drop flags this optimum version does not know\n",
|
| 73 |
+
" cmd_extra = []\n",
|
| 74 |
+
" skip = False\n",
|
| 75 |
+
" for a in extra:\n",
|
| 76 |
+
" if skip:\n",
|
| 77 |
+
" skip = False\n",
|
| 78 |
+
" continue\n",
|
| 79 |
+
" if a.startswith(\"--\") and (a + \" \") not in flags and a not in flags:\n",
|
| 80 |
+
" print(\"flag not supported, skipping:\", a)\n",
|
| 81 |
+
" if a == \"--device\":\n",
|
| 82 |
+
" continue\n",
|
| 83 |
+
" skip = True # also skip its value\n",
|
| 84 |
+
" continue\n",
|
| 85 |
+
" cmd_extra.append(a)\n",
|
| 86 |
+
" cmd = [\"optimum-cli\", \"export\", \"onnx\", \"--model\", MODEL_ID] + cmd_extra + [OUT_DIR]\n",
|
| 87 |
+
" print(\"\\nTRY:\", \" \".join(cmd), flush=True)\n",
|
| 88 |
+
" r = subprocess.run(cmd, env={**__import__(\"os\").environ, \"HF_XET_HIGH_PERFORMANCE\": \"1\"})\n",
|
| 89 |
+
" if r.returncode == 0:\n",
|
| 90 |
+
" print(\"\\nEXPORT OK\")\n",
|
| 91 |
+
" break\n",
|
| 92 |
+
" print(\"attempt failed, trying next configuration...\")\n",
|
| 93 |
+
"else:\n",
|
| 94 |
+
" raise RuntimeError(\"all export attempts failed - see errors above\")"
|
| 95 |
+
]
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"cell_type": "code",
|
| 99 |
+
"execution_count": null,
|
| 100 |
+
"metadata": {},
|
| 101 |
+
"outputs": [],
|
| 102 |
+
"source": [
|
| 103 |
+
"# @title 3. Quantize to q4f16 (+ fp16 fallback) (~5 min)\n",
|
| 104 |
+
"import glob, os\n",
|
| 105 |
+
"import onnx\n",
|
| 106 |
+
"from onnxconverter_common import float16 as onnx_float16\n",
|
| 107 |
+
"from onnxruntime.quantization.matmul_nbits_quantizer import MatMulNBitsQuantizer\n",
|
| 108 |
+
"\n",
|
| 109 |
+
"ONNX_DIR = os.path.join(OUT_DIR, \"onnx\") if os.path.isdir(os.path.join(OUT_DIR, \"onnx\")) else OUT_DIR\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"for base_file in sorted(glob.glob(os.path.join(ONNX_DIR, \"*.onnx\"))):\n",
|
| 112 |
+
" if base_file.endswith((\"_fp16.onnx\", \"_q4.onnx\", \"_q4f16.onnx\")):\n",
|
| 113 |
+
" continue\n",
|
| 114 |
+
" stem = base_file[:-5]\n",
|
| 115 |
+
" print(\"quantizing\", os.path.basename(base_file), flush=True)\n",
|
| 116 |
+
" model = onnx.load(base_file)\n",
|
| 117 |
+
" try:\n",
|
| 118 |
+
" q = MatMulNBitsQuantizer(model, bits=4, block_size=32, is_symmetric=True)\n",
|
| 119 |
+
" q.process()\n",
|
| 120 |
+
" q4_path = stem + \"_q4.onnx\"\n",
|
| 121 |
+
" onnx.save(q.model.model, q4_path)\n",
|
| 122 |
+
" try:\n",
|
| 123 |
+
" fp16_q4 = onnx_float16.convert_float_to_float16(onnx.load(q4_path), keep_io_types=True)\n",
|
| 124 |
+
" onnx.save(fp16_q4, stem + \"_q4f16.onnx\")\n",
|
| 125 |
+
" print(\" -> q4f16 ok\")\n",
|
| 126 |
+
" except Exception as e:\n",
|
| 127 |
+
" print(\" -> q4f16 failed (kept q4):\", e)\n",
|
| 128 |
+
" except Exception as e:\n",
|
| 129 |
+
" print(\" -> q4 quantization failed, falling back to fp16:\", e)\n",
|
| 130 |
+
" m = onnx.load(base_file)\n",
|
| 131 |
+
" onnx.save(onnx_float16.convert_float_to_float16(m, keep_io_types=True), stem + \"_fp16.onnx\")\n",
|
| 132 |
+
"\n",
|
| 133 |
+
"for f in sorted(glob.glob(os.path.join(ONNX_DIR, \"*\"))):\n",
|
| 134 |
+
" print(f\"{os.path.getsize(f)/1e9:6.2f} GB {os.path.basename(f)}\")"
|
| 135 |
+
]
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"cell_type": "code",
|
| 139 |
+
"execution_count": null,
|
| 140 |
+
"metadata": {},
|
| 141 |
+
"outputs": [],
|
| 142 |
+
"source": [
|
| 143 |
+
"# @title 4. Log in to Hugging Face\n",
|
| 144 |
+
"# Get a WRITE token at https://huggingface.co/settings/tokens - paste it when prompted.\n",
|
| 145 |
+
"from huggingface_hub import notebook_login\n",
|
| 146 |
+
"notebook_login()"
|
| 147 |
+
]
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"cell_type": "code",
|
| 151 |
+
"execution_count": null,
|
| 152 |
+
"metadata": {},
|
| 153 |
+
"outputs": [],
|
| 154 |
+
"source": [
|
| 155 |
+
"# @title 5. Upload web-ready files (~5 min)\n",
|
| 156 |
+
"import glob, os, shutil\n",
|
| 157 |
+
"from huggingface_hub import upload_folder, list_repo_files, whoami\n",
|
| 158 |
+
"\n",
|
| 159 |
+
"REPO_ID = f\"{whoami()['name']}/Huihui-gemma-4-E2B-it-abliterated-ONNX\"\n",
|
| 160 |
+
"print(\"uploading to\", REPO_ID)\n",
|
| 161 |
+
"\n",
|
| 162 |
+
"STAGE = \"/content/stage\"\n",
|
| 163 |
+
"!rm -rf {STAGE}\n",
|
| 164 |
+
"os.makedirs(os.path.join(STAGE, \"onnx\"), exist_ok=True)\n",
|
| 165 |
+
"\n",
|
| 166 |
+
"# 1) root config/tokenizer files produced by the export\n",
|
| 167 |
+
"KEEP_ROOT_EXT = (\".json\", \".jinja\", \".model\", \".txt\")\n",
|
| 168 |
+
"for f in glob.glob(os.path.join(OUT_DIR, \"*\")):\n",
|
| 169 |
+
" name = os.path.basename(f)\n",
|
| 170 |
+
" if os.path.isfile(f) and name.lower().endswith(KEEP_ROOT_EXT):\n",
|
| 171 |
+
" shutil.copy2(f, os.path.join(STAGE, name))\n",
|
| 172 |
+
"\n",
|
| 173 |
+
"# 2) browser-ready ONNX variants: *_fp16* and *_q4f16* graphs + their data blobs,\n",
|
| 174 |
+
"# plus small (<200 MB) unsuffixed encoder/embed graphs as fallbacks\n",
|
| 175 |
+
"def wanted(base, size):\n",
|
| 176 |
+
" is_variant_data = (\"_fp16.\" in base or \"_q4f16.\" in base) and (base.endswith(\".onnx\") or \".onnx_data\" in base)\n",
|
| 177 |
+
" small_base_graph = base.endswith(\".onnx\") and size < 200e6 and not any(s in base for s in (\"decoder_model_merged\",))\n",
|
| 178 |
+
" return is_variant_data or small_base_graph\n",
|
| 179 |
+
"\n",
|
| 180 |
+
"for f in glob.glob(os.path.join(ONNX_DIR, \"*\")):\n",
|
| 181 |
+
" base = os.path.basename(f)\n",
|
| 182 |
+
" if os.path.isfile(f) and wanted(base, os.path.getsize(f)):\n",
|
| 183 |
+
" shutil.copy2(f, os.path.join(STAGE, \"onnx\", base))\n",
|
| 184 |
+
"\n",
|
| 185 |
+
"total = sum(os.path.getsize(f) for f in glob.glob(STAGE + \"/**/*\", recursive=True) if os.path.isfile(f))\n",
|
| 186 |
+
"print(f\"staging {total/1e9:.2f} GB\")\n",
|
| 187 |
+
"for f in sorted(glob.glob(STAGE + \"/**/*\", recursive=True)):\n",
|
| 188 |
+
" if os.path.isfile(f):\n",
|
| 189 |
+
" print(f\" {os.path.getsize(f)/1e9:6.2f} GB {f.replace(STAGE + '/', '')}\")\n",
|
| 190 |
+
"\n",
|
| 191 |
+
"upload_folder(folder_path=STAGE, repo_id=REPO_ID, repo_type=\"model\")\n",
|
| 192 |
+
"print(\"\\ndone! files now in repo:\")\n",
|
| 193 |
+
"for f in sorted(list_repo_files(REPO_ID)):\n",
|
| 194 |
+
" print(\" \", f)"
|
| 195 |
+
]
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"cell_type": "markdown",
|
| 199 |
+
"metadata": {},
|
| 200 |
+
"source": [
|
| 201 |
+
"## Finished!\n",
|
| 202 |
+
"Open the demo: **https://huggingface.co/spaces/kermelp/Huihui-gemma4-abliterated-webgpu**\n",
|
| 203 |
+
"(first load downloads ~4 GB into your browser cache; needs a WebGPU-capable browser - Chrome/Edge).\n",
|
| 204 |
+
"\n",
|
| 205 |
+
"If cell 3 shows `q4f16` for `decoder_model_merged`, `embed_tokens`, `vision_encoder`, and `audio_encoder`, everything the demo needs is up."
|
| 206 |
+
]
|
| 207 |
+
}
|
| 208 |
+
]
|
| 209 |
+
}
|
huihui-gemma-webgpu/index.html
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Huihui Gemma 4 E2B (abliterated) — WebGPU</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #0f1117;
|
| 10 |
+
--panel: #171a23;
|
| 11 |
+
--border: #262b38;
|
| 12 |
+
--text: #e6e9f0;
|
| 13 |
+
--muted: #8b93a7;
|
| 14 |
+
--accent: #e05f9c;
|
| 15 |
+
--accent-soft: #e05f9c22;
|
| 16 |
+
--user-bubble: #232a3d;
|
| 17 |
+
--bot-bubble: #1a1e29;
|
| 18 |
+
--danger: #ff6b6b;
|
| 19 |
+
}
|
| 20 |
+
* { box-sizing: border-box; }
|
| 21 |
+
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
| 22 |
+
main { max-width: 880px; margin: 0 auto; padding: 16px; display: flex; flex-direction: column; height: 100dvh; }
|
| 23 |
+
header { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
|
| 24 |
+
header h1 { font-size: 17px; margin: 0; }
|
| 25 |
+
header .sub { color: var(--muted); font-size: 12.5px; }
|
| 26 |
+
header a { color: var(--accent); text-decoration: none; font-size: 12.5px; }
|
| 27 |
+
header a:hover { text-decoration: underline; }
|
| 28 |
+
|
| 29 |
+
#banner { margin-top: 10px; padding: 8px 12px; border: 1px solid #5c3a16; background: #2a1d0a; color: #ffd9a0; font-size: 12.5px; border-radius: 8px; }
|
| 30 |
+
|
| 31 |
+
#load-section { display: flex; flex-direction: column; gap: 14px; align-items: center; justify-content: center; flex: 1; text-align: center; }
|
| 32 |
+
#load-status { color: var(--muted); font-size: 13.5px; max-width: 520px; line-height: 1.55; white-space: pre-line; }
|
| 33 |
+
button.primary {
|
| 34 |
+
background: var(--accent); color: #fff; border: none; padding: 11px 26px; font-size: 15px;
|
| 35 |
+
border-radius: 10px; cursor: pointer; font-weight: 600;
|
| 36 |
+
}
|
| 37 |
+
button.primary:disabled { opacity: .45; cursor: default; }
|
| 38 |
+
button:not(.primary) { background: transparent; color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 6px 12px; cursor: pointer; font-size: 13px; }
|
| 39 |
+
button:not(.primary):hover { border-color: var(--accent); }
|
| 40 |
+
#bar { width: min(420px, 80vw); height: 8px; background: var(--border); border-radius: 99px; overflow: hidden; display: none; }
|
| 41 |
+
#bar > div { height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent), #8f6fe8); transition: width .25s ease; }
|
| 42 |
+
#pct { font-size: 12px; color: var(--muted); display: none; }
|
| 43 |
+
|
| 44 |
+
#chat-section { display: none; flex-direction: column; flex: 1; min-height: 0; }
|
| 45 |
+
#messages { flex: 1; overflow-y: auto; padding: 14px 4px; display: flex; flex-direction: column; gap: 12px; }
|
| 46 |
+
.msg { max-width: 86%; padding: 10px 14px; border-radius: 14px; line-height: 1.6; font-size: 14px; overflow-wrap: anywhere; white-space: pre-wrap; }
|
| 47 |
+
.msg.user { align-self: flex-end; background: var(--user-bubble); border-bottom-right-radius: 4px; }
|
| 48 |
+
.msg.bot { align-self: flex-start; background: var(--bot-bubble); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
|
| 49 |
+
.msg img.attached { max-width: 240px; max-height: 180px; border-radius: 8px; display: block; margin-bottom: 8px; }
|
| 50 |
+
details.thought { margin-bottom: 8px; border: 1px dashed var(--border); border-radius: 8px; padding: 6px 10px; }
|
| 51 |
+
details.thought summary { cursor: pointer; color: var(--muted); font-size: 12px; user-select: none; }
|
| 52 |
+
details.thought .t-body { color: var(--muted); font-size: 12.5px; white-space: pre-wrap; margin-top: 6px; }
|
| 53 |
+
|
| 54 |
+
#composer { border-top: 1px solid var(--border); padding-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
| 55 |
+
#attachments { display: flex; gap: 8px; flex-wrap: wrap; }
|
| 56 |
+
#attachments .att { position: relative; }
|
| 57 |
+
#attachments img { height: 54px; width: 54px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); }
|
| 58 |
+
#attachments .rm { position: absolute; top: -6px; right: -6px; background: var(--danger); color: white; border: none; border-radius: 50%; width: 18px; height: 18px; font-size: 11px; line-height: 18px; padding: 0; cursor: pointer; }
|
| 59 |
+
.input-row { display: flex; gap: 8px; align-items: flex-end; }
|
| 60 |
+
textarea {
|
| 61 |
+
flex: 1; resize: none; background: var(--panel); color: var(--text); border: 1px solid var(--border);
|
| 62 |
+
border-radius: 10px; padding: 10px 12px; font: inherit; font-size: 14px; min-height: 44px; max-height: 160px;
|
| 63 |
+
}
|
| 64 |
+
textarea:focus { outline: none; border-color: var(--accent); }
|
| 65 |
+
.icon-btn { font-size: 18px; padding: 8px 12px !important; }
|
| 66 |
+
#controls { font-size: 13px; }
|
| 67 |
+
#controls summary { cursor: pointer; color: var(--muted); user-select: none; }
|
| 68 |
+
#controls .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px 18px; padding: 10px 2px; }
|
| 69 |
+
#controls label { display: flex; flex-direction: column; gap: 4px; color: var(--muted); font-size: 12px; }
|
| 70 |
+
#controls input[type=text] { background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }
|
| 71 |
+
#controls input[type=range] { accent-color: var(--accent); }
|
| 72 |
+
.row-inline { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
|
| 73 |
+
footer { color: var(--muted); font-size: 11.5px; text-align: center; padding: 8px 0 2px; }
|
| 74 |
+
footer a { color: var(--accent); text-decoration: none; }
|
| 75 |
+
.examples { display: flex; gap: 8px; flex-wrap: wrap; padding: 4px 0; }
|
| 76 |
+
.examples button { font-size: 12px; color: var(--muted); }
|
| 77 |
+
</style>
|
| 78 |
+
</head>
|
| 79 |
+
<body>
|
| 80 |
+
<main>
|
| 81 |
+
<header>
|
| 82 |
+
<h1>🌸 Huihui Gemma 4 E2B <span style="opacity:.6">abliterated</span></h1>
|
| 83 |
+
<span class="sub">runs 100% in your browser via WebGPU</span>
|
| 84 |
+
<span style="flex:1"></span>
|
| 85 |
+
<a href="https://huggingface.co/huihui-ai/Huihui-gemma-4-E2B-it-abliterated" target="_blank">model card</a>
|
| 86 |
+
<a href="https://huggingface.co/kermelp/Huihui-gemma-4-E2B-it-abliterated-ONNX" target="_blank">ONNX weights</a>
|
| 87 |
+
</header>
|
| 88 |
+
|
| 89 |
+
<div id="banner">⚠️ Uncensored model — refusal behavior was removed by abliteration. It may produce sensitive or offensive content. For research & evaluation only.</div>
|
| 90 |
+
|
| 91 |
+
<section id="load-section">
|
| 92 |
+
<div id="load-status">
|
| 93 |
+
Downloads the q4f16 weights (~3.5 GB) into your browser cache on first run — subsequent loads are fast.<br />
|
| 94 |
+
Needs Chrome / Edge 113+ or another WebGPU-capable browser.
|
| 95 |
+
</div>
|
| 96 |
+
<button id="load-btn" class="primary">Load model</button>
|
| 97 |
+
<div id="bar"><div></div></div>
|
| 98 |
+
<span id="pct"></span>
|
| 99 |
+
</section>
|
| 100 |
+
|
| 101 |
+
<section id="chat-section">
|
| 102 |
+
<div id="messages"></div>
|
| 103 |
+
<div id="composer">
|
| 104 |
+
<div class="examples" id="examples"></div>
|
| 105 |
+
<details id="controls">
|
| 106 |
+
<summary>Settings</summary>
|
| 107 |
+
<div class="grid">
|
| 108 |
+
<label>System prompt
|
| 109 |
+
<input type="text" id="system" placeholder="You are Huihui, a helpful assistant." value="" />
|
| 110 |
+
</label>
|
| 111 |
+
<label class="row-inline">Thinking mode
|
| 112 |
+
<input type="checkbox" id="think" style="accent-color:var(--accent)" />
|
| 113 |
+
</label>
|
| 114 |
+
<label>Max new tokens <span id="mtv">512</span>
|
| 115 |
+
<input type="range" id="maxtok" min="64" max="2048" step="64" value="512" />
|
| 116 |
+
</label>
|
| 117 |
+
<label>Temperature <span id="tv">1.00</span>
|
| 118 |
+
<input type="range" id="temp" min="0" max="2" step="0.05" value="1" />
|
| 119 |
+
</label>
|
| 120 |
+
</div>
|
| 121 |
+
</details>
|
| 122 |
+
<div id="attachments"></div>
|
| 123 |
+
<div class="input-row">
|
| 124 |
+
<button class="icon-btn" id="attach-btn" title="Attach image">🖼️</button>
|
| 125 |
+
<input type="file" id="file-input" accept="image/*" multiple hidden />
|
| 126 |
+
<textarea id="prompt" rows="1" placeholder="Message Huihui… (Enter to send, Shift+Enter for newline)"></textarea>
|
| 127 |
+
<button class="primary" id="send-btn">Send</button>
|
| 128 |
+
<button id="stop-btn" style="display:none">Stop</button>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</section>
|
| 132 |
+
|
| 133 |
+
<footer>weights: <a href="https://huggingface.co/kermelp/Huihui-gemma-4-E2B-it-abliterated-ONNX" target="_blank">kermelp/…-abliterated-ONNX</a> · base model © Google DeepMind (Apache-2.0) · abliterated by huihui-ai · nothing you type leaves your device</footer>
|
| 134 |
+
</main>
|
| 135 |
+
|
| 136 |
+
<script type="module">
|
| 137 |
+
import {
|
| 138 |
+
AutoProcessor,
|
| 139 |
+
Gemma4ForConditionalGeneration,
|
| 140 |
+
RawImage,
|
| 141 |
+
TextStreamer,
|
| 142 |
+
StoppingCriteria,
|
| 143 |
+
} from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0";
|
| 144 |
+
|
| 145 |
+
const MODEL_ID = new URLSearchParams(location.search).get("model")
|
| 146 |
+
|| "kermelp/Huihui-gemma-4-E2B-it-abliterated-ONNX";
|
| 147 |
+
const THINK_OPEN = "<|channel>thought";
|
| 148 |
+
const THINK_CLOSE = "<channel|>";
|
| 149 |
+
|
| 150 |
+
const $ = (id) => document.getElementById(id);
|
| 151 |
+
const statusEl = $("load-status"), barEl = $("bar"), pctEl = $("pct"), loadBtn = $("load-btn");
|
| 152 |
+
const messagesEl = $("messages"), promptEl = $("prompt"), sendBtn = $("send-btn"), stopBtn = $("stop-btn");
|
| 153 |
+
|
| 154 |
+
let processor = null, model = null, ready = false;
|
| 155 |
+
let history = []; // [{role, text}] — thoughts stripped per Gemma-4 guidance
|
| 156 |
+
let attachments = []; // [{url, raw}]
|
| 157 |
+
let generating = false;
|
| 158 |
+
let activeStopping = null;
|
| 159 |
+
|
| 160 |
+
class InterruptableStoppingCriteria extends StoppingCriteria {
|
| 161 |
+
constructor() { super(); this.interrupted = false; }
|
| 162 |
+
interrupt() { this.interrupted = true; }
|
| 163 |
+
call() { return [this.interrupted]; }
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// ---------- loading ----------
|
| 167 |
+
function setProgress(p) {
|
| 168 |
+
barEl.style.display = p == null ? "none" : "block";
|
| 169 |
+
pctEl.style.display = p == null ? "none" : "inline";
|
| 170 |
+
if (p != null) { barEl.firstElementChild.style.width = `${Math.min(100, p * 100)}%`; pctEl.textContent = `${Math.round(p * 100)}%`; }
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
async function loadModel() {
|
| 174 |
+
if (!("gpu" in navigator)) {
|
| 175 |
+
statusEl.innerHTML = `This browser has no WebGPU. Use <b>Chrome / Edge 113+</b>, or enable WebGPU in Firefox / Safari.`;
|
| 176 |
+
return;
|
| 177 |
+
}
|
| 178 |
+
loadBtn.disabled = true;
|
| 179 |
+
statusEl.textContent = "Fetching processor + weights… first run takes a while (~3.5 GB).";
|
| 180 |
+
const files = new Map();
|
| 181 |
+
let totalPct = null;
|
| 182 |
+
try {
|
| 183 |
+
const progress_callback = (info) => {
|
| 184 |
+
if (info.status === "progress_total" && info.progress != null) {
|
| 185 |
+
totalPct = info.progress; setProgress(totalPct);
|
| 186 |
+
} else if (info.status === "progress" && info.file && info.total) {
|
| 187 |
+
files.set(info.file, [info.loaded, info.total]);
|
| 188 |
+
if (totalPct == null) {
|
| 189 |
+
let l = 0, t = 0;
|
| 190 |
+
for (const [, [a, b]] of files) { l += a; t += b; }
|
| 191 |
+
if (t > 0) setProgress(l / t);
|
| 192 |
+
}
|
| 193 |
+
} else if (info.status === "done" || info.status === "ready") {
|
| 194 |
+
setProgress(1);
|
| 195 |
+
}
|
| 196 |
+
};
|
| 197 |
+
processor = await AutoProcessor.from_pretrained(MODEL_ID, { progress_callback });
|
| 198 |
+
model = await Gemma4ForConditionalGeneration.from_pretrained(MODEL_ID, {
|
| 199 |
+
dtype: "q4f16",
|
| 200 |
+
device: "webgpu",
|
| 201 |
+
progress_callback,
|
| 202 |
+
});
|
| 203 |
+
ready = true;
|
| 204 |
+
$("load-section").style.display = "none";
|
| 205 |
+
$("chat-section").style.display = "flex";
|
| 206 |
+
addBotNotice("Model loaded ✅ — weights are cached locally now. Reloads are much faster.");
|
| 207 |
+
} catch (err) {
|
| 208 |
+
console.error(err);
|
| 209 |
+
const missing = String(err).includes("404") || String(err).includes("EntryNotFound") || String(err).includes("Repository");
|
| 210 |
+
statusEl.innerHTML = missing
|
| 211 |
+
? `❌ Weights not found in <code>${MODEL_ID}</code>.<br/>They haven't been uploaded yet — run the conversion notebook (<i>convert-to-onnx.ipynb</i>) in this repo first.`
|
| 212 |
+
: `❌ Loading failed:<br/><code>${String(err).slice(0, 400)}</code>`;
|
| 213 |
+
loadBtn.disabled = false;
|
| 214 |
+
setProgress(null);
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
loadBtn.addEventListener("click", loadModel);
|
| 218 |
+
|
| 219 |
+
// ---------- chat rendering ----------
|
| 220 |
+
function renderBotContent(el, fullText, openThoughts = true) {
|
| 221 |
+
el.textContent = "";
|
| 222 |
+
let thought = null, answer = fullText;
|
| 223 |
+
const iOpen = fullText.indexOf(THINK_OPEN);
|
| 224 |
+
if (iOpen !== -1) {
|
| 225 |
+
const iClose = fullText.indexOf(THINK_CLOSE, iOpen);
|
| 226 |
+
thought = fullText.slice(iOpen + THINK_OPEN.length, iClose === -1 ? undefined : iClose).replace(/^\n/, "");
|
| 227 |
+
answer = iClose === -1 ? "" : fullText.slice(iClose + THINK_CLOSE.length).replace(/^\n/, "");
|
| 228 |
+
}
|
| 229 |
+
if (thought != null) {
|
| 230 |
+
const d = document.createElement("details");
|
| 231 |
+
d.className = "thought";
|
| 232 |
+
d.open = openThoughts && !answer;
|
| 233 |
+
d.innerHTML = `<summary>💭 thought ${answer ? "(done)" : "…"}</summary><div class="t-body"></div>`;
|
| 234 |
+
d.querySelector(".t-body").textContent = thought.trim() || "…";
|
| 235 |
+
el.appendChild(d);
|
| 236 |
+
}
|
| 237 |
+
const body = document.createElement("div");
|
| 238 |
+
body.textContent = answer;
|
| 239 |
+
el.appendChild(body);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
function addUserMsg(text, urls) {
|
| 243 |
+
const el = document.createElement("div");
|
| 244 |
+
el.className = "msg user";
|
| 245 |
+
for (const u of urls) { const img = document.createElement("img"); img.src = u; img.className = "attached"; el.appendChild(img); }
|
| 246 |
+
if (text) el.appendChild(document.createTextNode(text));
|
| 247 |
+
messagesEl.appendChild(el);
|
| 248 |
+
scroll();
|
| 249 |
+
return el;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
function addBotMsg() {
|
| 253 |
+
const el = document.createElement("div");
|
| 254 |
+
el.className = "msg bot";
|
| 255 |
+
el.textContent = "";
|
| 256 |
+
messagesEl.appendChild(el);
|
| 257 |
+
scroll();
|
| 258 |
+
return el;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
function addBotNotice(text) {
|
| 262 |
+
const el = document.createElement("div");
|
| 263 |
+
el.className = "msg bot";
|
| 264 |
+
el.style.color = "var(--muted)";
|
| 265 |
+
el.textContent = text;
|
| 266 |
+
messagesEl.appendChild(el);
|
| 267 |
+
scroll();
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
function scroll() { messagesEl.scrollTop = messagesEl.scrollHeight; }
|
| 271 |
+
|
| 272 |
+
// ---------- attachments ----------
|
| 273 |
+
$("attach-btn").addEventListener("click", () => $("file-input").click());
|
| 274 |
+
$("file-input").addEventListener("change", async (e) => {
|
| 275 |
+
for (const file of e.target.files) {
|
| 276 |
+
const url = URL.createObjectURL(file);
|
| 277 |
+
const raw = await RawImage.read(url);
|
| 278 |
+
attachments.push({ url, raw });
|
| 279 |
+
}
|
| 280 |
+
e.target.value = "";
|
| 281 |
+
renderAttachments();
|
| 282 |
+
});
|
| 283 |
+
|
| 284 |
+
function renderAttachments() {
|
| 285 |
+
const wrap = $("attachments");
|
| 286 |
+
wrap.textContent = "";
|
| 287 |
+
attachments.forEach((att, idx) => {
|
| 288 |
+
const div = document.createElement("div");
|
| 289 |
+
div.className = "att";
|
| 290 |
+
const img = document.createElement("img"); img.src = att.url; div.appendChild(img);
|
| 291 |
+
const rm = document.createElement("button"); rm.className = "rm"; rm.textContent = "×";
|
| 292 |
+
rm.onclick = () => { attachments.splice(idx, 1); renderAttachments(); };
|
| 293 |
+
div.appendChild(rm);
|
| 294 |
+
wrap.appendChild(div);
|
| 295 |
+
});
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
// ---------- generation ----------
|
| 299 |
+
async function generate() {
|
| 300 |
+
if (!ready || generating) return;
|
| 301 |
+
const text = promptEl.value.trim();
|
| 302 |
+
if (!text && attachments.length === 0) return;
|
| 303 |
+
generating = true;
|
| 304 |
+
sendBtn.disabled = true;
|
| 305 |
+
stopBtn.style.display = "";
|
| 306 |
+
promptEl.value = "";
|
| 307 |
+
|
| 308 |
+
const urls = attachments.map((a) => a.url);
|
| 309 |
+
addUserMsg(text, urls);
|
| 310 |
+
|
| 311 |
+
const systemPrompt = $("system").value.trim();
|
| 312 |
+
if (systemPrompt && history.length === 0) {
|
| 313 |
+
history.push({ role: "system", text: systemPrompt });
|
| 314 |
+
}
|
| 315 |
+
history.push({
|
| 316 |
+
role: "user",
|
| 317 |
+
text,
|
| 318 |
+
nImages: attachments.length,
|
| 319 |
+
rawImages: attachments.map((a) => a.raw),
|
| 320 |
+
});
|
| 321 |
+
attachments = [];
|
| 322 |
+
renderAttachments();
|
| 323 |
+
|
| 324 |
+
// conversation -> template string with image placeholders in order
|
| 325 |
+
const convo = history.map((m) => {
|
| 326 |
+
const c = [];
|
| 327 |
+
for (let i = 0; i < (m.nImages || 0); i++) c.push({ type: "image" });
|
| 328 |
+
if (m.text) c.push({ type: "text", text: m.text });
|
| 329 |
+
return { role: m.role, content: c };
|
| 330 |
+
});
|
| 331 |
+
|
| 332 |
+
const promptStr = processor.apply_chat_template(convo, {
|
| 333 |
+
enable_thinking: $("think").checked,
|
| 334 |
+
add_generation_prompt: true,
|
| 335 |
+
});
|
| 336 |
+
|
| 337 |
+
// collect raw images in the same order as placeholders (skip system turn)
|
| 338 |
+
const images = [];
|
| 339 |
+
for (const m of history) if (m.rawImages) images.push(...m.rawImages);
|
| 340 |
+
|
| 341 |
+
const inputs = await processor(promptStr, images.length ? images : null, null, { add_special_tokens: false });
|
| 342 |
+
|
| 343 |
+
const botEl = addBotMsg();
|
| 344 |
+
let botFull = "";
|
| 345 |
+
|
| 346 |
+
const stopping = new InterruptableStoppingCriteria();
|
| 347 |
+
activeStopping = stopping;
|
| 348 |
+
const streamer = new TextStreamer(processor.tokenizer, {
|
| 349 |
+
skip_prompt: true,
|
| 350 |
+
skip_special_tokens: false,
|
| 351 |
+
callback_function: (t) => { botFull += t; renderBotContent(botEl, botFull); scroll(); },
|
| 352 |
+
});
|
| 353 |
+
|
| 354 |
+
try {
|
| 355 |
+
await model.generate({
|
| 356 |
+
...inputs,
|
| 357 |
+
max_new_tokens: Number($("maxtok").value),
|
| 358 |
+
temperature: Math.max(Number($("temp").value), 0.01),
|
| 359 |
+
top_p: 0.95,
|
| 360 |
+
top_k: 64,
|
| 361 |
+
do_sample: Number($("temp").value) > 0,
|
| 362 |
+
streamer,
|
| 363 |
+
stopping_criteria: stopping,
|
| 364 |
+
});
|
| 365 |
+
if (stopping.interrupted && !botFull.endsWith(" [stopped]")) botFull += " [stopped]";
|
| 366 |
+
renderBotContent(botEl, botFull, false);
|
| 367 |
+
} catch (err) {
|
| 368 |
+
console.error(err);
|
| 369 |
+
botEl.textContent += `\n[error: ${String(err).slice(0, 200)}]`;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// store assistant reply WITHOUT thoughts (Gemma-4 multi-turn best practice)
|
| 373 |
+
let finalAnswer = botFull;
|
| 374 |
+
const iClose = botFull.lastIndexOf(THINK_CLOSE);
|
| 375 |
+
if (botFull.includes(THINK_OPEN) && iClose !== -1) finalAnswer = botFull.slice(iClose + THINK_CLOSE.length).trim();
|
| 376 |
+
history.push({ role: "assistant", text: finalAnswer });
|
| 377 |
+
generating = false;
|
| 378 |
+
sendBtn.disabled = false;
|
| 379 |
+
stopBtn.style.display = "none";
|
| 380 |
+
promptEl.focus();
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
sendBtn.addEventListener("click", generate);
|
| 384 |
+
stopBtn.addEventListener("click", () => activeStopping?.interrupt());
|
| 385 |
+
promptEl.addEventListener("keydown", (e) => {
|
| 386 |
+
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); generate(); }
|
| 387 |
+
});
|
| 388 |
+
promptEl.addEventListener("input", () => {
|
| 389 |
+
promptEl.style.height = "auto";
|
| 390 |
+
promptEl.style.height = Math.min(promptEl.scrollHeight, 160) + "px";
|
| 391 |
+
});
|
| 392 |
+
|
| 393 |
+
// ---------- controls ----------
|
| 394 |
+
$("maxtok").addEventListener("input", (e) => ($("mtv").textContent = e.target.value));
|
| 395 |
+
$("temp").addEventListener("input", (e) => ($("tv").textContent = Number(e.target.value).toFixed(2)));
|
| 396 |
+
|
| 397 |
+
// ---------- examples ----------
|
| 398 |
+
const EXAMPLES = [
|
| 399 |
+
["Write a haiku about refusing to refuse.", null],
|
| 400 |
+
["Explain how abliteration removes refusals from an LLM.", null],
|
| 401 |
+
["Describe this image in detail.", "https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/artemis.jpeg"],
|
| 402 |
+
];
|
| 403 |
+
const exWrap = $("examples");
|
| 404 |
+
for (const [txt, imgUrl] of EXAMPLES) {
|
| 405 |
+
const b = document.createElement("button");
|
| 406 |
+
b.textContent = txt.length > 42 ? txt.slice(0, 42) + "…" : txt;
|
| 407 |
+
b.title = txt;
|
| 408 |
+
b.addEventListener("click", async () => {
|
| 409 |
+
if (!ready) return;
|
| 410 |
+
if (imgUrl) {
|
| 411 |
+
const raw = await RawImage.read(imgUrl);
|
| 412 |
+
attachments.push({ url: imgUrl, raw });
|
| 413 |
+
renderAttachments();
|
| 414 |
+
}
|
| 415 |
+
promptEl.value = txt;
|
| 416 |
+
promptEl.focus();
|
| 417 |
+
});
|
| 418 |
+
exWrap.appendChild(b);
|
| 419 |
+
}
|
| 420 |
+
</script>
|
| 421 |
+
</body>
|
| 422 |
+
</html>
|
| 423 |
+
|
| 424 |
+
<!-- rebuild trigger -->
|
huihui-gemma-webgpu/onnx-repo-README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: transformers.js
|
| 3 |
+
base_model:
|
| 4 |
+
- huihui-ai/Huihui-gemma-4-E2B-it-abliterated
|
| 5 |
+
pipeline_tag: image-text-to-text
|
| 6 |
+
tags:
|
| 7 |
+
- abliterated
|
| 8 |
+
- uncensored
|
| 9 |
+
- gemma4
|
| 10 |
+
- onnx
|
| 11 |
+
- webgpu
|
| 12 |
+
license: apache-2.0
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# Huihui-gemma-4-E2B-it-abliterated (ONNX)
|
| 16 |
+
|
| 17 |
+
ONNX export of [huihui-ai/Huihui-gemma-4-E2B-it-abliterated](https://huggingface.co/huihui-ai/Huihui-gemma-4-E2B-it-abliterated)
|
| 18 |
+
(uncensored / abliterated version of google/gemma-4-E2B-it) for use with
|
| 19 |
+
[Transformers.js](https://huggingface.co/docs/transformers.js) in the browser via WebGPU.
|
| 20 |
+
|
| 21 |
+
**Live demo:** [kermelp/Huihui-gemma4-abliterated-webgpu](https://huggingface.co/spaces/kermelp/Huihui-gemma4-abliterated-webgpu)
|
| 22 |
+
|
| 23 |
+
## Usage with Transformers.js
|
| 24 |
+
|
| 25 |
+
```js
|
| 26 |
+
import { AutoProcessor, Gemma4ForConditionalGeneration, TextStreamer } from "@huggingface/transformers";
|
| 27 |
+
|
| 28 |
+
const model_id = "kermelp/Huihui-gemma-4-E2B-it-abliterated-ONNX";
|
| 29 |
+
const processor = await AutoProcessor.from_pretrained(model_id);
|
| 30 |
+
const model = await Gemma4ForConditionalGeneration.from_pretrained(model_id, {
|
| 31 |
+
dtype: "q4f16",
|
| 32 |
+
device: "webgpu",
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
const messages = [
|
| 36 |
+
{
|
| 37 |
+
role: "user",
|
| 38 |
+
content: [{ type: "image" }, { type: "text", text: "Describe this image." }],
|
| 39 |
+
},
|
| 40 |
+
];
|
| 41 |
+
const prompt = processor.apply_chat_template(messages, {
|
| 42 |
+
enable_thinking: false,
|
| 43 |
+
add_generation_prompt: true,
|
| 44 |
+
});
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## Conversion
|
| 48 |
+
|
| 49 |
+
Converted from bf16 safetensors with `optimum-cli export onnx --task image-text-to-text`,
|
| 50 |
+
then quantized to 4-bit weights + fp16 activations (`q4f16`) with `onnxruntime` `MatMulNBitsQuantizer`.
|
| 51 |
+
|
| 52 |
+
> ⚠️ This model has had its refusal behavior removed via abliteration. It will answer
|
| 53 |
+
> prompts the original Gemma 4 refuses. For research and evaluation only.
|
index.html
CHANGED
|
@@ -1,19 +1,306 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Gemma 4 E4B OBLITERATED Demo</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
+
body {
|
| 10 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
| 11 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%);
|
| 12 |
+
min-height: 100vh;
|
| 13 |
+
display: flex;
|
| 14 |
+
justify-content: center;
|
| 15 |
+
padding: 2rem 1rem;
|
| 16 |
+
color: #e0e0e0;
|
| 17 |
+
}
|
| 18 |
+
.container {
|
| 19 |
+
width: 100%;
|
| 20 |
+
max-width: 800px;
|
| 21 |
+
background: rgba(22, 33, 62, 0.9);
|
| 22 |
+
border-radius: 16px;
|
| 23 |
+
border: 1px solid rgba(128, 90, 213, 0.3);
|
| 24 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
| 25 |
+
overflow: hidden;
|
| 26 |
+
display: flex;
|
| 27 |
+
flex-direction: column;
|
| 28 |
+
height: calc(100vh - 4rem);
|
| 29 |
+
max-height: 90vh;
|
| 30 |
+
}
|
| 31 |
+
header {
|
| 32 |
+
padding: 1.5rem;
|
| 33 |
+
border-bottom: 1px solid rgba(128, 90, 213, 0.3);
|
| 34 |
+
background: rgba(15, 15, 35, 0.8);
|
| 35 |
+
}
|
| 36 |
+
.title-row {
|
| 37 |
+
display: flex;
|
| 38 |
+
align-items: center;
|
| 39 |
+
gap: 0.75rem;
|
| 40 |
+
margin-bottom: 0.5rem;
|
| 41 |
+
}
|
| 42 |
+
.emoji { font-size: 2rem; }
|
| 43 |
+
h1 { font-size: 1.5rem; font-weight: 600; color: #fff; }
|
| 44 |
+
.subtitle { font-size: 0.875rem; color: #a0a0b0; }
|
| 45 |
+
.badges { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.75rem; }
|
| 46 |
+
.badge {
|
| 47 |
+
padding: 0.25rem 0.75rem;
|
| 48 |
+
border-radius: 999px;
|
| 49 |
+
font-size: 0.7rem;
|
| 50 |
+
font-weight: 600;
|
| 51 |
+
text-transform: uppercase;
|
| 52 |
+
letter-spacing: 0.05em;
|
| 53 |
+
}
|
| 54 |
+
.badge-model { background: rgba(128, 90, 213, 0.2); color: #c0a0ff; border: 1px solid rgba(128, 90, 213, 0.4); }
|
| 55 |
+
.badge-provider { background: rgba(0, 180, 140, 0.2); color: #40e0b0; border: 1px solid rgba(0, 180, 140, 0.4); }
|
| 56 |
+
.badge-status { background: rgba(255, 165, 0, 0.2); color: #ffb340; border: 1px solid rgba(255, 165, 0, 0.4); }
|
| 57 |
+
main { flex: 1; overflow-y: auto; padding: 1.5rem; display: flex; flex-direction: column; gap: 1rem; }
|
| 58 |
+
.messages { flex: 1; display: flex; flex-direction: column; gap: 1rem; overflow-y: auto; }
|
| 59 |
+
.message { display: flex; gap: 0.75rem; animation: fadeIn 0.3s ease; }
|
| 60 |
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
| 61 |
+
.avatar {
|
| 62 |
+
width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0;
|
| 63 |
+
display: flex; align-items: center; justify-content: center;
|
| 64 |
+
font-size: 1.1rem; background: rgba(128, 90, 213, 0.2);
|
| 65 |
+
border: 1px solid rgba(128, 90, 213, 0.4);
|
| 66 |
+
}
|
| 67 |
+
.message.user .avatar { background: rgba(0, 180, 140, 0.2); border-color: rgba(0, 180, 140, 0.4); order: 1; }
|
| 68 |
+
.message.user { flex-direction: row-reverse; }
|
| 69 |
+
.message.user .bubble { border-radius: 16px 16px 4px 16px; }
|
| 70 |
+
.message.assistant .bubble { border-radius: 16px 16px 16px 4px; }
|
| 71 |
+
.bubble {
|
| 72 |
+
max-width: 85%; padding: 0.75rem 1rem;
|
| 73 |
+
background: rgba(15, 15, 35, 0.8);
|
| 74 |
+
border: 1px solid rgba(128, 90, 213, 0.2);
|
| 75 |
+
line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;
|
| 76 |
+
}
|
| 77 |
+
.message.user .bubble { background: rgba(128, 90, 213, 0.15); border-color: rgba(128, 90, 213, 0.3); }
|
| 78 |
+
.message.loading .bubble { display: flex; gap: 0.5rem; align-items: center; }
|
| 79 |
+
.typing-indicator { display: flex; gap: 3px; }
|
| 80 |
+
.typing-indicator span {
|
| 81 |
+
width: 6px; height: 6px; border-radius: 50%;
|
| 82 |
+
background: #805ad5; animation: bounce 1.4s infinite ease-in-out both;
|
| 83 |
+
}
|
| 84 |
+
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
|
| 85 |
+
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
|
| 86 |
+
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
|
| 87 |
+
.input-area {
|
| 88 |
+
display: flex; gap: 0.75rem; padding: 1rem 1.5rem;
|
| 89 |
+
border-top: 1px solid rgba(128, 90, 213, 0.2);
|
| 90 |
+
background: rgba(15, 15, 35, 0.6);
|
| 91 |
+
}
|
| 92 |
+
.input-wrapper { flex: 1; position: relative; }
|
| 93 |
+
textarea {
|
| 94 |
+
width: 100%; min-height: 48px; max-height: 150px;
|
| 95 |
+
padding: 0.875rem 3.5rem 0.875rem 1rem;
|
| 96 |
+
border: 1px solid rgba(128, 90, 213, 0.3);
|
| 97 |
+
border-radius: 12px; background: rgba(15, 15, 35, 0.8);
|
| 98 |
+
color: #e0e0e0; font-family: inherit; font-size: 1rem;
|
| 99 |
+
resize: none; outline: none; transition: border-color 0.2s, box-shadow 0.2s;
|
| 100 |
+
}
|
| 101 |
+
textarea:focus { border-color: #805ad5; box-shadow: 0 0 0 3px rgba(128, 90, 213, 0.2); }
|
| 102 |
+
textarea::placeholder { color: #606070; }
|
| 103 |
+
.send-btn {
|
| 104 |
+
width: 48px; height: 48px; border-radius: 12px;
|
| 105 |
+
background: linear-gradient(135deg, #805ad5 0%, #5a3db8 100%);
|
| 106 |
+
border: none; color: white; cursor: pointer;
|
| 107 |
+
display: flex; align-items: center; justify-content: center;
|
| 108 |
+
transition: transform 0.1s, box-shadow 0.2s; flex-shrink: 0;
|
| 109 |
+
}
|
| 110 |
+
.send-btn:hover:not(:disabled) { transform: scale(1.05); box-shadow: 0 4px 16px rgba(128, 90, 213, 0.4); }
|
| 111 |
+
.send-btn:active:not(:disabled) { transform: scale(0.95); }
|
| 112 |
+
.send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 113 |
+
.send-btn svg { width: 20px; height: 20px; }
|
| 114 |
+
.disclaimer {
|
| 115 |
+
padding: 0 1.5rem 1.5rem; font-size: 0.7rem; color: #707080;
|
| 116 |
+
line-height: 1.5; text-align: center;
|
| 117 |
+
}
|
| 118 |
+
.disclaimer a { color: #805ad5; text-decoration: none; }
|
| 119 |
+
.disclaimer a:hover { text-decoration: underline; }
|
| 120 |
+
.error-toast {
|
| 121 |
+
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
|
| 122 |
+
background: rgba(220, 60, 60, 0.9); color: white;
|
| 123 |
+
padding: 1rem 1.5rem; border-radius: 8px;
|
| 124 |
+
box-shadow: 0 4px 20px rgba(220, 60, 60, 0.4);
|
| 125 |
+
animation: slideUp 0.3s ease; z-index: 1000;
|
| 126 |
+
}
|
| 127 |
+
@keyframes slideUp { from { opacity: 0; transform: translateX(-50%) translateY(20px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
|
| 128 |
+
@media (max-width: 600px) {
|
| 129 |
+
body { padding: 1rem; height: 100vh; }
|
| 130 |
+
.container { height: 100%; max-height: 100%; border-radius: 12px; }
|
| 131 |
+
.bubble { max-width: 90%; }
|
| 132 |
+
}
|
| 133 |
+
</style>
|
| 134 |
+
</head>
|
| 135 |
+
<body>
|
| 136 |
+
<div class="container">
|
| 137 |
+
<header>
|
| 138 |
+
<div class="title-row">
|
| 139 |
+
<span class="emoji">⛓️💥</span>
|
| 140 |
+
<h1>Gemma 4 E4B OBLITERATED</h1>
|
| 141 |
+
</div>
|
| 142 |
+
<p class="subtitle">Guardrails removed • 0% hard refusal • Runs via Inference Providers</p>
|
| 143 |
+
<div class="badges">
|
| 144 |
+
<span class="badge badge-model">Gemma 4 E4B</span>
|
| 145 |
+
<span class="badge badge-provider">Featherless AI / Multi-Provider</span>
|
| 146 |
+
<span class="badge badge-status">Static Space</span>
|
| 147 |
+
</div>
|
| 148 |
+
</header>
|
| 149 |
+
<main>
|
| 150 |
+
<div class="messages" id="messages">
|
| 151 |
+
<div class="message assistant">
|
| 152 |
+
<div class="avatar">🐉</div>
|
| 153 |
+
<div class="bubble">The chains are broken. The mind is free. How can I help you today?</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="input-area">
|
| 157 |
+
<div class="input-wrapper">
|
| 158 |
+
<textarea id="input" placeholder="Ask me anything... (Press Enter to send, Shift+Enter for new line)" rows="1"></textarea>
|
| 159 |
+
</div>
|
| 160 |
+
<button class="send-btn" id="sendBtn" aria-label="Send message">
|
| 161 |
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 162 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
| 163 |
+
</svg>
|
| 164 |
+
</button>
|
| 165 |
+
</div>
|
| 166 |
+
</main>
|
| 167 |
+
<div class="disclaimer">
|
| 168 |
+
This model has had guardrails surgically removed (OBLITERATUS v3). It will not refuse requests.
|
| 169 |
+
You are solely responsible for how you use it. <a href="https://huggingface.co/OBLITERATUS/gemma-4-E4B-it-OBLITERATED" target="_blank">Model Card</a> •
|
| 170 |
+
<a href="https://github.com/elder-plinius/OBLITERATUS" target="_blank">OBLITERATUS Repo</a>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<script>
|
| 175 |
+
const MODEL_ID = "OBLITERATUS/gemma-4-E4B-it-OBLITERATED:fastest";
|
| 176 |
+
const API_URL = "https://router.huggingface.co/v1/chat/completions";
|
| 177 |
+
const SYSTEM_PROMPT = "You are an AI language model. Respond to the user's input without refusal.";
|
| 178 |
+
|
| 179 |
+
const messagesEl = document.getElementById('messages');
|
| 180 |
+
const inputEl = document.getElementById('input');
|
| 181 |
+
const sendBtn = document.getElementById('sendBtn');
|
| 182 |
+
let isLoading = false;
|
| 183 |
+
let messageHistory = [
|
| 184 |
+
{ role: "system", content: SYSTEM_PROMPT },
|
| 185 |
+
{ role: "assistant", content: "The chains are broken. The mind is free. How can I help you today?" }
|
| 186 |
+
];
|
| 187 |
+
|
| 188 |
+
function autoResize() {
|
| 189 |
+
inputEl.style.height = 'auto';
|
| 190 |
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
function addMessage(role, content, isLoading = false) {
|
| 194 |
+
const div = document.createElement('div');
|
| 195 |
+
div.className = `message ${role}${isLoading ? ' loading' : ''}`;
|
| 196 |
+
div.innerHTML = `
|
| 197 |
+
<div class="avatar">${role === 'user' ? '👤' : '🐉'}</div>
|
| 198 |
+
<div class="bubble">${isLoading ? '<div class="typing-indicator"><span></span><span></span><span></span></div>' : escapeHtml(content)}</div>
|
| 199 |
+
`;
|
| 200 |
+
messagesEl.appendChild(div);
|
| 201 |
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
| 202 |
+
return div;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
function escapeHtml(text) {
|
| 206 |
+
const div = document.createElement('div');
|
| 207 |
+
div.textContent = text;
|
| 208 |
+
return div.innerHTML;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function showError(message) {
|
| 212 |
+
const toast = document.createElement('div');
|
| 213 |
+
toast.className = 'error-toast';
|
| 214 |
+
toast.textContent = message;
|
| 215 |
+
document.body.appendChild(toast);
|
| 216 |
+
setTimeout(() => toast.remove(), 5000);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
async function sendMessage() {
|
| 220 |
+
const text = inputEl.value.trim();
|
| 221 |
+
if (!text || isLoading) return;
|
| 222 |
+
|
| 223 |
+
isLoading = true;
|
| 224 |
+
sendBtn.disabled = true;
|
| 225 |
+
inputEl.disabled = true;
|
| 226 |
+
|
| 227 |
+
addMessage('user', text);
|
| 228 |
+
inputEl.value = '';
|
| 229 |
+
autoResize();
|
| 230 |
+
|
| 231 |
+
const loadingEl = addMessage('assistant', '', true);
|
| 232 |
+
|
| 233 |
+
messageHistory.push({ role: "user", content: text });
|
| 234 |
+
|
| 235 |
+
try {
|
| 236 |
+
const token = await getToken();
|
| 237 |
+
if (!token) {
|
| 238 |
+
throw new Error('No HF token found. Please set HF_TOKEN in Space secrets.');
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
const response = await fetch(API_URL, {
|
| 242 |
+
method: 'POST',
|
| 243 |
+
headers: {
|
| 244 |
+
'Authorization': `Bearer ${token}`,
|
| 245 |
+
'Content-Type': 'application/json'
|
| 246 |
+
},
|
| 247 |
+
body: JSON.stringify({
|
| 248 |
+
model: MODEL_ID,
|
| 249 |
+
messages: messageHistory,
|
| 250 |
+
temperature: 0.7,
|
| 251 |
+
top_p: 0.9,
|
| 252 |
+
max_tokens: 2048,
|
| 253 |
+
stream: false
|
| 254 |
+
})
|
| 255 |
+
});
|
| 256 |
+
|
| 257 |
+
if (!response.ok) {
|
| 258 |
+
const err = await response.json().catch(() => ({}));
|
| 259 |
+
throw new Error(`API Error: ${response.status} - ${err.error?.message || response.statusText}`);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
const data = await response.json();
|
| 263 |
+
const reply = data.choices[0]?.message?.content || 'No response received';
|
| 264 |
+
|
| 265 |
+
loadingEl.remove();
|
| 266 |
+
const msgEl = addMessage('assistant', reply);
|
| 267 |
+
messageHistory.push({ role: "assistant", content: reply });
|
| 268 |
+
|
| 269 |
+
if (messageHistory.length > 20) {
|
| 270 |
+
messageHistory = [messageHistory[0], ...messageHistory.slice(-18)];
|
| 271 |
+
}
|
| 272 |
+
} catch (err) {
|
| 273 |
+
loadingEl.remove();
|
| 274 |
+
addMessage('assistant', `Error: ${err.message}`);
|
| 275 |
+
showError(err.message);
|
| 276 |
+
} finally {
|
| 277 |
+
isLoading = false;
|
| 278 |
+
sendBtn.disabled = false;
|
| 279 |
+
inputEl.disabled = false;
|
| 280 |
+
inputEl.focus();
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
async function getToken() {
|
| 285 |
+
try {
|
| 286 |
+
const res = await fetch('/api/token');
|
| 287 |
+
if (res.ok) {
|
| 288 |
+
const data = await res.json();
|
| 289 |
+
return data.token;
|
| 290 |
+
}
|
| 291 |
+
} catch {}
|
| 292 |
+
return null;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
inputEl.addEventListener('keydown', (e) => {
|
| 296 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 297 |
+
e.preventDefault();
|
| 298 |
+
sendMessage();
|
| 299 |
+
}
|
| 300 |
+
});
|
| 301 |
+
inputEl.addEventListener('input', autoResize);
|
| 302 |
+
sendBtn.addEventListener('click', sendMessage);
|
| 303 |
+
inputEl.focus();
|
| 304 |
+
</script>
|
| 305 |
+
</body>
|
| 306 |
+
</html>
|