UWGZQ commited on
Commit
723de0d
·
verified ·
1 Parent(s): 2fd2663

ConCor-1: weights, remote code, processor and model card

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ 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
+ example.png filter=lfs diff=lfs merge=lfs -text
37
+ example_2.png filter=lfs diff=lfs merge=lfs -text
38
+ model.jpg filter=lfs diff=lfs merge=lfs -text
39
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ license: apache-2.0
4
+ language:
5
+ - en
6
+ pipeline_tag: image-segmentation
7
+ base_model:
8
+ - Qwen/Qwen3.5-0.8B
9
+ tags:
10
+ - vision-language
11
+ - multimodal
12
+ - vision-language-grounding
13
+ - concept-correspondence
14
+ - phrase-grounding
15
+ - referring-expression-segmentation
16
+ - open-vocabulary-segmentation
17
+ ---
18
+
19
+ # ConCor-1
20
+
21
+ ## Vision-Language Grounding as Bidirectional Concept Correspondence
22
+
23
+ [Jieyu Zhang](https://jieyuz2.github.io)<sup>1</sup>\*,
24
+ [Ziqi Gao](https://uwgzq.github.io)<sup>1,2</sup>\*,
25
+ [Luke Zettlemoyer](https://homes.cs.washington.edu/~lsz/)<sup>1,3</sup>,
26
+ [Ranjay Krishna](https://www.ranjaykrishna.com/)<sup>1</sup>
27
+
28
+ <sup>1</sup>University of Washington · <sup>2</sup>Allen Institute for AI · <sup>3</sup>FAIR at Meta
29
+ \*Equal contribution
30
+
31
+ **Quick links**
32
+
33
+ 📄 [Paper](https://arxiv.org) ·
34
+ 🌐 [Project page](https://uwgzq.github.io/papers/ConCor-1/) ·
35
+ 💻 [GitHub](https://github.com/uwGZQ/ConCor-1) ·
36
+ 🤗 [Data](https://huggingface.co/datasets/UWGZQ/ConCor-1-Data) ·
37
+ 🎨 [Demo](https://huggingface.co/spaces/UWGZQ/ConCor-1-demo)
38
+
39
+ ## Model Summary
40
+
41
+ ConCor-1 is a vision-language grounding model designed to explicitly predict **bidirectional concept correspondences** between language and visual content. Built on a pretrained Qwen3.5-0.8B vision-language backbone, it introduces a set of learnable bridge tokens, each representing a candidate text–image correspondence. By jointly attending to visual and textual tokens, these bridge tokens aggregate multimodal context to represent potential alignments between image regions and text spans. For each bridge token, ConCor-1 simultaneously predicts a text mask identifying the grounded text segment, an image mask localizing the corresponding visual instance, and a presence score indicating whether the proposed pairing is a valid correspondence.
42
+
43
+ ![ConCor-1 architecture: vision tokens from the ViT and language tokens from the text enter the pretrained VLM alongside a set of learnable bridge tokens; each bridge token's output state feeds a vision segmentation head, a text segmentation head and a presence head, producing one image mask, one text mask and one presence score per candidate correspondence.](model.jpg)
44
+
45
+
46
+ ## Usage
47
+
48
+ ### Environment
49
+
50
+ The released checkpoint is supported with the following core inference stack:
51
+
52
+ | Dependency | Supported version |
53
+ |---|---|
54
+ | Python | >=3.11 |
55
+ | PyTorch | 2.8.0 |
56
+ | torchvision | 0.23.0 |
57
+ | Transformers | 5.3.0 |
58
+
59
+ Install a PyTorch/torchvision build compatible with your CUDA environment first. Then install the remaining inference dependencies:
60
+
61
+ ```bash
62
+ pip install "transformers==5.3.0" "safetensors>=0.4.0" \
63
+ "huggingface_hub>=0.30.0" "numpy>=1.24" "pillow>=10.0" \
64
+ "flash-linear-attention>=0.4.1"
65
+ ```
66
+
67
+ The example below uses PyTorch SDPA for the backbone's full-attention layers. To use FlashAttention-2, install `flash-attn>=2.8.0` and set `attn_implementation="flash_attention_2"`. `causal-conv1d>=1.4.0` is an optional acceleration dependency for the linear-attention layers.
68
+
69
+ ### Image + caption
70
+
71
+ ```python
72
+ import torch
73
+ from PIL import Image
74
+ from transformers import AutoModel, AutoProcessor
75
+
76
+ model_id = "UWGZQ/ConCor-1" # or a local path to this repository
77
+
78
+ processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
79
+ model = AutoModel.from_pretrained(
80
+ model_id,
81
+ trust_remote_code=True,
82
+ dtype=torch.bfloat16,
83
+ attn_implementation="sdpa", # "flash_attention_2" also works
84
+ ).to("cuda").eval()
85
+
86
+ image = Image.open("example.png").convert("RGB")
87
+ text = (
88
+ "This image depicts a close-up of a brown bear in a natural outdoor setting. "
89
+ "The background consists of lush green grass. In the foreground, a large brown bear "
90
+ "is positioned centrally."
91
+ )
92
+ # A referring expression works the same way:
93
+ # text = "the large brown bear in the foreground"
94
+
95
+ inputs = processor(images=image, text=text, return_tensors="pt").to("cuda")
96
+ with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
97
+ outputs = model(**inputs)
98
+
99
+ correspondences = processor.post_process_correspondences(
100
+ outputs, text=text, target_sizes=[(image.height, image.width)]
101
+ )[0]
102
+
103
+ for correspondence in correspondences:
104
+ print(
105
+ f"{correspondence['presence_score']:.3f}",
106
+ correspondence["text_phrases"], # phrases of this correspondence's text mask
107
+ correspondence["text_spans"], # character spans into `text`
108
+ correspondence["mask"].shape, # bool array, (height, width)
109
+ )
110
+ ```
111
+
112
+
113
+ `post_process_correspondences` also takes `presence_threshold`, `text_threshold`, `image_threshold` and `nms_iou_threshold` (defaults `0.1`, `0.45`, `0.45`, `0.5`).
114
+
115
+ `example_inference.py` wraps this up as a script, including a mask-overlay renderer:
116
+
117
+ ```bash
118
+ python example_inference.py \
119
+ --image example.png \
120
+ --text "This image depicts a close-up of a brown bear in a natural outdoor setting. The background consists of lush green grass. In the foreground, a large brown bear is positioned centrally." \
121
+ --output overlay.png
122
+ ```
123
+
124
+
125
+ ### Image + category list
126
+
127
+ ```python
128
+ image = Image.open("example_2.png").convert("RGB")
129
+ text = "baseball player . baseball glove . grass . fence . dog"
130
+
131
+ inputs = processor(images=image, text=text, return_tensors="pt").to("cuda")
132
+ with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
133
+ outputs = model(**inputs)
134
+
135
+ for correspondence in processor.post_process_correspondences(
136
+ outputs, text=text, target_sizes=[(image.height, image.width)]
137
+ )[0]:
138
+ print(f"{correspondence['presence_score']:.3f}", correspondence["text_phrases"])
139
+ ```
140
+
141
+
142
+ ### Batched image–text pairs
143
+
144
+ ```python
145
+ images = [Image.open("example.png").convert("RGB"), Image.open("example_2.png").convert("RGB")]
146
+ texts = [
147
+ "This image depicts a close-up of a brown bear in a natural outdoor setting. "
148
+ "The background consists of lush green grass. In the foreground, a large brown bear "
149
+ "is positioned centrally.",
150
+ "baseball player . baseball glove . grass . fence . dog",
151
+ ]
152
+
153
+ inputs = processor(images=images, text=texts, return_tensors="pt").to("cuda")
154
+ with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
155
+ outputs = model(**inputs)
156
+
157
+ results = processor.post_process_correspondences(
158
+ outputs,
159
+ text=texts, # the same list of texts
160
+ target_sizes=[(image.height, image.width) for image in images], # one (height, width) per sample
161
+ )
162
+
163
+ for index, correspondences in enumerate(results):
164
+ print(f"sample {index}: {len(correspondences)} correspondence(s)")
165
+ for correspondence in correspondences:
166
+ print(f" {correspondence['presence_score']:.3f}", correspondence["text_phrases"])
167
+ ```
168
+
169
+
170
+
171
+ ## Files
172
+
173
+ | File | Purpose |
174
+ |---|---|
175
+ | `configuration_concor1.py` | `ConCor1Config` |
176
+ | `modeling_concor1.py` | `ConCor1ForConceptCorrespondence` and its heads |
177
+ | `processing_concor1.py` | `ConCor1Processor`: sequence construction + correspondence post-processing |
178
+ | `example_inference.py` | image + text → correspondences, with mask overlay |
179
+ | `example.png`, `example_2.png` | the images used in the examples above |
180
+ | `model.jpg` | the architecture figure above |
181
+ | `model.safetensors` | weights: bf16 backbone, fp32 prediction heads |
182
+ | `requirements.txt` | the inference dependencies listed above |
183
+
184
+
185
+
186
+ ## Demo
187
+
188
+ An interactive ZeroGPU demo lives at
189
+ [`UWGZQ/ConCor-1-demo`](https://huggingface.co/spaces/UWGZQ/ConCor-1-demo).
190
+
191
+
192
+ ## License and Use
193
+
194
+ The weights and the code in this repository are released under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. The backbone is [Qwen3.5-0.8B](https://huggingface.co/Qwen/Qwen3.5-0.8B), also Apache 2.0.
195
+
196
+
197
+
198
+ ## Citation
199
+
200
+ ```bibtex
201
+ @article{zhang2026concor,
202
+ title = {Vision-Language Grounding as Bidirectional Concept Correspondence},
203
+ author = {Zhang, Jieyu and Gao, Ziqi and Zettlemoyer, Luke and Krishna, Ranjay},
204
+ year = {2026}
205
+ }
206
+ ```
config.json ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "ConCor1ForConceptCorrespondence"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_concor1.ConCor1Config",
7
+ "AutoModel": "modeling_concor1.ConCor1ForConceptCorrespondence",
8
+ "AutoProcessor": "processing_concor1.ConCor1Processor"
9
+ },
10
+ "backbone_config": {
11
+ "image_token_id": 248056,
12
+ "model_type": "qwen3_5",
13
+ "text_config": {
14
+ "_name_or_path": "",
15
+ "architectures": null,
16
+ "attention_bias": false,
17
+ "attention_dropout": 0.0,
18
+ "attn_output_gate": true,
19
+ "bos_token_id": null,
20
+ "chunk_size_feed_forward": 0,
21
+ "dtype": "bfloat16",
22
+ "eos_token_id": 248044,
23
+ "full_attention_interval": 4,
24
+ "head_dim": 256,
25
+ "hidden_act": "silu",
26
+ "hidden_size": 1024,
27
+ "id2label": {
28
+ "0": "LABEL_0",
29
+ "1": "LABEL_1"
30
+ },
31
+ "initializer_range": 0.02,
32
+ "intermediate_size": 3584,
33
+ "is_encoder_decoder": false,
34
+ "label2id": {
35
+ "LABEL_0": 0,
36
+ "LABEL_1": 1
37
+ },
38
+ "layer_types": [
39
+ "linear_attention",
40
+ "linear_attention",
41
+ "linear_attention",
42
+ "full_attention",
43
+ "linear_attention",
44
+ "linear_attention",
45
+ "linear_attention",
46
+ "full_attention",
47
+ "linear_attention",
48
+ "linear_attention",
49
+ "linear_attention",
50
+ "full_attention",
51
+ "linear_attention",
52
+ "linear_attention",
53
+ "linear_attention",
54
+ "full_attention",
55
+ "linear_attention",
56
+ "linear_attention",
57
+ "linear_attention",
58
+ "full_attention",
59
+ "linear_attention",
60
+ "linear_attention",
61
+ "linear_attention",
62
+ "full_attention"
63
+ ],
64
+ "linear_conv_kernel_dim": 4,
65
+ "linear_key_head_dim": 128,
66
+ "linear_num_key_heads": 16,
67
+ "linear_num_value_heads": 16,
68
+ "linear_value_head_dim": 128,
69
+ "mamba_ssm_dtype": "float32",
70
+ "max_position_embeddings": 262144,
71
+ "mlp_only_layers": [],
72
+ "model_type": "qwen3_5_text",
73
+ "mtp_num_hidden_layers": 1,
74
+ "mtp_use_dedicated_embeddings": false,
75
+ "num_attention_heads": 8,
76
+ "num_hidden_layers": 24,
77
+ "num_key_value_heads": 2,
78
+ "output_attentions": false,
79
+ "output_hidden_states": false,
80
+ "pad_token_id": null,
81
+ "partial_rotary_factor": 0.25,
82
+ "problem_type": null,
83
+ "return_dict": true,
84
+ "rms_norm_eps": 1e-06,
85
+ "rope_parameters": {
86
+ "mrope_interleaved": true,
87
+ "mrope_section": [
88
+ 11,
89
+ 11,
90
+ 10
91
+ ],
92
+ "partial_rotary_factor": 0.25,
93
+ "rope_theta": 10000000,
94
+ "rope_type": "default"
95
+ },
96
+ "tie_word_embeddings": true,
97
+ "use_cache": true,
98
+ "vocab_size": 248462
99
+ },
100
+ "tie_word_embeddings": true,
101
+ "video_token_id": 248057,
102
+ "vision_config": {
103
+ "_name_or_path": "",
104
+ "architectures": null,
105
+ "chunk_size_feed_forward": 0,
106
+ "deepstack_visual_indexes": [],
107
+ "depth": 12,
108
+ "dtype": null,
109
+ "hidden_act": "gelu_pytorch_tanh",
110
+ "hidden_size": 768,
111
+ "id2label": {
112
+ "0": "LABEL_0",
113
+ "1": "LABEL_1"
114
+ },
115
+ "in_channels": 3,
116
+ "initializer_range": 0.02,
117
+ "intermediate_size": 3072,
118
+ "is_encoder_decoder": false,
119
+ "label2id": {
120
+ "LABEL_0": 0,
121
+ "LABEL_1": 1
122
+ },
123
+ "model_type": "qwen3_5",
124
+ "num_heads": 12,
125
+ "num_position_embeddings": 2304,
126
+ "out_hidden_size": 1024,
127
+ "output_attentions": false,
128
+ "output_hidden_states": false,
129
+ "patch_size": 16,
130
+ "problem_type": null,
131
+ "return_dict": true,
132
+ "spatial_merge_size": 2,
133
+ "temporal_patch_size": 2
134
+ },
135
+ "vision_end_token_id": 248054,
136
+ "vision_start_token_id": 248053,
137
+ "vocab_size": 248462
138
+ },
139
+ "bidirectional_full_attention": true,
140
+ "bridge_grid_levels": [
141
+ 1,
142
+ 2,
143
+ 3,
144
+ 4,
145
+ 5,
146
+ 6,
147
+ 7,
148
+ 8,
149
+ 9,
150
+ 10
151
+ ],
152
+ "bridge_token_id_start": 248077,
153
+ "correspondence_dim": 256,
154
+ "dtype": "bfloat16",
155
+ "fuse_vision_encoder_features": true,
156
+ "image_max_pixels": 1003520,
157
+ "image_min_pixels": 1003520,
158
+ "image_threshold": 0.45,
159
+ "merge_size": 2,
160
+ "model_type": "concor1",
161
+ "nms_iou_threshold": 0.5,
162
+ "num_bridge_tokens": 385,
163
+ "num_mask_upsample_blocks": 2,
164
+ "patch_size": 16,
165
+ "presence_hidden_dim": 256,
166
+ "presence_threshold": 0.1,
167
+ "text_threshold": 0.45,
168
+ "transformers_version": "5.3.0"
169
+ }
configuration_concor1.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2026 The ConCor-1 authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """ConCor-1 configuration.
16
+
17
+ ConCor-1 is the concept-correspondence model of *Vision-Language Grounding as
18
+ Bidirectional Concept Correspondence*. It wraps a pretrained Qwen3.5-0.8B
19
+ vision-language backbone, appends ``num_bridge_tokens`` learnable **bridge
20
+ tokens** to the multimodal sequence, and predicts, for every bridge token, an
21
+ image mask, a text mask, and a correspondence presence score.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import List, Optional, Sequence, Union
27
+
28
+ from transformers.configuration_utils import PretrainedConfig
29
+ from transformers.models.qwen3_5.configuration_qwen3_5 import Qwen3_5Config
30
+
31
+
32
+ class ConCor1Config(PretrainedConfig):
33
+ r"""Configuration class for [`ConCor1ForConceptCorrespondence`].
34
+
35
+ Args:
36
+ backbone_config (`Union[Qwen3_5Config, dict]`, *optional*):
37
+ Configuration of the Qwen3.5 vision-language backbone used as a
38
+ contextual image-text encoder. Defaults to the Qwen3.5-0.8B
39
+ architecture.
40
+ num_bridge_tokens (`int`, *optional*, defaults to 385):
41
+ Number of bridge tokens `Q` appended after the image and text
42
+ tokens. Each bridge token stands for one candidate image-text
43
+ correspondence. With the default multi-scale grid levels,
44
+ `Q = sum(s**2 for s in 1..10) = 385`.
45
+ bridge_grid_levels (`Sequence[int]`, *optional*, defaults to `(1, ..., 10)`):
46
+ Multi-scale spatial grid levels the bridge tokens are organised
47
+ into. Level `s` contributes `s**2` bridge tokens, ordered in raster
48
+ order over an `s x s` grid. Only used for the training-time
49
+ Hungarian assignment and for interpreting a bridge index; the
50
+ forward pass itself does not depend on it.
51
+ bridge_token_id_start (`int`, *optional*, defaults to 248077):
52
+ First token id used for bridge tokens. Qwen3.5's tokenizer defines
53
+ 248077 tokens while its embedding table has 248320 rows, so ids
54
+ from 248077 on are unused vocabulary slots; the embedding table is
55
+ grown when `num_bridge_tokens` needs more than the reserved slots.
56
+ correspondence_dim (`int`, *optional*, defaults to 256):
57
+ Dimension of the shared correspondence space that bridge, text and
58
+ visual features are projected into before the bilinear scorers.
59
+ presence_hidden_dim (`int`, *optional*, defaults to 256):
60
+ Hidden dimension of the presence head's SwiGLU MLP.
61
+ patch_size (`int`, *optional*, defaults to 16):
62
+ Vision-encoder patch size, in pixels.
63
+ merge_size (`int`, *optional*, defaults to 2):
64
+ Spatial merge factor of the backbone's patch merger. One merged
65
+ visual token therefore covers `patch_size * merge_size` pixels per
66
+ side (32 px for the released model).
67
+ num_mask_upsample_blocks (`int`, *optional*, defaults to 2):
68
+ Number of transposed-convolution blocks in the vision segmentation
69
+ head's convolutional decoder. Each block upsamples by 2, so image
70
+ masks are predicted on a grid of
71
+ `patch_size / 2**num_mask_upsample_blocks` pixel cells (4 px).
72
+ fuse_vision_encoder_features (`bool`, *optional*, defaults to `True`):
73
+ Fuse pre-merger ViT patch features into the expanded patch-level
74
+ features to restore local spatial detail.
75
+ bidirectional_full_attention (`bool`, *optional*, defaults to `True`):
76
+ Run the backbone's full-attention layers with a bidirectional mask
77
+ so bridge tokens can access the complete multimodal context. The
78
+ linear-attention layers keep their original behaviour. The released
79
+ checkpoint was trained this way; setting this to `False` reproduces
80
+ stock causal attention and will degrade predictions badly.
81
+ image_min_pixels (`int`, *optional*, defaults to 1003520):
82
+ `min_pixels` passed to the image processor; the released checkpoint
83
+ was trained and evaluated with `min_pixels == max_pixels`, i.e. a
84
+ fixed ~1.0 M pixel budget (1024 visual tokens).
85
+ image_max_pixels (`int`, *optional*, defaults to 1003520):
86
+ `max_pixels` passed to the image processor.
87
+ presence_threshold (`float`, *optional*, defaults to 0.1):
88
+ Default presence-score threshold used by the processor's
89
+ post-processing (paper: 0.1).
90
+ text_threshold (`float`, *optional*, defaults to 0.45):
91
+ Default text-mask probability threshold (paper: 0.45).
92
+ image_threshold (`float`, *optional*, defaults to 0.45):
93
+ Default image-mask probability threshold (paper: 0.45).
94
+ nms_iou_threshold (`float`, *optional*, defaults to 0.5):
95
+ Default IoU threshold for the correspondence NMS that removes
96
+ duplicate bridge predictions (paper: 0.5).
97
+
98
+ Example:
99
+
100
+ ```python
101
+ >>> from transformers import AutoConfig, AutoModel
102
+ >>> config = AutoConfig.from_pretrained("UWGZQ/ConCor-1", trust_remote_code=True)
103
+ >>> config.num_bridge_tokens
104
+ 385
105
+ ```
106
+ """
107
+
108
+ model_type = "concor1"
109
+ sub_configs = {"backbone_config": Qwen3_5Config}
110
+
111
+ def __init__(
112
+ self,
113
+ backbone_config: Optional[Union[Qwen3_5Config, dict]] = None,
114
+ num_bridge_tokens: int = 385,
115
+ bridge_grid_levels: Sequence[int] = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
116
+ bridge_token_id_start: int = 248077,
117
+ correspondence_dim: int = 256,
118
+ presence_hidden_dim: int = 256,
119
+ patch_size: int = 16,
120
+ merge_size: int = 2,
121
+ num_mask_upsample_blocks: int = 2,
122
+ fuse_vision_encoder_features: bool = True,
123
+ bidirectional_full_attention: bool = True,
124
+ image_min_pixels: int = 1003520,
125
+ image_max_pixels: int = 1003520,
126
+ presence_threshold: float = 0.1,
127
+ text_threshold: float = 0.45,
128
+ image_threshold: float = 0.45,
129
+ nms_iou_threshold: float = 0.5,
130
+ **kwargs,
131
+ ):
132
+ if backbone_config is None:
133
+ backbone_config = Qwen3_5Config()
134
+ elif isinstance(backbone_config, dict):
135
+ backbone_config = Qwen3_5Config(**backbone_config)
136
+ self.backbone_config = backbone_config
137
+
138
+ self.num_bridge_tokens = num_bridge_tokens
139
+ self.bridge_grid_levels = list(bridge_grid_levels)
140
+ self.bridge_token_id_start = bridge_token_id_start
141
+ self.correspondence_dim = correspondence_dim
142
+ self.presence_hidden_dim = presence_hidden_dim
143
+ self.patch_size = patch_size
144
+ self.merge_size = merge_size
145
+ self.num_mask_upsample_blocks = num_mask_upsample_blocks
146
+ self.fuse_vision_encoder_features = fuse_vision_encoder_features
147
+ self.bidirectional_full_attention = bidirectional_full_attention
148
+ self.image_min_pixels = image_min_pixels
149
+ self.image_max_pixels = image_max_pixels
150
+ self.presence_threshold = presence_threshold
151
+ self.text_threshold = text_threshold
152
+ self.image_threshold = image_threshold
153
+ self.nms_iou_threshold = nms_iou_threshold
154
+
155
+ super().__init__(**kwargs)
156
+
157
+ if self.bridge_grid_levels:
158
+ expected = sum(s * s for s in self.bridge_grid_levels)
159
+ if expected != self.num_bridge_tokens:
160
+ raise ValueError(
161
+ f"num_bridge_tokens={self.num_bridge_tokens} does not match "
162
+ f"bridge_grid_levels={self.bridge_grid_levels} (sum of s^2 = {expected})."
163
+ )
164
+
165
+ @property
166
+ def hidden_size(self) -> int:
167
+ """Backbone hidden size (bridge / text token feature dim)."""
168
+ return self.backbone_config.text_config.hidden_size
169
+
170
+ @property
171
+ def vision_hidden_size(self) -> int:
172
+ """Pre-merger ViT hidden size (patch-level feature dim)."""
173
+ return self.backbone_config.vision_config.hidden_size
174
+
175
+ @property
176
+ def bridge_token_ids(self) -> List[int]:
177
+ """Token ids of the `num_bridge_tokens` bridge slots, in order."""
178
+ start = self.bridge_token_id_start
179
+ return list(range(start, start + self.num_bridge_tokens))
180
+
181
+ @property
182
+ def merged_token_size(self) -> int:
183
+ """Pixels per side covered by one merged visual token (32 px)."""
184
+ return self.patch_size * self.merge_size
185
+
186
+ @property
187
+ def mask_cell_size(self) -> int:
188
+ """Pixels per side of one image-mask cell (4 px for the released model)."""
189
+ return self.patch_size // (2 ** self.num_mask_upsample_blocks)
190
+
191
+ @property
192
+ def bridge_cells(self) -> List[tuple]:
193
+ """`(level, row, col)` of every bridge token, in bridge order."""
194
+ cells = []
195
+ for level in self.bridge_grid_levels:
196
+ for row in range(level):
197
+ for col in range(level):
198
+ cells.append((level, row, col))
199
+ return cells
200
+
201
+
202
+ __all__ = ["ConCor1Config"]
example.png ADDED

Git LFS Details

  • SHA256: f129f1aa71773c9a9c759a2395da79411f918017b95da4224ff411e7073b11ed
  • Pointer size: 131 Bytes
  • Size of remote file: 885 kB
example_2.png ADDED

Git LFS Details

  • SHA256: 142197fd36f242511c43db0ba6919c9a4c5fd899e7070210b633873b792b7693
  • Pointer size: 131 Bytes
  • Size of remote file: 831 kB
example_inference.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+ """ConCor-1 inference example: an image + its text in, correspondences out.
4
+
5
+ Given an image and a paired text, ConCor-1 predicts the full set of
6
+ correspondences between visually referential text spans and instance-level
7
+ image segments — the text spans are *not* given as queries; the model decides
8
+ which parts of the text are grounded.
9
+
10
+ Usage (the bundled `example.png` with its COCONut-PanCap caption):
11
+ python example_inference.py --image example.png \
12
+ --text "This image depicts a close-up of a brown bear in a natural outdoor setting. \
13
+ The background consists of lush green grass. In the foreground, a large brown bear is \
14
+ positioned centrally."
15
+
16
+ # a category list works just as well as a caption
17
+ python example_inference.py --image example.png --text "bear . grass . tree . person"
18
+
19
+ # write a mask overlay next to the printed correspondences
20
+ python example_inference.py --image example.png --text "..." --output overlay.png
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ from pathlib import Path
27
+ from typing import List
28
+
29
+ import numpy as np
30
+ import torch
31
+ from PIL import Image, ImageDraw
32
+ from transformers import AutoModel, AutoProcessor
33
+
34
+ # Visually distinct overlay colours.
35
+ COLORS = [
36
+ (239, 64, 64), (64, 204, 64), (64, 115, 255), (255, 204, 0), (255, 115, 0),
37
+ (217, 51, 217), (0, 204, 217), (153, 89, 13), (102, 255, 102), (140, 26, 255),
38
+ (255, 153, 204), (0, 140, 0), (179, 179, 0), (0, 26, 153), (204, 140, 51),
39
+ (128, 128, 128), (255, 0, 128), (0, 255, 140), (140, 0, 0), (0, 140, 140),
40
+ ]
41
+
42
+
43
+ def overlay_masks(image: Image.Image, correspondences: List[dict], alpha: float = 0.5) -> Image.Image:
44
+ """Blend each correspondence's mask over the image and label it with its phrases."""
45
+ canvas = np.array(image.convert("RGB"), dtype=np.float32)
46
+ for index, correspondence in enumerate(correspondences):
47
+ mask = correspondence.get("mask")
48
+ if mask is None or not mask.any():
49
+ continue
50
+ color = np.array(COLORS[index % len(COLORS)], dtype=np.float32)
51
+ canvas[mask] = (1.0 - alpha) * canvas[mask] + alpha * color
52
+
53
+ overlaid = Image.fromarray(canvas.astype(np.uint8))
54
+ draw = ImageDraw.Draw(overlaid)
55
+ for index, correspondence in enumerate(correspondences):
56
+ mask = correspondence.get("mask")
57
+ if mask is None or not mask.any():
58
+ continue
59
+ rows, columns = np.nonzero(mask)
60
+ label = " / ".join(correspondence["text_phrases"]) or "(no text span)"
61
+ label = f"{label} {correspondence['presence_score']:.2f}"
62
+ anchor = (int(columns.min()), max(int(rows.min()) - 12, 0))
63
+ draw.text(anchor, label, fill=COLORS[index % len(COLORS)])
64
+ return overlaid
65
+
66
+
67
+ def main() -> None:
68
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
69
+ parser.add_argument("--model", default=str(Path(__file__).parent), help="model repo id or local path")
70
+ parser.add_argument("--image", required=True, type=Path)
71
+ parser.add_argument("--text", required=True, help="the text to ground (caption, category list, referring expression)")
72
+ parser.add_argument("--output", type=Path, default=None, help="write a mask overlay here (PNG)")
73
+ parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
74
+ parser.add_argument(
75
+ "--attn_implementation",
76
+ default="sdpa",
77
+ choices=["flash_attention_2", "sdpa", "eager"],
78
+ help="sdpa needs no extra dependency; flash_attention_2 reproduces the paper's numbers exactly",
79
+ )
80
+ parser.add_argument("--presence_threshold", type=float, default=None, help="default 0.1")
81
+ parser.add_argument("--text_threshold", type=float, default=None, help="default 0.45")
82
+ parser.add_argument("--image_threshold", type=float, default=None, help="default 0.45")
83
+ parser.add_argument("--nms_iou_threshold", type=float, default=None, help="default 0.5")
84
+ args = parser.parse_args()
85
+
86
+ processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True)
87
+ model = AutoModel.from_pretrained(
88
+ args.model,
89
+ trust_remote_code=True,
90
+ dtype=torch.bfloat16,
91
+ attn_implementation=args.attn_implementation,
92
+ ).to(args.device).eval()
93
+
94
+ image = Image.open(args.image).convert("RGB")
95
+ inputs = processor(images=image, text=args.text, return_tensors="pt").to(args.device)
96
+
97
+ with torch.inference_mode(), torch.autocast(args.device, dtype=torch.bfloat16):
98
+ outputs = model(**inputs)
99
+
100
+ correspondences = processor.post_process_correspondences(
101
+ outputs,
102
+ text=args.text,
103
+ target_sizes=[(image.height, image.width)],
104
+ presence_threshold=args.presence_threshold,
105
+ text_threshold=args.text_threshold,
106
+ image_threshold=args.image_threshold,
107
+ nms_iou_threshold=args.nms_iou_threshold,
108
+ )[0]
109
+
110
+ print(f"\nimage: {args.image} ({image.width}x{image.height})")
111
+ print(f"text: {args.text}")
112
+ print(f"\n{len(correspondences)} correspondence(s):")
113
+ for correspondence in correspondences:
114
+ mask = correspondence["mask"]
115
+ phrases = " / ".join(correspondence["text_phrases"]) or "(no text span)"
116
+ area = 100.0 * mask.mean() if mask is not None else 0.0
117
+ print(
118
+ f" presence={correspondence['presence_score']:.3f} "
119
+ f"bridge={correspondence['bridge_index']:3d} "
120
+ f"mask={area:5.2f}% of image spans={correspondence['text_spans']} | {phrases}"
121
+ )
122
+
123
+ if args.output is not None:
124
+ overlay_masks(image, correspondences).save(args.output)
125
+ print(f"\nwrote {args.output}")
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model.jpg ADDED

Git LFS Details

  • SHA256: 2f6ea9bd99c13bed37ff739d2627c512f4a8154d2d690a6484718566ab1f6504
  • Pointer size: 131 Bytes
  • Size of remote file: 229 kB
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4bdd5c7f935c7288241191066d0d3860d82ffe0d371071dd3f010e5317d9aab4
3
+ size 1760946056
modeling_concor1.py ADDED
@@ -0,0 +1,804 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2026 The ConCor-1 authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """PyTorch ConCor-1 model — vision-language grounding as bidirectional concept
16
+ correspondence.
17
+
18
+ ConCor-1 uses a pretrained Qwen3.5-0.8B vision-language model as a *contextual
19
+ image-text encoder* (not as a generator) and appends `Q` learnable **bridge
20
+ tokens** to the multimodal sequence:
21
+
22
+ [<vision_start>, <image_pad> x N_v, <vision_end>, text tokens x N_t, bridge tokens x Q]
23
+
24
+ For every bridge token, three lightweight heads predict one candidate
25
+ image-text correspondence:
26
+
27
+ * `presence_head` — a scalar presence score: is this a valid correspondence?
28
+ * `text_segmentation_head` — a binary mask over the input text tokens
29
+ * `vision_segmentation_head` — a binary mask over the image, on a 4-pixel cell grid
30
+
31
+ The backbone's full-attention layers are run with a **bidirectional** mask so
32
+ that bridge tokens see the complete multimodal context and can differentiate
33
+ from one another; the linear-attention (gated delta-net) layers keep their
34
+ original behaviour. This model is therefore not autoregressive: it does not
35
+ support `use_cache`, `past_key_values` or `generate()`.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import math
41
+ from dataclasses import dataclass
42
+ from typing import List, Optional, Tuple
43
+
44
+ import torch
45
+ import torch.nn as nn
46
+ import torch.nn.functional as F
47
+
48
+ from transformers.masking_utils import create_bidirectional_mask
49
+ from transformers.modeling_outputs import ModelOutput
50
+ from transformers.modeling_utils import PreTrainedModel
51
+ from transformers.models.qwen3_5 import modeling_qwen3_5 as qwen3_5
52
+ from transformers.models.qwen3_5.modeling_qwen3_5 import (
53
+ Qwen3_5Model,
54
+ Qwen3_5ModelOutputWithPast,
55
+ Qwen3_5TextModel,
56
+ )
57
+ from transformers.utils import logging
58
+
59
+ from .configuration_concor1 import ConCor1Config
60
+
61
+ logger = logging.get_logger(__name__)
62
+
63
+
64
+ # ══════════════════════════════════════════════════════════════════════════════
65
+ # Backbone: Qwen3.5 with bidirectional full-attention layers
66
+ # ══════════════════════════════════════════════════════════════════════════════
67
+
68
+
69
+ class ConCor1BidirectionalTextModel(Qwen3_5TextModel):
70
+ """Qwen3.5 text model whose *full-attention* layers are bidirectional.
71
+
72
+ Identical to [`Qwen3_5TextModel`] except that the mask handed to the
73
+ full-attention layers is built with `create_bidirectional_mask` instead of
74
+ `create_causal_mask`, and `is_causal=False` is forced so the attention
75
+ backend cannot silently fall back to causal masking. The linear-attention
76
+ layers are untouched, preserving Qwen3.5's pretrained hybrid-attention
77
+ structure.
78
+
79
+ Bidirectional attention is only defined for full-sequence forward passes,
80
+ so KV caching / incremental decoding is rejected.
81
+ """
82
+
83
+ @qwen3_5.merge_with_config_defaults
84
+ @qwen3_5.capture_outputs
85
+ def forward(
86
+ self,
87
+ input_ids: Optional[torch.LongTensor] = None,
88
+ attention_mask: Optional[torch.Tensor] = None,
89
+ position_ids: Optional[torch.LongTensor] = None,
90
+ past_key_values=None,
91
+ inputs_embeds: Optional[torch.FloatTensor] = None,
92
+ use_cache: Optional[bool] = None,
93
+ cache_position: Optional[torch.LongTensor] = None,
94
+ **kwargs,
95
+ ) -> Qwen3_5ModelOutputWithPast:
96
+ bidirectional = kwargs.pop("bidirectional_full_attention", True)
97
+
98
+ if (input_ids is None) ^ (inputs_embeds is not None):
99
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
100
+
101
+ if bidirectional:
102
+ if past_key_values is not None:
103
+ raise ValueError(
104
+ "ConCor-1's bidirectional attention does not support `past_key_values`; "
105
+ "use a full-sequence forward pass."
106
+ )
107
+ if use_cache:
108
+ raise ValueError(
109
+ "ConCor-1's bidirectional attention does not support `use_cache=True` or "
110
+ "autoregressive `generate()`. Pass `use_cache=False`."
111
+ )
112
+ use_cache = False
113
+
114
+ if inputs_embeds is None:
115
+ inputs_embeds = self.embed_tokens(input_ids)
116
+
117
+ if use_cache and past_key_values is None:
118
+ past_key_values = qwen3_5.Qwen3_5DynamicCache(config=self.config)
119
+
120
+ if cache_position is None:
121
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
122
+ cache_position = torch.arange(
123
+ past_seen_tokens,
124
+ past_seen_tokens + inputs_embeds.shape[1],
125
+ device=inputs_embeds.device,
126
+ )
127
+
128
+ if position_ids is None:
129
+ position_ids = cache_position.view(1, 1, -1).expand(4, inputs_embeds.shape[0], -1)
130
+ elif position_ids.ndim == 2:
131
+ position_ids = position_ids[None, ...].expand(4, position_ids.shape[0], -1)
132
+
133
+ if position_ids.ndim == 3 and position_ids.shape[0] == 4:
134
+ text_position_ids = position_ids[0]
135
+ position_ids = position_ids[1:]
136
+ else:
137
+ text_position_ids = None
138
+
139
+ # Linear-attention layers keep the original mask path.
140
+ linear_attn_mask = self._update_linear_attn_mask(attention_mask, cache_position)
141
+
142
+ if bidirectional:
143
+ full_attn_mask = create_bidirectional_mask(
144
+ config=self.config,
145
+ inputs_embeds=inputs_embeds,
146
+ attention_mask=attention_mask,
147
+ )
148
+ else:
149
+ full_attn_mask = qwen3_5.create_causal_mask(
150
+ config=self.config,
151
+ inputs_embeds=inputs_embeds,
152
+ attention_mask=attention_mask,
153
+ cache_position=cache_position,
154
+ past_key_values=past_key_values,
155
+ position_ids=text_position_ids,
156
+ )
157
+
158
+ hidden_states = inputs_embeds
159
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
160
+
161
+ for decoder_layer in self.layers[: self.config.num_hidden_layers]:
162
+ if decoder_layer.layer_type == "linear_attention":
163
+ layer_mask = linear_attn_mask
164
+ layer_kwargs = kwargs
165
+ else:
166
+ layer_mask = full_attn_mask
167
+ layer_kwargs = dict(kwargs)
168
+ if bidirectional:
169
+ layer_kwargs["is_causal"] = False
170
+
171
+ hidden_states = decoder_layer(
172
+ hidden_states,
173
+ position_embeddings=position_embeddings,
174
+ attention_mask=layer_mask,
175
+ position_ids=text_position_ids,
176
+ past_key_values=past_key_values,
177
+ use_cache=use_cache,
178
+ cache_position=cache_position,
179
+ **layer_kwargs,
180
+ )
181
+
182
+ hidden_states = self.norm(hidden_states)
183
+
184
+ return Qwen3_5ModelOutputWithPast(
185
+ last_hidden_state=hidden_states,
186
+ past_key_values=past_key_values,
187
+ )
188
+
189
+
190
+ class ConCor1VisionLanguageBackbone(Qwen3_5Model):
191
+ """Qwen3.5 vision-language backbone with a bidirectional text model."""
192
+
193
+ def __init__(self, config):
194
+ super().__init__(config)
195
+ # Same weights and layout as Qwen3_5TextModel; only the attention mask of
196
+ # the full-attention layers differs (see ConCor1BidirectionalTextModel).
197
+ self.language_model.__class__ = ConCor1BidirectionalTextModel
198
+
199
+
200
+ # ══════════════════════════════════════════════════════════════════════════════
201
+ # Building blocks
202
+ # ══════════════════════════════════════════════════════════════════════════════
203
+
204
+
205
+ class ConCor1SwiGLUProjection(nn.Module):
206
+ """SwiGLU projection `norm(silu(W_g x) * W_u x)`, following the backbone's FFN style."""
207
+
208
+ def __init__(self, in_features: int, out_features: int):
209
+ super().__init__()
210
+ self.gate_proj = nn.Linear(in_features, out_features, bias=False)
211
+ self.up_proj = nn.Linear(in_features, out_features, bias=False)
212
+ self.norm = nn.RMSNorm(out_features)
213
+
214
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
215
+ return self.norm(F.silu(self.gate_proj(hidden_states)) * self.up_proj(hidden_states))
216
+
217
+
218
+ class ConCor1LayerNorm2d(nn.LayerNorm):
219
+ """LayerNorm over the channel dimension of `(B, C, H, W)` tensors."""
220
+
221
+ def __init__(self, num_channels: int, eps: float = 1e-6):
222
+ super().__init__(num_channels, eps=eps, elementwise_affine=True)
223
+
224
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
225
+ hidden_states = hidden_states.permute(0, 2, 3, 1)
226
+ hidden_states = F.layer_norm(
227
+ hidden_states, self.normalized_shape, self.weight, self.bias, self.eps
228
+ )
229
+ return hidden_states.permute(0, 3, 1, 2)
230
+
231
+
232
+ class ConCor1PresenceHead(nn.Module):
233
+ """Presence head: SwiGLU MLP over a bridge token, projected to a scalar logit.
234
+
235
+ `z_pres[j] = W_o · norm(silu(W_g b_j) * W_u b_j)`
236
+ """
237
+
238
+ def __init__(self, hidden_size: int, intermediate_size: int):
239
+ super().__init__()
240
+ self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
241
+ self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
242
+ self.norm = nn.RMSNorm(intermediate_size)
243
+ self.out_proj = nn.Linear(intermediate_size, 1, bias=False)
244
+
245
+ def forward(self, bridge_features: torch.Tensor) -> torch.Tensor:
246
+ bridge_features = bridge_features.to(self.gate_proj.weight.dtype)
247
+ hidden = self.norm(
248
+ F.silu(self.gate_proj(bridge_features)) * self.up_proj(bridge_features)
249
+ )
250
+ return self.out_proj(hidden)
251
+
252
+
253
+ class ConCor1BilinearCorrespondenceScorer(nn.Module):
254
+ """Bilinear scorer between bridge tokens and a sequence of features.
255
+
256
+ Both sides are projected into a shared `correspondence_dim` space with
257
+ independent SwiGLU MLPs, then scored with a learnable bilinear form:
258
+
259
+ z[j, n] = phi_f(f_n)^T W phi_b(b_j)
260
+
261
+ Used both as the text segmentation head (features = text tokens) and as the
262
+ mask predictor of the vision segmentation head (features = decoded spatial
263
+ cells).
264
+ """
265
+
266
+ def __init__(self, feature_dim: int, bridge_dim: int, correspondence_dim: int = 256):
267
+ super().__init__()
268
+ self.correspondence_dim = correspondence_dim
269
+ self.feature_proj = ConCor1SwiGLUProjection(feature_dim, correspondence_dim)
270
+ self.bridge_proj = ConCor1SwiGLUProjection(bridge_dim, correspondence_dim)
271
+ self.bilinear = nn.Bilinear(correspondence_dim, correspondence_dim, 1, bias=False)
272
+
273
+ def forward(self, features: torch.Tensor, bridge_features: torch.Tensor) -> torch.Tensor:
274
+ """
275
+ Args:
276
+ features: `(B, N, feature_dim)`
277
+ bridge_features: `(B, Q, bridge_dim)`
278
+
279
+ Returns:
280
+ `(B, Q, N)` correspondence logits.
281
+ """
282
+ parameter_dtype = self.bilinear.weight.dtype
283
+ features = features.to(parameter_dtype)
284
+ bridge_features = bridge_features.to(parameter_dtype)
285
+ projected_features = self.feature_proj(features) # (B, N, C)
286
+ projected_bridges = self.bridge_proj(bridge_features) # (B, Q, C)
287
+ weight = self.bilinear.weight.squeeze(0) # (C, C)
288
+ logits = torch.einsum("bni, ij, bqj -> bqn", projected_features, weight, projected_bridges)
289
+ return logits.to(projected_features.dtype)
290
+
291
+
292
+ class ConCor1PatchExpander(nn.Module):
293
+ """Expand each merged visual token back into its `merge_size**2` patch features.
294
+
295
+ The backbone merges 2x2 patch neighbourhoods into one visual token, so mask
296
+ prediction over merged tokens would be spatially coarse. Four independent
297
+ SwiGLU MLPs map one merged token (`hidden_size`) to the top-left, top-right,
298
+ bottom-left and bottom-right pre-merger patch features (`vision_hidden_size`).
299
+
300
+ Output order is interleaved: the four children of a merged token are
301
+ contiguous, matching the pre-merger ViT patch order.
302
+ """
303
+
304
+ def __init__(self, hidden_size: int, patch_dim: int, num_children: int = 4):
305
+ super().__init__()
306
+ self.patch_dim = patch_dim
307
+ self.num_children = num_children
308
+ self.branches = nn.ModuleList(
309
+ [ConCor1SwiGLUProjection(hidden_size, patch_dim) for _ in range(num_children)]
310
+ )
311
+
312
+ def forward(
313
+ self,
314
+ visual_features: torch.Tensor,
315
+ patch_features: Optional[torch.Tensor] = None,
316
+ ) -> torch.Tensor:
317
+ """
318
+ Args:
319
+ visual_features: `(B, N_vis, hidden_size)` merged visual tokens.
320
+ patch_features: `(B, N_vis * num_children, patch_dim)`, optional
321
+ pre-merger ViT patch features fused into the expanded features.
322
+
323
+ Returns:
324
+ `(B, N_vis * num_children, patch_dim)`, interleaved.
325
+ """
326
+ visual_features = visual_features.to(self.branches[0].gate_proj.weight.dtype)
327
+ batch_size, num_visual, _ = visual_features.shape
328
+
329
+ if patch_features is not None:
330
+ expected = num_visual * self.num_children
331
+ if patch_features.shape[1] != expected:
332
+ raise ValueError(
333
+ f"patch_features.shape[1]={patch_features.shape[1]} != "
334
+ f"N_vis * num_children = {expected}"
335
+ )
336
+ if patch_features.shape[2] != self.patch_dim:
337
+ raise ValueError(
338
+ f"patch_features.shape[2]={patch_features.shape[2]} != patch_dim={self.patch_dim}"
339
+ )
340
+ patch_features = patch_features.view(
341
+ batch_size, num_visual, self.num_children, self.patch_dim
342
+ )
343
+
344
+ children = []
345
+ for index, branch in enumerate(self.branches):
346
+ child = branch(visual_features)
347
+ if patch_features is not None:
348
+ child = child + patch_features[:, :, index, :].to(
349
+ device=child.device, dtype=child.dtype
350
+ )
351
+ children.append(child)
352
+
353
+ stacked = torch.stack(children, dim=2) # (B, N_vis, num_children, patch_dim)
354
+ return stacked.reshape(batch_size, num_visual * self.num_children, self.patch_dim)
355
+
356
+
357
+ class ConCor1UpsampleBlock(nn.Module):
358
+ """2x upsampling block: transposed conv → SiLU → depthwise 3x3 refine → LayerNorm2d."""
359
+
360
+ def __init__(self, dim: int):
361
+ super().__init__()
362
+ self.up = nn.ConvTranspose2d(dim, dim, kernel_size=2, stride=2)
363
+ self.act = nn.SiLU()
364
+ self.refine = nn.Conv2d(dim, dim, kernel_size=3, padding=1, groups=dim, bias=False)
365
+ self.norm = ConCor1LayerNorm2d(dim)
366
+
367
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
368
+ hidden_states = self.up(hidden_states)
369
+ hidden_states = self.act(hidden_states)
370
+ hidden_states = self.refine(hidden_states)
371
+ return self.norm(hidden_states)
372
+
373
+
374
+ class ConCor1ConvolutionalDecoder(nn.Module):
375
+ """Lightweight convolutional decoder: `num_blocks` successive 2x upsamplings."""
376
+
377
+ def __init__(self, dim: int, num_blocks: int = 2):
378
+ super().__init__()
379
+ self.blocks = nn.ModuleList([ConCor1UpsampleBlock(dim) for _ in range(num_blocks)])
380
+
381
+ @property
382
+ def scale_factor(self) -> int:
383
+ return 2 ** len(self.blocks)
384
+
385
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
386
+ for block in self.blocks:
387
+ hidden_states = block(hidden_states)
388
+ return hidden_states
389
+
390
+
391
+ class ConCor1VisionSegmentationHead(nn.Module):
392
+ """Predict one image mask per bridge token.
393
+
394
+ Two components, as described in the paper:
395
+
396
+ 1. a *feature decoder* that reconstructs dense visual features from the
397
+ spatially compressed backbone visual tokens — `patch_expander`
398
+ (+ optional fusion of pre-merger ViT features) followed by
399
+ `convolutional_decoder`;
400
+ 2. a *mask predictor* that scores every decoded spatial cell against every
401
+ bridge token with a bilinear form — `mask_predictor`.
402
+ """
403
+
404
+ def __init__(self, config: ConCor1Config):
405
+ super().__init__()
406
+ self.merge_size = config.merge_size
407
+ self.patch_expander = ConCor1PatchExpander(
408
+ hidden_size=config.hidden_size,
409
+ patch_dim=config.vision_hidden_size,
410
+ num_children=config.merge_size ** 2,
411
+ )
412
+ self.convolutional_decoder = ConCor1ConvolutionalDecoder(
413
+ dim=config.vision_hidden_size,
414
+ num_blocks=config.num_mask_upsample_blocks,
415
+ )
416
+ self.mask_predictor = ConCor1BilinearCorrespondenceScorer(
417
+ feature_dim=config.vision_hidden_size,
418
+ bridge_dim=config.hidden_size,
419
+ correspondence_dim=config.correspondence_dim,
420
+ )
421
+
422
+ def decode_features(
423
+ self,
424
+ visual_features: torch.Tensor,
425
+ patch_features: Optional[torch.Tensor],
426
+ image_grid_thw: torch.Tensor,
427
+ visual_token_mask: torch.Tensor,
428
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
429
+ """Expand, fuse and upsample the visual tokens into a dense feature map.
430
+
431
+ Returns:
432
+ features: `(B, N_cells_max, patch_dim)` in row-major order, zero-padded.
433
+ grid_hw: `(B, 2)` — the `(height, width)` of each sample's cell grid.
434
+ """
435
+ expanded = self.patch_expander(visual_features, patch_features=patch_features)
436
+
437
+ batch_size = expanded.shape[0]
438
+ dim = expanded.shape[-1]
439
+ merge_size = self.merge_size
440
+ scale = self.convolutional_decoder.scale_factor
441
+
442
+ per_sample: List[torch.Tensor] = []
443
+ grid_hw = expanded.new_zeros((batch_size, 2), dtype=torch.long)
444
+
445
+ for index in range(batch_size):
446
+ num_visual = int(visual_token_mask[index].sum().item())
447
+ if num_visual == 0: # text-only sample
448
+ per_sample.append(expanded.new_zeros(1, dim))
449
+ continue
450
+
451
+ patch_h = int(image_grid_thw[index, 1].item())
452
+ patch_w = int(image_grid_thw[index, 2].item())
453
+ merged_h, merged_w = patch_h // merge_size, patch_w // merge_size
454
+
455
+ # Interleaved children → row-major 2D patch grid.
456
+ features = expanded[index, : num_visual * merge_size ** 2]
457
+ features = features.reshape(merged_h, merged_w, merge_size, merge_size, dim)
458
+ features = features.permute(0, 2, 1, 3, 4).reshape(patch_h, patch_w, dim)
459
+
460
+ # (1, D, patch_h, patch_w) → conv decoder → (1, D, patch_h * s, patch_w * s)
461
+ feature_map = features.permute(2, 0, 1).unsqueeze(0)
462
+ feature_map = self.convolutional_decoder(feature_map)
463
+
464
+ grid_hw[index, 0] = patch_h * scale
465
+ grid_hw[index, 1] = patch_w * scale
466
+ per_sample.append(feature_map.squeeze(0).permute(1, 2, 0).reshape(-1, dim))
467
+
468
+ num_cells = max(max(f.shape[0] for f in per_sample), 1)
469
+ padded = expanded.new_zeros(batch_size, num_cells, dim)
470
+ for index, features in enumerate(per_sample):
471
+ padded[index, : features.shape[0]] = features
472
+ return padded, grid_hw
473
+
474
+ def forward(
475
+ self,
476
+ visual_features: torch.Tensor,
477
+ bridge_features: torch.Tensor,
478
+ patch_features: Optional[torch.Tensor],
479
+ image_grid_thw: torch.Tensor,
480
+ visual_token_mask: torch.Tensor,
481
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
482
+ features, grid_hw = self.decode_features(
483
+ visual_features, patch_features, image_grid_thw, visual_token_mask
484
+ )
485
+ return self.mask_predictor(features, bridge_features), grid_hw
486
+
487
+
488
+ def extract_tokens_by_mask(hidden_states: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
489
+ """Gather the hidden states where `mask` is True, keeping left-to-right order.
490
+
491
+ Rows with fewer selected positions than the batch maximum are zero-padded.
492
+
493
+ Args:
494
+ hidden_states: `(B, S, D)`
495
+ mask: `(B, S)` boolean
496
+
497
+ Returns:
498
+ `(B, max_selected, D)`
499
+ """
500
+ _, _, dim = hidden_states.shape
501
+ is_valid = mask.bool()
502
+
503
+ max_count = max(int(is_valid.sum(dim=1).max().item()), 1)
504
+
505
+ sorted_indices = torch.argsort(is_valid.int(), dim=1, descending=True, stable=True)
506
+ selected = sorted_indices[:, :max_count]
507
+
508
+ extracted = torch.gather(hidden_states, 1, selected.unsqueeze(-1).expand(-1, -1, dim))
509
+ valid = torch.gather(is_valid, 1, selected)
510
+ return extracted * valid.unsqueeze(-1).to(extracted.dtype)
511
+
512
+
513
+ # ══════════════════════════════════════════════════════════════════════════════
514
+ # Model
515
+ # ══════════════════════════════════════════════════════════════════════════════
516
+
517
+
518
+ @dataclass
519
+ class ConCor1Output(ModelOutput):
520
+ """Correspondence predictions of [`ConCor1ForConceptCorrespondence`].
521
+
522
+ Args:
523
+ presence_logits: `(B, Q)` — logit that bridge `q` holds a valid
524
+ image-text correspondence.
525
+ text_mask_logits: `(B, Q, N_text)` — per-bridge logits over the text
526
+ tokens selected by `text_token_mask` (the bridge's text mask).
527
+ image_mask_logits: `(B, Q, N_cells)` — per-bridge logits over the decoded
528
+ image cells, row-major, zero-padded across the batch.
529
+ image_mask_grid_hw: `(B, 2)` — `(height, width)` of every sample's cell
530
+ grid, so `image_mask_logits[b, q, : h * w].reshape(h, w)` is the mask
531
+ map. One cell covers `config.mask_cell_size` pixels per side of the
532
+ (resized) image.
533
+ last_hidden_state: `(B, S, D)` — the backbone's final hidden states over
534
+ the full multimodal sequence.
535
+ """
536
+
537
+ presence_logits: Optional[torch.FloatTensor] = None
538
+ text_mask_logits: Optional[torch.FloatTensor] = None
539
+ image_mask_logits: Optional[torch.FloatTensor] = None
540
+ image_mask_grid_hw: Optional[torch.LongTensor] = None
541
+ last_hidden_state: Optional[torch.FloatTensor] = None
542
+
543
+
544
+ class ConCor1PreTrainedModel(PreTrainedModel):
545
+ config_class = ConCor1Config
546
+ base_model_prefix = "concor1"
547
+ supports_gradient_checkpointing = True
548
+ _no_split_modules = ["Qwen3_5DecoderLayer", "Qwen3_5VisionBlock"]
549
+ _keep_in_fp32_modules_strict = [
550
+ "presence_head",
551
+ "text_segmentation_head",
552
+ "vision_segmentation_head",
553
+ ]
554
+ _supports_flash_attn = True
555
+ _supports_sdpa = True
556
+ _supports_flex_attn = False
557
+ _can_compile_fullgraph = False
558
+
559
+ def _init_weights(self, module):
560
+ if isinstance(module, (nn.Linear, nn.Bilinear, nn.Conv2d, nn.ConvTranspose2d)):
561
+ nn.init.kaiming_uniform_(module.weight, a=math.sqrt(5))
562
+ if getattr(module, "bias", None) is not None:
563
+ nn.init.zeros_(module.bias)
564
+ elif isinstance(module, (nn.LayerNorm, nn.RMSNorm)):
565
+ if module.weight is not None:
566
+ nn.init.ones_(module.weight)
567
+ if getattr(module, "bias", None) is not None:
568
+ nn.init.zeros_(module.bias)
569
+
570
+
571
+ class ConCor1ForConceptCorrespondence(ConCor1PreTrainedModel):
572
+ """ConCor-1: bidirectional concept correspondence over an image-text pair.
573
+
574
+ Example:
575
+
576
+ ```python
577
+ >>> import requests
578
+ >>> import torch
579
+ >>> from PIL import Image
580
+ >>> from transformers import AutoModel, AutoProcessor
581
+
582
+ >>> processor = AutoProcessor.from_pretrained("UWGZQ/ConCor-1", trust_remote_code=True)
583
+ >>> model = AutoModel.from_pretrained("UWGZQ/ConCor-1", trust_remote_code=True, dtype=torch.bfloat16).cuda().eval()
584
+
585
+ >>> url = "http://images.cocodataset.org/val2017/000000000285.jpg"
586
+ >>> image = Image.open(requests.get(url, stream=True).raw).convert("RGB")
587
+ >>> text = (
588
+ ... "This image depicts a close-up of a brown bear in a natural outdoor setting. "
589
+ ... "The background consists of lush green grass. In the foreground, a large brown "
590
+ ... "bear is positioned centrally."
591
+ ... )
592
+ >>> inputs = processor(images=image, text=text, return_tensors="pt").to("cuda")
593
+ >>> with torch.inference_mode():
594
+ ... outputs = model(**inputs)
595
+ >>> correspondences = processor.post_process_correspondences(
596
+ ... outputs, text=text, target_sizes=[(image.height, image.width)]
597
+ ... )[0]
598
+ >>> [(round(c["presence_score"], 3), c["text_phrases"]) for c in correspondences]
599
+ ```
600
+ """
601
+
602
+ def __init__(self, config: ConCor1Config):
603
+ super().__init__(config)
604
+
605
+ if not config.bidirectional_full_attention:
606
+ logger.warning(
607
+ "bidirectional_full_attention=False runs the backbone's full-attention layers "
608
+ "causally. ConCor-1 was trained with bidirectional full attention; predictions "
609
+ "will be badly degraded."
610
+ )
611
+
612
+ backbone_config = config.backbone_config
613
+ backbone_config._attn_implementation = config._attn_implementation
614
+ self.backbone = ConCor1VisionLanguageBackbone(backbone_config)
615
+
616
+ self.presence_head = ConCor1PresenceHead(
617
+ hidden_size=config.hidden_size,
618
+ intermediate_size=config.presence_hidden_dim,
619
+ )
620
+ self.text_segmentation_head = ConCor1BilinearCorrespondenceScorer(
621
+ feature_dim=config.hidden_size,
622
+ bridge_dim=config.hidden_size,
623
+ correspondence_dim=config.correspondence_dim,
624
+ )
625
+ self.vision_segmentation_head = ConCor1VisionSegmentationHead(config)
626
+
627
+ self.post_init()
628
+
629
+ def get_input_embeddings(self) -> nn.Module:
630
+ return self.backbone.get_input_embeddings()
631
+
632
+ def set_input_embeddings(self, value: nn.Module) -> None:
633
+ self.backbone.set_input_embeddings(value)
634
+
635
+ def _encode(
636
+ self,
637
+ input_ids: torch.LongTensor,
638
+ pixel_values: Optional[torch.FloatTensor],
639
+ image_grid_thw: Optional[torch.LongTensor],
640
+ visual_token_mask: torch.BoolTensor,
641
+ attention_mask: Optional[torch.Tensor],
642
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
643
+ """Run the backbone and return `(hidden_states, pre_merger_patch_features)`.
644
+
645
+ This inlines `Qwen3_5Model.forward` so that the pre-merger ViT patch
646
+ features (needed by the vision segmentation head) can be kept without
647
+ running the vision tower twice.
648
+ """
649
+ inputs_embeds = self.backbone.get_input_embeddings()(input_ids)
650
+
651
+ patch_features = None
652
+ if pixel_values is not None:
653
+ vision_output = self.backbone.get_image_features(
654
+ pixel_values, image_grid_thw, return_dict=True
655
+ )
656
+ if self.config.fuse_vision_encoder_features:
657
+ patch_features = vision_output.last_hidden_state # (total_patches, patch_dim)
658
+ image_embeds = torch.cat(vision_output.pooler_output, dim=0).to(
659
+ inputs_embeds.device, inputs_embeds.dtype
660
+ )
661
+ image_mask, _ = self.backbone.get_placeholder_mask(
662
+ input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds
663
+ )
664
+ inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
665
+
666
+ mm_token_type_ids = torch.zeros_like(input_ids, dtype=torch.int32)
667
+ mm_token_type_ids[visual_token_mask] = 1
668
+ position_ids = self.backbone.compute_3d_position_ids(
669
+ input_ids=input_ids,
670
+ image_grid_thw=image_grid_thw,
671
+ video_grid_thw=None,
672
+ inputs_embeds=inputs_embeds,
673
+ attention_mask=attention_mask,
674
+ past_key_values=None,
675
+ mm_token_type_ids=mm_token_type_ids,
676
+ )
677
+
678
+ outputs = self.backbone.language_model(
679
+ input_ids=None,
680
+ inputs_embeds=inputs_embeds,
681
+ position_ids=position_ids,
682
+ attention_mask=attention_mask,
683
+ use_cache=False,
684
+ bidirectional_full_attention=self.config.bidirectional_full_attention,
685
+ )
686
+ return outputs.last_hidden_state, patch_features
687
+
688
+ def _gather_patch_features(
689
+ self,
690
+ patch_features: torch.Tensor,
691
+ image_grid_thw: torch.LongTensor,
692
+ visual_token_mask: torch.BoolTensor,
693
+ num_visual_tokens: int,
694
+ ) -> torch.Tensor:
695
+ """Batch and zero-pad per-image pre-merger patch features.
696
+
697
+ The vision tower flattens all images into one sequence; split it per
698
+ image and pad to `num_visual_tokens * merge_size**2`, keeping the
699
+ interleaved child order the patch expander produces.
700
+ """
701
+ batch_size = visual_token_mask.shape[0]
702
+ patch_dim = patch_features.shape[-1]
703
+ num_children = self.config.merge_size ** 2
704
+ padded = patch_features.new_zeros((batch_size, num_visual_tokens * num_children, patch_dim))
705
+
706
+ per_image = patch_features.split(image_grid_thw.prod(dim=1).tolist())
707
+ for index in range(batch_size):
708
+ num_visual = int(visual_token_mask[index].sum().item())
709
+ if num_visual == 0:
710
+ continue
711
+ num_patches = num_visual * num_children
712
+ padded[index, :num_patches] = per_image[index][:num_patches]
713
+ return padded
714
+
715
+ def forward(
716
+ self,
717
+ input_ids: torch.LongTensor,
718
+ bridge_token_mask: torch.BoolTensor,
719
+ text_token_mask: torch.BoolTensor,
720
+ visual_token_mask: Optional[torch.BoolTensor] = None,
721
+ attention_mask: Optional[torch.Tensor] = None,
722
+ pixel_values: Optional[torch.FloatTensor] = None,
723
+ image_grid_thw: Optional[torch.LongTensor] = None,
724
+ **kwargs,
725
+ ) -> ConCor1Output:
726
+ r"""
727
+ Args:
728
+ input_ids (`torch.LongTensor` of shape `(B, S)`):
729
+ Flat multimodal sequence: `<vision_start>`, `<image_pad>` x N_v,
730
+ `<vision_end>`, text tokens, then the `Q` bridge token ids.
731
+ bridge_token_mask (`torch.BoolTensor` of shape `(B, S)`):
732
+ True at the bridge-token positions.
733
+ text_token_mask (`torch.BoolTensor` of shape `(B, S)`):
734
+ True at the text positions the text masks are predicted over
735
+ (the input text, excluding the vision and bridge tokens).
736
+ visual_token_mask (`torch.BoolTensor` of shape `(B, S)`, *optional*):
737
+ True at the `<image_pad>` positions. Required with an image.
738
+ attention_mask (`torch.Tensor` of shape `(B, S)`, *optional*):
739
+ 1 for real tokens, 0 for padding.
740
+ pixel_values (`torch.FloatTensor`, *optional*):
741
+ Flattened image patches from the Qwen3.5 image processor.
742
+ image_grid_thw (`torch.LongTensor` of shape `(num_images, 3)`, *optional*):
743
+ Temporal / height / width patch counts per image.
744
+
745
+ Returns:
746
+ [`ConCor1Output`]
747
+ """
748
+ if kwargs.get("use_cache") or kwargs.get("past_key_values") is not None:
749
+ raise ValueError(
750
+ "ConCor-1 is not autoregressive: `use_cache` / `past_key_values` are not supported."
751
+ )
752
+ if pixel_values is not None and visual_token_mask is None:
753
+ raise ValueError("`visual_token_mask` is required when `pixel_values` is passed.")
754
+ if visual_token_mask is None:
755
+ visual_token_mask = torch.zeros_like(input_ids, dtype=torch.bool)
756
+ if attention_mask is None:
757
+ attention_mask = torch.ones_like(input_ids)
758
+
759
+ hidden_states, patch_features = self._encode(
760
+ input_ids=input_ids,
761
+ pixel_values=pixel_values,
762
+ image_grid_thw=image_grid_thw,
763
+ visual_token_mask=visual_token_mask,
764
+ attention_mask=attention_mask,
765
+ )
766
+
767
+ bridge_features = extract_tokens_by_mask(hidden_states, bridge_token_mask) # (B, Q, D)
768
+ text_features = extract_tokens_by_mask(hidden_states, text_token_mask) # (B, N_text, D)
769
+
770
+ presence_logits = self.presence_head(bridge_features).squeeze(-1) # (B, Q)
771
+ text_mask_logits = self.text_segmentation_head(text_features, bridge_features) # (B, Q, N_text)
772
+
773
+ image_mask_logits, image_mask_grid_hw = None, None
774
+ if pixel_values is not None:
775
+ visual_features = extract_tokens_by_mask(hidden_states, visual_token_mask)
776
+ if patch_features is not None:
777
+ patch_features = self._gather_patch_features(
778
+ patch_features,
779
+ image_grid_thw=image_grid_thw,
780
+ visual_token_mask=visual_token_mask,
781
+ num_visual_tokens=visual_features.shape[1],
782
+ )
783
+ image_mask_logits, image_mask_grid_hw = self.vision_segmentation_head(
784
+ visual_features=visual_features,
785
+ bridge_features=bridge_features,
786
+ patch_features=patch_features,
787
+ image_grid_thw=image_grid_thw,
788
+ visual_token_mask=visual_token_mask,
789
+ )
790
+
791
+ return ConCor1Output(
792
+ presence_logits=presence_logits,
793
+ text_mask_logits=text_mask_logits,
794
+ image_mask_logits=image_mask_logits,
795
+ image_mask_grid_hw=image_mask_grid_hw,
796
+ last_hidden_state=hidden_states,
797
+ )
798
+
799
+
800
+ __all__ = [
801
+ "ConCor1ForConceptCorrespondence",
802
+ "ConCor1PreTrainedModel",
803
+ "ConCor1Output",
804
+ ]
preprocessor_config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "size": {
3
+ "longest_edge": 16777216,
4
+ "shortest_edge": 65536
5
+ },
6
+ "patch_size": 16,
7
+ "temporal_patch_size": 2,
8
+ "merge_size": 2,
9
+ "image_mean": [
10
+ 0.5,
11
+ 0.5,
12
+ 0.5
13
+ ],
14
+ "image_std": [
15
+ 0.5,
16
+ 0.5,
17
+ 0.5
18
+ ],
19
+ "processor_class": "ConCor1Processor",
20
+ "image_processor_type": "Qwen2VLImageProcessorFast",
21
+ "min_pixels": 1003520,
22
+ "max_pixels": 1003520
23
+ }
processing_concor1.py ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2026 The ConCor-1 authors.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Processor for ConCor-1.
16
+
17
+ Turns an image-text pair into the flat multimodal sequence ConCor-1 expects
18
+ (image tokens, text tokens, bridge tokens) and turns the model's per-bridge
19
+ logits back into a set of image-text correspondences: a character-level text
20
+ span set and a binary image mask per correspondence.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import string
26
+ from typing import Dict, List, Optional, Sequence, Tuple, Union
27
+
28
+ import numpy as np
29
+ import torch
30
+ import torch.nn.functional as F
31
+
32
+ from transformers.feature_extraction_utils import BatchFeature
33
+ from transformers.processing_utils import ProcessorMixin
34
+
35
+ ImageInput = Union["PIL.Image.Image", Sequence["PIL.Image.Image"]]
36
+
37
+ _TRIM_CHARACTERS = frozenset(string.punctuation)
38
+
39
+
40
+ def compute_span_iou(spans_a: List[List[int]], spans_b: List[List[int]]) -> float:
41
+ """IoU of two character-span sets, treated as 1-D binary masks over the text."""
42
+ characters_a = {index for start, end in spans_a for index in range(start, end)}
43
+ characters_b = {index for start, end in spans_b for index in range(start, end)}
44
+ union = len(characters_a | characters_b)
45
+ if union == 0:
46
+ return 0.0
47
+ return len(characters_a & characters_b) / union
48
+
49
+
50
+ def compute_mask_iou(mask_a: np.ndarray, mask_b: np.ndarray) -> float:
51
+ """IoU of two binary masks."""
52
+ flat_a = mask_a.astype(bool).ravel()
53
+ flat_b = mask_b.astype(bool).ravel()
54
+ union = np.count_nonzero(flat_a | flat_b)
55
+ if union == 0:
56
+ return 0.0
57
+ return np.count_nonzero(flat_a & flat_b) / union
58
+
59
+
60
+ class ConCor1Processor(ProcessorMixin):
61
+ r"""Constructs a ConCor-1 processor from a Qwen3.5 image processor and tokenizer.
62
+
63
+ Args:
64
+ image_processor: the Qwen3.5 (`Qwen2VLImageProcessorFast`) image processor.
65
+ tokenizer: the Qwen3.5 tokenizer.
66
+ num_bridge_tokens (`int`, *optional*, defaults to 385):
67
+ Number of bridge tokens appended to every sequence.
68
+ bridge_token_id_start (`int`, *optional*, defaults to 248077):
69
+ Token id of the first bridge token.
70
+ patch_size (`int`, *optional*, defaults to 16): vision patch size.
71
+ merge_size (`int`, *optional*, defaults to 2): patch-merger factor.
72
+ min_pixels (`int`, *optional*, defaults to 1003520):
73
+ `min_pixels` forwarded to the image processor.
74
+ max_pixels (`int`, *optional*, defaults to 1003520):
75
+ `max_pixels` forwarded to the image processor. The released
76
+ checkpoint was trained with `min_pixels == max_pixels`, i.e. a fixed
77
+ ~1.0 M pixel budget.
78
+ presence_threshold (`float`, *optional*, defaults to 0.1)
79
+ text_threshold (`float`, *optional*, defaults to 0.45)
80
+ image_threshold (`float`, *optional*, defaults to 0.45)
81
+ nms_iou_threshold (`float`, *optional*, defaults to 0.5)
82
+
83
+ Example:
84
+
85
+ ```python
86
+ >>> text = "A close-up of a brown bear sitting in lush green grass."
87
+ >>> inputs = processor(images=image, text=text, return_tensors="pt")
88
+ >>> outputs = model(**inputs)
89
+ >>> correspondences = processor.post_process_correspondences(
90
+ ... outputs, text=text, target_sizes=[(image.height, image.width)]
91
+ ... )
92
+ ```
93
+ """
94
+
95
+ attributes = ["image_processor", "tokenizer"]
96
+
97
+ image_processor_class = "Qwen2VLImageProcessorFast"
98
+ tokenizer_class = "AutoTokenizer"
99
+
100
+ def __init__(
101
+ self,
102
+ image_processor=None,
103
+ tokenizer=None,
104
+ num_bridge_tokens: int = 385,
105
+ bridge_token_id_start: int = 248077,
106
+ patch_size: int = 16,
107
+ merge_size: int = 2,
108
+ num_mask_upsample_blocks: int = 2,
109
+ min_pixels: int = 1003520,
110
+ max_pixels: int = 1003520,
111
+ presence_threshold: float = 0.1,
112
+ text_threshold: float = 0.45,
113
+ image_threshold: float = 0.45,
114
+ nms_iou_threshold: float = 0.5,
115
+ **kwargs,
116
+ ):
117
+ self.num_bridge_tokens = num_bridge_tokens
118
+ self.bridge_token_id_start = bridge_token_id_start
119
+ self.patch_size = patch_size
120
+ self.merge_size = merge_size
121
+ self.num_mask_upsample_blocks = num_mask_upsample_blocks
122
+ self.min_pixels = min_pixels
123
+ self.max_pixels = max_pixels
124
+ self.presence_threshold = presence_threshold
125
+ self.text_threshold = text_threshold
126
+ self.image_threshold = image_threshold
127
+ self.nms_iou_threshold = nms_iou_threshold
128
+ super().__init__(image_processor, tokenizer, **kwargs)
129
+
130
+ @property
131
+ def bridge_token_ids(self) -> List[int]:
132
+ start = self.bridge_token_id_start
133
+ return list(range(start, start + self.num_bridge_tokens))
134
+
135
+
136
+ def __call__(
137
+ self,
138
+ images: Optional[ImageInput] = None,
139
+ text: Optional[Union[str, List[str]]] = None,
140
+ return_tensors: str = "pt",
141
+ **kwargs,
142
+ ) -> BatchFeature:
143
+ """Build ConCor-1's flat multimodal sequences for one or more image-text pairs.
144
+
145
+ The text is *not* wrapped in a chat template: ConCor-1 consumes the raw
146
+ text (a caption, a list of category names, a referring expression, ...)
147
+ directly, and predicts its text masks over exactly these tokens.
148
+
149
+ Args:
150
+ images: one PIL image, or a list with one image per text.
151
+ text: the paired text, or a list of texts.
152
+ return_tensors: only `"pt"` is supported.
153
+
154
+ Returns:
155
+ [`BatchFeature`] with `input_ids`, `attention_mask`,
156
+ `visual_token_mask`, `text_token_mask`, `bridge_token_mask` and —
157
+ when images are given — `pixel_values` and `image_grid_thw`.
158
+ """
159
+ if return_tensors != "pt":
160
+ raise ValueError(f"ConCor1Processor only supports return_tensors='pt', got {return_tensors!r}")
161
+ if text is None:
162
+ raise ValueError(
163
+ "ConCor-1 always grounds text: pass the image's paired `text` "
164
+ "(a caption, category list or referring expression)."
165
+ )
166
+
167
+ texts = [text] if isinstance(text, str) else list(text)
168
+ if images is None:
169
+ image_list = []
170
+ elif isinstance(images, (list, tuple)):
171
+ image_list = list(images)
172
+ else:
173
+ image_list = [images]
174
+
175
+ if image_list and len(image_list) != len(texts):
176
+ raise ValueError(
177
+ f"Got {len(image_list)} image(s) and {len(texts)} text(s); pass one image per text."
178
+ )
179
+
180
+ pixel_values, image_grid_thw = None, None
181
+ num_visual_tokens = [0] * len(texts)
182
+ if image_list:
183
+ image_inputs = self.image_processor(
184
+ images=image_list,
185
+ return_tensors="pt",
186
+ min_pixels=kwargs.pop("min_pixels", self.min_pixels),
187
+ max_pixels=kwargs.pop("max_pixels", self.max_pixels),
188
+ )
189
+ pixel_values = image_inputs["pixel_values"]
190
+ image_grid_thw = image_inputs["image_grid_thw"]
191
+ num_visual_tokens = [
192
+ int(grid.prod().item()) // (self.merge_size ** 2) for grid in image_grid_thw
193
+ ]
194
+
195
+ vision_start_id = self.tokenizer.convert_tokens_to_ids("<|vision_start|>")
196
+ vision_end_id = self.tokenizer.convert_tokens_to_ids("<|vision_end|>")
197
+ image_pad_id = self.tokenizer.convert_tokens_to_ids("<|image_pad|>")
198
+ bridge_ids = self.bridge_token_ids
199
+
200
+ sequences, visual_masks, text_masks, bridge_masks = [], [], [], []
201
+ for index, sample_text in enumerate(texts):
202
+ ids: List[int] = []
203
+ visual_mask: List[bool] = []
204
+ text_mask: List[bool] = []
205
+
206
+ if num_visual_tokens[index] > 0:
207
+ ids.append(vision_start_id)
208
+ ids.extend([image_pad_id] * num_visual_tokens[index])
209
+ ids.append(vision_end_id)
210
+ visual_mask.extend([False] + [True] * num_visual_tokens[index] + [False])
211
+ text_mask.extend([False] * (num_visual_tokens[index] + 2))
212
+
213
+ text_ids = self.tokenizer.encode(sample_text, add_special_tokens=False)
214
+ if not text_ids:
215
+ raise ValueError(f"Text {index} is empty after tokenization: {sample_text!r}")
216
+ ids.extend(text_ids)
217
+ visual_mask.extend([False] * len(text_ids))
218
+ text_mask.extend([True] * len(text_ids))
219
+
220
+ ids.extend(bridge_ids)
221
+ visual_mask.extend([False] * len(bridge_ids))
222
+ text_mask.extend([False] * len(bridge_ids))
223
+
224
+ bridge_mask = [False] * (len(ids) - len(bridge_ids)) + [True] * len(bridge_ids)
225
+
226
+ sequences.append(ids)
227
+ visual_masks.append(visual_mask)
228
+ text_masks.append(text_mask)
229
+ bridge_masks.append(bridge_mask)
230
+
231
+ # Right-pad to the longest sequence, as in training.
232
+ max_length = max(len(ids) for ids in sequences)
233
+ pad_token_id = self.tokenizer.pad_token_id or 0
234
+ batch_size = len(sequences)
235
+
236
+ input_ids = torch.full((batch_size, max_length), pad_token_id, dtype=torch.long)
237
+ attention_mask = torch.zeros((batch_size, max_length), dtype=torch.long)
238
+ visual_token_mask = torch.zeros((batch_size, max_length), dtype=torch.bool)
239
+ text_token_mask = torch.zeros((batch_size, max_length), dtype=torch.bool)
240
+ bridge_token_mask = torch.zeros((batch_size, max_length), dtype=torch.bool)
241
+
242
+ for index, ids in enumerate(sequences):
243
+ length = len(ids)
244
+ input_ids[index, :length] = torch.tensor(ids, dtype=torch.long)
245
+ attention_mask[index, :length] = 1
246
+ visual_token_mask[index, :length] = torch.tensor(visual_masks[index], dtype=torch.bool)
247
+ text_token_mask[index, :length] = torch.tensor(text_masks[index], dtype=torch.bool)
248
+ bridge_token_mask[index, :length] = torch.tensor(bridge_masks[index], dtype=torch.bool)
249
+
250
+ data = {
251
+ "input_ids": input_ids,
252
+ "attention_mask": attention_mask,
253
+ "visual_token_mask": visual_token_mask,
254
+ "text_token_mask": text_token_mask,
255
+ "bridge_token_mask": bridge_token_mask,
256
+ }
257
+ if pixel_values is not None:
258
+ data["pixel_values"] = pixel_values
259
+ data["image_grid_thw"] = image_grid_thw
260
+ return BatchFeature(data=data)
261
+
262
+ # ── Post-processing ──────────────────────────────────────────────────────
263
+
264
+ def text_logits_to_spans(
265
+ self,
266
+ text_logits: np.ndarray,
267
+ offset_mapping: Sequence[Tuple[int, int]],
268
+ text: str,
269
+ text_threshold: float,
270
+ ) -> List[List[int]]:
271
+ """Convert one bridge token's text-mask logits into character spans.
272
+
273
+ Active text tokens are mapped to their character ranges, merged into
274
+ maximal contiguous spans and trimmed of leading/trailing whitespace and
275
+ punctuation. A text mask may consist of several disjoint spans, which is
276
+ how discontinuous and co-referring mentions are represented.
277
+ """
278
+ probabilities = 1.0 / (1.0 + np.exp(-text_logits.astype(np.float64)))
279
+ active = probabilities >= text_threshold
280
+
281
+ characters = set()
282
+ for token_index in np.where(active)[0]:
283
+ if token_index < len(offset_mapping):
284
+ start, end = offset_mapping[token_index]
285
+ characters.update(range(int(start), int(end)))
286
+ if not characters:
287
+ return []
288
+
289
+ ordered = sorted(characters)
290
+ spans: List[List[int]] = []
291
+ start = ordered[0]
292
+ end = start + 1
293
+ for character in ordered[1:]:
294
+ if character == end:
295
+ end += 1
296
+ else:
297
+ spans.append([start, end])
298
+ start, end = character, character + 1
299
+ spans.append([start, end])
300
+
301
+ trimmed: List[List[int]] = []
302
+ for span_start, span_end in spans:
303
+ span_start = max(0, span_start)
304
+ span_end = min(len(text), span_end)
305
+ while span_start < span_end and (
306
+ text[span_start].isspace() or text[span_start] in _TRIM_CHARACTERS
307
+ ):
308
+ span_start += 1
309
+ while span_end > span_start and (
310
+ text[span_end - 1].isspace() or text[span_end - 1] in _TRIM_CHARACTERS
311
+ ):
312
+ span_end -= 1
313
+ if span_start < span_end:
314
+ trimmed.append([span_start, span_end])
315
+ return trimmed
316
+
317
+ @staticmethod
318
+ def suppress_duplicate_correspondences(
319
+ masks: List[Optional[np.ndarray]],
320
+ spans: List[List[List[int]]],
321
+ presence_scores: np.ndarray,
322
+ nms_iou_threshold: float,
323
+ ) -> List[int]:
324
+ """Greedy NMS over correspondences, ranked by presence score.
325
+
326
+ A candidate is suppressed only when it duplicates a higher-scoring one on
327
+ *both* sides of the correspondence: image-mask IoU and text-span IoU both
328
+ exceed `nms_iou_threshold`.
329
+ """
330
+ count = len(spans)
331
+ if nms_iou_threshold <= 0.0 or count <= 1:
332
+ return list(range(count))
333
+
334
+ kept: List[int] = []
335
+ for candidate in np.argsort(-presence_scores):
336
+ candidate = int(candidate)
337
+ suppressed = False
338
+ for reference in kept:
339
+ if masks[candidate] is not None and masks[reference] is not None:
340
+ if compute_mask_iou(masks[candidate], masks[reference]) < nms_iou_threshold:
341
+ continue
342
+ if compute_span_iou(spans[candidate], spans[reference]) >= nms_iou_threshold:
343
+ suppressed = True
344
+ break
345
+ if not suppressed:
346
+ kept.append(candidate)
347
+ return sorted(kept)
348
+
349
+ def post_process_correspondences(
350
+ self,
351
+ outputs,
352
+ text: Union[str, List[str]],
353
+ target_sizes: Optional[Sequence[Tuple[int, int]]] = None,
354
+ presence_threshold: Optional[float] = None,
355
+ text_threshold: Optional[float] = None,
356
+ image_threshold: Optional[float] = None,
357
+ nms_iou_threshold: Optional[float] = None,
358
+ return_masks: bool = True,
359
+ ) -> List[List[Dict]]:
360
+ """Turn per-bridge logits into a set of image-text correspondences.
361
+
362
+ Bridge tokens whose presence probability passes `presence_threshold` are
363
+ kept; each keeps a character-span text mask and a binary image mask
364
+ (logits bilinearly upsampled to `target_sizes`, then thresholded), and
365
+ duplicates are removed with NMS.
366
+
367
+ Args:
368
+ outputs: the [`ConCor1Output`] returned by the model.
369
+ text: the same text that was passed to `__call__`.
370
+ target_sizes: `(height, width)` per sample the image masks are
371
+ resized to — normally the original image size. Defaults to the
372
+ processed image resolution.
373
+ presence_threshold / text_threshold / image_threshold /
374
+ nms_iou_threshold: override the defaults (0.1 / 0.45 / 0.45 / 0.5).
375
+ return_masks: set to `False` to skip mask upsampling and return text
376
+ masks only.
377
+
378
+ Returns:
379
+ One list of correspondences per sample, sorted by decreasing
380
+ presence score. Each correspondence is a dict with
381
+ `bridge_index`, `presence_score`, `text_spans`, `text_phrases` and
382
+ (unless disabled) `mask`.
383
+ """
384
+ presence_threshold = (
385
+ self.presence_threshold if presence_threshold is None else presence_threshold
386
+ )
387
+ text_threshold = self.text_threshold if text_threshold is None else text_threshold
388
+ image_threshold = self.image_threshold if image_threshold is None else image_threshold
389
+ nms_iou_threshold = (
390
+ self.nms_iou_threshold if nms_iou_threshold is None else nms_iou_threshold
391
+ )
392
+
393
+ texts = [text] if isinstance(text, str) else list(text)
394
+ presence_probabilities = torch.sigmoid(outputs.presence_logits.float()).cpu().numpy()
395
+ batch_size = presence_probabilities.shape[0]
396
+ if len(texts) != batch_size:
397
+ raise ValueError(f"Got {len(texts)} text(s) for a batch of {batch_size}.")
398
+
399
+ results: List[List[Dict]] = []
400
+ for sample in range(batch_size):
401
+ sample_text = texts[sample]
402
+ offset_mapping = self.tokenizer(
403
+ sample_text, return_offsets_mapping=True, add_special_tokens=False
404
+ )["offset_mapping"]
405
+
406
+ active = np.where(presence_probabilities[sample] >= presence_threshold)[0]
407
+
408
+ spans_per_bridge: List[List[List[int]]] = []
409
+ masks_per_bridge: List[Optional[np.ndarray]] = []
410
+ for bridge_index in active:
411
+ text_logits = outputs.text_mask_logits[sample, bridge_index].float().cpu().numpy()
412
+ spans_per_bridge.append(
413
+ self.text_logits_to_spans(
414
+ text_logits, offset_mapping, sample_text, text_threshold
415
+ )
416
+ )
417
+ masks_per_bridge.append(
418
+ self._decode_image_mask(
419
+ outputs,
420
+ sample=sample,
421
+ bridge_index=int(bridge_index),
422
+ target_size=None if target_sizes is None else tuple(target_sizes[sample]),
423
+ image_threshold=image_threshold,
424
+ )
425
+ if return_masks
426
+ else None
427
+ )
428
+
429
+ kept = self.suppress_duplicate_correspondences(
430
+ masks_per_bridge,
431
+ spans_per_bridge,
432
+ presence_probabilities[sample][active],
433
+ nms_iou_threshold,
434
+ )
435
+
436
+ correspondences = []
437
+ for position in kept:
438
+ bridge_index = int(active[position])
439
+ spans = spans_per_bridge[position]
440
+ correspondence = {
441
+ "bridge_index": bridge_index,
442
+ "presence_score": float(presence_probabilities[sample, bridge_index]),
443
+ "text_spans": spans,
444
+ "text_phrases": [sample_text[start:end] for start, end in spans],
445
+ }
446
+ if return_masks:
447
+ correspondence["mask"] = masks_per_bridge[position]
448
+ correspondences.append(correspondence)
449
+
450
+ correspondences.sort(key=lambda item: -item["presence_score"])
451
+ results.append(correspondences)
452
+ return results
453
+
454
+ def _decode_image_mask(
455
+ self,
456
+ outputs,
457
+ sample: int,
458
+ bridge_index: int,
459
+ target_size: Optional[Tuple[int, int]],
460
+ image_threshold: float,
461
+ ) -> Optional[np.ndarray]:
462
+ """Reshape, upsample and threshold one bridge token's image-mask logits."""
463
+ if outputs.image_mask_logits is None:
464
+ return None
465
+
466
+ height = int(outputs.image_mask_grid_hw[sample, 0].item())
467
+ width = int(outputs.image_mask_grid_hw[sample, 1].item())
468
+ if height == 0 or width == 0:
469
+ return None
470
+
471
+ logits = outputs.image_mask_logits[sample, bridge_index, : height * width]
472
+ logits = logits.float().reshape(1, 1, height, width)
473
+
474
+ if target_size is None:
475
+ target_size = (height * self.mask_cell_size, width * self.mask_cell_size)
476
+
477
+ upsampled = F.interpolate(
478
+ logits, size=tuple(int(value) for value in target_size), mode="bilinear", align_corners=False
479
+ )
480
+ probabilities = torch.sigmoid(upsampled)[0, 0].cpu().numpy()
481
+ return probabilities >= image_threshold
482
+
483
+ @property
484
+ def mask_cell_size(self) -> int:
485
+ """Pixels per side of one image-mask cell in the processed image (4 px)."""
486
+ return self.patch_size // (2 ** self.num_mask_upsample_blocks)
487
+
488
+
489
+ __all__ = ["ConCor1Processor"]
processor_config.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "processor_class": "ConCor1Processor",
3
+ "auto_map": {
4
+ "AutoProcessor": "processing_concor1.ConCor1Processor"
5
+ },
6
+ "num_bridge_tokens": 385,
7
+ "bridge_token_id_start": 248077,
8
+ "patch_size": 16,
9
+ "merge_size": 2,
10
+ "num_mask_upsample_blocks": 2,
11
+ "min_pixels": 1003520,
12
+ "max_pixels": 1003520,
13
+ "presence_threshold": 0.1,
14
+ "text_threshold": 0.45,
15
+ "image_threshold": 0.45,
16
+ "nms_iou_threshold": 0.5
17
+ }
requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ConCor-1 inference requirements.
2
+ # Versions the released checkpoint was trained and evaluated with are pinned as
3
+ # lower bounds; transformers must be new enough to ship the Qwen3.5 architecture.
4
+ torch>=2.8.0
5
+ torchvision>=0.23.0 # required by the fast image processor
6
+ transformers==5.3.0
7
+ safetensors>=0.4.0
8
+ numpy>=1.24
9
+ pillow>=10.0
10
+
11
+ # Qwen3.5's gated-delta-net (linear-attention) kernels.
12
+ flash-linear-attention>=0.4.1
13
+
14
+ # Optional, and both need a CUDA compiler. Neither changes the set of
15
+ # correspondences the model returns.
16
+ # causal-conv1d: a fused causal convolution for the linear-attention layers;
17
+ # without it transformers falls back to a pure-torch path.
18
+ # flash-attn: exact reproduction of the paper's logits, with
19
+ # attn_implementation="flash_attention_2". The default,
20
+ # attn_implementation="sdpa", needs no extra dependency.
21
+ # causal-conv1d>=1.4.0
22
+ # flash-attn>=2.8.0
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5f9e4d4901a92b997e463c1f46055088b6cca5ca61a6522d1b9f64c4bb81cb42
3
+ size 12807982
tokenizer_config.json ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "added_tokens_decoder": {
4
+ "248044": {
5
+ "content": "<|endoftext|>",
6
+ "lstrip": false,
7
+ "normalized": false,
8
+ "rstrip": false,
9
+ "single_word": false,
10
+ "special": true
11
+ },
12
+ "248045": {
13
+ "content": "<|im_start|>",
14
+ "lstrip": false,
15
+ "normalized": false,
16
+ "rstrip": false,
17
+ "single_word": false,
18
+ "special": true
19
+ },
20
+ "248046": {
21
+ "content": "<|im_end|>",
22
+ "lstrip": false,
23
+ "normalized": false,
24
+ "rstrip": false,
25
+ "single_word": false,
26
+ "special": true
27
+ },
28
+ "248047": {
29
+ "content": "<|object_ref_start|>",
30
+ "lstrip": false,
31
+ "normalized": false,
32
+ "rstrip": false,
33
+ "single_word": false,
34
+ "special": true
35
+ },
36
+ "248048": {
37
+ "content": "<|object_ref_end|>",
38
+ "lstrip": false,
39
+ "normalized": false,
40
+ "rstrip": false,
41
+ "single_word": false,
42
+ "special": true
43
+ },
44
+ "248049": {
45
+ "content": "<|box_start|>",
46
+ "lstrip": false,
47
+ "normalized": false,
48
+ "rstrip": false,
49
+ "single_word": false,
50
+ "special": true
51
+ },
52
+ "248050": {
53
+ "content": "<|box_end|>",
54
+ "lstrip": false,
55
+ "normalized": false,
56
+ "rstrip": false,
57
+ "single_word": false,
58
+ "special": true
59
+ },
60
+ "248051": {
61
+ "content": "<|quad_start|>",
62
+ "lstrip": false,
63
+ "normalized": false,
64
+ "rstrip": false,
65
+ "single_word": false,
66
+ "special": true
67
+ },
68
+ "248052": {
69
+ "content": "<|quad_end|>",
70
+ "lstrip": false,
71
+ "normalized": false,
72
+ "rstrip": false,
73
+ "single_word": false,
74
+ "special": true
75
+ },
76
+ "248053": {
77
+ "content": "<|vision_start|>",
78
+ "lstrip": false,
79
+ "normalized": false,
80
+ "rstrip": false,
81
+ "single_word": false,
82
+ "special": true
83
+ },
84
+ "248054": {
85
+ "content": "<|vision_end|>",
86
+ "lstrip": false,
87
+ "normalized": false,
88
+ "rstrip": false,
89
+ "single_word": false,
90
+ "special": true
91
+ },
92
+ "248055": {
93
+ "content": "<|vision_pad|>",
94
+ "lstrip": false,
95
+ "normalized": false,
96
+ "rstrip": false,
97
+ "single_word": false,
98
+ "special": true
99
+ },
100
+ "248056": {
101
+ "content": "<|image_pad|>",
102
+ "lstrip": false,
103
+ "normalized": false,
104
+ "rstrip": false,
105
+ "single_word": false,
106
+ "special": true
107
+ },
108
+ "248057": {
109
+ "content": "<|video_pad|>",
110
+ "lstrip": false,
111
+ "normalized": false,
112
+ "rstrip": false,
113
+ "single_word": false,
114
+ "special": true
115
+ },
116
+ "248058": {
117
+ "content": "<tool_call>",
118
+ "lstrip": false,
119
+ "normalized": false,
120
+ "rstrip": false,
121
+ "single_word": false,
122
+ "special": false
123
+ },
124
+ "248059": {
125
+ "content": "</tool_call>",
126
+ "lstrip": false,
127
+ "normalized": false,
128
+ "rstrip": false,
129
+ "single_word": false,
130
+ "special": false
131
+ },
132
+ "248060": {
133
+ "content": "<|fim_prefix|>",
134
+ "lstrip": false,
135
+ "normalized": false,
136
+ "rstrip": false,
137
+ "single_word": false,
138
+ "special": false
139
+ },
140
+ "248061": {
141
+ "content": "<|fim_middle|>",
142
+ "lstrip": false,
143
+ "normalized": false,
144
+ "rstrip": false,
145
+ "single_word": false,
146
+ "special": false
147
+ },
148
+ "248062": {
149
+ "content": "<|fim_suffix|>",
150
+ "lstrip": false,
151
+ "normalized": false,
152
+ "rstrip": false,
153
+ "single_word": false,
154
+ "special": false
155
+ },
156
+ "248063": {
157
+ "content": "<|fim_pad|>",
158
+ "lstrip": false,
159
+ "normalized": false,
160
+ "rstrip": false,
161
+ "single_word": false,
162
+ "special": false
163
+ },
164
+ "248064": {
165
+ "content": "<|repo_name|>",
166
+ "lstrip": false,
167
+ "normalized": false,
168
+ "rstrip": false,
169
+ "single_word": false,
170
+ "special": false
171
+ },
172
+ "248065": {
173
+ "content": "<|file_sep|>",
174
+ "lstrip": false,
175
+ "normalized": false,
176
+ "rstrip": false,
177
+ "single_word": false,
178
+ "special": false
179
+ },
180
+ "248066": {
181
+ "content": "<tool_response>",
182
+ "lstrip": false,
183
+ "normalized": false,
184
+ "rstrip": false,
185
+ "single_word": false,
186
+ "special": false
187
+ },
188
+ "248067": {
189
+ "content": "</tool_response>",
190
+ "lstrip": false,
191
+ "normalized": false,
192
+ "rstrip": false,
193
+ "single_word": false,
194
+ "special": false
195
+ },
196
+ "248068": {
197
+ "content": "<think>",
198
+ "lstrip": false,
199
+ "normalized": false,
200
+ "rstrip": false,
201
+ "single_word": false,
202
+ "special": false
203
+ },
204
+ "248069": {
205
+ "content": "</think>",
206
+ "lstrip": false,
207
+ "normalized": false,
208
+ "rstrip": false,
209
+ "single_word": false,
210
+ "special": false
211
+ },
212
+ "248070": {
213
+ "content": "<|audio_start|>",
214
+ "lstrip": false,
215
+ "normalized": false,
216
+ "rstrip": false,
217
+ "single_word": false,
218
+ "special": true
219
+ },
220
+ "248071": {
221
+ "content": "<|audio_end|>",
222
+ "lstrip": false,
223
+ "normalized": false,
224
+ "rstrip": false,
225
+ "single_word": false,
226
+ "special": true
227
+ },
228
+ "248072": {
229
+ "content": "<tts_pad>",
230
+ "lstrip": false,
231
+ "normalized": false,
232
+ "rstrip": false,
233
+ "single_word": false,
234
+ "special": true
235
+ },
236
+ "248073": {
237
+ "content": "<tts_text_bos>",
238
+ "lstrip": false,
239
+ "normalized": false,
240
+ "rstrip": false,
241
+ "single_word": false,
242
+ "special": true
243
+ },
244
+ "248074": {
245
+ "content": "<tts_text_eod>",
246
+ "lstrip": false,
247
+ "normalized": false,
248
+ "rstrip": false,
249
+ "single_word": false,
250
+ "special": true
251
+ },
252
+ "248075": {
253
+ "content": "<tts_text_bos_single>",
254
+ "lstrip": false,
255
+ "normalized": false,
256
+ "rstrip": false,
257
+ "single_word": false,
258
+ "special": true
259
+ },
260
+ "248076": {
261
+ "content": "<|audio_pad|>",
262
+ "lstrip": false,
263
+ "normalized": false,
264
+ "rstrip": false,
265
+ "single_word": false,
266
+ "special": true
267
+ }
268
+ },
269
+ "additional_special_tokens": [
270
+ "<|im_start|>",
271
+ "<|im_end|>",
272
+ "<|object_ref_start|>",
273
+ "<|object_ref_end|>",
274
+ "<|box_start|>",
275
+ "<|box_end|>",
276
+ "<|quad_start|>",
277
+ "<|quad_end|>",
278
+ "<|vision_start|>",
279
+ "<|vision_end|>",
280
+ "<|vision_pad|>",
281
+ "<|image_pad|>",
282
+ "<|video_pad|>"
283
+ ],
284
+ "bos_token": null,
285
+ "chat_template": "{%- set image_count = namespace(value=0) %}\n{%- set video_count = namespace(value=0) %}\n{%- macro render_content(content, do_vision_count, is_system_content=false) %}\n {%- if content is string %}\n {{- content }}\n {%- elif content is iterable and content is not mapping %}\n {%- for item in content %}\n {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}\n {%- if is_system_content %}\n {{- raise_exception('System message cannot contain images.') }}\n {%- endif %}\n {%- if do_vision_count %}\n {%- set image_count.value = image_count.value + 1 %}\n {%- endif %}\n {%- if add_vision_id %}\n {{- 'Picture ' ~ image_count.value ~ ': ' }}\n {%- endif %}\n {{- '<|vision_start|><|image_pad|><|vision_end|>' }}\n {%- elif 'video' in item or item.type == 'video' %}\n {%- if is_system_content %}\n {{- raise_exception('System message cannot contain videos.') }}\n {%- endif %}\n {%- if do_vision_count %}\n {%- set video_count.value = video_count.value + 1 %}\n {%- endif %}\n {%- if add_vision_id %}\n {{- 'Video ' ~ video_count.value ~ ': ' }}\n {%- endif %}\n {{- '<|vision_start|><|video_pad|><|vision_end|>' }}\n {%- elif 'text' in item %}\n {{- item.text }}\n {%- else %}\n {{- raise_exception('Unexpected item type in content.') }}\n {%- endif %}\n {%- endfor %}\n {%- elif content is none or content is undefined %}\n {{- '' }}\n {%- else %}\n {{- raise_exception('Unexpected content type.') }}\n {%- endif %}\n{%- endmacro %}\n{%- if not messages %}\n {{- raise_exception('No messages provided.') }}\n{%- endif %}\n{%- if tools and tools is iterable and tools is not mapping %}\n {{- '<|im_start|>system\\n' }}\n {{- \"# Tools\\n\\nYou have access to the following functions:\\n\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\" }}\n {{- '\\n\\nIf you choose to call a function ONLY reply in the following format with NO suffix:\\n\\n<tool_call>\\n<function=example_function_name>\\n<parameter=example_parameter_1>\\nvalue_1\\n</parameter>\\n<parameter=example_parameter_2>\\nThis is the value for the second parameter\\nthat can span\\nmultiple lines\\n</parameter>\\n</function>\\n</tool_call>\\n\\n<IMPORTANT>\\nReminder:\\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\\n- Required parameters MUST be specified\\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\\n</IMPORTANT>' }}\n {%- if messages[0].role == 'system' %}\n {%- set content = render_content(messages[0].content, false, true)|trim %}\n {%- if content %}\n {{- '\\n\\n' + content }}\n {%- endif %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {%- set content = render_content(messages[0].content, false, true)|trim %}\n {{- '<|im_start|>system\\n' + content + '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" %}\n {%- set content = render_content(message.content, false)|trim %}\n {%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if ns.multi_step_tool %}\n {{- raise_exception('No user query found in messages.') }}\n{%- endif %}\n{%- for message in messages %}\n {%- set content = render_content(message.content, true)|trim %}\n {%- if message.role == \"system\" %}\n {%- if not loop.first %}\n {{- raise_exception('System message must be at the beginning.') }}\n {%- endif %}\n {%- elif message.role == \"user\" %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '</think>' in content %}\n {%- set reasoning_content = content.split('</think>')[0].rstrip('\\n').split('<think>')[-1].lstrip('\\n') %}\n {%- set content = content.split('</think>')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- set reasoning_content = reasoning_content|trim %}\n {%- if loop.index0 > ns.last_query_index %}\n {{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content + '\\n</think>\\n\\n' + content }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {%- if loop.first %}\n {%- if content|trim %}\n {{- '\\n\\n<tool_call>\\n<function=' + tool_call.name + '>\\n' }}\n {%- else %}\n {{- '<tool_call>\\n<function=' + tool_call.name + '>\\n' }}\n {%- endif %}\n {%- else %}\n {{- '\\n<tool_call>\\n<function=' + tool_call.name + '>\\n' }}\n {%- endif %}\n {%- if tool_call.arguments is defined %}\n {%- for args_name, args_value in tool_call.arguments|items %}\n {{- '<parameter=' + args_name + '>\\n' }}\n {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}\n {{- args_value }}\n {{- '\\n</parameter>\\n' }}\n {%- endfor %}\n {%- endif %}\n {{- '</function>\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.previtem and loop.previtem.role != \"tool\" %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {{- content }}\n {{- '\\n</tool_response>' }}\n {%- if not loop.last and loop.nextitem.role != \"tool\" %}\n {{- '<|im_end|>\\n' }}\n {%- elif loop.last %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- else %}\n {{- raise_exception('Unexpected message role.') }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is true %}\n {{- '<think>\\n' }}\n {%- else %}\n {{- '<think>\\n\\n</think>\\n\\n' }}\n {%- endif %}\n{%- endif %}",
286
+ "clean_up_tokenization_spaces": false,
287
+ "eos_token": "<|im_end|>",
288
+ "errors": "replace",
289
+ "model_max_length": 262144,
290
+ "pad_token": "<|endoftext|>",
291
+ "split_special_tokens": false,
292
+ "tokenizer_class": "Qwen2Tokenizer",
293
+ "unk_token": null,
294
+ "add_bos_token": false,
295
+ "pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
296
+ "extra_special_tokens": {
297
+ "audio_bos_token": "<|audio_start|>",
298
+ "audio_eos_token": "<|audio_end|>",
299
+ "audio_token": "<|audio_pad|>",
300
+ "image_token": "<|image_pad|>",
301
+ "video_token": "<|video_pad|>",
302
+ "vision_bos_token": "<|vision_start|>",
303
+ "vision_eos_token": "<|vision_end|>"
304
+ },
305
+ "processor_class": "ConCor1Processor"
306
+ }
vocab.json ADDED
The diff for this file is too large to render. See raw diff