abtonmoy commited on
Commit
a177d26
·
verified ·
1 Parent(s): 85b38b4

inference.py: embed_video (base's own video path, gate-guarded)

Browse files
Files changed (1) hide show
  1. inference.py +74 -0
inference.py CHANGED
@@ -173,6 +173,80 @@ class FusionEmbedder:
173
  pooled = self._pool(h, inputs["attention_mask"])
174
  return self._finish(pooled, dim)
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  # ------------------------------------------------------------------ cross-modal readout
177
  @staticmethod
178
  def center(embs: torch.Tensor) -> torch.Tensor:
 
173
  pooled = self._pool(h, inputs["attention_mask"])
174
  return self._finish(pooled, dim)
175
 
176
+ # ------------------------------------------------------------------ video
177
+ @torch.no_grad()
178
+ def embed_video(self, video, fps: Optional[float] = None,
179
+ max_frames: Optional[int] = None,
180
+ dim: Optional[int] = None) -> torch.Tensor:
181
+ """Embed a video through the frozen base model's own video path.
182
+
183
+ ``video`` is a file path/URL, or a pre-extracted frame sequence (a list of
184
+ PIL images and/or frame-image paths). Preprocessing follows the base model's
185
+ official usage (the Qwen3-VL-Embedding reference scripts):
186
+ ``qwen_vl_utils.process_vision_info`` with ``image_patch_size=16``, 1 fps
187
+ sampling up to 64 frames for path inputs, uniform temporal sampling of frame
188
+ sequences, a 7,864,320 total-pixel budget, and ``do_resize=False`` at the
189
+ processor because the vision utility already smart-resizes. Like images,
190
+ video is a non-audio input and takes the frozen path (no whitening, no
191
+ adapters). Requires qwen-vl-utils>=0.0.14; path inputs additionally need a
192
+ video decoder backend supported by it.
193
+ """
194
+ import numpy as np
195
+
196
+ gate = getattr(self.model, "_adapter_gate", None)
197
+ if gate is not None and gate.active:
198
+ # The video path runs through the same (hook-carrying) decoder layers;
199
+ # non-audio inputs must run with the gate closed so the adapter branch
200
+ # never runs. Mirrors the encode_text/embed_image guards.
201
+ raise RuntimeError("adapter gate is open during a video embed — "
202
+ "non-audio inputs must run with the gate closed")
203
+ try:
204
+ from qwen_vl_utils import process_vision_info
205
+ except ImportError as e:
206
+ raise ImportError(
207
+ "video embedding uses the base model's own preprocessing "
208
+ "package: pip install 'qwen-vl-utils>=0.0.14'") from e
209
+
210
+ # Constants from the base's reference implementation.
211
+ video_total_pixels = 10 * 768 * 32 * 32
212
+ default_fps, default_max_frames = 1.0, 64
213
+
214
+ if isinstance(video, (str, os.PathLike)):
215
+ v = str(video)
216
+ content = v if v.startswith(("http://", "https://")) \
217
+ else "file://" + os.path.abspath(v)
218
+ vkw = {"fps": fps or default_fps,
219
+ "max_frames": max_frames or default_max_frames,
220
+ "total_pixels": video_total_pixels}
221
+ else:
222
+ frames = list(video)
223
+ if not frames:
224
+ raise ValueError("empty frame sequence")
225
+ mf = max_frames or default_max_frames
226
+ if len(frames) > mf:
227
+ # Uniform temporal sampling, as in the base's sample_frames.
228
+ idx = np.linspace(0, len(frames) - 1, mf, dtype=int)
229
+ frames = [frames[i] for i in idx]
230
+ content = ["file://" + os.path.abspath(str(f))
231
+ if isinstance(f, (str, os.PathLike)) else f
232
+ for f in frames]
233
+ vkw = {"total_pixels": video_total_pixels}
234
+
235
+ conversation = [{"role": "user",
236
+ "content": [{"type": "video", "video": content, **vkw}]}]
237
+ _, video_inputs, video_kwargs = process_vision_info(
238
+ conversation, image_patch_size=16,
239
+ return_video_metadata=True, return_video_kwargs=True)
240
+ videos, video_metadata = zip(*video_inputs)
241
+ text = _chat(DOC_INSTRUCTION, "<|vision_start|><|video_pad|><|vision_end|>")
242
+ inputs = self.proc(text=[text], videos=list(videos),
243
+ video_metadata=list(video_metadata),
244
+ do_resize=False, return_tensors="pt",
245
+ **video_kwargs).to(self.device)
246
+ h = self.full(**inputs).last_hidden_state
247
+ pooled = self._pool(h, inputs["attention_mask"])
248
+ return self._finish(pooled, dim)
249
+
250
  # ------------------------------------------------------------------ cross-modal readout
251
  @staticmethod
252
  def center(embs: torch.Tensor) -> torch.Tensor: