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

Add embed_video: the frozen base's own video path (qwen-vl-utils preprocessing, official defaults), gate-guarded like images; bitwise-verified against the base and the reference loader

Browse files
Files changed (1) hide show
  1. modeling_fusion_embedding.py +80 -4
modeling_fusion_embedding.py CHANGED
@@ -1,6 +1,6 @@
1
  """HF remote-code modeling for the fusion-embedding family (AutoModel + trust_remote_code).
2
 
3
- One embedding space for text, images, and audio. The checkpoint on this repository holds
4
  ONLY the trained components (perceiver-resampler connector, diagonal text whitening,
5
  logit scale and — generation 2 — the modality-gated deep adapters); the frozen
6
  Qwen3-VL-Embedding-2B base and the frozen Qwen2.5-Omni audio tower are downloaded from
@@ -12,16 +12,18 @@ their own repositories on first use and are byte-identical to their releases.
12
  t = model.embed_text("a dog barks in the distance")
13
  a = model.embed_audio("dog.wav")
14
  i = model.embed_image("dog.jpg")
 
15
 
16
  The embed_* methods reproduce the repository's reference ``inference.py`` exactly (same
17
  chat templates, truncation, pooling, whitening, Matryoshka truncation and normalization);
18
  outputs are bitwise-identical to that loader on the same hardware. Non-audio inputs never
19
  execute the generation-2 adapter branch (the gate returns the frozen layers' output
20
- untouched), so text/image outputs are bit-for-bit those of generation 1 and of the base's
21
- computation path.
22
 
23
  Requires: transformers>=4.46 (with the Qwen2.5-Omni model classes), torchvision, pillow,
24
- soundfile, librosa. A CUDA GPU is recommended (~14 GB at bf16).
 
25
  """
26
 
27
  from __future__ import annotations
@@ -439,6 +441,80 @@ class FusionEmbeddingModel(PreTrainedModel):
439
  pooled = last_token_pool(h, inputs["attention_mask"])
440
  return self._finish(pooled, dim)
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  # ------------------------------------------------------------- batched
443
  @torch.no_grad()
444
  def embed_text_batch(self, texts, instruction: str = DEFAULT_QUERY_INSTRUCTION,
 
1
  """HF remote-code modeling for the fusion-embedding family (AutoModel + trust_remote_code).
2
 
3
+ One embedding space for text, images, video, and audio. The checkpoint on this repository holds
4
  ONLY the trained components (perceiver-resampler connector, diagonal text whitening,
5
  logit scale and — generation 2 — the modality-gated deep adapters); the frozen
6
  Qwen3-VL-Embedding-2B base and the frozen Qwen2.5-Omni audio tower are downloaded from
 
12
  t = model.embed_text("a dog barks in the distance")
13
  a = model.embed_audio("dog.wav")
14
  i = model.embed_image("dog.jpg")
15
+ v = model.embed_video("dog.mp4") # or a list of PIL frames
16
 
17
  The embed_* methods reproduce the repository's reference ``inference.py`` exactly (same
18
  chat templates, truncation, pooling, whitening, Matryoshka truncation and normalization);
19
  outputs are bitwise-identical to that loader on the same hardware. Non-audio inputs never
20
  execute the generation-2 adapter branch (the gate returns the frozen layers' output
21
+ untouched), so text/image/video outputs are bit-for-bit those of generation 1 and of the
22
+ base's computation path.
23
 
24
  Requires: transformers>=4.46 (with the Qwen2.5-Omni model classes), torchvision, pillow,
25
+ soundfile, librosa; video embedding additionally requires qwen-vl-utils>=0.0.14 (the
26
+ base model's own video preprocessing). A CUDA GPU is recommended (~14 GB at bf16).
27
  """
28
 
29
  from __future__ import annotations
 
441
  pooled = last_token_pool(h, inputs["attention_mask"])
442
  return self._finish(pooled, dim)
443
 
444
+ @torch.no_grad()
445
+ def embed_video(self, video, fps: Optional[float] = None,
446
+ max_frames: Optional[int] = None,
447
+ dim: Optional[int] = None) -> torch.Tensor:
448
+ """Embed a video through the frozen base model's own video path.
449
+
450
+ ``video`` is a file path/URL, or a pre-extracted frame sequence (a list
451
+ of PIL images and/or frame-image paths). Preprocessing follows the base
452
+ model's official usage (the Qwen3-VL-Embedding reference scripts):
453
+ ``qwen_vl_utils.process_vision_info`` with ``image_patch_size=16``,
454
+ 1 fps sampling up to 64 frames for path inputs, uniform temporal
455
+ sampling of frame sequences, a 7,864,320 total-pixel budget
456
+ (10 x 768 x 32 x 32), and ``do_resize=False`` at the processor because
457
+ the vision utility already smart-resizes. Like images, video is a
458
+ non-audio input: it takes the frozen path (no whitening, no adapters).
459
+ Path inputs additionally need a video decoder backend supported by
460
+ ``qwen_vl_utils`` (e.g. torchvision/decord); frame sequences do not.
461
+ """
462
+ import numpy as np
463
+
464
+ self._ensure_backbones()
465
+ if self._rt["gate"].active:
466
+ # The video path runs through the same (hook-carrying) decoder
467
+ # layers as text and images; non-audio inputs must execute with
468
+ # the gate closed so the adapter branch never runs.
469
+ raise RuntimeError("adapter gate is open during a video embed — "
470
+ "non-audio inputs must run with the gate closed")
471
+ try:
472
+ from qwen_vl_utils import process_vision_info
473
+ except ImportError as e: # pragma: no cover - environment dependent
474
+ raise ImportError(
475
+ "video embedding uses the base model's own preprocessing "
476
+ "package: pip install 'qwen-vl-utils>=0.0.14'") from e
477
+
478
+ # Constants from the base's reference implementation.
479
+ video_total_pixels = 10 * 768 * 32 * 32
480
+ default_fps, default_max_frames = 1.0, 64
481
+
482
+ if isinstance(video, (str, os.PathLike)):
483
+ v = str(video)
484
+ content = v if v.startswith(("http://", "https://")) \
485
+ else "file://" + os.path.abspath(v)
486
+ vkw = {"fps": fps or default_fps,
487
+ "max_frames": max_frames or default_max_frames,
488
+ "total_pixels": video_total_pixels}
489
+ else:
490
+ frames = list(video)
491
+ if not frames:
492
+ raise ValueError("empty frame sequence")
493
+ mf = max_frames or default_max_frames
494
+ if len(frames) > mf:
495
+ # Uniform temporal sampling, as in the base's sample_frames.
496
+ idx = np.linspace(0, len(frames) - 1, mf, dtype=int)
497
+ frames = [frames[i] for i in idx]
498
+ content = ["file://" + os.path.abspath(str(f))
499
+ if isinstance(f, (str, os.PathLike)) else f
500
+ for f in frames]
501
+ vkw = {"total_pixels": video_total_pixels}
502
+
503
+ conversation = [{"role": "user",
504
+ "content": [{"type": "video", "video": content, **vkw}]}]
505
+ _, video_inputs, video_kwargs = process_vision_info(
506
+ conversation, image_patch_size=16,
507
+ return_video_metadata=True, return_video_kwargs=True)
508
+ videos, video_metadata = zip(*video_inputs)
509
+ text = _chat(DOC_INSTRUCTION, "<|vision_start|><|video_pad|><|vision_end|>")
510
+ inputs = self._rt["proc"](text=[text], videos=list(videos),
511
+ video_metadata=list(video_metadata),
512
+ do_resize=False, return_tensors="pt",
513
+ **video_kwargs).to(self._device)
514
+ h = self._rt["full"](**inputs).last_hidden_state
515
+ pooled = last_token_pool(h, inputs["attention_mask"])
516
+ return self._finish(pooled, dim)
517
+
518
  # ------------------------------------------------------------- batched
519
  @torch.no_grad()
520
  def embed_text_batch(self, texts, instruction: str = DEFAULT_QUERY_INSTRUCTION,