Instructions to use Neohosseinism/gemma4-stack with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use Neohosseinism/gemma4-stack with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M # Run inference directly in the terminal: llama cli -hf Neohosseinism/gemma4-stack:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M # Run inference directly in the terminal: llama cli -hf Neohosseinism/gemma4-stack:Q4_K_M
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf Neohosseinism/gemma4-stack:Q4_K_M # Run inference directly in the terminal: ./llama-cli -hf Neohosseinism/gemma4-stack:Q4_K_M
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf Neohosseinism/gemma4-stack:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama-cli -hf Neohosseinism/gemma4-stack:Q4_K_M
Use Docker
docker model run hf.co/Neohosseinism/gemma4-stack:Q4_K_M
- LM Studio
- Jan
- Ollama
How to use Neohosseinism/gemma4-stack with Ollama:
ollama run hf.co/Neohosseinism/gemma4-stack:Q4_K_M
- Unsloth Studio
How to use Neohosseinism/gemma4-stack with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for Neohosseinism/gemma4-stack to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for Neohosseinism/gemma4-stack to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for Neohosseinism/gemma4-stack to start chatting
- Pi
How to use Neohosseinism/gemma4-stack with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "Neohosseinism/gemma4-stack:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- OpenClaw new
How to use Neohosseinism/gemma4-stack with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "Neohosseinism/gemma4-stack:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
- Docker Model Runner
How to use Neohosseinism/gemma4-stack with Docker Model Runner:
docker model run hf.co/Neohosseinism/gemma4-stack:Q4_K_M
- Lemonade
How to use Neohosseinism/gemma4-stack with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull Neohosseinism/gemma4-stack:Q4_K_M
Run and chat with the model
lemonade run user.gemma4-stack-Q4_K_M
List all available models
lemonade list
- Hermes Agent
How to use Neohosseinism/gemma4-stack with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Neohosseinism/gemma4-stack:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default Neohosseinism/gemma4-stack:Q4_K_M
Run Hermes
hermes
- Atomic Chat
| """ | |
| title: Gemma 4 Omni (Audio) | |
| author: gemma4-stack | |
| version: 0.1.0 | |
| required_open_webui_version: 0.5.0 | |
| description: > | |
| Sends an attached audio clip to Gemma 4 as a NATIVE multimodal input | |
| (OpenAI `input_audio` content part) via llama-swap/llama-server — the model | |
| "hears" the audio instead of transcribing it with Whisper. Resamples to | |
| 16 kHz mono first (Gemma 4's reliable envelope, clips <= ~30 s). | |
| HOW TO USE | |
| Admin → Functions → "+" → paste this file → Save → enable it. | |
| In a new chat pick the model "Gemma 4 · Omni (audio)", attach a short audio | |
| clip, type your question (Persian or any language), send. | |
| FRAGILE BIT (read me) | |
| Open WebUI hands a Pipe only FILE REFERENCES, not bytes. _resolve_local_path() | |
| below fetches the real file from Open WebUI's store. That store API moves | |
| between versions — if audio isn't found, adjust _resolve_local_path() to your | |
| installed Open WebUI version first (see the strategies inside). | |
| """ | |
| import os | |
| import json | |
| import base64 | |
| import glob | |
| import shutil | |
| import subprocess | |
| import tempfile | |
| from typing import List, Optional | |
| import requests | |
| from pydantic import BaseModel, Field | |
| AUDIO_EXTS = (".wav", ".mp3", ".flac", ".ogg", ".m4a", ".webm", ".aac", ".opus") | |
| DATA_DIR = os.environ.get("DATA_DIR", "/app/backend/data") | |
| UPLOADS_DIR = os.path.join(DATA_DIR, "uploads") | |
| class Pipe: | |
| class Valves(BaseModel): | |
| LLAMASWAP_URL: str = Field( | |
| default="http://llama-swap:8080/v1", | |
| description="OpenAI-compatible base URL of llama-swap.", | |
| ) | |
| MODEL: str = Field( | |
| default="gemma-e4b", | |
| description="llama-swap model key (audio-capable: gemma-e4b/gemma-12b).", | |
| ) | |
| API_KEY: str = Field(default="sk-local", description="Bearer key for llama-swap.") | |
| TEMPERATURE: float = Field(default=1.0) | |
| TOP_K: int = Field(default=64) | |
| TOP_P: float = Field(default=0.95) | |
| MAX_TOKENS: int = Field(default=512) | |
| TARGET_SR: int = Field(default=16000, description="Resample rate (Gemma wants 16 kHz mono).") | |
| DEFAULT_PROMPT: str = Field( | |
| default="این فایل صوتی را دقیق بنویس و در صورت نیاز توضیح بده.", | |
| description="Used when the user attaches audio without typing a question.", | |
| ) | |
| def __init__(self): | |
| self.valves = self.Valves() | |
| def pipes(self): | |
| return [{"id": "gemma4-audio", "name": "Gemma 4 · Omni (audio)"}] | |
| # ------------------------------------------------------------------ helpers | |
| def _collect_file_refs(self, body, __files__, __metadata__) -> List[dict]: | |
| refs = [] | |
| if __files__: | |
| refs += __files__ | |
| if isinstance(__metadata__, dict): | |
| refs += __metadata__.get("files", []) or [] | |
| meta = (body or {}).get("metadata", {}) or {} | |
| refs += meta.get("files", []) or [] | |
| refs += (body or {}).get("files", []) or [] | |
| return refs | |
| def _ref_id_and_name(self, ref: dict): | |
| # Open WebUI nests the actual record under "file" in some versions. | |
| inner = ref.get("file", ref) if isinstance(ref, dict) else {} | |
| fid = ref.get("id") or inner.get("id") | |
| name = ( | |
| ref.get("name") | |
| or inner.get("filename") | |
| or (inner.get("meta") or {}).get("name") | |
| or "" | |
| ) | |
| ctype = (inner.get("meta") or {}).get("content_type", "") or ref.get("type", "") | |
| return fid, name, ctype | |
| def _resolve_local_path(self, fid, name) -> Optional[str]: | |
| """Turn a file reference into a real on-disk path. Version-sensitive.""" | |
| # Strategy 1: official Files model. | |
| try: | |
| from open_webui.models.files import Files # type: ignore | |
| rec = Files.get_file_by_id(fid) | |
| if rec is not None: | |
| p = getattr(rec, "path", None) or (getattr(rec, "meta", {}) or {}).get("path") | |
| if p: | |
| if not os.path.isabs(p): | |
| p = os.path.join(DATA_DIR, p) | |
| if os.path.exists(p): | |
| return p | |
| except Exception: | |
| pass | |
| # Strategy 2: storage provider abstraction. | |
| try: | |
| from open_webui.storage.provider import Storage # type: ignore | |
| p = Storage.get_file(f"uploads/{fid}") # may raise / vary | |
| if p and os.path.exists(p): | |
| return p | |
| except Exception: | |
| pass | |
| # Strategy 3: scan the uploads dir for <id> or <name>. | |
| for pattern in (f"*{fid}*", f"*{name}*"): | |
| if not pattern.strip("*"): | |
| continue | |
| hits = glob.glob(os.path.join(UPLOADS_DIR, pattern)) | |
| hits = [h for h in hits if os.path.isfile(h)] | |
| if hits: | |
| return max(hits, key=os.path.getmtime) | |
| return None | |
| def _to_16k_mono_wav(self, src: str) -> (str, str): | |
| """Return (path, format). Resample via ffmpeg if available, else pass through.""" | |
| ffmpeg = shutil.which("ffmpeg") | |
| if ffmpeg: | |
| out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name | |
| try: | |
| subprocess.run( | |
| [ffmpeg, "-y", "-i", src, "-ar", str(self.valves.TARGET_SR), | |
| "-ac", "1", "-f", "wav", out], | |
| check=True, capture_output=True, | |
| ) | |
| return out, "wav" | |
| except Exception: | |
| pass | |
| ext = os.path.splitext(src)[1].lower().lstrip(".") or "wav" | |
| return src, ("wav" if ext not in ("mp3", "flac", "wav") else ext) | |
| def _latest_user_text(self, body) -> str: | |
| for msg in reversed((body or {}).get("messages", [])): | |
| if msg.get("role") == "user": | |
| c = msg.get("content") | |
| if isinstance(c, str): | |
| return c.strip() | |
| if isinstance(c, list): | |
| parts = [p.get("text", "") for p in c if p.get("type") == "text"] | |
| return " ".join(t for t in parts if t).strip() | |
| return "" | |
| # --------------------------------------------------------------------- main | |
| def pipe(self, body: dict, __user__=None, __request__=None, | |
| __files__=None, __metadata__=None): | |
| refs = self._collect_file_refs(body, __files__, __metadata__) | |
| audio_paths = [] | |
| for ref in refs: | |
| fid, name, ctype = self._ref_id_and_name(ref) | |
| is_audio = ctype.startswith("audio") or name.lower().endswith(AUDIO_EXTS) | |
| if not is_audio: | |
| continue | |
| local = self._resolve_local_path(fid, name) | |
| if local: | |
| audio_paths.append(local) | |
| if not audio_paths: | |
| return ( | |
| "⚠️ No audio found. Attach a short clip (≤ ~30 s) and ask your " | |
| "question. If you *did* attach audio, the file-store lookup needs " | |
| "adapting to your Open WebUI version — see _resolve_local_path()." | |
| ) | |
| text = self._latest_user_text(body) or self.valves.DEFAULT_PROMPT | |
| content = [{"type": "text", "text": text}] | |
| for p in audio_paths: | |
| wav, fmt = self._to_16k_mono_wav(p) | |
| with open(wav, "rb") as f: | |
| content.append({ | |
| "type": "input_audio", | |
| "input_audio": { | |
| "data": base64.b64encode(f.read()).decode("ascii"), | |
| "format": fmt, | |
| }, | |
| }) | |
| payload = { | |
| "model": self.valves.MODEL, | |
| "messages": [{"role": "user", "content": content}], | |
| "temperature": self.valves.TEMPERATURE, | |
| "top_k": self.valves.TOP_K, | |
| "top_p": self.valves.TOP_P, | |
| "max_tokens": self.valves.MAX_TOKENS, | |
| "stream": True, | |
| } | |
| headers = {"Authorization": f"Bearer {self.valves.API_KEY}"} | |
| url = self.valves.LLAMASWAP_URL.rstrip("/") + "/chat/completions" | |
| def gen(): | |
| with requests.post(url, json=payload, headers=headers, | |
| stream=True, timeout=600) as r: | |
| r.raise_for_status() | |
| for line in r.iter_lines(decode_unicode=True): | |
| if not line or not line.startswith("data:"): | |
| continue | |
| data = line[len("data:"):].strip() | |
| if data == "[DONE]": | |
| break | |
| try: | |
| delta = json.loads(data)["choices"][0]["delta"] | |
| piece = delta.get("content") | |
| if piece: | |
| yield piece | |
| except Exception: | |
| continue | |
| return gen() | |