Image-Text-to-Text
MLX
Safetensors
inkling_mm_model
Mixture of Experts
multimodal
inkling
thinking-machines
conversational
Instructions to use pipenetwork/Inkling-MLX-8bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use pipenetwork/Inkling-MLX-8bit 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-8bit") config = load_config("pipenetwork/Inkling-MLX-8bit") # 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-8bit 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-8bit"
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-8bit" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use pipenetwork/Inkling-MLX-8bit 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-8bit"
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-8bit
Run Hermes
hermes
- OpenClaw new
How to use pipenetwork/Inkling-MLX-8bit 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-8bit"
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-8bit" \ --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"
| """Image + audio preprocessing for Inkling (MLX), ported from the reference | |
| `InklingImageProcessor` / `InklingFeatureExtractor` / `InklingProcessor`. | |
| * image -> pixel_values [num_patches, T=2, 40, 40, 3] (feeds `VisionModel`) | |
| * audio -> audio_input_ids [num_frames, 80] dMel bins (feeds `AudioModel`) | |
| `InklingProcessor.apply` builds the full multimodal input (input_ids + features) | |
| from a chat message list, inserting the right number of placeholder soft-tokens. | |
| Uses numpy/PIL + transformers' mel filterbank; no torch needed at inference. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import numpy as np | |
| # CLIP normalization (OPENAI_CLIP_MEAN / STD), per processor_config.json | |
| CLIP_MEAN = np.array([0.48145466, 0.4578275, 0.40821073], dtype=np.float32) | |
| CLIP_STD = np.array([0.26862954, 0.26130258, 0.27577711], dtype=np.float32) | |
| PATCH = 40 # image patch size (== vision patch_size) | |
| TEMPORAL = 2 # temporal_patch_size (images duplicated across 2 frames) | |
| # audio (processor_config.json / feature_extraction_inkling.py) | |
| SR = 16000 | |
| HOP = 800 # audio_token_duration_s (0.05) * SR | |
| WIN = 1600 # * window_size_multiplier (2.0) | |
| N_FFT = 1600 | |
| N_MEL = 80 | |
| DMEL_BINS = 16 | |
| DMEL_MIN, DMEL_MAX = -7.0, 2.0 | |
| # special tokens | |
| IMAGE_TOKEN_ID = 200054 # <|unused_200054|> (soft-token slot) | |
| AUDIO_TOKEN_ID = 200053 # <|unused_200053|> | |
| IMAGE_BOS = "<|content_image|>" | |
| AUDIO_BOS = "<|content_audio_input|>" | |
| # ------------------------------- image ------------------------------- | |
| def preprocess_image(image) -> tuple[np.ndarray, int]: | |
| """PIL.Image or HxWx3 uint8 array -> (pixel_values [N,2,40,40,3] float32, N).""" | |
| if hasattr(image, "convert"): | |
| image = np.asarray(image.convert("RGB")) | |
| image = np.asarray(image) | |
| if image.ndim == 2: | |
| image = np.stack([image] * 3, axis=-1) | |
| img = image[..., :3].astype(np.float32).transpose(2, 0, 1) # -> [C, H, W] | |
| C, H, W = img.shape | |
| num_rows = (H + PATCH - 1) // PATCH | |
| num_cols = W // PATCH + 1 # reference: W//P + 1 | |
| patches = [] | |
| for i in range(num_rows): | |
| for j in range(num_cols): | |
| p = img[:, i * PATCH:(i + 1) * PATCH, j * PATCH:(j + 1) * PATCH] # may be < 40 | |
| padded = np.full((C, PATCH, PATCH), -1.0, dtype=np.float32) # pad value -1.0 | |
| padded[:, : p.shape[1], : p.shape[2]] = p | |
| patches.append(padded) | |
| patches = np.stack(patches, axis=0) # [N, C, 40, 40] | |
| # rescale (1/255) + CLIP normalize per channel | |
| patches = patches / 255.0 | |
| patches = (patches - CLIP_MEAN[None, :, None, None]) / CLIP_STD[None, :, None, None] | |
| # add temporal dim, duplicate x2, then -> [N, T, H, W, C] | |
| patches = np.repeat(patches[..., None], TEMPORAL, axis=-1) # [N, C, 40, 40, 2] | |
| pixel_values = patches.transpose(0, 4, 2, 3, 1) # [N, 2, 40, 40, C] | |
| return pixel_values.astype(np.float32), pixel_values.shape[0] | |
| # ------------------------------- audio ------------------------------- | |
| _mel_fb = None | |
| def _mel_filters() -> np.ndarray: | |
| global _mel_fb | |
| if _mel_fb is None: | |
| from transformers.audio_utils import mel_filter_bank | |
| fb = mel_filter_bank(num_frequency_bins=N_FFT // 2 + 1, num_mel_filters=N_MEL, | |
| min_frequency=0.0, max_frequency=SR / 2.0, sampling_rate=SR, | |
| norm="slaney", mel_scale="slaney") # [801, 80] | |
| _mel_fb = np.ascontiguousarray(fb.T, dtype=np.float32) # [80, 801] | |
| return _mel_fb | |
| def _log_mel(waveform: np.ndarray) -> np.ndarray: | |
| """raw mono waveform -> log10-mel spectrogram [num_frames, 80].""" | |
| wav = np.asarray(waveform, dtype=np.float32).reshape(-1) | |
| right = math.ceil(wav.shape[0] / HOP) * HOP - wav.shape[0] | |
| left = max(N_FFT - HOP, 0) | |
| wav = np.pad(wav, (left, right)) | |
| window = np.hanning(WIN + 1)[:-1].astype(np.float32) # periodic Hann | |
| n_frames = 1 + (wav.shape[0] - N_FFT) // HOP # center=False | |
| frames = np.stack([wav[i * HOP: i * HOP + N_FFT] * window for i in range(n_frames)]) # [T, N_FFT] | |
| mag = np.abs(np.fft.rfft(frames, n=N_FFT, axis=-1)) # [T, 801] | |
| mag = np.maximum(mag, 1e-10) | |
| mel = _mel_filters() @ mag.T # [80, T] | |
| mel = np.log10(np.maximum(mel, 1e-10)) | |
| return mel.T # [T, 80] | |
| def preprocess_audio(waveform: np.ndarray, sampling_rate: int = SR) -> np.ndarray: | |
| """raw 16 kHz mono waveform -> dMel bin ids [num_frames, 80] (int32, 0..15).""" | |
| if sampling_rate != SR: | |
| raise ValueError(f"Inkling audio expects {SR} Hz, got {sampling_rate}") | |
| mel = _log_mel(waveform) # [T, 80] log10 | |
| n_valid = math.ceil(len(np.asarray(waveform).reshape(-1)) / HOP) | |
| mel = mel[:n_valid] # drop trailing pad frames | |
| centers = np.linspace(DMEL_MIN, DMEL_MAX, DMEL_BINS) # 16 bin centers | |
| clamped = np.clip(mel.astype(np.float64), DMEL_MIN, DMEL_MAX) | |
| bins = np.abs(clamped[..., None] - centers).argmin(-1) # nearest center | |
| return bins.astype(np.int32) # [T, 80] | |
| # --------------------------- prompt assembly --------------------------- | |
| class InklingProcessor: | |
| """Assembles multimodal model inputs from chat messages with image/audio parts. | |
| Content parts: {"type":"text","text":...}, {"type":"image","image":PIL/array}, | |
| {"type":"audio","audio":waveform, "sampling_rate":16000}. | |
| """ | |
| def __init__(self, tokenizer, chat_template: str): | |
| self.tok = tokenizer | |
| self.chat_template = chat_template | |
| self.image_bos_id = tokenizer.encode(IMAGE_BOS, add_special_tokens=False)[0] | |
| self.audio_bos_id = tokenizer.encode(AUDIO_BOS, add_special_tokens=False)[0] | |
| def apply(self, messages, reasoning_effort: str = "none"): | |
| import mlx.core as mx | |
| pixel_values, audio_ids = [], [] | |
| # Render text via the chat template with placeholders stripped to a sentinel, | |
| # then splice media spans in. We build ids directly for robustness. | |
| ids: list[int] = [] | |
| def emit_text(s): | |
| ids.extend(self.tok.encode(s, add_special_tokens=False)) | |
| # header: thinking-effort system message (matches chat_template) | |
| eff = {"none": 0.0, "minimal": 0.1, "low": 0.2, "medium": 0.7, "high": 0.9, "max": 0.99}[reasoning_effort] | |
| emit_text(f"<|message_system|><|content_text|>Thinking effort level: {0 if eff == 0 else eff}<|end_message|>") | |
| for msg in messages: | |
| role = {"user": "<|message_user|>", "assistant": "<|message_model|>", | |
| "system": "<|message_system|>"}[msg["role"]] | |
| content = msg["content"] | |
| if isinstance(content, str): | |
| content = [{"type": "text", "text": content}] | |
| for part in content: | |
| t = part.get("type", "text") | |
| if t == "text": | |
| emit_text(role + "<|content_text|>" + part["text"] + "<|end_message|>") | |
| elif t == "image": | |
| pv, n = preprocess_image(part["image"]) | |
| pixel_values.append(pv) | |
| ids.append(self.tok.encode(role, add_special_tokens=False)[0]) | |
| ids.append(self.image_bos_id) | |
| ids.extend([IMAGE_TOKEN_ID] * n) | |
| ids.extend(self.tok.encode("<|end_message|>", add_special_tokens=False)) | |
| elif t == "audio": | |
| aid = preprocess_audio(part["audio"], part.get("sampling_rate", SR)) | |
| audio_ids.append(aid) | |
| ids.append(self.tok.encode(role, add_special_tokens=False)[0]) | |
| ids.append(self.audio_bos_id) | |
| ids.extend([AUDIO_TOKEN_ID] * aid.shape[0]) | |
| ids.extend(self.tok.encode("<|end_message|>", add_special_tokens=False)) | |
| emit_text("<|message_model|>") # generation prompt | |
| out = {"input_ids": ids} | |
| if pixel_values: | |
| out["pixel_values"] = mx.array(np.concatenate(pixel_values, axis=0)) | |
| if audio_ids: | |
| out["audio_input_ids"] = mx.array(np.concatenate(audio_ids, axis=0)) | |
| return out | |