pipenetwork commited on
Commit
1e723dd
·
verified ·
1 Parent(s): 332fb95

Add processing.py to bundled loader

Browse files
Files changed (1) hide show
  1. 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