CCCCyx commited on
Commit
2e600c2
·
verified ·
1 Parent(s): 404f9d8

Add files using upload-large-folder tool

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
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
37
+ assets/benchmark-streaming.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/architecture.png filter=lfs diff=lfs merge=lfs -text
39
+ assets/logo.png filter=lfs diff=lfs merge=lfs -text
KV_QUANTIZATION.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # MOSS-VL quantized KV-cache variant
2
+
3
+ - Weights: bitsandbytes NF4 W4A16
4
+ - KV cache: HQQ 8-bit
5
+ - Quantization group size: 64
6
+ - BF16 residual window: 128 tokens per layer
7
+ - Vision/cross-attention weights remain BF16.
8
+ - Requires the quant venv with the selected KV backend installed.
QUANTIZATION.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # MOSS-VL Streaming quantized checkpoint
2
+
3
+ - Variant: `w4_keep_ends`
4
+ - Method: `bitsandbytes NF4 W4A16 keep-first-last-4`
5
+ - Compute dtype: `bfloat16`
6
+ - Quantized Linear modules: `240`
7
+ - Vision tower, cross-attention, embeddings, lm_head and norms remain BF16.
README.md CHANGED
@@ -1,3 +1,196 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ language:
4
+ - en
5
+ - zh
6
+ library_name: transformers
7
+ pipeline_tag: video-text-to-text
8
+ base_model: OpenMOSS-Team/MOSS-VL-Realtime
9
+ tags:
10
+ - MOSS-VL
11
+ - realtime
12
+ - streaming
13
+ - video-understanding
14
+ - bitsandbytes
15
+ - NF4
16
+ - quantized
17
+ - custom_code
18
  ---
19
+
20
+ # MOSS-VL-Realtime W4A16 NF4 + KV8 HQQ
21
+
22
+ This is the 24 GiB quantized release of MOSS-VL-Realtime. It keeps the original
23
+ timestamp-aware streaming interface and can also use the offline image/video
24
+ helpers from the standard checkpoint.
25
+
26
+ ## Quantization profile
27
+
28
+ | Component | Format |
29
+ | --- | --- |
30
+ | Most language layers | bitsandbytes NF4 4-bit weights with double quantization |
31
+ | First/last language layers and multimodal modules | BF16 |
32
+ | Activations and compute | BF16 |
33
+ | Transformers KV cache | HQQ INT8 |
34
+ | Attention backend | FlashAttention 2 |
35
+
36
+ The checkpoint carries its bitsandbytes configuration, HQQ cache configuration,
37
+ and MOSS-VL remote modeling code. Load the directory directly; do not add a
38
+ second runtime quantization configuration.
39
+
40
+ ## Hardware requirements
41
+
42
+ The model is designed to run on a single NVIDIA GPU with 24 GB of VRAM. Use
43
+ FlashAttention 2 and `frame_queue_size=1` for the 24 GB realtime profile.
44
+
45
+ ## Environment
46
+
47
+ ### Installation
48
+
49
+ Use the standard MOSS-VL repository requirements, then add the two quantization
50
+ backends required by this checkpoint:
51
+
52
+ ```bash
53
+ git clone https://github.com/OpenMOSS/MOSS-VL.git
54
+ cd MOSS-VL
55
+
56
+ conda create -n moss_vl_quant python=3.12 pip -y
57
+ conda activate moss_vl_quant
58
+ pip install -i https://pypi.org/simple --no-build-isolation -r requirements.txt
59
+ pip install -i https://pypi.org/simple \
60
+ bitsandbytes==0.49.2 \
61
+ hqq==0.2.8.post1
62
+ python -m pip check
63
+ ```
64
+
65
+ The standard release environment uses the following core stack:
66
+
67
+ | Package | Version |
68
+ | --- | --- |
69
+ | Python | 3.12 |
70
+ | PyTorch | 2.8.0 + CUDA 12.8 |
71
+ | Transformers | 4.57.1 |
72
+ | Accelerate | 1.12.0 |
73
+ | FlashAttention | 2.8.1 |
74
+ | bitsandbytes | 0.49.2 |
75
+ | HQQ | 0.2.8.post1 |
76
+
77
+ Video decoding also requires FFmpeg to be available in `PATH`.
78
+
79
+ ## Load the model
80
+
81
+ Keep `attn_implementation` set to `flash_attention_2` for the 24 GB profile.
82
+
83
+ ```python
84
+ import torch
85
+ from transformers import AutoModelForCausalLM, AutoProcessor
86
+
87
+ checkpoint = "/path/to/mossvl_streaming_w4a16_nf4_keep_first4_last4_kv8_hqq"
88
+
89
+ processor = AutoProcessor.from_pretrained(
90
+ checkpoint,
91
+ trust_remote_code=True,
92
+ frame_extract_num_threads=1,
93
+ )
94
+ model = AutoModelForCausalLM.from_pretrained(
95
+ checkpoint,
96
+ trust_remote_code=True,
97
+ device_map="auto",
98
+ torch_dtype=torch.bfloat16,
99
+ attn_implementation="flash_attention_2",
100
+ )
101
+ model.eval()
102
+ ```
103
+
104
+ `generation_config.json` automatically enables the HQQ INT8 KV cache. Do not
105
+ override it with a BF16/dynamic cache when using the 24 GB profile.
106
+
107
+ ## Realtime inference
108
+
109
+ The application supplies PIL-compatible frames with non-decreasing timestamps.
110
+ Use `frame_queue_size=1` for the 24 GB realtime profile.
111
+
112
+ ```python
113
+ import time
114
+ from PIL import Image
115
+
116
+ session = model.create_realtime_session(
117
+ processor,
118
+ initial_prompt=(
119
+ "Describe important changes in the video as they happen. "
120
+ "Stay silent when there is no meaningful update."
121
+ ),
122
+ frame_queue_size=1,
123
+ max_tokens_per_turn=12,
124
+ max_new_tokens=4096,
125
+ do_sample=False,
126
+ )
127
+
128
+ frame_paths = [
129
+ "data/frame_0001.jpg",
130
+ "data/frame_0002.jpg",
131
+ "data/frame_0003.jpg",
132
+ ]
133
+
134
+ try:
135
+ session.start()
136
+ for index, frame_path in enumerate(frame_paths):
137
+ image = Image.open(frame_path).convert("RGB")
138
+ session.push_frame(image, timestamp=float(index))
139
+
140
+ while True:
141
+ chunk = session.poll_output(timeout=0.0)
142
+ if chunk is None:
143
+ break
144
+ print(chunk, end="", flush=True)
145
+
146
+ time.sleep(1.0)
147
+
148
+ session.push_prompt("What changed in the latest frames?")
149
+ deadline = time.monotonic() + 5.0
150
+ while time.monotonic() < deadline:
151
+ chunk = session.poll_output(timeout=0.1)
152
+ if chunk is not None:
153
+ print(chunk, end="", flush=True)
154
+ finally:
155
+ session.close()
156
+ ```
157
+
158
+ One model instance supports one active realtime session. The model may emit
159
+ control tokens such as `<|silence|>`, `<|round_start|>`, and `<|round_end|>`;
160
+ applications should filter or render them according to their protocol.
161
+
162
+ ## Offline video inference
163
+
164
+ ```python
165
+ text = model.offline_video_generate(
166
+ processor,
167
+ prompt="Describe this video.",
168
+ video="data/example_video.mp4",
169
+ shortest_edge=4096,
170
+ longest_edge=16777216,
171
+ video_max_pixels=201326592,
172
+ patch_size=16,
173
+ temporal_patch_size=1,
174
+ merge_size=2,
175
+ video_fps=1.0,
176
+ min_frames=1,
177
+ max_frames=256,
178
+ num_extract_threads=4,
179
+ image_mean=[0.5, 0.5, 0.5],
180
+ image_std=[0.5, 0.5, 0.5],
181
+ max_new_tokens=256,
182
+ temperature=1.0,
183
+ top_k=50,
184
+ top_p=1.0,
185
+ repetition_penalty=1.0,
186
+ do_sample=False,
187
+ vision_chunked_length=64,
188
+ )
189
+ print(text)
190
+ ```
191
+
192
+ ## Configuration files
193
+
194
+ - `config.json`: model and NF4 weight configuration.
195
+ - `generation_config.json`: HQQ KV8 configuration.
196
+ - `modeling_moss_vl.py`: checkpoint-local MOSS-VL and QuantizedCache code.
added_tokens.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "</think>": 151668,
3
+ "</tool_call>": 151658,
4
+ "</tool_response>": 151666,
5
+ "<think>": 151667,
6
+ "<tool_call>": 151657,
7
+ "<tool_response>": 151665,
8
+ "<|box_end|>": 151649,
9
+ "<|box_start|>": 151648,
10
+ "<|endoftext|>": 151643,
11
+ "<|file_sep|>": 151664,
12
+ "<|fim_middle|>": 151660,
13
+ "<|fim_pad|>": 151662,
14
+ "<|fim_prefix|>": 151659,
15
+ "<|fim_suffix|>": 151661,
16
+ "<|im_end|>": 151645,
17
+ "<|im_start|>": 151644,
18
+ "<|image_pad|>": 151655,
19
+ "<|object_ref_end|>": 151647,
20
+ "<|object_ref_start|>": 151646,
21
+ "<|quad_end|>": 151651,
22
+ "<|quad_start|>": 151650,
23
+ "<|repo_name|>": 151663,
24
+ "<|response|>": 151672,
25
+ "<|silence|>": 151671,
26
+ "<|time_end|>": 151670,
27
+ "<|time_start|>": 151669,
28
+ "<|video_pad|>": 151656,
29
+ "<|vision_end|>": 151653,
30
+ "<|vision_pad|>": 151654,
31
+ "<|vision_start|>": 151652
32
+ }
assets/architecture.png ADDED

Git LFS Details

  • SHA256: 28e51b34e031c884c540601d6b6651576c65836dc252694a1dc7dc0b1fc15237
  • Pointer size: 132 Bytes
  • Size of remote file: 6.09 MB
assets/benchmark-streaming.png ADDED

Git LFS Details

  • SHA256: 7366164e8e0b8cca177659d9d595219923b4bd8ecfc0ca2d1d77b42f3abb258c
  • Pointer size: 132 Bytes
  • Size of remote file: 1.07 MB
assets/logo.png ADDED

Git LFS Details

  • SHA256: 37bf0241f5fbab73fc955827f477befc95b88b0d3618c31628099b70ea1dd232
  • Pointer size: 131 Bytes
  • Size of remote file: 941 kB
chat_template.jinja ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0].role == 'system' %}
4
+ {%- if messages[0].content is string %}
5
+ {{- messages[0].content }}
6
+ {%- else %}
7
+ {%- for content in messages[0].content %}
8
+ {%- if 'text' in content %}
9
+ {{- content.text }}
10
+ {%- endif %}
11
+ {%- endfor %}
12
+ {%- endif %}
13
+ {{- '\n\n' }}
14
+ {%- endif %}
15
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
16
+ {%- for tool in tools %}
17
+ {{- "\n" }}
18
+ {{- tool | tojson }}
19
+ {%- endfor %}
20
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
21
+ {%- else %}
22
+ {%- if messages[0].role == 'system' %}
23
+ {{- '<|im_start|>system\n' }}
24
+ {%- if messages[0].content is string %}
25
+ {{- messages[0].content }}
26
+ {%- else %}
27
+ {%- for content in messages[0].content %}
28
+ {%- if 'text' in content %}
29
+ {{- content.text }}
30
+ {%- endif %}
31
+ {%- endfor %}
32
+ {%- endif %}
33
+ {{- '<|im_end|>\n' }}
34
+ {%- endif %}
35
+ {%- endif %}
36
+ {%- set image_count = namespace(value=0) %}
37
+ {%- set video_count = namespace(value=0) %}
38
+ {%- for message in messages %}
39
+ {%- if message.role == "user" %}
40
+ {{- '<|im_start|>' + message.role + '\n' }}
41
+ {%- if message.content is string %}
42
+ {{- message.content }}
43
+ {%- else %}
44
+ {%- for content in message.content %}
45
+ {%- if content.type == 'image' or 'image' in content or 'image_url' in content %}
46
+ {%- set image_count.value = image_count.value + 1 %}
47
+ {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
48
+ <|image|>
49
+ {%- elif content.type == 'video' or 'video' in content %}
50
+ {%- set video_count.value = video_count.value + 1 %}
51
+ {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
52
+ <|video|>
53
+ {%- elif 'text' in content %}
54
+ {{- content.text }}
55
+ {%- endif %}
56
+ {%- endfor %}
57
+ {%- endif %}
58
+ {{- '<|im_end|>\n' }}
59
+ {%- elif message.role == "assistant" %}
60
+ {{- '<|im_start|>' + message.role + '\n' }}
61
+ {%- if message.content is string %}
62
+ {{- message.content }}
63
+ {%- else %}
64
+ {%- for content_item in message.content %}
65
+ {%- if 'text' in content_item %}
66
+ {{- content_item.text }}
67
+ {%- endif %}
68
+ {%- endfor %}
69
+ {%- endif %}
70
+ {%- if message.tool_calls %}
71
+ {%- for tool_call in message.tool_calls %}
72
+ {%- if (loop.first and message.content) or (not loop.first) %}
73
+ {{- '\n' }}
74
+ {%- endif %}
75
+ {%- if tool_call.function %}
76
+ {%- set tool_call = tool_call.function %}
77
+ {%- endif %}
78
+ {{- '<tool_call>\n{"name": "' }}
79
+ {{- tool_call.name }}
80
+ {{- '", "arguments": ' }}
81
+ {%- if tool_call.arguments is string %}
82
+ {{- tool_call.arguments }}
83
+ {%- else %}
84
+ {{- tool_call.arguments | tojson }}
85
+ {%- endif %}
86
+ {{- '}\n</tool_call>' }}
87
+ {%- endfor %}
88
+ {%- endif %}
89
+ {{- '<|im_end|>\n' }}
90
+ {%- elif message.role == "tool" %}
91
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
92
+ {{- '<|im_start|>user' }}
93
+ {%- endif %}
94
+ {{- '\n<tool_response>\n' }}
95
+ {%- if message.content is string %}
96
+ {{- message.content }}
97
+ {%- else %}
98
+ {%- for content in message.content %}
99
+ {%- if content.type == 'image' or 'image' in content or 'image_url' in content %}
100
+ {%- set image_count.value = image_count.value + 1 %}
101
+ {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}
102
+ <|image|>
103
+ {%- elif content.type == 'video' or 'video' in content %}
104
+ {%- set video_count.value = video_count.value + 1 %}
105
+ {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}
106
+ <|video|>
107
+ {%- elif 'text' in content %}
108
+ {{- content.text }}
109
+ {%- endif %}
110
+ {%- endfor %}
111
+ {%- endif %}
112
+ {{- '\n</tool_response>' }}
113
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
114
+ {{- '<|im_end|>\n' }}
115
+ {%- endif %}
116
+ {%- endif %}
117
+ {%- endfor %}
118
+ {%- if add_generation_prompt %}
119
+ {{- '<|im_start|>assistant\n' }}
120
+ {%- endif %}
chat_template.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "chat_template": "{%- if tools %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].role == 'system' %}\n {%- if messages[0].content is string %}\n {{- messages[0].content }}\n {%- else %}\n {%- for content in messages[0].content %}\n {%- if 'text' in content %}\n {{- content.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '\\n\\n' }}\n {%- endif %}\n {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {{- '<|im_start|>system\\n' }}\n {%- if messages[0].content is string %}\n {{- messages[0].content }}\n {%- else %}\n {%- for content in messages[0].content %}\n {%- if 'text' in content %}\n {{- content.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set image_count = namespace(value=0) %}\n{%- set video_count = namespace(value=0) %}\n{%- for message in messages %}\n {%- if message.role == \"user\" %}\n {{- '<|im_start|>' + message.role + '\\n' }}\n {%- if message.content is string %}\n {{- message.content }}\n {%- else %}\n {%- for content in message.content %}\n {%- if content.type == 'image' or 'image' in content or 'image_url' in content %}\n {%- set image_count.value = image_count.value + 1 %}\n {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}\n <|image|>\n {%- elif content.type == 'video' or 'video' in content %}\n {%- set video_count.value = video_count.value + 1 %}\n {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}\n <|video|>\n {%- elif 'text' in content %}\n {{- content.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"assistant\" %}\n {{- '<|im_start|>' + message.role + '\\n' }}\n {%- if message.content is string %}\n {{- message.content }}\n {%- else %}\n {%- for content_item in message.content %}\n {%- if 'text' in content_item %}\n {{- content_item.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {%- if message.tool_calls %}\n {%- for tool_call in message.tool_calls %}\n {%- if (loop.first and message.content) or (not loop.first) %}\n {{- '\\n' }}\n {%- endif %}\n {%- if tool_call.function %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {{- '<tool_call>\\n{\"name\": \"' }}\n {{- tool_call.name }}\n {{- '\", \"arguments\": ' }}\n {%- if tool_call.arguments is string %}\n {{- tool_call.arguments }}\n {%- else %}\n {{- tool_call.arguments | tojson }}\n {%- endif %}\n {{- '}\\n</tool_call>' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n<tool_response>\\n' }}\n {%- if message.content is string %}\n {{- message.content }}\n {%- else %}\n {%- for content in message.content %}\n {%- if content.type == 'image' or 'image' in content or 'image_url' in content %}\n {%- set image_count.value = image_count.value + 1 %}\n {%- if add_vision_id %}Picture {{ image_count.value }}: {% endif -%}\n <|image|>\n {%- elif content.type == 'video' or 'video' in content %}\n {%- set video_count.value = video_count.value + 1 %}\n {%- if add_vision_id %}Video {{ video_count.value }}: {% endif -%}\n <|video|>\n {%- elif 'text' in content %}\n {{- content.text }}\n {%- endif %}\n {%- endfor %}\n {%- endif %}\n {{- '\\n</tool_response>' }}\n {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}\n"
3
+ }
config.json ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "MossVLForConditionalGeneration"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_moss_vl.MossVLConfig",
7
+ "AutoModel": "modeling_moss_vl.MossVLForConditionalGeneration",
8
+ "AutoModelForCausalLM": "modeling_moss_vl.MossVLForConditionalGeneration"
9
+ },
10
+ "dtype": "bfloat16",
11
+ "image_token_id": 151655,
12
+ "model_type": "moss_vl",
13
+ "quantization_config": {
14
+ "_load_in_4bit": true,
15
+ "_load_in_8bit": false,
16
+ "bnb_4bit_compute_dtype": "bfloat16",
17
+ "bnb_4bit_quant_storage": "uint8",
18
+ "bnb_4bit_quant_type": "nf4",
19
+ "bnb_4bit_use_double_quant": true,
20
+ "llm_int8_enable_fp32_cpu_offload": false,
21
+ "llm_int8_has_fp16_weight": false,
22
+ "llm_int8_skip_modules": [
23
+ "model.visual",
24
+ "cross_attn",
25
+ "lm_head",
26
+ "model.language_model.layers.0",
27
+ "model.language_model.layers.1",
28
+ "model.language_model.layers.2",
29
+ "model.language_model.layers.3",
30
+ "model.language_model.layers.44",
31
+ "model.language_model.layers.45",
32
+ "model.language_model.layers.46",
33
+ "model.language_model.layers.47"
34
+ ],
35
+ "llm_int8_threshold": 6.0,
36
+ "load_in_4bit": true,
37
+ "load_in_8bit": false,
38
+ "quant_method": "bitsandbytes"
39
+ },
40
+ "text_config": {
41
+ "attention_bias": false,
42
+ "attention_dropout": 0.0,
43
+ "bos_token_id": 151643,
44
+ "cross_attention_layers": [
45
+ 2,
46
+ 6,
47
+ 10,
48
+ 14,
49
+ 18,
50
+ 22,
51
+ 26,
52
+ 30,
53
+ 34,
54
+ 38,
55
+ 42,
56
+ 46
57
+ ],
58
+ "dtype": "bfloat16",
59
+ "eos_token_id": 151645,
60
+ "head_dim": 128,
61
+ "hidden_act": "silu",
62
+ "hidden_size": 4096,
63
+ "initializer_range": 0.02,
64
+ "intermediate_size": 12288,
65
+ "max_position_embeddings": 262144,
66
+ "model_type": "moss_vl_text",
67
+ "num_attention_heads": 32,
68
+ "num_hidden_layers": 48,
69
+ "num_key_value_heads": 8,
70
+ "rms_norm_eps": 1e-06,
71
+ "rope_scaling": {
72
+ "mrope_interleaved": true,
73
+ "mrope_section": [
74
+ 24,
75
+ 20,
76
+ 20
77
+ ],
78
+ "rope_type": "default"
79
+ },
80
+ "rope_theta": 5000000,
81
+ "use_cache": true,
82
+ "vocab_size": 151936
83
+ },
84
+ "tie_word_embeddings": false,
85
+ "transformers_version": "4.57.1",
86
+ "video_token_id": 151656,
87
+ "vision_config": {
88
+ "deepstack_visual_indexes": [
89
+ 8,
90
+ 16,
91
+ 24
92
+ ],
93
+ "depth": 27,
94
+ "dtype": "bfloat16",
95
+ "hidden_act": "gelu_pytorch_tanh",
96
+ "hidden_size": 1152,
97
+ "in_channels": 3,
98
+ "initializer_range": 0.02,
99
+ "intermediate_size": 4304,
100
+ "model_type": "moss_vl_vision",
101
+ "num_heads": 16,
102
+ "num_position_embeddings": 2304,
103
+ "out_hidden_size": 4096,
104
+ "patch_size": 16,
105
+ "spatial_merge_size": 2,
106
+ "temporal_patch_size": 1
107
+ },
108
+ "vision_end_token_id": 151653,
109
+ "vision_seq_pad_multiple": 1,
110
+ "vision_start_token_id": 151652
111
+ }
configuration_moss_vl.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2025 The HuggingFace Inc. team. All rights reserved.
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
+ """MossVL model configuration"""
16
+
17
+ from transformers.configuration_utils import PretrainedConfig
18
+ from transformers.modeling_rope_utils import rope_config_validation
19
+ from transformers.utils import logging
20
+
21
+ logger = logging.get_logger(__name__)
22
+
23
+
24
+ class MossVLVisionConfig(PretrainedConfig):
25
+ """
26
+ Configuration for MossVL Vision Model
27
+ """
28
+
29
+ model_type = "moss_vl_vision"
30
+ base_config_key = "vision_config"
31
+
32
+ def __init__(
33
+ self,
34
+ depth=27,
35
+ hidden_size=1152,
36
+ hidden_act="gelu_pytorch_tanh",
37
+ intermediate_size=4304,
38
+ num_heads=16,
39
+ in_channels=3,
40
+ patch_size=16,
41
+ spatial_merge_size=2,
42
+ temporal_patch_size=1,
43
+ out_hidden_size=3584,
44
+ num_position_embeddings=2304,
45
+ deepstack_visual_indexes=[8, 16, 24],
46
+ initializer_range=0.02,
47
+ **kwargs,
48
+ ):
49
+ super().__init__(**kwargs)
50
+ self.depth = depth
51
+ self.hidden_size = hidden_size
52
+ self.hidden_act = hidden_act
53
+ self.intermediate_size = intermediate_size
54
+ self.num_heads = num_heads
55
+ self.in_channels = in_channels
56
+ self.patch_size = patch_size
57
+ self.spatial_merge_size = spatial_merge_size
58
+ self.temporal_patch_size = temporal_patch_size
59
+ self.out_hidden_size = out_hidden_size
60
+ self.num_position_embeddings = num_position_embeddings
61
+ self.initializer_range = initializer_range
62
+ self.deepstack_visual_indexes = deepstack_visual_indexes
63
+
64
+
65
+ class MossVLTextConfig(PretrainedConfig):
66
+ """
67
+ Configuration for MossVL Text Model
68
+ """
69
+
70
+ model_type = "moss_vl_text"
71
+ base_config_key = "text_config"
72
+
73
+ def __init__(
74
+ self,
75
+ vocab_size=151936,
76
+ hidden_size=4096,
77
+ intermediate_size=22016,
78
+ num_hidden_layers=32,
79
+ num_attention_heads=32,
80
+ num_key_value_heads=32,
81
+ head_dim=128,
82
+ hidden_act="silu",
83
+ max_position_embeddings=128000,
84
+ initializer_range=0.02,
85
+ rms_norm_eps=1e-6,
86
+ use_cache=True,
87
+ tie_word_embeddings=False,
88
+ rope_theta=5000000.0,
89
+ rope_scaling=None,
90
+ attention_bias=False,
91
+ attention_dropout=0.0,
92
+ # Cross attention specific
93
+ cross_attention_layers=None, # List of layer indices to insert cross attention
94
+ **kwargs,
95
+ ):
96
+
97
+ self.vocab_size = vocab_size
98
+ self.max_position_embeddings = max_position_embeddings
99
+ self.hidden_size = hidden_size
100
+ self.intermediate_size = intermediate_size
101
+ self.num_hidden_layers = num_hidden_layers
102
+ self.num_attention_heads = num_attention_heads
103
+
104
+ # for backward compatibility
105
+ if num_key_value_heads is None:
106
+ num_key_value_heads = num_attention_heads
107
+
108
+ self.num_key_value_heads = num_key_value_heads
109
+ self.head_dim = head_dim
110
+ self.hidden_act = hidden_act
111
+ self.initializer_range = initializer_range
112
+ self.rms_norm_eps = rms_norm_eps
113
+ self.use_cache = use_cache
114
+ self.rope_theta = rope_theta
115
+ self.rope_scaling = rope_scaling
116
+ self.attention_bias = attention_bias
117
+ self.attention_dropout = attention_dropout
118
+
119
+ rope_config_validation(self, ignore_keys={"mrope_section", "mrope_interleaved"})
120
+ self.cross_attention_layers = cross_attention_layers or [2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46]
121
+ super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs)
122
+
123
+ class MossVLConfig(PretrainedConfig):
124
+ """
125
+ Configuration for MossVL Model
126
+ """
127
+
128
+ model_type = "moss_vl"
129
+ sub_configs = {"vision_config": MossVLVisionConfig, "text_config": MossVLTextConfig}
130
+ keys_to_ignore_at_inference = ["past_key_values"]
131
+
132
+ def __init__(
133
+ self,
134
+ text_config=None,
135
+ vision_config=None,
136
+ image_token_id=151655,
137
+ video_token_id=151656,
138
+ vision_start_token_id=151652,
139
+ vision_end_token_id=151653,
140
+ vision_seq_pad_multiple=8,
141
+ tie_word_embeddings=False,
142
+ **kwargs,
143
+ ):
144
+ if isinstance(vision_config, dict):
145
+ self.vision_config = self.sub_configs["vision_config"](**vision_config)
146
+ elif vision_config is None:
147
+ self.vision_config = self.sub_configs["vision_config"]()
148
+
149
+ if isinstance(text_config, dict):
150
+ self.text_config = self.sub_configs["text_config"](**text_config)
151
+ elif text_config is None:
152
+ self.text_config = self.sub_configs["text_config"]()
153
+
154
+ self.image_token_id = image_token_id
155
+ self.video_token_id = video_token_id
156
+ self.vision_start_token_id = vision_start_token_id
157
+
158
+
159
+ self.vision_end_token_id = vision_end_token_id
160
+ self.vision_seq_pad_multiple = vision_seq_pad_multiple
161
+ super().__init__(**kwargs, tie_word_embeddings=tie_word_embeddings)
162
+
163
+
164
+ __all__ = ["MossVLConfig", "MossVLTextConfig"]
generation_config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 151643,
4
+ "eos_token_id": 151645,
5
+ "transformers_version": "4.57.1",
6
+ "cache_implementation": "quantized",
7
+ "cache_config": {
8
+ "backend": "hqq",
9
+ "nbits": 8,
10
+ "axis_key": 0,
11
+ "axis_value": 0,
12
+ "q_group_size": 64,
13
+ "residual_length": 128
14
+ }
15
+ }
kv_quantization_report.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "created_at": "2026-07-26T06:34:21.056464+00:00",
3
+ "source_weight_checkpoint": "/inspire/qb-ilm/project/video-understanding/public/train/moss_vl_streaming/8B/quant/mossvl_streaming_w4a16_nf4_keep_first4_last4",
4
+ "weight_quantization": "bitsandbytes NF4 W4A16",
5
+ "kv_cache_quantization": {
6
+ "backend": "hqq",
7
+ "nbits": 8,
8
+ "axis_key": 0,
9
+ "axis_value": 0,
10
+ "q_group_size": 64,
11
+ "residual_length": 128
12
+ },
13
+ "storage": "Model shards are hard-linked to the source checkpoint when the filesystem supports hard links.",
14
+ "compatibility_patch": "Cross-attention materializes the quantized history one layer at a time because the original MOSS-VL code directly reads cache_layer.keys."
15
+ }
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model-00001-of-00003.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b0219c7d3d132e72d80cc6f0d04aa20e98edce88dea6d282ace84539c81e8c12
3
+ size 3962258708
model-00002-of-00003.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:12114b03fc2c0735059a5617610848d480b5bfb04fe13569fa7e24776b8b4ee1
3
+ size 3993030849
model-00003-of-00003.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db7736eefc60465956ece6b3748a5175b60b764375d50d8c13bf67f947a18704
3
+ size 3886708579
model.safetensors.index.json ADDED
The diff for this file is too large to render. See raw diff
 
modeling_moss_vl.py ADDED
The diff for this file is too large to render. See raw diff
 
preprocessor_config.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoImageProcessor": "processing_moss_vl.MossVLImageProcessorFast",
4
+ "AutoProcessor": "processing_moss_vl.MossVLProcessor"
5
+ },
6
+ "crop_size": null,
7
+ "data_format": "channels_first",
8
+ "default_to_square": true,
9
+ "device": null,
10
+ "disable_grouping": null,
11
+ "do_center_crop": null,
12
+ "do_convert_rgb": true,
13
+ "do_normalize": true,
14
+ "do_pad": null,
15
+ "do_rescale": true,
16
+ "do_resize": true,
17
+ "image_mean": [
18
+ 0.5,
19
+ 0.5,
20
+ 0.5
21
+ ],
22
+ "image_processor_type": "MossVLImageProcessorFast",
23
+ "image_std": [
24
+ 0.5,
25
+ 0.5,
26
+ 0.5
27
+ ],
28
+ "input_data_format": null,
29
+ "max_pixels": null,
30
+ "merge_size": 2,
31
+ "min_pixels": null,
32
+ "multi_image_max_pixels": 65536000,
33
+ "pad_size": null,
34
+ "patch_size": 16,
35
+ "processor_class": "MossVLProcessor",
36
+ "resample": 3,
37
+ "rescale_factor": 0.00392156862745098,
38
+ "return_tensors": null,
39
+ "size": {
40
+ "longest_edge": 16777216,
41
+ "shortest_edge": 4096
42
+ },
43
+ "temporal_patch_size": 1
44
+ }
processing_moss_vl.py ADDED
@@ -0,0 +1,1143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2025 The FNLP Vision Team and The HuggingFace Inc. team. All rights reserved.
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
+ """
16
+ Processor class for Moss-VL.
17
+ """
18
+
19
+ from typing import Any, Dict, List, Optional, Union
20
+
21
+ import numpy as np
22
+ import torch
23
+ from torchvision.transforms.v2 import functional as F
24
+ from PIL import Image
25
+ from transformers.feature_extraction_utils import BatchFeature
26
+ from transformers.image_utils import ImageInput, SizeDict
27
+ from transformers.image_processing_utils_fast import group_images_by_shape, reorder_images
28
+ from transformers.utils import TensorType
29
+ from transformers.processing_utils import (
30
+ ImagesKwargs,
31
+ ProcessingKwargs,
32
+ ProcessorMixin,
33
+ Unpack,
34
+ VideosKwargs,
35
+ )
36
+ from transformers.tokenization_utils_base import PreTokenizedInput, TextInput
37
+ from transformers.utils import logging
38
+ from transformers.models.qwen2_vl.image_processing_qwen2_vl_fast import Qwen2VLImageProcessorFast
39
+ from transformers.models.qwen2_vl.image_processing_qwen2_vl import smart_resize
40
+
41
+
42
+ logger = logging.get_logger(__name__)
43
+
44
+
45
+ class MossVLImageProcessorFast(Qwen2VLImageProcessorFast):
46
+ """
47
+ Custom image processor that overrides _preprocess to support multi_image_max_pixels.
48
+ Inherits from Qwen2VLImageProcessorFast.
49
+ """
50
+ # Multi-image batch total pixels limit (read from config)
51
+ multi_image_max_pixels = None
52
+
53
+
54
+ def _preprocess(
55
+ self,
56
+ images: list["torch.Tensor"],
57
+ do_resize: bool,
58
+ size: SizeDict,
59
+ interpolation: Optional["F.InterpolationMode"],
60
+ do_rescale: bool,
61
+ rescale_factor: float,
62
+ do_normalize: bool,
63
+ image_mean: Optional[Union[float, list[float]]],
64
+ image_std: Optional[Union[float, list[float]]],
65
+ patch_size: int,
66
+ temporal_patch_size: int,
67
+ merge_size: int,
68
+ disable_grouping: Optional[bool],
69
+ return_tensors: Optional[Union[str, TensorType]],
70
+ **kwargs,
71
+ ):
72
+ """Override _preprocess to use custom smart_resize with batch-level max_pixels.
73
+
74
+ multi_image_max_pixels is treated as a batch-level total budget, proportionally allocated
75
+ to each image based on its original pixel count. min_pixels remains a per-image
76
+ constraint. multi_image_max_pixels can be configured separately from longest_edge.
77
+ """
78
+ min_pixels = size["shortest_edge"]
79
+ max_pixels = size["longest_edge"] # Per-image upper limit
80
+ # Use multi_image_max_pixels if configured, otherwise fall back to longest_edge
81
+ multi_image_max_pixels = getattr(self, "multi_image_max_pixels", None) or max_pixels
82
+
83
+ # Calculate total original pixels across all images in the batch
84
+ # This is used to proportionally allocate max_pixels to each image
85
+ total_original_pixels = sum(img.shape[-2] * img.shape[-1] for img in images)
86
+
87
+ # Group images by size for batched resizing
88
+ grouped_images, grouped_images_index = group_images_by_shape(images, disable_grouping=disable_grouping)
89
+ resized_images_grouped = {}
90
+ for shape, stacked_images in grouped_images.items():
91
+ height, width = stacked_images.shape[-2:]
92
+ if do_resize:
93
+ # Calculate proportional max_pixels for images with this shape
94
+ # Each image's max_pixels is allocated based on its proportion of total pixels
95
+ original_pixels = height * width
96
+ if total_original_pixels > 0:
97
+ proportion = original_pixels / total_original_pixels
98
+ proportional_max_pixels = int(multi_image_max_pixels * proportion)
99
+ else:
100
+ proportional_max_pixels = multi_image_max_pixels
101
+
102
+ # Ensure proportional max_pixels is within [min_pixels, max_pixels] range
103
+ # min_pixels: per-image lower limit (shortest_edge)
104
+ # max_pixels: per-image upper limit (longest_edge)
105
+ proportional_max_pixels = max(proportional_max_pixels, min_pixels)
106
+ proportional_max_pixels = min(proportional_max_pixels, max_pixels)
107
+
108
+ resized_height, resized_width = smart_resize(
109
+ height,
110
+ width,
111
+ factor=patch_size * merge_size,
112
+ min_pixels=min_pixels,
113
+ max_pixels=proportional_max_pixels,
114
+ )
115
+ stacked_images = self.resize(
116
+ image=stacked_images,
117
+ size=SizeDict(height=resized_height, width=resized_width),
118
+ interpolation=interpolation,
119
+ )
120
+ resized_images_grouped[shape] = stacked_images
121
+ resized_images = reorder_images(resized_images_grouped, grouped_images_index)
122
+
123
+ # Warn if multi-image batch exceeds multi_image_max_pixels due to min_pixels constraint
124
+ if len(images) > 1:
125
+ total_resized_pixels = sum(img.shape[-2] * img.shape[-1] for img in resized_images)
126
+ if total_resized_pixels > multi_image_max_pixels:
127
+ logger.warning_once(
128
+ f"Multi-image batch total pixels ({total_resized_pixels}) exceeds multi_image_max_pixels ({multi_image_max_pixels}). "
129
+ f"This may happen when image_count * min_pixels > multi_image_max_pixels."
130
+ )
131
+
132
+ # Group images by size for further processing
133
+ # Needed in case do_resize is False, or resize returns images with different sizes
134
+ grouped_images, grouped_images_index = group_images_by_shape(resized_images, disable_grouping=disable_grouping)
135
+ processed_images_grouped = {}
136
+ processed_grids = {}
137
+ for shape, stacked_images in grouped_images.items():
138
+ resized_height, resized_width = stacked_images.shape[-2:]
139
+ # Fused rescale and normalize
140
+ patches = self.rescale_and_normalize(
141
+ stacked_images, do_rescale, rescale_factor, do_normalize, image_mean, image_std
142
+ )
143
+ if patches.ndim == 4:
144
+ # add a temporal dimension if we have images
145
+ patches = patches.unsqueeze(1)
146
+ if patches.shape[1] % temporal_patch_size != 0:
147
+ repeats = patches[:, -1:].repeat(1, temporal_patch_size - 1, 1, 1, 1)
148
+ patches = torch.cat([patches, repeats], dim=1)
149
+ batch_size, grid_t, channel = patches.shape[:3]
150
+ grid_t = grid_t // temporal_patch_size
151
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
152
+
153
+ patches = patches.view(
154
+ batch_size,
155
+ grid_t,
156
+ temporal_patch_size,
157
+ channel,
158
+ grid_h // merge_size,
159
+ merge_size,
160
+ patch_size,
161
+ grid_w // merge_size,
162
+ merge_size,
163
+ patch_size,
164
+ )
165
+ # Reorder dimensions to group grid and patch information for subsequent flattening.
166
+ # (batch, grid_t, grid_h, grid_w, merge_h, merge_w, channel, temp_patch_size, patch_h, patch_w)
167
+ patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
168
+ flatten_patches = patches.reshape(
169
+ batch_size,
170
+ grid_t * grid_h * grid_w,
171
+ channel * temporal_patch_size * patch_size * patch_size,
172
+ )
173
+
174
+ processed_images_grouped[shape] = flatten_patches
175
+ processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
176
+
177
+ processed_images = reorder_images(processed_images_grouped, grouped_images_index)
178
+ processed_grids = reorder_images(processed_grids, grouped_images_index)
179
+ pixel_values = torch.cat(processed_images, dim=0)
180
+ image_grid_thw = torch.tensor(processed_grids)
181
+
182
+ return BatchFeature(
183
+ data={"pixel_values": pixel_values, "image_grid_thw": image_grid_thw}, tensor_type=return_tensors
184
+ )
185
+
186
+ def _to_numpy(x):
187
+ """
188
+ Convert various tensor types to numpy array.
189
+ Supports torch.Tensor, tf.Tensor, jax.Array, np.ndarray, lists, and primitives.
190
+
191
+ Args:
192
+ x: Input value that can be a tensor from various frameworks or a Python primitive
193
+
194
+ Returns:
195
+ np.ndarray: NumPy array representation of the input
196
+ """
197
+ # Already numpy
198
+ if isinstance(x, np.ndarray):
199
+ return x
200
+
201
+ # Torch tensor or TensorFlow tensor (both have .numpy() method)
202
+ if hasattr(x, 'numpy'):
203
+ # For torch tensors on CUDA, need to move to CPU first
204
+ if hasattr(x, 'cpu'):
205
+ return x.cpu().numpy()
206
+ # For TensorFlow or already on CPU
207
+ return x.numpy()
208
+
209
+ # JAX arrays and other array-like objects that support __array__ protocol
210
+ if hasattr(x, '__array__'):
211
+ return np.asarray(x)
212
+
213
+ # Python primitives (list, tuple, int, float)
214
+ return np.array(x)
215
+
216
+
217
+ def _split_array_or_tensor(x, split_indices):
218
+ """Split along the first dimension while preserving tensor/array type."""
219
+ split_indices = [int(idx) for idx in split_indices]
220
+ if isinstance(x, torch.Tensor):
221
+ if not split_indices:
222
+ return [x]
223
+ chunks = []
224
+ start = 0
225
+ for end in split_indices:
226
+ chunks.append(x[start:end])
227
+ start = end
228
+ chunks.append(x[start:])
229
+ return chunks
230
+ return np.split(x, split_indices)
231
+
232
+
233
+ def _concat_array_or_tensor(items, axis=0):
234
+ """Concatenate while preserving tensor/array type and device."""
235
+ if not items:
236
+ return None
237
+
238
+ if any(isinstance(item, torch.Tensor) for item in items):
239
+ ref = next(item for item in items if isinstance(item, torch.Tensor))
240
+ tensor_items = [
241
+ item
242
+ if isinstance(item, torch.Tensor)
243
+ else torch.as_tensor(item, device=ref.device, dtype=ref.dtype)
244
+ for item in items
245
+ ]
246
+ return torch.cat(tensor_items, dim=axis)
247
+
248
+ return np.concatenate(items, axis=axis)
249
+
250
+
251
+ def _stack_array_or_tensor(items, axis=0):
252
+ """Stack while preserving tensor/array type and device."""
253
+ if not items:
254
+ return None
255
+
256
+ if any(isinstance(item, torch.Tensor) for item in items):
257
+ ref = next(item for item in items if isinstance(item, torch.Tensor))
258
+ tensor_items = [
259
+ item
260
+ if isinstance(item, torch.Tensor)
261
+ else torch.as_tensor(item, device=ref.device, dtype=ref.dtype)
262
+ for item in items
263
+ ]
264
+ return torch.stack(tensor_items, dim=axis)
265
+
266
+ return np.stack(items, axis=axis)
267
+
268
+
269
+ class MossVLImagesKwargs(ImagesKwargs):
270
+ min_pixels: Optional[int]
271
+ max_pixels: Optional[int]
272
+ patch_size: Optional[int]
273
+ temporal_patch_size: Optional[int]
274
+ merge_size: Optional[int]
275
+
276
+
277
+
278
+ class MossVLVideosKwargs(VideosKwargs, total=False):
279
+ video_fps: Optional[Union[int, float]]
280
+ min_frames: Optional[int]
281
+ max_frames: Optional[int]
282
+ num_extract_threads: Optional[int]
283
+
284
+
285
+ class MossVLProcessorKwargs(ProcessingKwargs, total=False):
286
+ images_kwargs: MossVLImagesKwargs
287
+ videos_kwargs: MossVLVideosKwargs
288
+ # _defaults = {
289
+ # "text_kwargs": {
290
+ # "padding": True, # 👈 启用 padding
291
+ # "padding_side": "left", # 👈 左 padding
292
+ # "pad_to_multiple_of": 8, # 👈 pad 到 8 的倍数
293
+ # "return_token_type_ids": False,
294
+ # "return_mm_token_type_ids": False,
295
+ # },
296
+ # "videos_kwargs": {"return_metadata": True},
297
+ # }
298
+ _defaults = {
299
+ "text_kwargs": {
300
+ "padding": False,
301
+ "return_token_type_ids": False,
302
+ "return_mm_token_type_ids": False,
303
+ },
304
+ "videos_kwargs": {"return_metadata": True},
305
+ }
306
+
307
+ class MossVLProcessor(ProcessorMixin):
308
+ r"""
309
+ Constructs a Moss-VL processor which wraps a Qwen2VL image processor, Moss-VL video processor and a Qwen2 tokenizer
310
+ into a single processor.
311
+
312
+ [`MossVLProcessor`] offers all the functionalities of [`Qwen2VLImageProcessor`], [`MossVLVideoProcessor`] and [`Qwen2TokenizerFast`].
313
+ See the [`~MossVLProcessor.__call__`] and [`~MossVLProcessor.decode`] for more information.
314
+
315
+ Args:
316
+ image_processor ([`Qwen2VLImageProcessor`], *optional*):
317
+ The image processor is a required input.
318
+ tokenizer ([`Qwen2TokenizerFast`], *optional*):
319
+ The tokenizer is a required input.
320
+ video_processor ([`MossVLVideoProcessor`], *optional*):
321
+ The video processor is a required input.
322
+ chat_template (`str`, *optional*): A Jinja template which will be used to convert lists of messages
323
+ in a chat into a tokenizable string.
324
+ """
325
+
326
+ attributes = ["image_processor", "tokenizer", "video_processor"]
327
+ image_processor_class = "AutoImageProcessor"
328
+ video_processor_class = "AutoVideoProcessor"
329
+ tokenizer_class = ("Qwen2Tokenizer", "Qwen2TokenizerFast")
330
+
331
+ def __init__(
332
+ self,
333
+ image_processor=None,
334
+ tokenizer=None,
335
+ video_processor=None,
336
+ chat_template=None,
337
+ **kwargs
338
+ ):
339
+ super().__init__(image_processor, tokenizer, video_processor, chat_template=chat_template)
340
+
341
+
342
+ self.image_token = "<|image_pad|>" if not hasattr(tokenizer, "image_token") else tokenizer.image_token
343
+ self.video_token = "<|video_pad|>" if not hasattr(tokenizer, "video_token") else tokenizer.video_token
344
+
345
+
346
+ self.image_token_id = (
347
+ tokenizer.image_token_id
348
+ if getattr(tokenizer, "image_token_id", None)
349
+ else tokenizer.convert_tokens_to_ids(self.image_token)
350
+ )
351
+ self.video_token_id = (
352
+ tokenizer.video_token_id
353
+ if getattr(tokenizer, "video_token_id", None)
354
+ else tokenizer.convert_tokens_to_ids(self.video_token)
355
+ )
356
+
357
+ self.vision_start_token = (
358
+ "<|vision_start|>" if not hasattr(tokenizer, "vision_start_token") else tokenizer.vision_start_token
359
+ )
360
+ self.vision_end_token = (
361
+ "<|vision_end|>" if not hasattr(tokenizer, "vision_end_token") else tokenizer.vision_end_token
362
+ )
363
+
364
+ # Placeholders used in input text
365
+ self.image_placeholder = "<|image|>"
366
+ self.video_placeholder = "<|video|>"
367
+
368
+ self.time_start_token = "<|time_start|>"
369
+ self.time_end_token = "<|time_end|>"
370
+
371
+ # EOS token for labels generation (assistant's response should end with this)
372
+ self.im_end_token = "<|im_end|>"
373
+ self.im_end_token_id = tokenizer.convert_tokens_to_ids(self.im_end_token)
374
+
375
+ # Vision-related token ids (all should be masked in labels)
376
+ self.vision_start_token_id = tokenizer.convert_tokens_to_ids(self.vision_start_token)
377
+ self.vision_end_token_id = tokenizer.convert_tokens_to_ids(self.vision_end_token)
378
+
379
+ # Token ids that should always be masked in labels (e.g. <|image_pad|>)
380
+ self.mask_token_ids = {self.image_token_id}
381
+
382
+ def __call__(
383
+ self,
384
+ text: Union[TextInput, PreTokenizedInput, List[TextInput], List[PreTokenizedInput]] = None,
385
+ images: ImageInput = None,
386
+ videos: Union[str, Dict[str, Any], List[Union[str, Dict[str, Any]]]] = None,
387
+ labels_spans: Optional[Union[List[tuple], List[List[tuple]]]] = None,
388
+ ignore_index: int = -100,
389
+ **kwargs: Unpack[MossVLProcessorKwargs],
390
+ ) -> BatchFeature:
391
+ """
392
+ Main method to prepare for the model one or several sequences(s) and image(s)/video(s).
393
+
394
+ Args:
395
+ text (`str`, `list[str]`, `list[list[str]]`):
396
+ The sequence or batch of sequences to be encoded.
397
+ images (`PIL.Image.Image`, `np.ndarray`, `torch.Tensor`, `list[PIL.Image.Image]`, `list[np.ndarray]`, `list[torch.Tensor]`):
398
+ The image or batch of images to be prepared.
399
+ videos (`str`, `Dict`, `list[str]`, `list[Dict]`):
400
+ The video or batch of videos to be prepared. Each video can be:
401
+ - A string path to a video file
402
+ - A dict with keys:
403
+ - "video_path": str, path to the video file
404
+ - "segments": list of segments, where each segment is:
405
+ - [start, end]: a time segment (left-closed, right-open interval in seconds)
406
+ - [time]: a single frame at the specified time (in seconds)
407
+ The number of segments should match the number of video placeholders in the text.
408
+ labels_spans (`list[list[int]]`, `list[list[list[int]]]`, *optional*):
409
+ Character-level spans indicating assistant regions in original text.
410
+ Each span is a [start, end] list with inclusive start and exclusive end.
411
+ Example: [[10, 50], [100, 150]] means characters [10:50) and [100:150) are assistant.
412
+ Note: Use list (not tuple) for spans as they will be modified in place during processing.
413
+ When provided, the processor will generate `labels` in the output, where:
414
+ - Non-assistant tokens have value `ignore_index` (-100 by default)
415
+ - Image tokens always have value `ignore_index` even in assistant part
416
+ - Other assistant tokens have their token id as label
417
+ ignore_index (`int`, *optional*, defaults to -100):
418
+ Value for masked positions in labels.
419
+ return_tensors (`str` or [`~utils.TensorType`], *optional*):
420
+ If set, will return tensors of a particular framework. Acceptable values are:
421
+ - `'tf'`: Return TensorFlow `tf.constant` objects.
422
+ - `'pt'`: Return PyTorch `torch.Tensor` objects.
423
+ - `'np'`: Return NumPy `np.ndarray` objects.
424
+ - `'jax'`: Return JAX `jnp.ndarray` objects.
425
+
426
+
427
+ Returns:
428
+ [`BatchFeature`]: A [`BatchFeature`] with the following fields:
429
+ - **input_ids** -- List of token ids to be fed to a model.
430
+ - **attention_mask** -- List of indices specifying which tokens should be attended to by the model.
431
+ - **pixel_values** -- Pixel values to be fed to a model (concatenation of images and videos).
432
+ - **grid_thw** -- List of grid sizes (t, h, w) for each media item.
433
+ - **media_nums_per_sample** -- List of number of media items per sample.
434
+ - **labels** -- (Optional) Labels for training, only present when `labels_spans` is provided.
435
+ """
436
+ # Merge kwargs with defaults
437
+ output_kwargs = self._merge_kwargs(
438
+ MossVLProcessorKwargs,
439
+ tokenizer_init_kwargs=self.tokenizer.init_kwargs,
440
+ **kwargs,
441
+ )
442
+
443
+ # Step 1: Process images if provided
444
+ if images is not None:
445
+ images_kwargs = output_kwargs["images_kwargs"].copy()
446
+ images_kwargs["return_tensors"] = None
447
+ image_inputs = self.image_processor(images=images, **images_kwargs)
448
+ image_grid_thw = image_inputs["image_grid_thw"]
449
+ else:
450
+ image_inputs = {}
451
+ image_grid_thw = None
452
+
453
+ # Step 2: Process videos if provided
454
+ if videos is not None:
455
+ videos_kwargs = output_kwargs["videos_kwargs"].copy()
456
+ videos_kwargs["return_tensors"] = None
457
+ videos_inputs = self.video_processor(videos=videos, **videos_kwargs)
458
+ video_grid_thw = videos_inputs["video_grid_thw"]
459
+ # If user has not requested video metadata, pop it
460
+ if "return_metadata" not in kwargs:
461
+ video_metadata = videos_inputs.pop("video_metadata")
462
+ else:
463
+ video_metadata = videos_inputs["video_metadata"]
464
+ else:
465
+ videos_inputs = {}
466
+ video_grid_thw = None
467
+ video_metadata = None
468
+
469
+ # Step 3: Process text with placeholder replacement
470
+ if text is None or (isinstance(text, str) and len(text.strip()) == 0):
471
+ raise ValueError("Text input is required for MossVL processor and cannot be empty.")
472
+
473
+ if not isinstance(text, list):
474
+ text = [text]
475
+
476
+ text = text.copy() # Copy to avoid in-place modifications
477
+
478
+ # Prepare labels_spans if provided
479
+ # labels_spans format: List[List[List[int]]] - batch of samples, each sample has multiple spans
480
+ # Each span is [start, end] (list, not tuple) so it can be modified in place
481
+ should_create_labels = labels_spans is not None
482
+ if should_create_labels:
483
+ # Ensure batch format: convert single sample spans to batch format
484
+ # Single sample: [[start, end], [start, end], ...]
485
+ # Batch: [[[start, end], ...], [[start, end], ...], ...]
486
+ if labels_spans and isinstance(labels_spans[0], list) and len(labels_spans[0]) == 2 and isinstance(labels_spans[0][0], int):
487
+ labels_spans = [labels_spans]
488
+
489
+ # Step 3.0-pre: Check if we need to reorder (when both images and videos exist)
490
+ # If only one media type exists, we can skip the expensive split+reorder+concat
491
+ has_images = images is not None and "pixel_values" in image_inputs
492
+ has_videos = videos is not None and "pixel_values_videos" in videos_inputs
493
+ needs_reorder = has_images and has_videos
494
+
495
+ image_pixel_values_list = []
496
+ video_pixel_values_list = []
497
+
498
+ # Step 3.0: Record the order of media in original text (before replacement)
499
+ # This will be used later to correctly order pixel_values and grid_thw
500
+ media_order_per_sample = []
501
+ for i in range(len(text)):
502
+ media_order = []
503
+ temp_text = text[i]
504
+ pos = 0
505
+ while pos < len(temp_text):
506
+ img_pos = temp_text.find(self.image_placeholder, pos)
507
+ vid_pos = temp_text.find(self.video_placeholder, pos)
508
+
509
+ if img_pos == -1 and vid_pos == -1:
510
+ break
511
+
512
+ if img_pos != -1 and (vid_pos == -1 or img_pos < vid_pos):
513
+ media_order.append(("image", img_pos))
514
+ pos = img_pos + len(self.image_placeholder)
515
+ elif vid_pos != -1:
516
+ media_order.append(("video", vid_pos))
517
+ pos = vid_pos + len(self.video_placeholder)
518
+
519
+ media_order_per_sample.append(media_order)
520
+
521
+ # Step 3.0.1: Check if any sample has no media (empty samples need blank image)
522
+ # If there are empty samples, we need to enter slow path to handle them properly
523
+ has_empty_samples = any(len(order) == 0 for order in media_order_per_sample)
524
+ if has_empty_samples:
525
+ needs_reorder = True
526
+
527
+ # Split pixel values for reordering if needed
528
+ if needs_reorder:
529
+ if has_images:
530
+ flat_pixel_values = image_inputs["pixel_values"]
531
+ flat_grid_thw = image_inputs["image_grid_thw"]
532
+ # grid_thw is (t, h, w), num_patches = t * h * w
533
+ patch_counts = [int(np.prod(_to_numpy(grid))) for grid in flat_grid_thw]
534
+ if len(patch_counts) == 1:
535
+ # Single image case: no need to split
536
+ image_pixel_values_list = [flat_pixel_values]
537
+ elif len(patch_counts) > 1:
538
+ # Multiple images: split by cumulative counts
539
+ split_indices = np.cumsum(patch_counts)[:-1]
540
+ image_pixel_values_list = _split_array_or_tensor(
541
+ flat_pixel_values, split_indices
542
+ )
543
+
544
+ if has_videos:
545
+ flat_video_values = videos_inputs["pixel_values_videos"]
546
+ flat_video_grid = videos_inputs["video_grid_thw"]
547
+ video_patch_counts = [int(np.prod(_to_numpy(grid))) for grid in flat_video_grid]
548
+ if len(video_patch_counts) == 1:
549
+ # Single video case: no need to split
550
+ video_pixel_values_list = [flat_video_values]
551
+ elif len(video_patch_counts) > 1:
552
+ # Multiple videos: split by cumulative counts
553
+ split_indices = np.cumsum(video_patch_counts)[:-1]
554
+ video_pixel_values_list = _split_array_or_tensor(
555
+ flat_video_values, split_indices
556
+ )
557
+
558
+ # Step 3.1: Replace placeholders (simple replacement, no expansion yet)
559
+ # In MossVL, one image placeholder = one image token
560
+ # One video placeholder = one video token (will be expanded later)
561
+ for i in range(len(text)):
562
+ if should_create_labels:
563
+ # Replace and update spans for image placeholders
564
+ text[i], labels_spans[i] = self._replace_and_update_spans(
565
+ text[i], self.image_placeholder, self.image_token, labels_spans[i]
566
+ )
567
+ # Replace and update spans for video placeholders
568
+ text[i], labels_spans[i] = self._replace_and_update_spans(
569
+ text[i], self.video_placeholder, self.video_token, labels_spans[i]
570
+ )
571
+ else:
572
+ text[i] = text[i].replace(self.image_placeholder, self.image_token)
573
+ text[i] = text[i].replace(self.video_placeholder, self.video_token)
574
+
575
+ # Step 3.2: Validate token counts
576
+ n_images_in_text = [t.count(self.image_token) for t in text]
577
+ n_videos_in_text = [t.count(self.video_token) for t in text]
578
+
579
+ # Count placeholders in text
580
+ total_images_in_text = sum(n_images_in_text)
581
+ total_videos_in_text = sum(n_videos_in_text)
582
+
583
+ # Count actual images and videos provided
584
+ total_images_provided = len(image_grid_thw) if image_grid_thw is not None else 0
585
+ total_videos_provided = len(video_grid_thw) if video_grid_thw is not None else 0
586
+
587
+ # Validate image counts
588
+ if total_images_in_text != total_images_provided:
589
+ raise ValueError(
590
+ "Number of image tokens does not match number of images provided. "
591
+ f"Found {total_images_in_text} image tokens in text and {total_images_provided} images."
592
+ )
593
+
594
+ # Validate video counts
595
+ if total_videos_in_text != total_videos_provided:
596
+ raise ValueError(
597
+ "Number of video tokens does not match number of videos provided. "
598
+ f"Found {total_videos_in_text} video tokens in text and {total_videos_provided} videos."
599
+ )
600
+
601
+ # Step 3.3: Expand video tokens with timestamps
602
+ # Now expand each video token to multiple tokens (one per frame) with timestamps
603
+ if video_grid_thw is not None:
604
+ index = 0
605
+ for i in range(len(text)):
606
+ while self.video_token in text[i]:
607
+ metadata = video_metadata[index]
608
+ if metadata.fps is None:
609
+ logger.warning_once(
610
+ "MossVL requires frame timestamps to construct prompts, but the `fps` of the input video could not be inferred. "
611
+ "Probably `video_metadata` was missing from inputs and you passed pre-sampled frames. "
612
+ "Defaulting to `fps=24`. Please provide `video_metadata` for more accurate results."
613
+ )
614
+ metadata.fps = 24 if metadata.fps is None else metadata.fps
615
+
616
+ # Calculate timestamps
617
+ # Use actual_timestamps if available (for segments), otherwise use frames_indices
618
+ actual_timestamps = getattr(metadata, 'actual_timestamps', None)
619
+ curr_timestamp = self._calculate_timestamps(
620
+ metadata.frames_indices,
621
+ metadata.total_num_frames,
622
+ metadata.fps,
623
+ metadata.duration,
624
+ self.video_processor.temporal_patch_size,
625
+ actual_timestamps=actual_timestamps,
626
+ )
627
+
628
+ # Build video placeholder: one video token per frame with timestamp
629
+ # video_grid_thw[index][0] is the temporal dimension (number of frames after merging)
630
+
631
+ video_tokens = []
632
+ for frame_idx in range(video_grid_thw[index][0]):
633
+ curr_time = curr_timestamp[frame_idx]
634
+ # Format: <|time_start|>X.X seconds<|time_end|><|image_pad|>
635
+ video_tokens.append(
636
+ f"{self.time_start_token}{curr_time:.1f} seconds{self.time_end_token}{self.image_token}"
637
+ )
638
+
639
+ # Wrap the entire video sequence with vision_start and vision_end tokens
640
+ video_placeholder = f"{self.vision_start_token}{''.join(video_tokens)}{self.vision_end_token}"
641
+
642
+ # Replace the video token with expanded sequence and update spans if needed
643
+ if should_create_labels:
644
+ text[i], labels_spans[i] = self._replace_and_update_spans(
645
+ text[i], self.video_token, video_placeholder, labels_spans[i], replace_count=1
646
+ )
647
+ else:
648
+ text[i] = text[i].replace(self.video_token, video_placeholder, 1)
649
+ index += 1
650
+
651
+
652
+
653
+ # Step 4: Tokenize text
654
+ return_tensors = output_kwargs["text_kwargs"].pop("return_tensors", None)
655
+ return_mm_token_type_ids = output_kwargs["text_kwargs"].pop("return_mm_token_type_ids", None)
656
+
657
+ # Request offset_mapping if we need to create labels
658
+ if should_create_labels:
659
+ output_kwargs["text_kwargs"]["return_offsets_mapping"] = True
660
+
661
+ text_inputs = self.tokenizer(text, **output_kwargs["text_kwargs"])
662
+
663
+ # ignore check_special_mm_tokens nums in test and input ids.
664
+ # self._check_special_mm_tokens(text, text_inputs, modalities=["image", "video"])
665
+
666
+ # Create labels if labels_spans was provided
667
+ if should_create_labels:
668
+ offset_mapping = text_inputs.pop("offset_mapping")
669
+ labels = self._create_labels_from_spans(
670
+ text_inputs["input_ids"],
671
+ offset_mapping,
672
+ labels_spans,
673
+ ignore_index
674
+ )
675
+
676
+ if return_mm_token_type_ids:
677
+ array_ids = np.array(text_inputs["input_ids"])
678
+ mm_token_type_ids = np.zeros_like(text_inputs["input_ids"])
679
+ mm_token_type_ids[array_ids == self.image_token_id] = 1
680
+ text_inputs["mm_token_type_ids"] = mm_token_type_ids.tolist()
681
+
682
+ # Step 5: Concatenate pixel_values and grid_thw in sequence order
683
+ # Prepare output
684
+ output_data = {**text_inputs}
685
+
686
+ if not needs_reorder:
687
+ # Fast path: only one media type, no reordering needed
688
+ final_pixel_values = []
689
+ final_grid_thw = []
690
+
691
+ if has_images:
692
+ final_pixel_values.append(image_inputs["pixel_values"])
693
+ final_grid_thw.extend(image_grid_thw)
694
+
695
+ if has_videos:
696
+ final_pixel_values.append(videos_inputs["pixel_values_videos"])
697
+ final_grid_thw.extend(video_grid_thw)
698
+
699
+ if final_pixel_values:
700
+ output_data["pixel_values"] = np.concatenate(final_pixel_values, axis=0) if len(final_pixel_values) > 1 else final_pixel_values[0]
701
+
702
+ if final_grid_thw:
703
+ output_data["grid_thw"] = np.stack(final_grid_thw, axis=0)
704
+
705
+ # Calculate media_nums_per_sample
706
+ media_nums_per_sample = []
707
+ for batch_idx in range(len(text)):
708
+ media_order = media_order_per_sample[batch_idx]
709
+ media_nums_per_sample.append(len(media_order) if len(media_order) > 0 else 1)
710
+
711
+ # Don't add media_nums_per_sample to output_data yet
712
+ # Will add it after BatchFeature to keep it as list
713
+
714
+ else:
715
+ # Slow path: both images and videos exist, need reordering
716
+ final_pixel_values = []
717
+ final_grid_thw = []
718
+ media_nums_per_sample = []
719
+
720
+ # Global indices to track position in flattened image/video arrays
721
+ global_image_idx = 0
722
+ global_video_idx = 0
723
+
724
+ for batch_idx in range(len(text)):
725
+ # Use the recorded media order from Step 3.0
726
+ media_order = media_order_per_sample[batch_idx]
727
+
728
+ if len(media_order) == 0:
729
+ # If no media provided for this sample, add a blank image
730
+ media_nums_per_sample.append(1)
731
+ min_pixels = 128 * 128
732
+ patch_size = getattr(self.image_processor, "patch_size", None) or 16
733
+ temporal_patch_size = getattr(self.image_processor, "temporal_patch_size", None) or 1
734
+ merge_size = getattr(self.image_processor, "merge_size", None) or 2
735
+
736
+ factor = patch_size * merge_size
737
+ side = int(np.ceil(np.sqrt(min_pixels) / factor) * factor)
738
+ grid_h = side // patch_size
739
+ grid_w = side // patch_size
740
+ grid_t = 1
741
+
742
+ # Channel = 3 (RGB)
743
+ channel = 3
744
+ dim = channel * temporal_patch_size * patch_size * patch_size
745
+ num_patches = grid_t * grid_h * grid_w
746
+
747
+ blank_pixel_values = np.zeros((num_patches, dim), dtype=np.float32)
748
+ blank_grid_thw = np.array([grid_t, grid_h, grid_w], dtype=np.int64)
749
+
750
+ final_pixel_values.append(blank_pixel_values)
751
+ final_grid_thw.append(blank_grid_thw)
752
+ else:
753
+ media_nums_per_sample.append(len(media_order))
754
+
755
+ # Collect media data according to the recorded order
756
+ for media_type, _ in media_order:
757
+ if media_type == "image" and image_grid_thw is not None:
758
+ # Get image data
759
+ if image_pixel_values_list:
760
+ final_pixel_values.append(image_pixel_values_list[global_image_idx])
761
+ final_grid_thw.append(image_grid_thw[global_image_idx])
762
+ global_image_idx += 1
763
+ elif media_type == "video" and video_grid_thw is not None:
764
+ # Get video data
765
+ if video_pixel_values_list:
766
+ final_pixel_values.append(video_pixel_values_list[global_video_idx])
767
+ final_grid_thw.append(video_grid_thw[global_video_idx])
768
+ global_video_idx += 1
769
+
770
+ # Concatenate/stack to unified format
771
+ if final_pixel_values:
772
+ output_data["pixel_values"] = _concat_array_or_tensor(
773
+ final_pixel_values, axis=0
774
+ )
775
+
776
+ if final_grid_thw:
777
+ output_data["grid_thw"] = _stack_array_or_tensor(
778
+ final_grid_thw, axis=0
779
+ )
780
+
781
+ # Don't add media_nums_per_sample to output_data yet
782
+ # Will add it after BatchFeature to keep it as list
783
+
784
+ # Create cross_attention_mask using media_nums_per_sample
785
+ if "input_ids" in output_data and "grid_thw" in output_data and media_nums_per_sample:
786
+ cross_attention_mask = self._create_cross_attention_mask(
787
+ output_data["input_ids"],
788
+ output_data["grid_thw"],
789
+ media_nums_per_sample,
790
+ output_data.get("attention_mask", None)
791
+ )
792
+ output_data["cross_attention_mask"] = cross_attention_mask
793
+
794
+ # Add labels to output if created
795
+ if should_create_labels:
796
+ output_data["labels"] = labels
797
+
798
+ # BatchFeature will handle conversion to pt/tf/jax/np based on tensor_type
799
+ batch_feature = BatchFeature(data=output_data, tensor_type=return_tensors)
800
+
801
+ # Add media_nums_per_sample after BatchFeature to keep it as list (not tensor)
802
+ if media_nums_per_sample:
803
+ batch_feature["media_nums_per_sample"] = media_nums_per_sample
804
+
805
+ return batch_feature
806
+
807
+ def _create_cross_attention_mask(self, input_ids, grid_thw, media_nums_per_sample, attention_mask=None):
808
+ """
809
+ Create cross_attention_mask of shape (batch_size, 1, text_len, num_images).
810
+ Video frames are treated as individual images.
811
+ Mask values: True for masked, False for visible.
812
+ Causal masking: text can see images that appear at or before the text position.
813
+
814
+ Args:
815
+ input_ids: List of token ids
816
+ grid_thw: Grid sizes for each media item
817
+ media_nums_per_sample: Number of media items per sample
818
+ attention_mask: Optional attention mask to filter out padding positions
819
+ """
820
+ batch_size = len(input_ids)
821
+ max_text_len = max(len(ids) for ids in input_ids)
822
+
823
+ # Calculate total frames per sample to find max_num_frames
824
+ total_frames_per_sample = []
825
+ media_idx = 0
826
+ for b in range(batch_size):
827
+ num_media = media_nums_per_sample[b]
828
+ if num_media == 0:
829
+ total_frames_per_sample.append(0)
830
+ continue
831
+
832
+ sample_frames = 0
833
+ for _ in range(num_media):
834
+ # grid_thw is (N, 3) where first dim is t (num_frames)
835
+ t = grid_thw[media_idx][0]
836
+ if isinstance(t, torch.Tensor):
837
+ t = int(t.item())
838
+ else:
839
+ t = int(t)
840
+ sample_frames += t
841
+ media_idx += 1
842
+ total_frames_per_sample.append(sample_frames)
843
+
844
+ max_num_frames = max(total_frames_per_sample) if total_frames_per_sample else 0
845
+
846
+ if max_num_frames == 0:
847
+ return None
848
+
849
+ # Vectorized implementation for speed
850
+
851
+ # 1. Pad input_ids to create a tensor
852
+ # We use -1 as pad value since token ids are positive
853
+ input_ids_tensor = torch.full((batch_size, max_text_len), -1, dtype=torch.long)
854
+ for b, ids in enumerate(input_ids):
855
+ l = len(ids)
856
+ input_ids_tensor[b, :l] = torch.tensor(ids, dtype=torch.long)
857
+
858
+ # 2. Identify image tokens
859
+ is_image_token = (input_ids_tensor == self.image_token_id)
860
+
861
+ # 3. Compute cumulative image tokens (how many image tokens appeared up to position t)
862
+ # shape: (batch_size, text_len)
863
+ cum_image_tokens = is_image_token.cumsum(dim=1)
864
+
865
+ # 4. Create frame indices
866
+ # shape: (1, 1, max_num_frames)
867
+ frame_indices = torch.arange(max_num_frames).reshape(1, 1, -1)
868
+
869
+ # 5. Determine visibility based on causal relationship
870
+ # Text at `t` sees frame `i` if `cum_image_tokens[t] > i`
871
+ # Because if frame `i` is the (i+1)-th image token, it becomes visible when count reaches i+1
872
+ # shape: (batch_size, text_len, max_num_frames)
873
+ visible_mask = cum_image_tokens.unsqueeze(-1) > frame_indices
874
+
875
+ # 6. Apply attention_mask if provided
876
+ if attention_mask is not None:
877
+ # Convert to tensor if needed
878
+ if isinstance(attention_mask, torch.Tensor):
879
+ attn_mask_tensor = attention_mask
880
+ else:
881
+ # List of lists
882
+ attn_mask_tensor = torch.zeros((batch_size, max_text_len), dtype=torch.long)
883
+ for b, mask_row in enumerate(attention_mask):
884
+ l = len(mask_row)
885
+ attn_mask_tensor[b, :l] = torch.tensor(mask_row, dtype=torch.long)
886
+
887
+ # shape: (batch_size, text_len, 1)
888
+ valid_text = (attn_mask_tensor.unsqueeze(-1) == 1)
889
+ visible_mask = visible_mask & valid_text
890
+
891
+ # 7. Mask out frames that don't exist for a sample
892
+ # shape: (batch_size, 1, 1)
893
+ total_frames_tensor = torch.tensor(total_frames_per_sample).reshape(batch_size, 1, 1)
894
+ # shape: (batch_size, 1, max_num_frames)
895
+ valid_frames = frame_indices < total_frames_tensor
896
+
897
+ visible_mask = visible_mask & valid_frames
898
+
899
+ # 8. Create final mask (True for masked, False for visible)
900
+ mask = ~visible_mask
901
+
902
+ # 9. Add channel dimension: (batch_size, 1, text_len, max_num_frames)
903
+ mask = mask.unsqueeze(1)
904
+
905
+ return mask
906
+
907
+ def _replace_and_update_spans(
908
+ self,
909
+ text: str,
910
+ old_str: str,
911
+ new_str: str,
912
+ spans: List[List[int]],
913
+ replace_count: int = -1
914
+ ) -> tuple:
915
+ """
916
+ Replace occurrences of old_str with new_str and update spans accordingly.
917
+
918
+ Args:
919
+ text: The text to perform replacement on
920
+ old_str: String to be replaced
921
+ new_str: String to replace with
922
+ spans: List of [start, end] spans to update (modified in place)
923
+ replace_count: Maximum number of replacements (-1 for all)
924
+
925
+ Returns:
926
+ Tuple of (new_text, updated_spans)
927
+ """
928
+ delta = len(new_str) - len(old_str)
929
+ result_text = text
930
+ count = 0
931
+ search_start = 0
932
+
933
+ while True:
934
+ pos = result_text.find(old_str, search_start)
935
+ if pos == -1:
936
+ break
937
+ if replace_count != -1 and count >= replace_count:
938
+ break
939
+
940
+ # Update all spans that come after this position
941
+ for span in spans:
942
+ if span[0] > pos:
943
+ # Span starts after replacement point
944
+ span[0] += delta
945
+ span[1] += delta
946
+ elif span[1] > pos:
947
+ # Span ends after replacement point (spans the replacement)
948
+ span[1] += delta
949
+
950
+ # Perform the replacement
951
+ result_text = result_text[:pos] + new_str + result_text[pos + len(old_str):]
952
+ search_start = pos + len(new_str)
953
+ count += 1
954
+
955
+ return result_text, spans
956
+
957
+ def _create_labels_from_spans(
958
+ self,
959
+ input_ids: List[List[int]],
960
+ offset_mapping: List[List[tuple]],
961
+ labels_spans: List[List[List[int]]],
962
+ ignore_index: int = -100,
963
+ mask_token_ids: Optional[set] = None
964
+ ) -> List[List[int]]:
965
+ """
966
+ Create labels from spans and offset_mapping.
967
+
968
+ Args:
969
+ input_ids: Tokenized input ids
970
+ offset_mapping: Character offsets for each token from tokenizer (special tokens included)
971
+ labels_spans: Updated spans indicating assistant regions (after text transformations)
972
+ ignore_index: Value for masked positions
973
+ mask_token_ids: Set of token ids that should always be masked (set to ignore_index)
974
+ in labels, regardless of whether they fall inside a span.
975
+ Defaults to self.mask_token_ids if not provided.
976
+
977
+ Returns:
978
+ labels: List of label ids, same shape as input_ids
979
+
980
+ Note:
981
+ - Tokenizer's offset_mapping already includes correct offsets for special tokens in text
982
+ - Only need to mask tokens inside <|vision_start|>...<|vision_end|>
983
+ - Tokens whose id is in mask_token_ids are always masked
984
+ - All other tokens in spans (including special tokens like <|im_end|>) get labels
985
+ """
986
+ if mask_token_ids is None:
987
+ mask_token_ids = self.mask_token_ids
988
+
989
+ batch_labels = []
990
+
991
+ for batch_idx in range(len(input_ids)):
992
+ ids = input_ids[batch_idx]
993
+ offsets = offset_mapping[batch_idx]
994
+ spans = labels_spans[batch_idx]
995
+
996
+ labels = [ignore_index] * len(ids)
997
+
998
+ # Process each span: find token range and set labels
999
+ for span_start, span_end in spans:
1000
+ in_vision = False
1001
+
1002
+ # Find tokens that overlap with this span
1003
+ for token_idx, (token_id, (char_start, char_end)) in enumerate(zip(ids, offsets)):
1004
+ # Skip tokens completely before this span
1005
+ if char_end <= span_start:
1006
+ continue
1007
+ # Stop when tokens are completely after this span
1008
+ if char_start >= span_end:
1009
+ break
1010
+
1011
+ # Token overlaps with span, process it
1012
+ # Track vision region: <|vision_start|> ... <|vision_end|>
1013
+ if token_id == self.vision_start_token_id:
1014
+ in_vision = True
1015
+ continue
1016
+ if token_id == self.vision_end_token_id:
1017
+ in_vision = False
1018
+ continue
1019
+
1020
+ # Skip tokens inside vision region
1021
+ if in_vision:
1022
+ continue
1023
+
1024
+ # Always mask special tokens that should never have labels
1025
+ if token_id in mask_token_ids:
1026
+ continue
1027
+
1028
+ # Set label for this token
1029
+ labels[token_idx] = token_id
1030
+
1031
+ batch_labels.append(labels)
1032
+
1033
+ return batch_labels
1034
+
1035
+ def _calculate_timestamps(
1036
+ self,
1037
+ frames_indices: Optional[Union[List[int], np.ndarray]],
1038
+ total_num_frames: int,
1039
+ video_fps: float,
1040
+ duration: float,
1041
+ merge_size: int = 1,
1042
+ actual_timestamps: Optional[List[float]] = None
1043
+ ):
1044
+ """
1045
+ Calculate timestamps for video frames.
1046
+
1047
+ Args:
1048
+ frames_indices: Actual frame indices extracted (if available)
1049
+ total_num_frames: Total number of sampled frames
1050
+ video_fps: Video frames per second
1051
+ duration: Video duration in seconds
1052
+ merge_size: Temporal merge size
1053
+ actual_timestamps: Pre-calculated actual timestamps (for segments)
1054
+
1055
+ Returns:
1056
+ List of timestamps (one per merged temporal patch)
1057
+ """
1058
+ # If actual timestamps are provided (from segment), use them directly
1059
+ if actual_timestamps is not None:
1060
+ timestamps = list(actual_timestamps)
1061
+
1062
+ # Pad timestamps to be multiple of merge_size
1063
+ if len(timestamps) % merge_size != 0:
1064
+ timestamps.extend([timestamps[-1]] * (merge_size - len(timestamps) % merge_size))
1065
+
1066
+ # Frames are merged by merge_size, so we average the timestamps within each temporal patch
1067
+ timestamps = [
1068
+ (timestamps[i] + timestamps[i + merge_size - 1]) / 2
1069
+ for i in range(0, len(timestamps), merge_size)
1070
+ ]
1071
+ return timestamps
1072
+
1073
+ # Use frames_indices if available, otherwise generate uniformly sampled indices
1074
+ if frames_indices is not None:
1075
+ if isinstance(frames_indices, np.ndarray):
1076
+ indices = frames_indices.tolist()
1077
+ else:
1078
+ indices = list(frames_indices)
1079
+ else:
1080
+ # Generate uniformly sampled frame indices
1081
+ if total_num_frames <= 1:
1082
+ indices = [0]
1083
+ else:
1084
+ # Uniformly sample frames across the video duration
1085
+ indices = np.linspace(0, duration * video_fps - 1, total_num_frames).astype(np.int32).tolist()
1086
+
1087
+ # Pad indices to be multiple of merge_size
1088
+ if len(indices) % merge_size != 0:
1089
+ indices.extend([indices[-1]] * (merge_size - len(indices) % merge_size))
1090
+
1091
+ # Convert frame indices to timestamps
1092
+ timestamps = [idx / video_fps for idx in indices]
1093
+
1094
+ # Frames are merged by merge_size, so we average the timestamps within each temporal patch
1095
+ timestamps = [
1096
+ (timestamps[i] + timestamps[i + merge_size - 1]) / 2
1097
+ for i in range(0, len(timestamps), merge_size)
1098
+ ]
1099
+ return timestamps
1100
+
1101
+ def batch_decode(self, *args, **kwargs):
1102
+ """
1103
+ This method forwards all its arguments to the tokenizer's batch_decode.
1104
+ Please refer to the docstring of this method for more information.
1105
+ """
1106
+ return self.tokenizer.batch_decode(*args, **kwargs)
1107
+
1108
+ def decode(self, *args, **kwargs):
1109
+ """
1110
+ This method forwards all its arguments to the tokenizer's decode.
1111
+ Please refer to the docstring of this method for more information.
1112
+ """
1113
+ return self.tokenizer.decode(*args, **kwargs)
1114
+
1115
+ def post_process_image_text_to_text(
1116
+ self, generated_outputs, skip_special_tokens=True, clean_up_tokenization_spaces=False, **kwargs
1117
+ ):
1118
+ """
1119
+ Post-process the output of the model to decode the text.
1120
+
1121
+ Args:
1122
+ generated_outputs (`torch.Tensor` or `np.ndarray`):
1123
+ The output of the model `generate` function. The output is expected to be a tensor
1124
+ of shape `(batch_size, sequence_length)` or `(sequence_length,)`.
1125
+ skip_special_tokens (`bool`, *optional*, defaults to `True`):
1126
+ Whether or not to remove special tokens in the output.
1127
+ clean_up_tokenization_spaces (`bool`, *optional*, defaults to `False`):
1128
+ Whether or not to clean up the tokenization spaces.
1129
+ **kwargs:
1130
+ Additional arguments to be passed to the tokenizer's `batch_decode` method.
1131
+
1132
+ Returns:
1133
+ `list[str]`: The decoded text.
1134
+ """
1135
+ return self.tokenizer.batch_decode(
1136
+ generated_outputs,
1137
+ skip_special_tokens=skip_special_tokens,
1138
+ clean_up_tokenization_spaces=clean_up_tokenization_spaces,
1139
+ **kwargs,
1140
+ )
1141
+
1142
+
1143
+ __all__ = ["MossVLProcessor", "MossVLImageProcessorFast"]
processor_config.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoProcessor": "processing_moss_vl.MossVLProcessor"
4
+ },
5
+ "processor_class": "MossVLProcessor"
6
+ }
quantization_report.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "created_at": "2026-07-26T06:19:47.245758+00:00",
3
+ "source": "/inspire/qb-ilm/project/video-understanding/public/train/moss_vl_streaming/8B/final_release/mossvl_streaming_final_release",
4
+ "output": "/inspire/qb-ilm/project/video-understanding/public/train/moss_vl_streaming/8B/quant/mossvl_streaming_w4a16_nf4_keep_first4_last4",
5
+ "variant": "w4_keep_ends",
6
+ "method": "bitsandbytes NF4 W4A16 keep-first-last-4",
7
+ "compute_dtype": "bfloat16",
8
+ "skip_modules": [
9
+ "model.visual",
10
+ "cross_attn",
11
+ "lm_head",
12
+ "model.language_model.layers.0",
13
+ "model.language_model.layers.1",
14
+ "model.language_model.layers.2",
15
+ "model.language_model.layers.3",
16
+ "model.language_model.layers.44",
17
+ "model.language_model.layers.45",
18
+ "model.language_model.layers.46",
19
+ "model.language_model.layers.47"
20
+ ],
21
+ "quantized_kind": "4bit",
22
+ "quantized_linear_count": 240,
23
+ "quantized_module_samples": [
24
+ "model.language_model.layers.4.self_attn.q_proj",
25
+ "model.language_model.layers.4.self_attn.k_proj",
26
+ "model.language_model.layers.4.self_attn.v_proj",
27
+ "model.language_model.layers.4.self_attn.o_proj",
28
+ "model.language_model.layers.4.mlp.gate_proj",
29
+ "model.language_model.layers.4.mlp.up_proj",
30
+ "model.language_model.layers.4.mlp.down_proj",
31
+ "model.language_model.layers.5.self_attn.q_proj",
32
+ "model.language_model.layers.5.self_attn.k_proj",
33
+ "model.language_model.layers.5.self_attn.v_proj",
34
+ "model.language_model.layers.5.self_attn.o_proj",
35
+ "model.language_model.layers.5.mlp.gate_proj",
36
+ "model.language_model.layers.5.mlp.up_proj",
37
+ "model.language_model.layers.5.mlp.down_proj",
38
+ "model.language_model.layers.6.mlp.gate_proj",
39
+ "model.language_model.layers.6.mlp.up_proj",
40
+ "model.language_model.layers.6.mlp.down_proj",
41
+ "model.language_model.layers.7.self_attn.q_proj",
42
+ "model.language_model.layers.7.self_attn.k_proj",
43
+ "model.language_model.layers.7.self_attn.v_proj"
44
+ ],
45
+ "peak_gpu_memory_gib_during_conversion": 11.038,
46
+ "gpu": "NVIDIA H200",
47
+ "versions": {
48
+ "torch": "2.8.0+cu128",
49
+ "transformers": "4.57.1",
50
+ "accelerate": "1.12.0",
51
+ "bitsandbytes": "0.49.2"
52
+ },
53
+ "reload_verified": true,
54
+ "reload_quantized_kind": "4bit",
55
+ "reload_quantized_linear_count": 240,
56
+ "peak_gpu_memory_gib_after_reload": 14.549
57
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<|im_start|>",
4
+ "<|im_end|>",
5
+ "<|object_ref_start|>",
6
+ "<|object_ref_end|>",
7
+ "<|box_start|>",
8
+ "<|box_end|>",
9
+ "<|quad_start|>",
10
+ "<|quad_end|>",
11
+ "<|vision_start|>",
12
+ "<|vision_end|>",
13
+ "<|vision_pad|>",
14
+ "<|image_pad|>",
15
+ "<|video_pad|>",
16
+ "<|time_start|>",
17
+ "<|time_end|>",
18
+ "<|silence|>",
19
+ "<|response|>"
20
+ ],
21
+ "eos_token": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false
27
+ },
28
+ "pad_token": {
29
+ "content": "<|endoftext|>",
30
+ "lstrip": false,
31
+ "normalized": false,
32
+ "rstrip": false,
33
+ "single_word": false
34
+ }
35
+ }
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e7bbd0f9784004df51aca562befc3c7a8f294b4045aa8685536c35804c9aa493
3
+ size 11423411
tokenizer_config.json ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_prefix_space": false,
4
+ "added_tokens_decoder": {
5
+ "151643": {
6
+ "content": "<|endoftext|>",
7
+ "lstrip": false,
8
+ "normalized": false,
9
+ "rstrip": false,
10
+ "single_word": false,
11
+ "special": true
12
+ },
13
+ "151644": {
14
+ "content": "<|im_start|>",
15
+ "lstrip": false,
16
+ "normalized": false,
17
+ "rstrip": false,
18
+ "single_word": false,
19
+ "special": true
20
+ },
21
+ "151645": {
22
+ "content": "<|im_end|>",
23
+ "lstrip": false,
24
+ "normalized": false,
25
+ "rstrip": false,
26
+ "single_word": false,
27
+ "special": true
28
+ },
29
+ "151646": {
30
+ "content": "<|object_ref_start|>",
31
+ "lstrip": false,
32
+ "normalized": false,
33
+ "rstrip": false,
34
+ "single_word": false,
35
+ "special": true
36
+ },
37
+ "151647": {
38
+ "content": "<|object_ref_end|>",
39
+ "lstrip": false,
40
+ "normalized": false,
41
+ "rstrip": false,
42
+ "single_word": false,
43
+ "special": true
44
+ },
45
+ "151648": {
46
+ "content": "<|box_start|>",
47
+ "lstrip": false,
48
+ "normalized": false,
49
+ "rstrip": false,
50
+ "single_word": false,
51
+ "special": true
52
+ },
53
+ "151649": {
54
+ "content": "<|box_end|>",
55
+ "lstrip": false,
56
+ "normalized": false,
57
+ "rstrip": false,
58
+ "single_word": false,
59
+ "special": true
60
+ },
61
+ "151650": {
62
+ "content": "<|quad_start|>",
63
+ "lstrip": false,
64
+ "normalized": false,
65
+ "rstrip": false,
66
+ "single_word": false,
67
+ "special": true
68
+ },
69
+ "151651": {
70
+ "content": "<|quad_end|>",
71
+ "lstrip": false,
72
+ "normalized": false,
73
+ "rstrip": false,
74
+ "single_word": false,
75
+ "special": true
76
+ },
77
+ "151652": {
78
+ "content": "<|vision_start|>",
79
+ "lstrip": false,
80
+ "normalized": false,
81
+ "rstrip": false,
82
+ "single_word": false,
83
+ "special": true
84
+ },
85
+ "151653": {
86
+ "content": "<|vision_end|>",
87
+ "lstrip": false,
88
+ "normalized": false,
89
+ "rstrip": false,
90
+ "single_word": false,
91
+ "special": true
92
+ },
93
+ "151654": {
94
+ "content": "<|vision_pad|>",
95
+ "lstrip": false,
96
+ "normalized": false,
97
+ "rstrip": false,
98
+ "single_word": false,
99
+ "special": true
100
+ },
101
+ "151655": {
102
+ "content": "<|image_pad|>",
103
+ "lstrip": false,
104
+ "normalized": false,
105
+ "rstrip": false,
106
+ "single_word": false,
107
+ "special": true
108
+ },
109
+ "151656": {
110
+ "content": "<|video_pad|>",
111
+ "lstrip": false,
112
+ "normalized": false,
113
+ "rstrip": false,
114
+ "single_word": false,
115
+ "special": true
116
+ },
117
+ "151657": {
118
+ "content": "<tool_call>",
119
+ "lstrip": false,
120
+ "normalized": false,
121
+ "rstrip": false,
122
+ "single_word": false,
123
+ "special": false
124
+ },
125
+ "151658": {
126
+ "content": "</tool_call>",
127
+ "lstrip": false,
128
+ "normalized": false,
129
+ "rstrip": false,
130
+ "single_word": false,
131
+ "special": false
132
+ },
133
+ "151659": {
134
+ "content": "<|fim_prefix|>",
135
+ "lstrip": false,
136
+ "normalized": false,
137
+ "rstrip": false,
138
+ "single_word": false,
139
+ "special": false
140
+ },
141
+ "151660": {
142
+ "content": "<|fim_middle|>",
143
+ "lstrip": false,
144
+ "normalized": false,
145
+ "rstrip": false,
146
+ "single_word": false,
147
+ "special": false
148
+ },
149
+ "151661": {
150
+ "content": "<|fim_suffix|>",
151
+ "lstrip": false,
152
+ "normalized": false,
153
+ "rstrip": false,
154
+ "single_word": false,
155
+ "special": false
156
+ },
157
+ "151662": {
158
+ "content": "<|fim_pad|>",
159
+ "lstrip": false,
160
+ "normalized": false,
161
+ "rstrip": false,
162
+ "single_word": false,
163
+ "special": false
164
+ },
165
+ "151663": {
166
+ "content": "<|repo_name|>",
167
+ "lstrip": false,
168
+ "normalized": false,
169
+ "rstrip": false,
170
+ "single_word": false,
171
+ "special": false
172
+ },
173
+ "151664": {
174
+ "content": "<|file_sep|>",
175
+ "lstrip": false,
176
+ "normalized": false,
177
+ "rstrip": false,
178
+ "single_word": false,
179
+ "special": false
180
+ },
181
+ "151665": {
182
+ "content": "<tool_response>",
183
+ "lstrip": false,
184
+ "normalized": false,
185
+ "rstrip": false,
186
+ "single_word": false,
187
+ "special": false
188
+ },
189
+ "151666": {
190
+ "content": "</tool_response>",
191
+ "lstrip": false,
192
+ "normalized": false,
193
+ "rstrip": false,
194
+ "single_word": false,
195
+ "special": false
196
+ },
197
+ "151667": {
198
+ "content": "<think>",
199
+ "lstrip": false,
200
+ "normalized": false,
201
+ "rstrip": false,
202
+ "single_word": false,
203
+ "special": false
204
+ },
205
+ "151668": {
206
+ "content": "</think>",
207
+ "lstrip": false,
208
+ "normalized": false,
209
+ "rstrip": false,
210
+ "single_word": false,
211
+ "special": false
212
+ },
213
+ "151669": {
214
+ "content": "<|time_start|>",
215
+ "lstrip": false,
216
+ "normalized": false,
217
+ "rstrip": false,
218
+ "single_word": false,
219
+ "special": true
220
+ },
221
+ "151670": {
222
+ "content": "<|time_end|>",
223
+ "lstrip": false,
224
+ "normalized": false,
225
+ "rstrip": false,
226
+ "single_word": false,
227
+ "special": true
228
+ },
229
+ "151671": {
230
+ "content": "<|silence|>",
231
+ "lstrip": false,
232
+ "normalized": false,
233
+ "rstrip": false,
234
+ "single_word": false,
235
+ "special": true
236
+ },
237
+ "151672": {
238
+ "content": "<|response|>",
239
+ "lstrip": false,
240
+ "normalized": false,
241
+ "rstrip": false,
242
+ "single_word": false,
243
+ "special": true
244
+ }
245
+ },
246
+ "additional_special_tokens": [
247
+ "<|im_start|>",
248
+ "<|im_end|>",
249
+ "<|object_ref_start|>",
250
+ "<|object_ref_end|>",
251
+ "<|box_start|>",
252
+ "<|box_end|>",
253
+ "<|quad_start|>",
254
+ "<|quad_end|>",
255
+ "<|vision_start|>",
256
+ "<|vision_end|>",
257
+ "<|vision_pad|>",
258
+ "<|image_pad|>",
259
+ "<|video_pad|>",
260
+ "<|time_start|>",
261
+ "<|time_end|>",
262
+ "<|silence|>",
263
+ "<|response|>"
264
+ ],
265
+ "auto_map": {
266
+ "AutoProcessor": "processing_moss_vl.MossVLProcessor"
267
+ },
268
+ "bos_token": null,
269
+ "clean_up_tokenization_spaces": false,
270
+ "eos_token": "<|im_end|>",
271
+ "errors": "replace",
272
+ "extra_special_tokens": {},
273
+ "model_max_length": 262144,
274
+ "pad_token": "<|endoftext|>",
275
+ "processor_class": "MossVLProcessor",
276
+ "split_special_tokens": false,
277
+ "tokenizer_class": "Qwen2Tokenizer",
278
+ "unk_token": null
279
+ }
video_preprocessor_config.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoProcessor": "processing_moss_vl.MossVLProcessor",
4
+ "AutoVideoProcessor": "video_processing_moss_vl.MossVLVideoProcessor"
5
+ },
6
+ "crop_size": null,
7
+ "data_format": "channels_first",
8
+ "default_to_square": true,
9
+ "device": null,
10
+ "do_center_crop": null,
11
+ "do_convert_rgb": true,
12
+ "do_normalize": true,
13
+ "do_rescale": true,
14
+ "do_resize": true,
15
+ "do_sample_frames": true,
16
+ "fps": null,
17
+ "image_mean": [
18
+ 0.5,
19
+ 0.5,
20
+ 0.5
21
+ ],
22
+ "image_std": [
23
+ 0.5,
24
+ 0.5,
25
+ 0.5
26
+ ],
27
+ "input_data_format": null,
28
+ "max_frames": 256,
29
+ "merge_size": 2,
30
+ "min_frames": 1,
31
+ "num_extract_threads": 4,
32
+ "num_frames": null,
33
+ "pad_size": null,
34
+ "patch_size": 16,
35
+ "processor_class": "MossVLProcessor",
36
+ "resample": 3,
37
+ "rescale_factor": 0.00392156862745098,
38
+ "return_metadata": false,
39
+ "size": {
40
+ "longest_edge": 16777216,
41
+ "shortest_edge": 4096
42
+ },
43
+ "temporal_patch_size": 1,
44
+ "video_fps": 1.0,
45
+ "video_max_pixels": 65536000,
46
+ "video_metadata": null,
47
+ "video_processor_type": "MossVLVideoProcessor"
48
+ }
video_processing_moss_vl.py ADDED
@@ -0,0 +1,1248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2025 The FNLP Vision Team and The HuggingFace Inc. team. All rights reserved.
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
+ """video processor class for Moss-VL."""
16
+
17
+ import json
18
+ import logging as system_logging
19
+ import math
20
+ import os
21
+ import re
22
+ import subprocess
23
+ import traceback
24
+ from functools import lru_cache
25
+ from typing import Any, Dict, List, Optional, Union
26
+
27
+ import numpy as np
28
+ import torch
29
+ from joblib import Parallel, delayed
30
+ from torchcodec.decoders import VideoDecoder
31
+
32
+ from transformers.feature_extraction_utils import BatchFeature
33
+ from transformers.image_utils import ChannelDimension, PILImageResampling, SizeDict, get_image_size, validate_kwargs
34
+ from transformers.processing_utils import Unpack, VideosKwargs
35
+ from transformers.utils import TensorType, add_start_docstrings, logging
36
+ from transformers.video_processing_utils import BASE_VIDEO_PROCESSOR_DOCSTRING, BaseVideoProcessor
37
+ from transformers.video_utils import VideoMetadata, group_videos_by_shape, reorder_videos
38
+
39
+
40
+ logger = logging.get_logger(__name__)
41
+
42
+ TORCHCODEC_TIMESTAMP_EPSILON = 1e-6
43
+
44
+
45
+ # -----------------------------------------------------------------------------
46
+ # Torchcodec video frame extraction utilities
47
+ # -----------------------------------------------------------------------------
48
+
49
+ def check_video_for_extra_streams_and_errors(video_path: str) -> dict:
50
+ """
51
+ Check if video file has abnormal streams or errors reported by ffprobe.
52
+
53
+ Args:
54
+ video_path: Path to the video file.
55
+
56
+ Returns:
57
+ A dictionary containing:
58
+ - 'has_extra_streams': bool, whether there are streams other than video and audio.
59
+ - 'unsupported_codec_errors': list, all "Unsupported codec" error messages.
60
+ - 'ffprobe_output_error': str, other errors/warnings from ffprobe stderr.
61
+ - 'ffprobe_successful': bool, whether ffprobe command executed successfully (return code 0).
62
+ - 'stream_details': list, codec_type and index for each stream.
63
+ - 'num_streams': int, total number of streams identified in the video file.
64
+ """
65
+ result = {
66
+ 'has_extra_streams': False,
67
+ 'unsupported_codec_errors': [],
68
+ 'ffprobe_output_error': '',
69
+ 'ffprobe_successful': False,
70
+ 'stream_details': [],
71
+ 'num_streams': 0
72
+ }
73
+
74
+ command = [
75
+ "ffprobe",
76
+ "-v", "error",
77
+ "-show_streams",
78
+ "-show_format",
79
+ "-of", "json",
80
+ video_path
81
+ ]
82
+
83
+ try:
84
+ process = subprocess.run(
85
+ command,
86
+ capture_output=True,
87
+ text=True,
88
+ check=False
89
+ )
90
+ result['ffprobe_successful'] = (process.returncode == 0)
91
+
92
+ if process.stderr:
93
+ result['ffprobe_output_error'] = process.stderr
94
+ unsupported_codec_pattern = re.compile(r"Unsupported codec with id \d+ for input stream \d+")
95
+ result['unsupported_codec_errors'] = unsupported_codec_pattern.findall(process.stderr)
96
+
97
+ if process.stdout:
98
+ ffprobe_data = json.loads(process.stdout)
99
+ if 'streams' in ffprobe_data:
100
+ result['num_streams'] = len(ffprobe_data['streams'])
101
+ for stream in ffprobe_data['streams']:
102
+ stream_type = stream.get('codec_type')
103
+ stream_index = stream.get('index')
104
+ result['stream_details'].append({'index': stream_index, 'codec_type': stream_type})
105
+ if stream_type not in ['video', 'audio']:
106
+ result['has_extra_streams'] = True
107
+
108
+ if 'format' in ffprobe_data and 'nb_streams' in ffprobe_data['format']:
109
+ if result['num_streams'] == 0:
110
+ result['num_streams'] = ffprobe_data['format']['nb_streams']
111
+ elif result['num_streams'] != ffprobe_data['format']['nb_streams']:
112
+ logger.warning(
113
+ f"Number of streams in 'streams' list ({result['num_streams']}) "
114
+ f"differs from 'nb_streams' in 'format' ({ffprobe_data['format']['nb_streams']})."
115
+ )
116
+ except FileNotFoundError:
117
+ result['ffprobe_output_error'] = "ffprobe command not found. Please ensure FFmpeg is installed and in your PATH."
118
+ result['ffprobe_successful'] = False
119
+ except json.JSONDecodeError:
120
+ result['ffprobe_output_error'] = "Failed to parse ffprobe JSON output. Check ffprobe installation or video file."
121
+ result['ffprobe_successful'] = False
122
+ except Exception as e:
123
+ result['ffprobe_output_error'] = f"An unexpected error occurred: {e}"
124
+ result['ffprobe_successful'] = False
125
+
126
+ return result
127
+
128
+
129
+ def remove_video_extra_stream_ffmpeg(input_video: str, output_video: str) -> bool:
130
+ """
131
+ Remove extra streams from video using ffmpeg.
132
+
133
+ Args:
134
+ input_video: Path to input video.
135
+ output_video: Path to output video.
136
+
137
+ Returns:
138
+ bool: True if successful, False otherwise.
139
+ """
140
+ command_list = [
141
+ "ffmpeg", "-y", "-i", input_video,
142
+ "-map", "0:v:0",
143
+ "-c", "copy",
144
+ "-an",
145
+ "-sn",
146
+ "-dn",
147
+ "-map_metadata", "-1",
148
+ "-map_chapters", "-1",
149
+ "-movflags", "faststart",
150
+ output_video,
151
+ ]
152
+
153
+ try:
154
+ subprocess.run(command_list, shell=False, check=True, capture_output=True)
155
+ return True
156
+ except subprocess.CalledProcessError as e:
157
+ system_logging.error(f"Command execution failed with return code: {e.returncode}, video: {input_video}")
158
+ system_logging.error(f"Error output:\n{e.stderr}")
159
+ return False
160
+ except FileNotFoundError:
161
+ system_logging.error("Error: ffmpeg command not found. Please ensure ffmpeg is installed and in PATH.")
162
+ return False
163
+ except Exception as e:
164
+ system_logging.error(f"Unexpected error executing command: {e}, video: {input_video}", exc_info=True)
165
+ return False
166
+
167
+
168
+ def clean_video_streams(video_path: str) -> str:
169
+ """
170
+ Clean video streams if extra streams are detected.
171
+
172
+ Args:
173
+ video_path: Path to the video file.
174
+
175
+ Returns:
176
+ str: Path to cleaned video (or original if no cleaning needed).
177
+ """
178
+ ffprobe_res = check_video_for_extra_streams_and_errors(video_path)
179
+ if ffprobe_res['has_extra_streams']:
180
+ base_name = os.path.basename(video_path)
181
+ output_folder = os.path.dirname(video_path)
182
+ file_name_without_ext, file_ext = os.path.splitext(base_name)
183
+ new_base_name = f"{file_name_without_ext}_fix{file_ext}"
184
+ video_path_output = os.path.join(output_folder, new_base_name)
185
+
186
+ process_flag = remove_video_extra_stream_ffmpeg(video_path, video_path_output)
187
+ if not process_flag:
188
+ logger.warning("Failed to remove extra streams with ffmpeg")
189
+ return video_path
190
+ return video_path_output
191
+ return video_path
192
+
193
+
194
+ @lru_cache(maxsize=8192)
195
+ def cached_clean_video_streams(video_path: str) -> str:
196
+ return clean_video_streams(video_path)
197
+
198
+
199
+ def clamp_timestamps_for_torchcodec(timestamps: List[float], torchcodec_metadata) -> List[float]:
200
+ if not timestamps:
201
+ return timestamps
202
+
203
+ min_pts = torchcodec_metadata.begin_stream_seconds_from_content
204
+ if min_pts is None:
205
+ min_pts = 0.0
206
+
207
+ max_pts_candidates = []
208
+ if torchcodec_metadata.num_frames_from_content and torchcodec_metadata.average_fps:
209
+ max_pts_candidates.append(
210
+ (torchcodec_metadata.num_frames_from_content - 1) / torchcodec_metadata.average_fps + min_pts
211
+ )
212
+ if torchcodec_metadata.end_stream_seconds_from_content is not None:
213
+ # TorchCodec requires requested PTS to be strictly smaller than the content end.
214
+ max_pts_candidates.append(torchcodec_metadata.end_stream_seconds_from_content - TORCHCODEC_TIMESTAMP_EPSILON)
215
+ if not max_pts_candidates and torchcodec_metadata.duration_seconds is not None:
216
+ max_pts_candidates.append(torchcodec_metadata.duration_seconds - TORCHCODEC_TIMESTAMP_EPSILON)
217
+
218
+ if max_pts_candidates:
219
+ max_pts = max(min_pts, min(max_pts_candidates))
220
+ return [max(min_pts, min(float(t), max_pts)) for t in timestamps]
221
+ if min_pts > 0:
222
+ return [max(min_pts, float(t)) for t in timestamps]
223
+ return [float(t) for t in timestamps]
224
+
225
+
226
+ def split_indices(indices: List[Union[int, float]], num_chunks: int) -> List[List[Union[int, float]]]:
227
+ """
228
+ Split an index list into roughly equal chunks.
229
+
230
+ Args:
231
+ indices: List of indices to split.
232
+ num_chunks: Number of chunks to create.
233
+
234
+ Returns:
235
+ List of index chunks.
236
+ """
237
+ chunk_size = len(indices) // num_chunks
238
+ chunks = []
239
+ for i in range(num_chunks - 1):
240
+ chunks.append(indices[i * chunk_size:(i + 1) * chunk_size])
241
+ chunks.append(indices[(num_chunks - 1) * chunk_size:])
242
+ return chunks
243
+
244
+
245
+ def decode_sequentially(indices: List[int], video_path: str, ffmpeg_threads: int = 0):
246
+ """
247
+ Decode frames sequentially from a video.
248
+
249
+ Args:
250
+ indices: List of frame indices to decode.
251
+ video_path: Path to the video file.
252
+ ffmpeg_threads: Number of ffmpeg threads to use.
253
+
254
+ Returns:
255
+ FrameBatch from torchcodec.
256
+ """
257
+ decoder = VideoDecoder(video_path, num_ffmpeg_threads=ffmpeg_threads)
258
+ try:
259
+ return decoder.get_frames_at(indices)
260
+ finally:
261
+ del decoder
262
+
263
+
264
+ def decode_with_multithreading(indices: List[int], num_threads: int, video_path: str) -> dict:
265
+ """
266
+ Decode frames using multithreading with joblib.
267
+
268
+ Args:
269
+ indices: List of frame indices to decode.
270
+ num_threads: Number of threads to use.
271
+ video_path: Path to the video file.
272
+
273
+ Returns:
274
+ dict: Contains 'data', 'duration_seconds', 'pts_seconds' tensors.
275
+ """
276
+ chunks = split_indices(indices, num_chunks=num_threads)
277
+ results = Parallel(n_jobs=num_threads, prefer="threads", verbose=0)(
278
+ delayed(decode_sequentially)(chunk, video_path) for chunk in chunks
279
+ )
280
+
281
+ return {
282
+ "data": torch.cat([frame_batch.data for frame_batch in results], dim=0),
283
+ "duration_seconds": torch.cat([frame_batch.duration_seconds for frame_batch in results], dim=0),
284
+ "pts_seconds": torch.cat([frame_batch.pts_seconds for frame_batch in results], dim=0)
285
+ }
286
+
287
+
288
+ def decode_sequentially_timestamp(timestamp_list: List[float], video_path: str, ffmpeg_threads: int = 0):
289
+ """
290
+ Decode frames sequentially from a video based on timestamps.
291
+
292
+ Args:
293
+ timestamp_list: List of timestamps (in seconds) to decode.
294
+ video_path: Path to the video file.
295
+ ffmpeg_threads: Number of ffmpeg threads to use.
296
+
297
+ Returns:
298
+ FrameBatch from torchcodec.
299
+ """
300
+ decoder = VideoDecoder(video_path, num_ffmpeg_threads=ffmpeg_threads)
301
+ try:
302
+ metadata = decoder.metadata
303
+ timestamp_list = clamp_timestamps_for_torchcodec(timestamp_list, metadata)
304
+
305
+ return decoder.get_frames_played_at(timestamp_list)
306
+ finally:
307
+ del decoder
308
+
309
+
310
+ def timestamp_decode_with_multithreading(timestamp_list: List[float], num_threads: int, video_path: str) -> dict:
311
+ """
312
+ Decode frames using multithreading based on timestamps.
313
+
314
+ Args:
315
+ timestamp_list: List of timestamps (in seconds) to decode.
316
+ num_threads: Number of threads to use.
317
+ video_path: Path to the video file.
318
+
319
+ Returns:
320
+ dict: Contains 'data', 'duration_seconds', 'pts_seconds' tensors.
321
+ """
322
+ chunks = split_indices(timestamp_list, num_chunks=num_threads)
323
+ results = Parallel(n_jobs=num_threads, prefer="threads", verbose=0)(
324
+ delayed(decode_sequentially_timestamp)(chunk, video_path) for chunk in chunks
325
+ )
326
+
327
+ # Concatenate results from all threads
328
+ data_list = [frame_batch.data for frame_batch in results]
329
+ duration_list = [frame_batch.duration_seconds for frame_batch in results]
330
+ pts_list = [frame_batch.pts_seconds for frame_batch in results]
331
+
332
+ if not data_list:
333
+ logger.warning("No frames were successfully decoded.")
334
+ return {"data": torch.empty(0), "duration_seconds": torch.empty(0), "pts_seconds": torch.empty(0)}
335
+
336
+ return {
337
+ "data": torch.cat(data_list, dim=0),
338
+ "duration_seconds": torch.cat(duration_list, dim=0),
339
+ "pts_seconds": torch.cat(pts_list, dim=0)
340
+ }
341
+
342
+
343
+ def extract_frames_with_torchcodec(
344
+ video_path: str,
345
+ sample_frames_count: int,
346
+ num_threads: int = 4,
347
+
348
+ ) -> Optional[dict]:
349
+ """
350
+ Extract frames from video using torchcodec with multithreading.
351
+
352
+ Args:
353
+ video_path: Path to the video file.
354
+ sample_frames_count: Number of frames to sample.
355
+ num_threads: Number of threads to use for extraction.
356
+ sampling_method: Sampling method, either "index" (uniform frame indices) or "timestamp" (uniform timestamps).
357
+
358
+ Returns:
359
+ dict: Contains 'data' (N, C, H, W), 'duration_seconds' (N,), 'pts_seconds' (N,) tensors.
360
+ Returns None if extraction fails.
361
+ """
362
+ try:
363
+ video_path = cached_clean_video_streams(video_path)
364
+ decoder = VideoDecoder(video_path, num_ffmpeg_threads=0)
365
+ metadata = decoder.metadata
366
+
367
+
368
+ total_frames_in_video = metadata.num_frames_from_content
369
+
370
+ effective_sample_count = min(sample_frames_count, total_frames_in_video)
371
+ if effective_sample_count == 0:
372
+ logger.error("Cannot extract frames: video has 0 frames or specified frame count is 0")
373
+ return None
374
+
375
+ # Generate uniform frame indices
376
+ frame_indices = np.linspace(0, total_frames_in_video - 1, effective_sample_count).astype(np.int32)
377
+ # Ensure indices are valid and remove duplicates
378
+ frame_indices = np.unique(np.clip(frame_indices, 0, total_frames_in_video - 1))
379
+
380
+ result = decode_with_multithreading(frame_indices.tolist(), num_threads=num_threads, video_path=video_path)
381
+ # Add frame_indices to the result for later use
382
+ result["frame_indices"] = frame_indices
383
+ return result
384
+
385
+
386
+
387
+ except Exception:
388
+ traceback.print_exc()
389
+ return None
390
+
391
+
392
+ def smart_resize(
393
+ num_frames: int,
394
+ height: int,
395
+ width: int,
396
+ temporal_factor: int = 1,
397
+ factor: int = 32,
398
+ min_pixels: int = 128 * 128,
399
+ max_pixels: int = 16 * 16 * 2 * 2 * 2 * 6144,
400
+ per_frame_min_pixels: int = None,
401
+ per_frame_max_pixels: int = None,
402
+ ):
403
+ if num_frames < temporal_factor:
404
+ raise ValueError(f"t:{num_frames} must be larger than temporal_factor:{temporal_factor}")
405
+ if height < factor or width < factor:
406
+ raise ValueError(f"height:{height} or width:{width} must be larger than factor:{factor}")
407
+ elif max(height, width) / min(height, width) > 200:
408
+ raise ValueError(
409
+ f"absolute aspect ratio must be smaller than 200, got {max(height, width) / min(height, width)}"
410
+ )
411
+ h_bar = round(height / factor) * factor
412
+ w_bar = round(width / factor) * factor
413
+ t_bar = round(num_frames / temporal_factor) * temporal_factor
414
+
415
+ # Step 1: Apply per-frame upper limit constraint
416
+ if per_frame_max_pixels is not None and h_bar * w_bar > per_frame_max_pixels:
417
+ beta = math.sqrt((height * width) / per_frame_max_pixels)
418
+ h_bar = max(factor, math.floor(height / beta / factor) * factor)
419
+ w_bar = max(factor, math.floor(width / beta / factor) * factor)
420
+
421
+ # Step 2: Apply 3D volume constraints (frames * height * width)
422
+ if t_bar * h_bar * w_bar > max_pixels:
423
+ beta = math.sqrt((num_frames * height * width) / max_pixels)
424
+ h_bar = max(factor, math.floor(height / beta / factor) * factor)
425
+ w_bar = max(factor, math.floor(width / beta / factor) * factor)
426
+ elif t_bar * h_bar * w_bar < min_pixels:
427
+ beta = math.sqrt(min_pixels / (num_frames * height * width))
428
+ h_bar = math.ceil(height * beta / factor) * factor
429
+ w_bar = math.ceil(width * beta / factor) * factor
430
+
431
+ # Step 3: Ensure per-frame lower limit is respected (after volume constraint)
432
+ # This guarantees single frame stays within [per_frame_min_pixels, per_frame_max_pixels]
433
+ if per_frame_min_pixels is not None and h_bar * w_bar < per_frame_min_pixels:
434
+ beta = math.sqrt(per_frame_min_pixels / (height * width))
435
+ h_bar = math.ceil(height * beta / factor) * factor
436
+ w_bar = math.ceil(width * beta / factor) * factor
437
+
438
+ return h_bar, w_bar
439
+
440
+
441
+ class MossVLVideoProcessorInitKwargs(VideosKwargs):
442
+ patch_size: Optional[int]
443
+ temporal_patch_size: Optional[int]
444
+ merge_size: Optional[int]
445
+ min_frames: Optional[int]
446
+ max_frames: Optional[int]
447
+ video_fps: Optional[Union[int, float]]
448
+ num_extract_threads: Optional[int]
449
+ # Total 3D volume budget across all videos; distributed proportionally per video by T*H*W
450
+ video_max_pixels: Optional[int]
451
+
452
+
453
+ @add_start_docstrings(
454
+ "Constructs a fast Moss-VL video processor that dynamically resizes videos based on the original videos.",
455
+ BASE_VIDEO_PROCESSOR_DOCSTRING,
456
+ """
457
+ patch_size (`int`, *optional*, defaults to 16):
458
+ The spacial patch size of the vision encoder.
459
+ temporal_patch_size (`int`, *optional*, defaults to 1):
460
+ The temporal patch size of the vision encoder.
461
+ merge_size (`int`, *optional*, defaults to 2):
462
+ The merge size of the vision encoder to llm encoder.
463
+ video_fps (`float`, *optional*, defaults to 1.0):
464
+ Target frames per second for video sampling.
465
+ min_frames (`int`, *optional*, defaults to 1):
466
+ Minimum number of frames to sample from a video.
467
+ max_frames (`int`, *optional*, defaults to 256):
468
+ Maximum number of frames to sample from a video.
469
+ num_extract_threads (`int`, *optional*, defaults to 4):
470
+ Number of threads to use for frame extraction.
471
+ """,
472
+ )
473
+ class MossVLVideoProcessor(BaseVideoProcessor):
474
+ resample = PILImageResampling.BICUBIC
475
+ size = {"shortest_edge": 128 * 32 * 32, "longest_edge": 32 * 32 * 768}
476
+ image_mean = [0.5, 0.5, 0.5]
477
+ image_std = [0.5, 0.5, 0.5]
478
+ do_resize = True
479
+ do_rescale = True
480
+ do_normalize = True
481
+ do_convert_rgb = True
482
+ patch_size = 16
483
+ temporal_patch_size = 1
484
+ merge_size = 2
485
+ video_fps = 1.0
486
+ min_frames = 1
487
+ max_frames = 256
488
+ num_extract_threads = 4
489
+ do_sample_frames = True
490
+ # Total 3D volume budget across all videos; distributed proportionally per video by T*H*W
491
+ video_max_pixels = None # read from config
492
+ valid_kwargs = MossVLVideoProcessorInitKwargs
493
+ model_input_names = ["pixel_values_videos", "video_grid_thw"]
494
+
495
+ def __init__(self, **kwargs: Unpack[MossVLVideoProcessorInitKwargs]):
496
+ super().__init__(**kwargs)
497
+ if self.size is not None and (
498
+ self.size.get("shortest_edge", None) is None or self.size.get("longest_edge", None) is None
499
+ ):
500
+ raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.")
501
+
502
+ def _further_process_kwargs(
503
+ self,
504
+ size: Optional[SizeDict] = None,
505
+ **kwargs,
506
+ ) -> dict:
507
+ """
508
+ Update kwargs that need further processing before being validated
509
+ Can be overridden by subclasses to customize the processing of kwargs.
510
+ """
511
+ if size is not None and ("shortest_edge" not in size or "longest_edge" not in size):
512
+ raise ValueError("size must contain 'shortest_edge' and 'longest_edge' keys.")
513
+
514
+ return super()._further_process_kwargs(size=size, **kwargs)
515
+
516
+ def _get_video_path_from_input(self, video_input: Union[str, Dict[str, Any]]) -> str:
517
+ """Normalize a video input into a video path."""
518
+ if isinstance(video_input, dict):
519
+ return video_input["video_path"]
520
+ return video_input
521
+
522
+ def _get_video_duration_seconds(self, video_input: Union[str, Dict[str, Any]]) -> float:
523
+ """Get video duration in seconds for weighted frame-budget allocation."""
524
+ video_path = cached_clean_video_streams(self._get_video_path_from_input(video_input))
525
+ decoder = VideoDecoder(video_path, num_ffmpeg_threads=0)
526
+ try:
527
+ metadata = decoder.metadata
528
+ duration = None
529
+ if (
530
+ metadata.end_stream_seconds_from_content is not None
531
+ and metadata.begin_stream_seconds_from_content is not None
532
+ ):
533
+ duration = metadata.end_stream_seconds_from_content - metadata.begin_stream_seconds_from_content
534
+ if duration is None or duration <= 0:
535
+ duration = metadata.duration_seconds
536
+ return max(0.0, float(duration or 0.0))
537
+ finally:
538
+ del decoder
539
+
540
+ def _allocate_max_frames_for_multiple_videos(
541
+ self,
542
+ video_inputs: List[Union[str, Dict[str, Any]]],
543
+ total_max_frames: Optional[int],
544
+ ) -> List[Optional[int]]:
545
+ """
546
+ Treat max_frames as a total budget for multi-video input and allocate it by duration.
547
+
548
+ The returned values are per-video max_frames. Segment dict inputs still keep their
549
+ existing per-segment weighting logic after receiving the video-level allocation.
550
+ """
551
+ if not video_inputs:
552
+ return []
553
+ if total_max_frames is None or len(video_inputs) == 1:
554
+ return [total_max_frames] * len(video_inputs)
555
+
556
+ total_max_frames = int(total_max_frames)
557
+ num_videos = len(video_inputs)
558
+ if total_max_frames < num_videos:
559
+ logger.warning(
560
+ "Received max_frames=%s for %s videos. At least one frame per video is required, "
561
+ "so falling back to 1 frame per video.",
562
+ total_max_frames,
563
+ num_videos,
564
+ )
565
+ return [1] * num_videos
566
+
567
+ video_durations = [self._get_video_duration_seconds(video_input) for video_input in video_inputs]
568
+ total_duration = sum(video_durations)
569
+
570
+ # Reserve one frame per video first, then distribute the remaining budget by duration.
571
+ allocations = [1] * num_videos
572
+ remaining_budget = total_max_frames - num_videos
573
+ if remaining_budget == 0:
574
+ return allocations
575
+
576
+ if total_duration <= 0:
577
+ raw_extra_allocations = [remaining_budget / num_videos] * num_videos
578
+ else:
579
+ raw_extra_allocations = [
580
+ remaining_budget * (duration / total_duration) for duration in video_durations
581
+ ]
582
+
583
+ base_extra_allocations = [int(math.floor(value)) for value in raw_extra_allocations]
584
+ allocations = [base + extra for base, extra in zip(allocations, base_extra_allocations)]
585
+
586
+ remainder = remaining_budget - sum(base_extra_allocations)
587
+ if remainder > 0:
588
+ fractional_parts = [
589
+ (raw_value - base_value, index)
590
+ for index, (raw_value, base_value) in enumerate(zip(raw_extra_allocations, base_extra_allocations))
591
+ ]
592
+ fractional_parts.sort(key=lambda item: (-item[0], item[1]))
593
+ for _, index in fractional_parts[:remainder]:
594
+ allocations[index] += 1
595
+
596
+ return allocations
597
+
598
+ def calculate_num_frames(
599
+ self,
600
+ metadata: VideoMetadata,
601
+ num_frames: Optional[int] = None,
602
+ fps: Optional[Union[int, float]] = None,
603
+ min_frames: Optional[int] = None,
604
+ max_frames: Optional[int] = None,
605
+ **kwargs,
606
+ ) -> int:
607
+ """
608
+ Calculate the number of frames to sample using fps-based logic with min/max constraints.
609
+
610
+ Logic:
611
+ 1. Calculate target_frames based on fps and video duration
612
+ 2. Apply min_frames and max_frames constraints
613
+ 3. Apply max_allowed_frames protection (rough cap from total video_max_pixels budget)
614
+ 4. Return the number of frames to sample
615
+
616
+ Args:
617
+ metadata (`VideoMetadata`):
618
+ Metadata of the video containing information about total duration, fps and total number of frames.
619
+ num_frames (`int`, *optional*):
620
+ Maximum number of frames to sample. If provided, overrides fps-based calculation.
621
+ fps (`int` or `float`, *optional*):
622
+ Target frames to sample per second. Defaults to `self.video_fps`.
623
+ min_frames (`int`, *optional*):
624
+ Minimum number of frames to sample. If None, uses self.min_frames.
625
+ max_frames (`int`, *optional*):
626
+ Maximum number of frames to sample. If None, uses self.max_frames.
627
+ Returns:
628
+ int:
629
+ Number of frames to sample.
630
+ """
631
+ if fps is not None and num_frames is not None:
632
+ raise ValueError("`num_frames` and `fps` are mutually exclusive arguments, please use only one!")
633
+
634
+ total_num_frames = metadata.total_num_frames
635
+
636
+ # Use provided min/max or fall back to defaults
637
+ effective_min_frames = min_frames if min_frames is not None else self.min_frames
638
+ effective_max_frames = max_frames if max_frames is not None else self.max_frames
639
+
640
+ # Rough per-video frame cap derived from the multi-video total budget
641
+ # (exact allocation happens later in _preprocess via weighted distribution)
642
+ per_frame_min_pixels = self.size.get("shortest_edge", None) if self.size else None
643
+ video_max_pixels = getattr(self, "video_max_pixels", None)
644
+ if per_frame_min_pixels is not None and video_max_pixels is not None and per_frame_min_pixels > 0:
645
+ max_allowed_frames = video_max_pixels // per_frame_min_pixels
646
+ effective_max_frames = min(effective_max_frames, max_allowed_frames)
647
+
648
+ # Get video duration
649
+ if hasattr(metadata, 'duration') and metadata.duration is not None:
650
+ duration = metadata.duration
651
+ else:
652
+ video_fps = metadata.fps
653
+ if video_fps is not None and video_fps > 0:
654
+ duration = total_num_frames / video_fps
655
+ else:
656
+ # Fallback: assume 24 fps
657
+ video_fps = 24.0
658
+ duration = total_num_frames / video_fps
659
+ logger.warning_once(
660
+ "Could not determine video fps from metadata, defaulting to 24 fps for duration calculation."
661
+ )
662
+
663
+ # Use provided fps or default
664
+ target_fps = fps if fps is not None else self.video_fps
665
+
666
+ # Calculate target frames based on fps and duration
667
+ if num_frames is None:
668
+ # Calculate how many frames we should sample based on target fps
669
+ target_total_frames = int(math.ceil(duration * target_fps - 1e-6))
670
+
671
+ # Apply min/max constraints
672
+ sample_frames = max(target_total_frames, effective_min_frames)
673
+ sample_frames = min(sample_frames, effective_max_frames, total_num_frames)
674
+ else:
675
+ # If num_frames is explicitly provided, use it directly with constraints
676
+ sample_frames = min(max(num_frames, effective_min_frames), effective_max_frames, total_num_frames)
677
+
678
+ return sample_frames
679
+
680
+
681
+ def _decode_timestamps_with_decoder(
682
+ self,
683
+ decoder: VideoDecoder,
684
+ timestamps: List[float],
685
+ chunk_size: int = 128,
686
+ ) -> torch.Tensor:
687
+ if not timestamps:
688
+ return torch.empty(0)
689
+
690
+ frame_chunks = []
691
+ for start in range(0, len(timestamps), chunk_size):
692
+ frame_batch = decoder.get_frames_played_at(timestamps[start:start + chunk_size])
693
+ frame_chunks.append(frame_batch.data)
694
+
695
+ if len(frame_chunks) == 1:
696
+ return frame_chunks[0]
697
+ return torch.cat(frame_chunks, dim=0)
698
+
699
+ def _clamp_timestamps_for_decoder(
700
+ self,
701
+ timestamps: List[float],
702
+ torchcodec_metadata,
703
+ ) -> List[float]:
704
+ return clamp_timestamps_for_torchcodec(timestamps, torchcodec_metadata)
705
+
706
+ def _fetch_video_segments_batched(
707
+ self,
708
+ video_path: str,
709
+ segments: List[List[float]],
710
+ min_frames: Optional[int] = None,
711
+ max_frames: Optional[int] = None,
712
+ video_fps: Optional[float] = None,
713
+ ):
714
+ min_frames = max(1, min_frames if min_frames is not None else self.min_frames)
715
+ max_frames = max(1, max_frames if max_frames is not None else self.max_frames)
716
+ target_video_fps = video_fps if video_fps is not None else self.video_fps
717
+
718
+ video_path = cached_clean_video_streams(video_path)
719
+ decoder = VideoDecoder(video_path, num_ffmpeg_threads=0)
720
+ try:
721
+ torchcodec_metadata = decoder.metadata
722
+ source_video_fps = torchcodec_metadata.average_fps
723
+
724
+ duration = None
725
+ if (
726
+ torchcodec_metadata.end_stream_seconds_from_content is not None
727
+ and torchcodec_metadata.begin_stream_seconds_from_content is not None
728
+ ):
729
+ duration = (
730
+ torchcodec_metadata.end_stream_seconds_from_content
731
+ - torchcodec_metadata.begin_stream_seconds_from_content
732
+ )
733
+ if duration is None or duration <= 0:
734
+ duration = torchcodec_metadata.duration_seconds
735
+
736
+ segment_durations = [
737
+ segment[1] - segment[0] if len(segment) == 2 else None
738
+ for segment in segments
739
+ ]
740
+ total_segment_duration = sum(d for d in segment_durations if d is not None)
741
+ num_range_segments = sum(1 for d in segment_durations if d is not None)
742
+
743
+ segment_timestamps = []
744
+ decode_timestamps = []
745
+ for i, segment in enumerate(segments):
746
+ if len(segment) == 1:
747
+ actual_timestamps = self._clamp_timestamps_for_decoder([segment[0]], torchcodec_metadata)
748
+ segment_timestamps.append(actual_timestamps)
749
+ decode_timestamps.extend(actual_timestamps)
750
+ continue
751
+
752
+ start_time, end_time = segment
753
+ segment_duration = end_time - start_time
754
+ target_frames = int(math.ceil(segment_duration * target_video_fps))
755
+
756
+ if total_segment_duration > 0:
757
+ weight = segment_durations[i] / total_segment_duration
758
+ else:
759
+ weight = 1.0 / num_range_segments if num_range_segments > 0 else 1.0
760
+
761
+ weighted_min_frames = max(1, int(round(min_frames * weight)))
762
+ weighted_max_frames = max(1, int(round(max_frames * weight)))
763
+ target_frames = max(target_frames, weighted_min_frames)
764
+ target_frames = min(target_frames, weighted_max_frames)
765
+
766
+ if target_frames == 1:
767
+ actual_timestamps = [start_time]
768
+ else:
769
+ actual_timestamps = np.linspace(
770
+ start_time,
771
+ end_time,
772
+ target_frames,
773
+ endpoint=False,
774
+ ).tolist()
775
+
776
+ actual_timestamps = self._clamp_timestamps_for_decoder(actual_timestamps, torchcodec_metadata)
777
+ segment_timestamps.append(actual_timestamps)
778
+ decode_timestamps.extend(actual_timestamps)
779
+
780
+ flat_frames = self._decode_timestamps_with_decoder(decoder, decode_timestamps)
781
+
782
+ videos = []
783
+ metadata = []
784
+ frame_offset = 0
785
+ for actual_timestamps in segment_timestamps:
786
+ sample_count = len(actual_timestamps)
787
+ video_tensor = flat_frames[frame_offset:frame_offset + sample_count]
788
+ frame_offset += sample_count
789
+
790
+ video_metadata = VideoMetadata(
791
+ total_num_frames=sample_count,
792
+ fps=source_video_fps,
793
+ duration=duration,
794
+ video_backend="torchcodec",
795
+ height=torchcodec_metadata.height,
796
+ width=torchcodec_metadata.width,
797
+ frames_indices=None,
798
+ )
799
+ video_metadata.actual_timestamps = actual_timestamps
800
+
801
+ videos.append(video_tensor)
802
+ metadata.append(video_metadata)
803
+
804
+ return videos, metadata
805
+ finally:
806
+ del decoder
807
+
808
+
809
+ def _fetch_video_segment(
810
+ self,
811
+ video_path: str,
812
+ segment: List[float],
813
+ min_frames: Optional[int] = None,
814
+ max_frames: Optional[int] = None,
815
+ video_fps: Optional[float] = None,
816
+ ):
817
+ """
818
+ Fetch video frames for a specific segment.
819
+
820
+ Args:
821
+ video_path: Path to the video file
822
+ segment: [start, end] for a segment (left-closed, right-open) or [time] for a single frame
823
+ min_frames: Minimum frames for this segment (weighted). Defaults to self.min_frames. Must be >= 1.
824
+ max_frames: Maximum frames for this segment (weighted). Defaults to self.max_frames. Must be >= 1.
825
+ video_fps: Target frames per second for video sampling. If None, uses self.video_fps.
826
+
827
+ Returns:
828
+ Tuple of (video_tensor, video_metadata)
829
+ """
830
+ # Use provided min/max or fall back to defaults, ensure >= 1
831
+ min_frames = max(1, min_frames if min_frames is not None else self.min_frames)
832
+ max_frames = max(1, max_frames if max_frames is not None else self.max_frames)
833
+ # Use provided video_fps or fall back to self.video_fps
834
+ target_video_fps = video_fps if video_fps is not None else self.video_fps
835
+
836
+ video_path = clean_video_streams(video_path)
837
+ decoder = VideoDecoder(video_path, num_ffmpeg_threads=0)
838
+ try:
839
+ torchcodec_metadata = decoder.metadata
840
+
841
+ video_fps = torchcodec_metadata.average_fps
842
+
843
+ # Calculate duration
844
+ duration = None
845
+ if torchcodec_metadata.end_stream_seconds_from_content is not None and torchcodec_metadata.begin_stream_seconds_from_content is not None:
846
+ duration = torchcodec_metadata.end_stream_seconds_from_content - torchcodec_metadata.begin_stream_seconds_from_content
847
+ if duration is None or duration <= 0:
848
+ duration = torchcodec_metadata.duration_seconds
849
+
850
+ if len(segment) == 1:
851
+ # Single frame at specified time
852
+ actual_timestamps = self._clamp_timestamps_for_decoder([segment[0]], torchcodec_metadata)
853
+ frame_batch = decoder.get_frames_played_at(actual_timestamps)
854
+ video_tensor = frame_batch.data
855
+ sample_count = 1
856
+ else:
857
+ # Segment [start, end) - left-closed, right-open interval
858
+ start_time, end_time = segment
859
+ segment_duration = end_time - start_time
860
+
861
+ # Calculate number of frames to sample for this segment
862
+ target_frames = int(math.ceil(segment_duration * target_video_fps))
863
+ target_frames = max(target_frames, min_frames)
864
+ target_frames = min(target_frames, max_frames)
865
+
866
+ # Generate timestamps for uniform sampling within segment
867
+ if target_frames == 1:
868
+ actual_timestamps = [start_time] # Use start_time for single frame
869
+ else:
870
+ # Sample uniformly within [start, end), endpoint=False for left-closed right-open
871
+ actual_timestamps = np.linspace(start_time, end_time, target_frames, endpoint=False).tolist()
872
+
873
+ actual_timestamps = self._clamp_timestamps_for_decoder(actual_timestamps, torchcodec_metadata)
874
+
875
+ # Use multithreading for extraction
876
+ result = timestamp_decode_with_multithreading(actual_timestamps, self.num_extract_threads, video_path)
877
+ video_tensor = result["data"]
878
+ sample_count = len(actual_timestamps)
879
+
880
+ # Create VideoMetadata
881
+ video_metadata = VideoMetadata(
882
+ total_num_frames=sample_count,
883
+ fps=video_fps,
884
+ duration=duration,
885
+ video_backend="torchcodec",
886
+ height=torchcodec_metadata.height,
887
+ width=torchcodec_metadata.width,
888
+ frames_indices=None
889
+ )
890
+
891
+ # Store actual timestamps as a custom attribute for _calculate_timestamps to use
892
+ video_metadata.actual_timestamps = actual_timestamps
893
+
894
+ return video_tensor, video_metadata
895
+ finally:
896
+ del decoder
897
+
898
+ def fetch_videos(
899
+ self,
900
+ video_url_or_urls: Union[str, Dict[str, Any], List[Union[str, Dict[str, Any]]]],
901
+ sample_indices_fn=None,
902
+ video_fps: Optional[float] = None,
903
+ min_frames: Optional[int] = None,
904
+ max_frames: Optional[int] = None,
905
+ ):
906
+ """
907
+ Override fetch_videos to use torchcodec for frame extraction.
908
+
909
+ This method uses torchcodec with multithreading for efficient frame extraction.
910
+ Frame count is calculated by the calculate_num_frames method
911
+ (fps-based with min/max constraints).
912
+
913
+ Args:
914
+ video_url_or_urls: Can be one of:
915
+ - str: Single video path
916
+ - Dict: Video with segments {"video_path": str, "segments": List[List[float]]}
917
+ - List[Union[str, Dict]]: List of video paths or segment dicts
918
+ sample_indices_fn: (Not used) Kept for compatibility with base class signature.
919
+ video_fps: Target frames per second for video sampling. If None, uses self.video_fps.
920
+ min_frames: Minimum number of frames to sample. If None, uses self.min_frames.
921
+ max_frames: Maximum number of frames to sample. If None, uses self.max_frames.
922
+
923
+ Returns:
924
+ Tuple of (videos, metadata) where videos are torch.Tensors and metadata are VideoMetadata objects.
925
+ """
926
+ # Use provided values or fall back to self defaults
927
+ effective_video_fps = video_fps if video_fps is not None else self.video_fps
928
+ effective_min_frames = min_frames if min_frames is not None else self.min_frames
929
+ effective_max_frames = max_frames if max_frames is not None else self.max_frames
930
+ # Handle recursive calls for lists
931
+ if isinstance(video_url_or_urls, list):
932
+ all_videos = []
933
+ all_metadata = []
934
+ if len(video_url_or_urls) == 1:
935
+ per_video_max_frames = [effective_max_frames]
936
+ else:
937
+ per_video_max_frames = self._allocate_max_frames_for_multiple_videos(
938
+ video_url_or_urls,
939
+ effective_max_frames,
940
+ )
941
+ for x, allocated_max_frames in zip(video_url_or_urls, per_video_max_frames):
942
+ result = self.fetch_videos(
943
+ x,
944
+ video_fps=effective_video_fps,
945
+ min_frames=effective_min_frames,
946
+ max_frames=allocated_max_frames,
947
+ )
948
+ # Check if result is from segment expansion (returns lists) or single item
949
+ if isinstance(result[0], list):
950
+ all_videos.extend(result[0])
951
+ all_metadata.extend(result[1])
952
+ else:
953
+ all_videos.append(result[0])
954
+ all_metadata.append(result[1])
955
+ return all_videos, all_metadata
956
+
957
+ # Handle dict with segments - returns lists (one per segment)
958
+ if isinstance(video_url_or_urls, dict):
959
+ video_path = video_url_or_urls["video_path"]
960
+ segments = video_url_or_urls["segments"]
961
+
962
+ return self._fetch_video_segments_batched(
963
+ video_path,
964
+ segments,
965
+ min_frames=effective_min_frames,
966
+ max_frames=effective_max_frames,
967
+ video_fps=effective_video_fps,
968
+ )
969
+
970
+ # Single video path
971
+ video_path = video_url_or_urls
972
+
973
+ # Clean video streams first (remove extra streams if needed)
974
+ video_path = cached_clean_video_streams(video_path)
975
+
976
+ decoder = None
977
+ try:
978
+ # Create VideoDecoder only once for both metadata and frame extraction
979
+ decoder = VideoDecoder(video_path, num_ffmpeg_threads=0)
980
+ torchcodec_metadata = decoder.metadata
981
+
982
+ duration = None
983
+ if torchcodec_metadata.end_stream_seconds_from_content is not None and torchcodec_metadata.begin_stream_seconds_from_content is not None:
984
+ duration = torchcodec_metadata.end_stream_seconds_from_content - torchcodec_metadata.begin_stream_seconds_from_content
985
+
986
+ if duration is None or duration <= 0:
987
+ duration = torchcodec_metadata.duration_seconds
988
+
989
+ # Use num_frames_from_content for accurate frame count (consistent with extraction)
990
+ total_frames_in_video = torchcodec_metadata.num_frames_from_content
991
+
992
+ # Create VideoMetadata object for sample_frames method
993
+ temp_metadata = VideoMetadata(
994
+ total_num_frames=total_frames_in_video,
995
+ fps=torchcodec_metadata.average_fps,
996
+ duration=duration,
997
+ video_backend="torchcodec",
998
+ height=torchcodec_metadata.height,
999
+ width=torchcodec_metadata.width,
1000
+ frames_indices=None
1001
+ )
1002
+
1003
+ # Use calculate_num_frames method to get the number of frames to sample
1004
+ sample_frames_count = self.calculate_num_frames(
1005
+ temp_metadata,
1006
+ fps=effective_video_fps,
1007
+ min_frames=effective_min_frames,
1008
+ max_frames=effective_max_frames,
1009
+ )
1010
+
1011
+ # Ensure sample count is valid
1012
+ effective_sample_count = min(sample_frames_count, total_frames_in_video)
1013
+ if effective_sample_count == 0:
1014
+ raise ValueError(f"Cannot extract frames: video has 0 frames or specified frame count is 0")
1015
+
1016
+ # Generate uniform frame indices
1017
+ frame_indices = np.linspace(0, total_frames_in_video - 1, effective_sample_count).astype(np.int32)
1018
+ # Ensure indices are valid and remove duplicates
1019
+ frame_indices = np.unique(np.clip(frame_indices, 0, total_frames_in_video - 1))
1020
+
1021
+ # Extract frames using multithreading (decoder is created inside each thread for thread safety)
1022
+ result = decode_with_multithreading(frame_indices.tolist(), num_threads=self.num_extract_threads, video_path=video_path)
1023
+
1024
+ # Extract frame tensor (N, C, H, W)
1025
+ frames_tensor = result["data"]
1026
+
1027
+ # Create final VideoMetadata object
1028
+ video_metadata = VideoMetadata(
1029
+ total_num_frames=len(frame_indices),
1030
+ fps=torchcodec_metadata.average_fps,
1031
+ duration=duration,
1032
+ video_backend="torchcodec",
1033
+ height=torchcodec_metadata.height,
1034
+ width=torchcodec_metadata.width,
1035
+ frames_indices=frame_indices
1036
+ )
1037
+
1038
+ # Ensure frames are in (T, C, H, W) format
1039
+ if frames_tensor.dim() == 4: # (N, C, H, W)
1040
+ video_tensor = frames_tensor
1041
+ else:
1042
+ raise ValueError(f"Unexpected frame tensor shape: {frames_tensor.shape}")
1043
+
1044
+ return video_tensor, video_metadata
1045
+
1046
+ except Exception as e:
1047
+ logger.error(f"Error loading video {video_path}: {e}")
1048
+ traceback.print_exc()
1049
+ raise ValueError(f"Failed to load video {video_path}: {e}")
1050
+ finally:
1051
+ if decoder is not None:
1052
+ del decoder
1053
+
1054
+ def _preprocess(
1055
+ self,
1056
+ videos: list[torch.Tensor],
1057
+ do_convert_rgb: bool = True,
1058
+ do_resize: bool = True,
1059
+ size: Optional[SizeDict] = None,
1060
+ interpolation: PILImageResampling = PILImageResampling.BICUBIC,
1061
+ do_rescale: bool = True,
1062
+ rescale_factor: float = 1 / 255.0,
1063
+ do_normalize: bool = True,
1064
+ image_mean: Optional[Union[float, list[float]]] = None,
1065
+ image_std: Optional[Union[float, list[float]]] = None,
1066
+ patch_size: Optional[int] = None,
1067
+ temporal_patch_size: Optional[int] = None,
1068
+ merge_size: Optional[int] = None,
1069
+ return_tensors: Optional[Union[str, TensorType]] = None,
1070
+ **kwargs,
1071
+ ):
1072
+ grouped_videos, grouped_videos_index = group_videos_by_shape(videos)
1073
+ resized_videos_grouped = {}
1074
+
1075
+ video_max_pixels = getattr(self, "video_max_pixels", None)
1076
+ if video_max_pixels is not None:
1077
+ total_volume = sum(
1078
+ sv.shape[0] * sv.shape[1] * sv.shape[3] * sv.shape[4]
1079
+ for sv in grouped_videos.values()
1080
+ )
1081
+ else:
1082
+ total_volume = 0
1083
+
1084
+ for shape, stacked_videos in grouped_videos.items():
1085
+ B, T, C, H, W = stacked_videos.shape
1086
+ num_frames, height, width = T, H, W
1087
+ # Convert to RGB if needed (reuse from base class)
1088
+ if do_convert_rgb:
1089
+ stacked_videos = self.convert_to_rgb(stacked_videos)
1090
+ if do_resize:
1091
+ if video_max_pixels is not None and total_volume > 0:
1092
+ allocated_max_pixels = int(video_max_pixels * (T * H * W) / total_volume)
1093
+ else:
1094
+ allocated_max_pixels = size.longest_edge
1095
+ resized_height, resized_width = smart_resize(
1096
+ num_frames=num_frames,
1097
+ height=height,
1098
+ width=width,
1099
+ temporal_factor=temporal_patch_size,
1100
+ factor=patch_size * merge_size,
1101
+ min_pixels=size.shortest_edge,
1102
+ max_pixels=allocated_max_pixels,
1103
+ per_frame_min_pixels=size.shortest_edge,
1104
+ per_frame_max_pixels=size.longest_edge,
1105
+ )
1106
+ stacked_videos = stacked_videos.view(B * T, C, H, W)
1107
+ stacked_videos = self.resize(
1108
+ stacked_videos,
1109
+ size=SizeDict(height=resized_height, width=resized_width),
1110
+ interpolation=interpolation,
1111
+ )
1112
+ stacked_videos = stacked_videos.view(B, T, C, resized_height, resized_width)
1113
+ resized_videos_grouped[shape] = stacked_videos
1114
+ resized_videos = reorder_videos(resized_videos_grouped, grouped_videos_index)
1115
+
1116
+ # Group videos by size for further processing
1117
+ # Needed in case do_resize is False, or resize returns videos with different sizes
1118
+ grouped_videos, grouped_videos_index = group_videos_by_shape(resized_videos)
1119
+ processed_videos_grouped = {}
1120
+ processed_grids = {}
1121
+ for shape, stacked_videos in grouped_videos.items():
1122
+ resized_height, resized_width = get_image_size(stacked_videos[0], channel_dim=ChannelDimension.FIRST)
1123
+
1124
+ # Fused rescale and normalize
1125
+ stacked_videos = self.rescale_and_normalize(
1126
+ stacked_videos, do_rescale, rescale_factor, do_normalize, image_mean, image_std
1127
+ )
1128
+ patches = stacked_videos
1129
+
1130
+ # Check that videos have `num_frames` divisible by `temporal_patch_size`
1131
+ if patches.shape[1] % temporal_patch_size != 0:
1132
+ repeats = patches[:, -1:].repeat(1, temporal_patch_size - 1, 1, 1, 1)
1133
+ patches = torch.cat([patches, repeats], dim=1)
1134
+ batch_size, grid_t, channel = patches.shape[:3]
1135
+ grid_t = grid_t // temporal_patch_size
1136
+ grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
1137
+
1138
+ patches = patches.view(
1139
+ batch_size,
1140
+ grid_t,
1141
+ temporal_patch_size,
1142
+ channel,
1143
+ grid_h // merge_size,
1144
+ merge_size,
1145
+ patch_size,
1146
+ grid_w // merge_size,
1147
+ merge_size,
1148
+ patch_size,
1149
+ )
1150
+ patches = patches.permute(0, 1, 4, 7, 5, 8, 3, 2, 6, 9)
1151
+ flatten_patches = patches.reshape(
1152
+ batch_size,
1153
+ grid_t * grid_h * grid_w,
1154
+ channel * temporal_patch_size * patch_size * patch_size,
1155
+ )
1156
+
1157
+ processed_videos_grouped[shape] = flatten_patches
1158
+ processed_grids[shape] = [[grid_t, grid_h, grid_w]] * batch_size
1159
+
1160
+ processed_videos = reorder_videos(processed_videos_grouped, grouped_videos_index)
1161
+ processed_grids = reorder_videos(processed_grids, grouped_videos_index)
1162
+ pixel_values_videos = torch.cat(processed_videos, dim=0)
1163
+ video_grid_thw = torch.tensor(processed_grids)
1164
+ data = {
1165
+ "pixel_values_videos": pixel_values_videos,
1166
+ "video_grid_thw": video_grid_thw,
1167
+ }
1168
+
1169
+ return BatchFeature(data=data, tensor_type=return_tensors)
1170
+
1171
+ def preprocess(
1172
+ self,
1173
+ videos: Union[str, Dict[str, Any], List[Union[str, Dict[str, Any]]]],
1174
+ **kwargs,
1175
+ ) -> BatchFeature:
1176
+ """
1177
+ Preprocess videos for the model.
1178
+
1179
+ This method overrides the base class to handle two video input formats:
1180
+ 1. String path: "path/to/video.mp4"
1181
+ 2. Dict with segments: {"video_path": "...", "segment": [[start, end], [time], ...]}
1182
+
1183
+ Args:
1184
+ videos: Video input(s) in one of the supported formats.
1185
+ **kwargs: Additional arguments passed to _preprocess.
1186
+
1187
+ Returns:
1188
+ BatchFeature with pixel_values_videos, video_grid_thw, and optionally video_metadata.
1189
+ """
1190
+ # Validate kwargs
1191
+ validate_kwargs(
1192
+ captured_kwargs=kwargs.keys(),
1193
+ valid_processor_keys=list(self.valid_kwargs.__annotations__.keys()) + ["return_tensors"],
1194
+ )
1195
+
1196
+ # Set default kwargs from self
1197
+ for kwarg_name in self.valid_kwargs.__annotations__:
1198
+ kwargs.setdefault(kwarg_name, getattr(self, kwarg_name, None))
1199
+
1200
+ # Pop kwargs that are handled separately
1201
+ return_tensors = kwargs.pop("return_tensors", None)
1202
+ return_metadata = kwargs.pop("return_metadata", False)
1203
+ input_data_format = kwargs.pop("input_data_format", None)
1204
+ device = kwargs.pop("device", None)
1205
+ kwargs.pop("video_metadata", None) # We generate our own metadata
1206
+ kwargs.pop("do_sample_frames", None) # We handle sampling ourselves
1207
+ kwargs.pop("data_format", None) # Not used
1208
+
1209
+ # Normalize input to list format
1210
+ if not isinstance(videos, list):
1211
+ videos = [videos]
1212
+
1213
+ # Get video processing params from kwargs (may be passed explicitly for per-batch configuration)
1214
+ video_fps = kwargs.pop("video_fps", None)
1215
+ min_frames = kwargs.pop("min_frames", None)
1216
+ max_frames = kwargs.pop("max_frames", None)
1217
+
1218
+ # Use fetch_videos to handle both string and dict formats
1219
+ video_tensors, video_metadata = self.fetch_videos(
1220
+ videos,
1221
+ video_fps=video_fps,
1222
+ min_frames=min_frames,
1223
+ max_frames=max_frames,
1224
+ )
1225
+
1226
+ # Prepare video tensors using _prepare_input_videos
1227
+ prepared_videos = self._prepare_input_videos(
1228
+ videos=video_tensors,
1229
+ input_data_format=input_data_format,
1230
+ device=device,
1231
+ )
1232
+
1233
+ # Process kwargs for _preprocess
1234
+ kwargs = self._further_process_kwargs(**kwargs)
1235
+ self._validate_preprocess_kwargs(**kwargs)
1236
+
1237
+ # Call _preprocess with prepared videos
1238
+ result = self._preprocess(videos=prepared_videos, return_tensors=return_tensors, **kwargs)
1239
+
1240
+ # Add metadata if requested
1241
+ if return_metadata:
1242
+ result["video_metadata"] = video_metadata
1243
+
1244
+ return result
1245
+
1246
+
1247
+ __all__ = ["MossVLVideoProcessor"]
1248
+
vocab.json ADDED
The diff for this file is too large to render. See raw diff