Image-Text-to-Text
MLX
Safetensors
inkling_mm_model
Mixture of Experts
multimodal
inkling
thinking-machines
conversational
Instructions to use pipenetwork/Inkling-MLX-4bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use pipenetwork/Inkling-MLX-4bit with MLX:
# Make sure mlx-vlm is installed # pip install --upgrade mlx-vlm from mlx_vlm import load, generate from mlx_vlm.prompt_utils import apply_chat_template from mlx_vlm.utils import load_config # Load the model model, processor = load("pipenetwork/Inkling-MLX-4bit") config = load_config("pipenetwork/Inkling-MLX-4bit") # Prepare input image = ["http://images.cocodataset.org/val2017/000000039769.jpg"] prompt = "Describe this image." # Apply chat template formatted_prompt = apply_chat_template( processor, config, prompt, num_images=1 ) # Generate output output = generate(model, processor, formatted_prompt, image) print(output) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Pi
How to use pipenetwork/Inkling-MLX-4bit with Pi:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "pipenetwork/Inkling-MLX-4bit"
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "mlx-lm": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "pipenetwork/Inkling-MLX-4bit" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use pipenetwork/Inkling-MLX-4bit with Hermes Agent:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "pipenetwork/Inkling-MLX-4bit"
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 pipenetwork/Inkling-MLX-4bit
Run Hermes
hermes
- OpenClaw new
How to use pipenetwork/Inkling-MLX-4bit with OpenClaw:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "pipenetwork/Inkling-MLX-4bit"
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 "pipenetwork/Inkling-MLX-4bit" \ --custom-provider-id mlx-lm \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Add processing.py to bundled loader
Browse files- inkling_mlx/processing.py +177 -0
inkling_mlx/processing.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Image + audio preprocessing for Inkling (MLX), ported from the reference
|
| 2 |
+
`InklingImageProcessor` / `InklingFeatureExtractor` / `InklingProcessor`.
|
| 3 |
+
|
| 4 |
+
* image -> pixel_values [num_patches, T=2, 40, 40, 3] (feeds `VisionModel`)
|
| 5 |
+
* audio -> audio_input_ids [num_frames, 80] dMel bins (feeds `AudioModel`)
|
| 6 |
+
|
| 7 |
+
`InklingProcessor.apply` builds the full multimodal input (input_ids + features)
|
| 8 |
+
from a chat message list, inserting the right number of placeholder soft-tokens.
|
| 9 |
+
Uses numpy/PIL + transformers' mel filterbank; no torch needed at inference.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import math
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
# CLIP normalization (OPENAI_CLIP_MEAN / STD), per processor_config.json
|
| 19 |
+
CLIP_MEAN = np.array([0.48145466, 0.4578275, 0.40821073], dtype=np.float32)
|
| 20 |
+
CLIP_STD = np.array([0.26862954, 0.26130258, 0.27577711], dtype=np.float32)
|
| 21 |
+
|
| 22 |
+
PATCH = 40 # image patch size (== vision patch_size)
|
| 23 |
+
TEMPORAL = 2 # temporal_patch_size (images duplicated across 2 frames)
|
| 24 |
+
|
| 25 |
+
# audio (processor_config.json / feature_extraction_inkling.py)
|
| 26 |
+
SR = 16000
|
| 27 |
+
HOP = 800 # audio_token_duration_s (0.05) * SR
|
| 28 |
+
WIN = 1600 # * window_size_multiplier (2.0)
|
| 29 |
+
N_FFT = 1600
|
| 30 |
+
N_MEL = 80
|
| 31 |
+
DMEL_BINS = 16
|
| 32 |
+
DMEL_MIN, DMEL_MAX = -7.0, 2.0
|
| 33 |
+
|
| 34 |
+
# special tokens
|
| 35 |
+
IMAGE_TOKEN_ID = 200054 # <|unused_200054|> (soft-token slot)
|
| 36 |
+
AUDIO_TOKEN_ID = 200053 # <|unused_200053|>
|
| 37 |
+
IMAGE_BOS = "<|content_image|>"
|
| 38 |
+
AUDIO_BOS = "<|content_audio_input|>"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ------------------------------- image -------------------------------
|
| 42 |
+
|
| 43 |
+
def preprocess_image(image) -> tuple[np.ndarray, int]:
|
| 44 |
+
"""PIL.Image or HxWx3 uint8 array -> (pixel_values [N,2,40,40,3] float32, N)."""
|
| 45 |
+
if hasattr(image, "convert"):
|
| 46 |
+
image = np.asarray(image.convert("RGB"))
|
| 47 |
+
image = np.asarray(image)
|
| 48 |
+
if image.ndim == 2:
|
| 49 |
+
image = np.stack([image] * 3, axis=-1)
|
| 50 |
+
img = image[..., :3].astype(np.float32).transpose(2, 0, 1) # -> [C, H, W]
|
| 51 |
+
C, H, W = img.shape
|
| 52 |
+
|
| 53 |
+
num_rows = (H + PATCH - 1) // PATCH
|
| 54 |
+
num_cols = W // PATCH + 1 # reference: W//P + 1
|
| 55 |
+
patches = []
|
| 56 |
+
for i in range(num_rows):
|
| 57 |
+
for j in range(num_cols):
|
| 58 |
+
p = img[:, i * PATCH:(i + 1) * PATCH, j * PATCH:(j + 1) * PATCH] # may be < 40
|
| 59 |
+
padded = np.full((C, PATCH, PATCH), -1.0, dtype=np.float32) # pad value -1.0
|
| 60 |
+
padded[:, : p.shape[1], : p.shape[2]] = p
|
| 61 |
+
patches.append(padded)
|
| 62 |
+
patches = np.stack(patches, axis=0) # [N, C, 40, 40]
|
| 63 |
+
|
| 64 |
+
# rescale (1/255) + CLIP normalize per channel
|
| 65 |
+
patches = patches / 255.0
|
| 66 |
+
patches = (patches - CLIP_MEAN[None, :, None, None]) / CLIP_STD[None, :, None, None]
|
| 67 |
+
|
| 68 |
+
# add temporal dim, duplicate x2, then -> [N, T, H, W, C]
|
| 69 |
+
patches = np.repeat(patches[..., None], TEMPORAL, axis=-1) # [N, C, 40, 40, 2]
|
| 70 |
+
pixel_values = patches.transpose(0, 4, 2, 3, 1) # [N, 2, 40, 40, C]
|
| 71 |
+
return pixel_values.astype(np.float32), pixel_values.shape[0]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ------------------------------- audio -------------------------------
|
| 75 |
+
|
| 76 |
+
_mel_fb = None
|
| 77 |
+
def _mel_filters() -> np.ndarray:
|
| 78 |
+
global _mel_fb
|
| 79 |
+
if _mel_fb is None:
|
| 80 |
+
from transformers.audio_utils import mel_filter_bank
|
| 81 |
+
fb = mel_filter_bank(num_frequency_bins=N_FFT // 2 + 1, num_mel_filters=N_MEL,
|
| 82 |
+
min_frequency=0.0, max_frequency=SR / 2.0, sampling_rate=SR,
|
| 83 |
+
norm="slaney", mel_scale="slaney") # [801, 80]
|
| 84 |
+
_mel_fb = np.ascontiguousarray(fb.T, dtype=np.float32) # [80, 801]
|
| 85 |
+
return _mel_fb
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _log_mel(waveform: np.ndarray) -> np.ndarray:
|
| 89 |
+
"""raw mono waveform -> log10-mel spectrogram [num_frames, 80]."""
|
| 90 |
+
wav = np.asarray(waveform, dtype=np.float32).reshape(-1)
|
| 91 |
+
right = math.ceil(wav.shape[0] / HOP) * HOP - wav.shape[0]
|
| 92 |
+
left = max(N_FFT - HOP, 0)
|
| 93 |
+
wav = np.pad(wav, (left, right))
|
| 94 |
+
window = np.hanning(WIN + 1)[:-1].astype(np.float32) # periodic Hann
|
| 95 |
+
n_frames = 1 + (wav.shape[0] - N_FFT) // HOP # center=False
|
| 96 |
+
frames = np.stack([wav[i * HOP: i * HOP + N_FFT] * window for i in range(n_frames)]) # [T, N_FFT]
|
| 97 |
+
mag = np.abs(np.fft.rfft(frames, n=N_FFT, axis=-1)) # [T, 801]
|
| 98 |
+
mag = np.maximum(mag, 1e-10)
|
| 99 |
+
mel = _mel_filters() @ mag.T # [80, T]
|
| 100 |
+
mel = np.log10(np.maximum(mel, 1e-10))
|
| 101 |
+
return mel.T # [T, 80]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def preprocess_audio(waveform: np.ndarray, sampling_rate: int = SR) -> np.ndarray:
|
| 105 |
+
"""raw 16 kHz mono waveform -> dMel bin ids [num_frames, 80] (int32, 0..15)."""
|
| 106 |
+
if sampling_rate != SR:
|
| 107 |
+
raise ValueError(f"Inkling audio expects {SR} Hz, got {sampling_rate}")
|
| 108 |
+
mel = _log_mel(waveform) # [T, 80] log10
|
| 109 |
+
n_valid = math.ceil(len(np.asarray(waveform).reshape(-1)) / HOP)
|
| 110 |
+
mel = mel[:n_valid] # drop trailing pad frames
|
| 111 |
+
centers = np.linspace(DMEL_MIN, DMEL_MAX, DMEL_BINS) # 16 bin centers
|
| 112 |
+
clamped = np.clip(mel.astype(np.float64), DMEL_MIN, DMEL_MAX)
|
| 113 |
+
bins = np.abs(clamped[..., None] - centers).argmin(-1) # nearest center
|
| 114 |
+
return bins.astype(np.int32) # [T, 80]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# --------------------------- prompt assembly ---------------------------
|
| 118 |
+
|
| 119 |
+
class InklingProcessor:
|
| 120 |
+
"""Assembles multimodal model inputs from chat messages with image/audio parts.
|
| 121 |
+
|
| 122 |
+
Content parts: {"type":"text","text":...}, {"type":"image","image":PIL/array},
|
| 123 |
+
{"type":"audio","audio":waveform, "sampling_rate":16000}.
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
def __init__(self, tokenizer, chat_template: str):
|
| 127 |
+
self.tok = tokenizer
|
| 128 |
+
self.chat_template = chat_template
|
| 129 |
+
self.image_bos_id = tokenizer.encode(IMAGE_BOS, add_special_tokens=False)[0]
|
| 130 |
+
self.audio_bos_id = tokenizer.encode(AUDIO_BOS, add_special_tokens=False)[0]
|
| 131 |
+
|
| 132 |
+
def apply(self, messages, reasoning_effort: str = "none"):
|
| 133 |
+
import mlx.core as mx
|
| 134 |
+
pixel_values, audio_ids = [], []
|
| 135 |
+
# Render text via the chat template with placeholders stripped to a sentinel,
|
| 136 |
+
# then splice media spans in. We build ids directly for robustness.
|
| 137 |
+
ids: list[int] = []
|
| 138 |
+
|
| 139 |
+
def emit_text(s):
|
| 140 |
+
ids.extend(self.tok.encode(s, add_special_tokens=False))
|
| 141 |
+
|
| 142 |
+
# header: thinking-effort system message (matches chat_template)
|
| 143 |
+
eff = {"none": 0.0, "minimal": 0.1, "low": 0.2, "medium": 0.7, "high": 0.9, "max": 0.99}[reasoning_effort]
|
| 144 |
+
emit_text(f"<|message_system|><|content_text|>Thinking effort level: {0 if eff == 0 else eff}<|end_message|>")
|
| 145 |
+
|
| 146 |
+
for msg in messages:
|
| 147 |
+
role = {"user": "<|message_user|>", "assistant": "<|message_model|>",
|
| 148 |
+
"system": "<|message_system|>"}[msg["role"]]
|
| 149 |
+
content = msg["content"]
|
| 150 |
+
if isinstance(content, str):
|
| 151 |
+
content = [{"type": "text", "text": content}]
|
| 152 |
+
for part in content:
|
| 153 |
+
t = part.get("type", "text")
|
| 154 |
+
if t == "text":
|
| 155 |
+
emit_text(role + "<|content_text|>" + part["text"] + "<|end_message|>")
|
| 156 |
+
elif t == "image":
|
| 157 |
+
pv, n = preprocess_image(part["image"])
|
| 158 |
+
pixel_values.append(pv)
|
| 159 |
+
ids.append(self.tok.encode(role, add_special_tokens=False)[0])
|
| 160 |
+
ids.append(self.image_bos_id)
|
| 161 |
+
ids.extend([IMAGE_TOKEN_ID] * n)
|
| 162 |
+
ids.extend(self.tok.encode("<|end_message|>", add_special_tokens=False))
|
| 163 |
+
elif t == "audio":
|
| 164 |
+
aid = preprocess_audio(part["audio"], part.get("sampling_rate", SR))
|
| 165 |
+
audio_ids.append(aid)
|
| 166 |
+
ids.append(self.tok.encode(role, add_special_tokens=False)[0])
|
| 167 |
+
ids.append(self.audio_bos_id)
|
| 168 |
+
ids.extend([AUDIO_TOKEN_ID] * aid.shape[0])
|
| 169 |
+
ids.extend(self.tok.encode("<|end_message|>", add_special_tokens=False))
|
| 170 |
+
emit_text("<|message_model|>") # generation prompt
|
| 171 |
+
|
| 172 |
+
out = {"input_ids": ids}
|
| 173 |
+
if pixel_values:
|
| 174 |
+
out["pixel_values"] = mx.array(np.concatenate(pixel_values, axis=0))
|
| 175 |
+
if audio_ids:
|
| 176 |
+
out["audio_input_ids"] = mx.array(np.concatenate(audio_ids, axis=0))
|
| 177 |
+
return out
|