Issactoto commited on
Commit
2cf8d3b
·
verified ·
1 Parent(s): a7917d6

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
chat_template.jinja ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0].role == 'system' %}
4
+ {{- messages[0].content + '\n\n' }}
5
+ {%- endif %}
6
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
7
+ {%- for tool in tools %}
8
+ {{- "\n" }}
9
+ {{- tool | tojson }}
10
+ {%- endfor %}
11
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
12
+ {%- else %}
13
+ {%- if messages[0].role == 'system' %}
14
+ {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
15
+ {%- endif %}
16
+ {%- endif %}
17
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
18
+ {%- for message in messages[::-1] %}
19
+ {%- set index = (messages|length - 1) - loop.index0 %}
20
+ {%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
21
+ {%- set ns.multi_step_tool = false %}
22
+ {%- set ns.last_query_index = index %}
23
+ {%- endif %}
24
+ {%- endfor %}
25
+ {%- for message in messages %}
26
+ {%- if message.content is string %}
27
+ {%- set content = message.content %}
28
+ {%- else %}
29
+ {%- set content = '' %}
30
+ {%- endif %}
31
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
32
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
33
+ {%- elif message.role == "assistant" %}
34
+ {%- set reasoning_content = '' %}
35
+ {%- if message.reasoning_content is string %}
36
+ {%- set reasoning_content = message.reasoning_content %}
37
+ {%- else %}
38
+ {%- if '</think>' in content %}
39
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
40
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
41
+ {%- endif %}
42
+ {%- endif %}
43
+ {%- if loop.index0 > ns.last_query_index %}
44
+ {%- if loop.last or (not loop.last and reasoning_content) %}
45
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
46
+ {%- else %}
47
+ {{- '<|im_start|>' + message.role + '\n' + content }}
48
+ {%- endif %}
49
+ {%- else %}
50
+ {{- '<|im_start|>' + message.role + '\n' + content }}
51
+ {%- endif %}
52
+ {%- if message.tool_calls %}
53
+ {%- for tool_call in message.tool_calls %}
54
+ {%- if (loop.first and content) or (not loop.first) %}
55
+ {{- '\n' }}
56
+ {%- endif %}
57
+ {%- if tool_call.function %}
58
+ {%- set tool_call = tool_call.function %}
59
+ {%- endif %}
60
+ {{- '<tool_call>\n{"name": "' }}
61
+ {{- tool_call.name }}
62
+ {{- '", "arguments": ' }}
63
+ {%- if tool_call.arguments is string %}
64
+ {{- tool_call.arguments }}
65
+ {%- else %}
66
+ {{- tool_call.arguments | tojson }}
67
+ {%- endif %}
68
+ {{- '}\n</tool_call>' }}
69
+ {%- endfor %}
70
+ {%- endif %}
71
+ {{- '<|im_end|>\n' }}
72
+ {%- elif message.role == "tool" %}
73
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
74
+ {{- '<|im_start|>user' }}
75
+ {%- endif %}
76
+ {{- '\n<tool_response>\n' }}
77
+ {{- content }}
78
+ {{- '\n</tool_response>' }}
79
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
80
+ {{- '<|im_end|>\n' }}
81
+ {%- endif %}
82
+ {%- endif %}
83
+ {%- endfor %}
84
+ {%- if add_generation_prompt %}
85
+ {{- '<|im_start|>assistant\n' }}
86
+ {%- endif %}
config.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoConfig": "configuration_qwen_vision.QwenVisionConfig",
4
+ "AutoModelForVision2Seq": "modeling_qwen_vision.QwenVisionForConditionalGeneration",
5
+ "AutoProcessor": "processing_qwen_vision.QwenVisionProcessor"
6
+ },
7
+ "clip_model_id": "openai/clip-vit-base-patch32",
8
+ "img_token": "[IMG]",
9
+ "img_token_count": 32,
10
+ "img_token_id": 151670,
11
+ "llm_hidden_size": 2560,
12
+ "llm_model_id": "Issactoto/qwen4b-instruct-cantone-ft",
13
+ "max_new_tokens": 256,
14
+ "model_type": "qwen_vision",
15
+ "projector_scale": 0.08,
16
+ "transformers_version": "5.0.0",
17
+ "vision_hidden_size": 768
18
+ }
configuration_qwen_vision.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ Configuration for QwenVision — a multimodal model combining
4
+ openai/clip-vit-base-patch32 + linear projector + Qwen-based causal LLM.
5
+ """
6
+
7
+ from transformers import PretrainedConfig
8
+
9
+
10
+ class QwenVisionConfig(PretrainedConfig):
11
+ model_type = "qwen_vision"
12
+
13
+ def __init__(
14
+ self,
15
+ clip_model_id: str = "openai/clip-vit-base-patch32",
16
+ vision_hidden_size: int = 768,
17
+ img_token: str = "[IMG]",
18
+ img_token_id: int = -1, # ← also add this (see Bug 2)
19
+ img_token_count: int = 32,
20
+ projector_scale: float = 0.08,
21
+ llm_model_id: str = "Issactoto/qwen4b-instruct-cantone-ft",
22
+ llm_hidden_size: int = 2560,
23
+ max_new_tokens: int = 256,
24
+ **kwargs,
25
+ ):
26
+ super().__init__(**kwargs)
27
+ self.clip_model_id = clip_model_id
28
+ self.vision_hidden_size = vision_hidden_size
29
+ self.img_token = img_token
30
+ self.img_token_id = img_token_id # ← persist this
31
+ self.img_token_count = img_token_count
32
+ self.projector_scale = projector_scale
33
+ self.llm_model_id = llm_model_id
34
+ self.llm_hidden_size = llm_hidden_size
35
+ self.max_new_tokens = max_new_tokens
modeling_qwen_vision.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ QwenVisionForConditionalGeneration
4
+ -----------------------------------
5
+ A HuggingFace-compatible Vision2Seq model combining:
6
+ - openai/clip-vit-base-patch32 (frozen visual encoder)
7
+ - Linear projector (768 → LLM hidden size)
8
+ - Qwen-based causal LLM
9
+
10
+ Weights layout on disk
11
+ ----------------------
12
+ config.json ← QwenVisionConfig
13
+ multimodal_adapter.pt ← projector + img_embed (your trained weights)
14
+ tokenizer files ← saved alongside (includes [IMG] token)
15
+
16
+ The LLM and CLIP are loaded from their HF hub IDs stored in config.json,
17
+ so you don't need to re-upload those large weights.
18
+ """
19
+
20
+ import os
21
+ from typing import Optional, List, Union
22
+
23
+ import torch
24
+ import torch.nn as nn
25
+ from transformers import (
26
+ PreTrainedModel,
27
+ AutoTokenizer,
28
+ AutoModelForCausalLM,
29
+ CLIPVisionModel,
30
+ CLIPImageProcessor,
31
+ BitsAndBytesConfig,
32
+ GenerationConfig,
33
+ )
34
+ from transformers.modeling_outputs import CausalLMOutputWithPast
35
+
36
+ from configuration_qwen_vision import QwenVisionConfig
37
+
38
+
39
+ class QwenVisionForConditionalGeneration(PreTrainedModel):
40
+ """
41
+ Multimodal model compatible with AutoModelForVision2Seq.
42
+
43
+ Usage
44
+ -----
45
+ model = QwenVisionForConditionalGeneration.from_pretrained(
46
+ "Issactoto/qwen4b-instruct-image-project"
47
+ )
48
+ """
49
+
50
+ config_class = QwenVisionConfig
51
+ # Tell HF which sub-modules carry their own configs (none here — we load by ID)
52
+ base_model_prefix = "llm"
53
+ supports_gradient_checkpointing = False
54
+
55
+ # ------------------------------------------------------------------ #
56
+ # Construction #
57
+ # ------------------------------------------------------------------ #
58
+
59
+ def __init__(self, config: QwenVisionConfig, load_sub_models: bool = True):
60
+ super().__init__(config)
61
+ self.config = config
62
+
63
+ if load_sub_models:
64
+ self._build_vision_encoder()
65
+ self._build_llm()
66
+ # Projector and img_embed are always created (weights loaded later)
67
+ self._build_projector()
68
+
69
+ # ---- sub-model builders ------------------------------------------ #
70
+
71
+ def _build_vision_encoder(self):
72
+ self.vision = CLIPVisionModel.from_pretrained(
73
+ self.config.clip_model_id,
74
+ torch_dtype=torch.float16,
75
+ )
76
+ for p in self.vision.parameters():
77
+ p.requires_grad = False
78
+
79
+ def _build_llm(self, load_in_4bit: bool = True):
80
+ if load_in_4bit:
81
+ bnb_config = BitsAndBytesConfig(
82
+ load_in_4bit=True,
83
+ bnb_4bit_quant_type="nf4",
84
+ bnb_4bit_compute_dtype=torch.float16,
85
+ )
86
+ else:
87
+ bnb_config = None
88
+
89
+ self.llm = AutoModelForCausalLM.from_pretrained(
90
+ self.config.llm_model_id,
91
+ quantization_config=bnb_config,
92
+ device_map="auto",
93
+ )
94
+ for p in self.llm.parameters():
95
+ p.requires_grad = False
96
+
97
+ self.llm_dtype = self.llm.get_input_embeddings().weight.dtype
98
+
99
+ def _build_projector(self):
100
+ v_dim = self.config.vision_hidden_size # 768
101
+ l_dim = self.config.llm_hidden_size # 2560
102
+
103
+ # Infer dtype from LLM if available, else default float16
104
+ dtype = getattr(self, "llm_dtype", torch.float16)
105
+
106
+ self.projector = nn.Linear(v_dim, l_dim).to(dtype=dtype)
107
+ nn.init.normal_(self.projector.weight, std=0.02)
108
+
109
+ self.img_embed = nn.Parameter(torch.zeros(l_dim, dtype=dtype))
110
+
111
+ # ------------------------------------------------------------------ #
112
+ # save_pretrained / from_pretrained hooks #
113
+ # ------------------------------------------------------------------ #
114
+
115
+ def save_pretrained(self, save_directory: str, **kwargs):
116
+ """
117
+ Saves:
118
+ - config.json
119
+ - multimodal_adapter.pt (projector + img_embed only)
120
+
121
+ The LLM and CLIP are NOT re-saved; they are referenced by their
122
+ hub IDs in config.json and will be downloaded on load.
123
+ """
124
+ os.makedirs(save_directory, exist_ok=True)
125
+ self.config.save_pretrained(save_directory)
126
+
127
+ adapter_path = os.path.join(save_directory, "multimodal_adapter.pt")
128
+ torch.save(
129
+ {
130
+ "projector": self.projector.state_dict(),
131
+ "img_embed": self.img_embed.data,
132
+ },
133
+ adapter_path,
134
+ )
135
+ print(f"[QwenVision] Saved adapter weights → {adapter_path}")
136
+
137
+ @classmethod
138
+ def from_pretrained(
139
+ cls,
140
+ pretrained_model_name_or_path: str,
141
+ load_in_4bit: bool = True,
142
+ **kwargs,
143
+ ):
144
+ """
145
+ Loads:
146
+ 1. QwenVisionConfig from config.json
147
+ 2. CLIP vision encoder (from config.clip_model_id)
148
+ 3. Qwen LLM (from config.llm_model_id, 4-bit by default)
149
+ 4. Projector + img_embed from multimodal_adapter.pt
150
+ """
151
+ # 1. Config
152
+ config = QwenVisionConfig.from_pretrained(
153
+ pretrained_model_name_or_path, **kwargs
154
+ )
155
+
156
+ # 2. Instantiate (this calls __init__ which builds sub-models)
157
+ model = cls(config, load_sub_models=True)
158
+
159
+ # Override 4-bit setting if caller passed it
160
+ if not load_in_4bit:
161
+ # Rebuild LLM without quantisation
162
+ model._build_llm(load_in_4bit=False)
163
+
164
+ # 3. Load adapter weights
165
+ adapter_path = os.path.join(
166
+ pretrained_model_name_or_path, "multimodal_adapter.pt"
167
+ )
168
+ if not os.path.isfile(adapter_path):
169
+ # Try downloading from hub
170
+ from huggingface_hub import hf_hub_download
171
+ adapter_path = hf_hub_download(
172
+ repo_id=pretrained_model_name_or_path,
173
+ filename="multimodal_adapter.pt",
174
+ )
175
+
176
+ device = next(model.llm.parameters()).device
177
+ ckpt = torch.load(adapter_path, map_location=device)
178
+ model.projector.load_state_dict(ckpt["projector"])
179
+ model.img_embed = nn.Parameter(ckpt["img_embed"].to(device))
180
+ print(f"[QwenVision] Loaded adapter weights from {adapter_path}")
181
+
182
+ return model
183
+
184
+ # ------------------------------------------------------------------ #
185
+ # Forward #
186
+ # ------------------------------------------------------------------ #
187
+
188
+ def forward(
189
+ self,
190
+ pixel_values: torch.Tensor,
191
+ input_ids: torch.Tensor,
192
+ attention_mask: Optional[torch.Tensor] = None,
193
+ labels: Optional[torch.Tensor] = None,
194
+ **kwargs,
195
+ ) -> CausalLMOutputWithPast:
196
+
197
+ device = input_ids.device
198
+ img_token_id = self.config.img_token_id # set during processor init
199
+
200
+ # ---- 1. Encode image ----------------------------------------- #
201
+ with torch.no_grad():
202
+ vision_out = self.vision(pixel_values.to(next(self.vision.parameters()).device))
203
+ image_embed = vision_out.pooler_output.to(self.llm_dtype)
204
+
205
+ # ---- 2. Project → LLM space ---------------------------------- #
206
+ # [B, 1, L] → expand to [B, IMG_TOKEN_COUNT, L]
207
+ image_tokens = (
208
+ self.projector(image_embed)
209
+ .unsqueeze(1)
210
+ .expand(-1, self.config.img_token_count, -1)
211
+ )
212
+ image_tokens = image_tokens + self.img_embed.unsqueeze(0).unsqueeze(0)
213
+
214
+ # ---- 3. Build inputs_embeds with image injected -------------- #
215
+ inputs_embeds = self.llm.get_input_embeddings()(input_ids).clone()
216
+ mask = input_ids == img_token_id
217
+ b_idx, s_idx = mask.nonzero(as_tuple=True)
218
+ if b_idx.numel() > 0:
219
+ patch_idx = s_idx % self.config.img_token_count
220
+ inputs_embeds[b_idx, s_idx] = image_tokens[b_idx, patch_idx]
221
+
222
+ # ---- 4. LLM forward ------------------------------------------ #
223
+ return self.llm(
224
+ inputs_embeds=inputs_embeds,
225
+ attention_mask=attention_mask,
226
+ labels=labels,
227
+ )
228
+
229
+ # ------------------------------------------------------------------ #
230
+ # generate() — wires into HF GenerationMixin #
231
+ # ------------------------------------------------------------------ #
232
+
233
+ def prepare_inputs_for_generation(
234
+ self,
235
+ input_ids,
236
+ pixel_values=None,
237
+ attention_mask=None,
238
+ past_key_values=None,
239
+ inputs_embeds=None,
240
+ **kwargs,
241
+ ):
242
+ """Called by model.generate() on each decoding step."""
243
+
244
+ # On the first step we inject image embeddings.
245
+ # On subsequent steps past_key_values is set, so we only pass the
246
+ # last token (standard autoregressive generation).
247
+
248
+ if past_key_values is not None:
249
+ # Subsequent decoding steps — just the new token
250
+ input_ids = input_ids[:, -1:]
251
+ return dict(
252
+ input_ids=input_ids,
253
+ pixel_values=None, # already encoded
254
+ attention_mask=attention_mask,
255
+ past_key_values=past_key_values,
256
+ inputs_embeds=None,
257
+ )
258
+
259
+ # First step — build full inputs_embeds with image
260
+ if pixel_values is not None:
261
+ img_token_id = self.config.img_token_id
262
+ device = input_ids.device
263
+
264
+ with torch.no_grad():
265
+ vision_out = self.vision(pixel_values.to(next(self.vision.parameters()).device))
266
+ image_embed = vision_out.pooler_output.to(self.llm_dtype)
267
+
268
+ image_tokens = (
269
+ self.projector(image_embed)
270
+ .unsqueeze(1)
271
+ .expand(-1, self.config.img_token_count, -1)
272
+ )
273
+ image_tokens = image_tokens + self.img_embed.unsqueeze(0).unsqueeze(0)
274
+
275
+ inputs_embeds = self.llm.get_input_embeddings()(input_ids).clone()
276
+ mask = input_ids == img_token_id
277
+ b_idx, s_idx = mask.nonzero(as_tuple=True)
278
+ if b_idx.numel() > 0:
279
+ patch_idx = s_idx % self.config.img_token_count
280
+ inputs_embeds[b_idx, s_idx] = image_tokens[b_idx, patch_idx]
281
+
282
+ return dict(
283
+ input_ids=None, # replaced by inputs_embeds
284
+ inputs_embeds=inputs_embeds,
285
+ pixel_values=None,
286
+ attention_mask=attention_mask,
287
+ past_key_values=past_key_values,
288
+ )
289
+
290
+ return dict(
291
+ input_ids=input_ids,
292
+ attention_mask=attention_mask,
293
+ past_key_values=past_key_values,
294
+ )
295
+
296
+ def get_input_embeddings(self):
297
+ return self.llm.get_input_embeddings()
298
+
299
+ def set_input_embeddings(self, value):
300
+ self.llm.set_input_embeddings(value)
301
+
302
+ def get_output_embeddings(self):
303
+ return self.llm.get_output_embeddings()
304
+
305
+ def can_generate(self):
306
+ return True
multimodal_adapter.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03e4b9afd80d3e21e46f7b3b0d027e49a5dc6fe67b5ba14aa540f38fba23ccef
3
+ size 3944708
processing_qwen_vision.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ """
4
+ QwenVisionProcessor
5
+ --------------------
6
+ Wraps CLIPImageProcessor (for images) + Qwen tokenizer (for text) into a
7
+ single AutoProcessor-compatible class.
8
+
9
+ Supports apply_chat_template() so callers can use the exact same interface
10
+ as granite-vision or LLaVA.
11
+ """
12
+
13
+ import os
14
+ from typing import List, Optional, Union
15
+ from PIL import Image
16
+
17
+ from transformers import (
18
+ ProcessorMixin,
19
+ CLIPImageProcessor,
20
+ AutoTokenizer,
21
+ BatchEncoding,
22
+ )
23
+
24
+
25
+ IMG_TOKEN = "[IMG]"
26
+ IMG_TOKEN_COUNT = 32
27
+
28
+
29
+ class QwenVisionProcessor(ProcessorMixin):
30
+ """
31
+ Processor for QwenVisionForConditionalGeneration.
32
+
33
+ Attributes exposed for AutoProcessor
34
+ -------------------------------------
35
+ attributes = ["image_processor", "tokenizer"]
36
+ """
37
+
38
+ # Required by ProcessorMixin / AutoProcessor registry
39
+ attributes = ["image_processor", "tokenizer"]
40
+ image_processor_class = "CLIPImageProcessor"
41
+ tokenizer_class = "AutoTokenizer"
42
+
43
+ def __init__(self, image_processor: CLIPImageProcessor, tokenizer):
44
+ super().__init__(image_processor, tokenizer)
45
+ self.image_processor = image_processor
46
+ self.tokenizer = tokenizer
47
+
48
+ # Ensure [IMG] token exists
49
+ if tokenizer.convert_tokens_to_ids(IMG_TOKEN) == tokenizer.unk_token_id:
50
+ tokenizer.add_tokens([IMG_TOKEN])
51
+
52
+ self.img_token = IMG_TOKEN
53
+ self.img_token_id = tokenizer.convert_tokens_to_ids(IMG_TOKEN)
54
+ self.img_token_count = IMG_TOKEN_COUNT
55
+
56
+ # ------------------------------------------------------------------ #
57
+ # Factory methods #
58
+ # ------------------------------------------------------------------ #
59
+
60
+ @classmethod
61
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs):
62
+ image_processor = CLIPImageProcessor.from_pretrained(
63
+ "openai/clip-vit-base-patch32"
64
+ )
65
+ tokenizer = AutoTokenizer.from_pretrained(
66
+ pretrained_model_name_or_path, **kwargs
67
+ )
68
+ return cls(image_processor=image_processor, tokenizer=tokenizer)
69
+
70
+ def save_pretrained(self, save_directory: str, **kwargs):
71
+ os.makedirs(save_directory, exist_ok=True)
72
+ self.image_processor.save_pretrained(save_directory)
73
+ self.tokenizer.save_pretrained(save_directory)
74
+
75
+ # ------------------------------------------------------------------ #
76
+ # apply_chat_template — mirrors the granite-vision interface #
77
+ # ------------------------------------------------------------------ #
78
+
79
+ def apply_chat_template(
80
+ self,
81
+ conversation: List[dict],
82
+ add_generation_prompt: bool = True,
83
+ tokenize: bool = True,
84
+ return_dict: bool = True,
85
+ return_tensors: Optional[str] = "pt",
86
+ images: Optional[List[Image.Image]] = None,
87
+ max_length: int = 512,
88
+ padding: Union[bool, str] = True,
89
+ truncation: bool = True,
90
+ enable_thinking: bool = False,
91
+ **kwargs,
92
+ ) -> BatchEncoding:
93
+ """
94
+ Parameters
95
+ ----------
96
+ conversation : list of dicts
97
+ Each dict has "role" and "content".
98
+ Content can be a string, or a list of dicts with "type" keys:
99
+ {"type": "image", "url": "/path/to/img.png"}
100
+ {"type": "text", "text": "Your question"}
101
+ images : optional pre-loaded PIL images (overrides url extraction)
102
+ """
103
+
104
+ # ---- Extract images from conversation ------------------------ #
105
+ extracted_images: List[Image.Image] = []
106
+ text_messages = []
107
+
108
+ for turn in conversation:
109
+ role = turn["role"]
110
+ content = turn["content"]
111
+
112
+ if isinstance(content, str):
113
+ text_messages.append({"role": role, "content": content})
114
+ continue
115
+
116
+ # List of content blocks
117
+ text_parts = []
118
+ for block in content:
119
+ if block.get("type") == "image":
120
+ # Load image from url/path if not supplied externally
121
+ if images is None:
122
+ url = block.get("url") or block.get("path")
123
+ if url:
124
+ extracted_images.append(Image.open(url).convert("RGB"))
125
+ # Replace with [IMG] tokens placeholder
126
+ img_placeholder = " ".join([self.img_token] * self.img_token_count)
127
+ text_parts.append(img_placeholder)
128
+ elif block.get("type") == "text":
129
+ text_parts.append(block["text"])
130
+
131
+ text_messages.append({"role": role, "content": " ".join(text_parts)})
132
+
133
+ # If caller provided images explicitly, use those
134
+ if images is not None:
135
+ extracted_images = images
136
+
137
+ # ---- Build prompt string via tokenizer's chat template ------- #
138
+ prompt_text = self.tokenizer.apply_chat_template(
139
+ text_messages,
140
+ tokenize=False,
141
+ add_generation_prompt=add_generation_prompt,
142
+ enable_thinking=enable_thinking,
143
+ )
144
+
145
+ if not tokenize:
146
+ return prompt_text # type: ignore
147
+
148
+ # ---- Tokenise text ------------------------------------------- #
149
+ encoding = self.tokenizer(
150
+ prompt_text,
151
+ return_tensors=return_tensors,
152
+ padding=padding,
153
+ truncation=truncation,
154
+ max_length=max_length,
155
+ add_special_tokens=False,
156
+ )
157
+
158
+ # ---- Process images ------------------------------------------ #
159
+ if extracted_images:
160
+ pixel_values = self.image_processor(
161
+ images=extracted_images, return_tensors=return_tensors
162
+ )["pixel_values"]
163
+ encoding["pixel_values"] = pixel_values
164
+ else:
165
+ # No image supplied — caller must add pixel_values separately
166
+ pass
167
+
168
+ if return_dict:
169
+ return BatchEncoding(encoding)
170
+ return encoding
171
+
172
+ # ------------------------------------------------------------------ #
173
+ # Standard __call__ #
174
+ # ------------------------------------------------------------------ #
175
+
176
+ def __call__(
177
+ self,
178
+ text: Optional[Union[str, List[str]]] = None,
179
+ images: Optional[Union[Image.Image, List[Image.Image]]] = None,
180
+ return_tensors: Optional[str] = "pt",
181
+ padding: Union[bool, str] = True,
182
+ truncation: bool = True,
183
+ max_length: int = 512,
184
+ **kwargs,
185
+ ) -> BatchEncoding:
186
+
187
+ encoding = {}
188
+
189
+ if text is not None:
190
+ text_enc = self.tokenizer(
191
+ text,
192
+ return_tensors=return_tensors,
193
+ padding=padding,
194
+ truncation=truncation,
195
+ max_length=max_length,
196
+ **kwargs,
197
+ )
198
+ encoding.update(text_enc)
199
+
200
+ if images is not None:
201
+ if isinstance(images, Image.Image):
202
+ images = [images]
203
+ pixel_values = self.image_processor(
204
+ images=images, return_tensors=return_tensors
205
+ )["pixel_values"]
206
+ encoding["pixel_values"] = pixel_values
207
+
208
+ return BatchEncoding(encoding)
209
+
210
+ def decode(self, *args, **kwargs):
211
+ return self.tokenizer.decode(*args, **kwargs)
212
+
213
+ def batch_decode(self, *args, **kwargs):
214
+ return self.tokenizer.batch_decode(*args, **kwargs)
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a1519b02f3a5c1885951e21c729ecde6c737fd4b176697f41f4418bd9e7642c
3
+ size 11423022
tokenizer_config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": null,
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|im_end|>",
7
+ "errors": "replace",
8
+ "is_local": false,
9
+ "model_max_length": 262144,
10
+ "pad_token": "<|PAD_TOKEN|>",
11
+ "padding_side": "left",
12
+ "split_special_tokens": false,
13
+ "tokenizer_class": "Qwen2Tokenizer",
14
+ "unk_token": null
15
+ }