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 Desktop
- 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 @earendil-works/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
- 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
- OpenClaw
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"
Commit ·
ea3571c
1
Parent(s): 89bf59d
Add openwebui functions; exclude personal data backups
Browse files- .gitignore +2 -1
- openwebui/functions/gemma4_audio_pipe.py +220 -0
.gitignore
CHANGED
|
@@ -4,8 +4,9 @@
|
|
| 4 |
# rendered llama-swap config
|
| 5 |
llama-swap/config.yaml
|
| 6 |
|
| 7 |
-
# Open WebUI persistent data (db, vector store, uploads)
|
| 8 |
openwebui/data/
|
|
|
|
| 9 |
|
| 10 |
# bench output
|
| 11 |
scripts/bench-results/
|
|
|
|
| 4 |
# rendered llama-swap config
|
| 5 |
llama-swap/config.yaml
|
| 6 |
|
| 7 |
+
# Open WebUI persistent data (db, vector store, uploads) and its backups
|
| 8 |
openwebui/data/
|
| 9 |
+
openwebui/data.bak-*/
|
| 10 |
|
| 11 |
# bench output
|
| 12 |
scripts/bench-results/
|
openwebui/functions/gemma4_audio_pipe.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
title: Gemma 4 Omni (Audio)
|
| 3 |
+
author: gemma4-stack
|
| 4 |
+
version: 0.1.0
|
| 5 |
+
required_open_webui_version: 0.5.0
|
| 6 |
+
description: >
|
| 7 |
+
Sends an attached audio clip to Gemma 4 as a NATIVE multimodal input
|
| 8 |
+
(OpenAI `input_audio` content part) via llama-swap/llama-server — the model
|
| 9 |
+
"hears" the audio instead of transcribing it with Whisper. Resamples to
|
| 10 |
+
16 kHz mono first (Gemma 4's reliable envelope, clips <= ~30 s).
|
| 11 |
+
|
| 12 |
+
HOW TO USE
|
| 13 |
+
Admin → Functions → "+" → paste this file → Save → enable it.
|
| 14 |
+
In a new chat pick the model "Gemma 4 · Omni (audio)", attach a short audio
|
| 15 |
+
clip, type your question (Persian or any language), send.
|
| 16 |
+
|
| 17 |
+
FRAGILE BIT (read me)
|
| 18 |
+
Open WebUI hands a Pipe only FILE REFERENCES, not bytes. _resolve_local_path()
|
| 19 |
+
below fetches the real file from Open WebUI's store. That store API moves
|
| 20 |
+
between versions — if audio isn't found, adjust _resolve_local_path() to your
|
| 21 |
+
installed Open WebUI version first (see the strategies inside).
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
import os
|
| 25 |
+
import json
|
| 26 |
+
import base64
|
| 27 |
+
import glob
|
| 28 |
+
import shutil
|
| 29 |
+
import subprocess
|
| 30 |
+
import tempfile
|
| 31 |
+
from typing import List, Optional
|
| 32 |
+
|
| 33 |
+
import requests
|
| 34 |
+
from pydantic import BaseModel, Field
|
| 35 |
+
|
| 36 |
+
AUDIO_EXTS = (".wav", ".mp3", ".flac", ".ogg", ".m4a", ".webm", ".aac", ".opus")
|
| 37 |
+
DATA_DIR = os.environ.get("DATA_DIR", "/app/backend/data")
|
| 38 |
+
UPLOADS_DIR = os.path.join(DATA_DIR, "uploads")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Pipe:
|
| 42 |
+
class Valves(BaseModel):
|
| 43 |
+
LLAMASWAP_URL: str = Field(
|
| 44 |
+
default="http://llama-swap:8080/v1",
|
| 45 |
+
description="OpenAI-compatible base URL of llama-swap.",
|
| 46 |
+
)
|
| 47 |
+
MODEL: str = Field(
|
| 48 |
+
default="gemma-e4b",
|
| 49 |
+
description="llama-swap model key (audio-capable: gemma-e4b/gemma-12b).",
|
| 50 |
+
)
|
| 51 |
+
API_KEY: str = Field(default="sk-local", description="Bearer key for llama-swap.")
|
| 52 |
+
TEMPERATURE: float = Field(default=1.0)
|
| 53 |
+
TOP_K: int = Field(default=64)
|
| 54 |
+
TOP_P: float = Field(default=0.95)
|
| 55 |
+
MAX_TOKENS: int = Field(default=512)
|
| 56 |
+
TARGET_SR: int = Field(default=16000, description="Resample rate (Gemma wants 16 kHz mono).")
|
| 57 |
+
DEFAULT_PROMPT: str = Field(
|
| 58 |
+
default="این فایل صوتی را دقیق بنویس و در صورت نیاز توضیح بده.",
|
| 59 |
+
description="Used when the user attaches audio without typing a question.",
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def __init__(self):
|
| 63 |
+
self.valves = self.Valves()
|
| 64 |
+
|
| 65 |
+
def pipes(self):
|
| 66 |
+
return [{"id": "gemma4-audio", "name": "Gemma 4 · Omni (audio)"}]
|
| 67 |
+
|
| 68 |
+
# ------------------------------------------------------------------ helpers
|
| 69 |
+
def _collect_file_refs(self, body, __files__, __metadata__) -> List[dict]:
|
| 70 |
+
refs = []
|
| 71 |
+
if __files__:
|
| 72 |
+
refs += __files__
|
| 73 |
+
if isinstance(__metadata__, dict):
|
| 74 |
+
refs += __metadata__.get("files", []) or []
|
| 75 |
+
meta = (body or {}).get("metadata", {}) or {}
|
| 76 |
+
refs += meta.get("files", []) or []
|
| 77 |
+
refs += (body or {}).get("files", []) or []
|
| 78 |
+
return refs
|
| 79 |
+
|
| 80 |
+
def _ref_id_and_name(self, ref: dict):
|
| 81 |
+
# Open WebUI nests the actual record under "file" in some versions.
|
| 82 |
+
inner = ref.get("file", ref) if isinstance(ref, dict) else {}
|
| 83 |
+
fid = ref.get("id") or inner.get("id")
|
| 84 |
+
name = (
|
| 85 |
+
ref.get("name")
|
| 86 |
+
or inner.get("filename")
|
| 87 |
+
or (inner.get("meta") or {}).get("name")
|
| 88 |
+
or ""
|
| 89 |
+
)
|
| 90 |
+
ctype = (inner.get("meta") or {}).get("content_type", "") or ref.get("type", "")
|
| 91 |
+
return fid, name, ctype
|
| 92 |
+
|
| 93 |
+
def _resolve_local_path(self, fid, name) -> Optional[str]:
|
| 94 |
+
"""Turn a file reference into a real on-disk path. Version-sensitive."""
|
| 95 |
+
# Strategy 1: official Files model.
|
| 96 |
+
try:
|
| 97 |
+
from open_webui.models.files import Files # type: ignore
|
| 98 |
+
|
| 99 |
+
rec = Files.get_file_by_id(fid)
|
| 100 |
+
if rec is not None:
|
| 101 |
+
p = getattr(rec, "path", None) or (getattr(rec, "meta", {}) or {}).get("path")
|
| 102 |
+
if p:
|
| 103 |
+
if not os.path.isabs(p):
|
| 104 |
+
p = os.path.join(DATA_DIR, p)
|
| 105 |
+
if os.path.exists(p):
|
| 106 |
+
return p
|
| 107 |
+
except Exception:
|
| 108 |
+
pass
|
| 109 |
+
# Strategy 2: storage provider abstraction.
|
| 110 |
+
try:
|
| 111 |
+
from open_webui.storage.provider import Storage # type: ignore
|
| 112 |
+
|
| 113 |
+
p = Storage.get_file(f"uploads/{fid}") # may raise / vary
|
| 114 |
+
if p and os.path.exists(p):
|
| 115 |
+
return p
|
| 116 |
+
except Exception:
|
| 117 |
+
pass
|
| 118 |
+
# Strategy 3: scan the uploads dir for <id> or <name>.
|
| 119 |
+
for pattern in (f"*{fid}*", f"*{name}*"):
|
| 120 |
+
if not pattern.strip("*"):
|
| 121 |
+
continue
|
| 122 |
+
hits = glob.glob(os.path.join(UPLOADS_DIR, pattern))
|
| 123 |
+
hits = [h for h in hits if os.path.isfile(h)]
|
| 124 |
+
if hits:
|
| 125 |
+
return max(hits, key=os.path.getmtime)
|
| 126 |
+
return None
|
| 127 |
+
|
| 128 |
+
def _to_16k_mono_wav(self, src: str) -> (str, str):
|
| 129 |
+
"""Return (path, format). Resample via ffmpeg if available, else pass through."""
|
| 130 |
+
ffmpeg = shutil.which("ffmpeg")
|
| 131 |
+
if ffmpeg:
|
| 132 |
+
out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
| 133 |
+
try:
|
| 134 |
+
subprocess.run(
|
| 135 |
+
[ffmpeg, "-y", "-i", src, "-ar", str(self.valves.TARGET_SR),
|
| 136 |
+
"-ac", "1", "-f", "wav", out],
|
| 137 |
+
check=True, capture_output=True,
|
| 138 |
+
)
|
| 139 |
+
return out, "wav"
|
| 140 |
+
except Exception:
|
| 141 |
+
pass
|
| 142 |
+
ext = os.path.splitext(src)[1].lower().lstrip(".") or "wav"
|
| 143 |
+
return src, ("wav" if ext not in ("mp3", "flac", "wav") else ext)
|
| 144 |
+
|
| 145 |
+
def _latest_user_text(self, body) -> str:
|
| 146 |
+
for msg in reversed((body or {}).get("messages", [])):
|
| 147 |
+
if msg.get("role") == "user":
|
| 148 |
+
c = msg.get("content")
|
| 149 |
+
if isinstance(c, str):
|
| 150 |
+
return c.strip()
|
| 151 |
+
if isinstance(c, list):
|
| 152 |
+
parts = [p.get("text", "") for p in c if p.get("type") == "text"]
|
| 153 |
+
return " ".join(t for t in parts if t).strip()
|
| 154 |
+
return ""
|
| 155 |
+
|
| 156 |
+
# --------------------------------------------------------------------- main
|
| 157 |
+
def pipe(self, body: dict, __user__=None, __request__=None,
|
| 158 |
+
__files__=None, __metadata__=None):
|
| 159 |
+
refs = self._collect_file_refs(body, __files__, __metadata__)
|
| 160 |
+
audio_paths = []
|
| 161 |
+
for ref in refs:
|
| 162 |
+
fid, name, ctype = self._ref_id_and_name(ref)
|
| 163 |
+
is_audio = ctype.startswith("audio") or name.lower().endswith(AUDIO_EXTS)
|
| 164 |
+
if not is_audio:
|
| 165 |
+
continue
|
| 166 |
+
local = self._resolve_local_path(fid, name)
|
| 167 |
+
if local:
|
| 168 |
+
audio_paths.append(local)
|
| 169 |
+
|
| 170 |
+
if not audio_paths:
|
| 171 |
+
return (
|
| 172 |
+
"⚠️ No audio found. Attach a short clip (≤ ~30 s) and ask your "
|
| 173 |
+
"question. If you *did* attach audio, the file-store lookup needs "
|
| 174 |
+
"adapting to your Open WebUI version — see _resolve_local_path()."
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
text = self._latest_user_text(body) or self.valves.DEFAULT_PROMPT
|
| 178 |
+
content = [{"type": "text", "text": text}]
|
| 179 |
+
for p in audio_paths:
|
| 180 |
+
wav, fmt = self._to_16k_mono_wav(p)
|
| 181 |
+
with open(wav, "rb") as f:
|
| 182 |
+
content.append({
|
| 183 |
+
"type": "input_audio",
|
| 184 |
+
"input_audio": {
|
| 185 |
+
"data": base64.b64encode(f.read()).decode("ascii"),
|
| 186 |
+
"format": fmt,
|
| 187 |
+
},
|
| 188 |
+
})
|
| 189 |
+
|
| 190 |
+
payload = {
|
| 191 |
+
"model": self.valves.MODEL,
|
| 192 |
+
"messages": [{"role": "user", "content": content}],
|
| 193 |
+
"temperature": self.valves.TEMPERATURE,
|
| 194 |
+
"top_k": self.valves.TOP_K,
|
| 195 |
+
"top_p": self.valves.TOP_P,
|
| 196 |
+
"max_tokens": self.valves.MAX_TOKENS,
|
| 197 |
+
"stream": True,
|
| 198 |
+
}
|
| 199 |
+
headers = {"Authorization": f"Bearer {self.valves.API_KEY}"}
|
| 200 |
+
url = self.valves.LLAMASWAP_URL.rstrip("/") + "/chat/completions"
|
| 201 |
+
|
| 202 |
+
def gen():
|
| 203 |
+
with requests.post(url, json=payload, headers=headers,
|
| 204 |
+
stream=True, timeout=600) as r:
|
| 205 |
+
r.raise_for_status()
|
| 206 |
+
for line in r.iter_lines(decode_unicode=True):
|
| 207 |
+
if not line or not line.startswith("data:"):
|
| 208 |
+
continue
|
| 209 |
+
data = line[len("data:"):].strip()
|
| 210 |
+
if data == "[DONE]":
|
| 211 |
+
break
|
| 212 |
+
try:
|
| 213 |
+
delta = json.loads(data)["choices"][0]["delta"]
|
| 214 |
+
piece = delta.get("content")
|
| 215 |
+
if piece:
|
| 216 |
+
yield piece
|
| 217 |
+
except Exception:
|
| 218 |
+
continue
|
| 219 |
+
|
| 220 |
+
return gen()
|