Shaoguang Wang · Weiyu Guo · Ziyang Chen · Xuming Hu · Hui Xiong
The Hong Kong University of Science and Technology (Guangzhou)
Keyframe selection as per-question modality routing — an LLM decides where each query should look.
Three parallel expert streams are dynamically fused by a query-aware gate; the top-K timestamped frames and subtitles pass to a downstream MLLM.
- 🧩 Training-free — off-the-shelf experts, no fine-tuning.
- 🎛️ Query-modulated — an LLM weights three experts per question.
- 🔀 Look and listen — dynamically fuses visual and subtitle cues.
- 📈 State-of-the-art — up to +6.4 on Video-MME Long, across backbones.
The three expert streams, their paper names, and the off-the-shelf model behind each:
| Code name | Paper name | Expert model | Gating weight |
|---|---|---|---|
score_logic |
Visual Grounding | YOLO-World | w_logic |
score_relevance |
Global Matching | BLIP (image-text matching) | w_relevance |
score_narrative |
Contextual Alignment | Sentence-BERT (all-mpnet-base-v2) |
w_narrative |
How the gate works (4 steps)
- An LLM (GPT-4o by default) reads the query and emits a weight triple
[w_logic, w_relevance, w_narrative]that sums to 1. - Each stream is normalized with a unified Min-Max + masked temperature Softmax pipeline (τ = 0.5).
- The streams are fused into a single
score_fusiondistribution, and its top-K seconds become keyframes. - Each selected frame is anchored with its
[Image at H:MM:SS]timestamp and (optionally) subtitles, then passed to a downstream MLLM.
Offline demo — no videos, models, or API key required. Fuses two sample records (all three streams pre-filled) with the static equal strategy, to confirm the code runs and show the input schema:
pip install "numpy<2" tqdm
python fuse_scores.py --input_json Data/demo_raw_scores.json \
--output_dir ./demo_out --strategy equal --temperature 0.5
# -> ./demo_out/demo_raw_scores_fusion_equal_T0.5.json (adds a score_fusion field)Data/demo_raw_scores.json documents, by example, the score_logic / score_relevance / score_narrative arrays each Stage-1 producer must attach per record.
Full run — once .env, Datasets/, and the Stage-1 score streams are ready, run_example.sh runs Stage 2 → Stage 3 on LongVideoBench with GPT-4o (K = 8):
bash run_example.sh
# override any knob: DATASET=videomme BACKEND=qwen3_32b K=32 bash run_example.shThe four stages — prepare_data.py (Stage 0) → score producers (Stage 1) → fuse_scores.py (Stage 2) → run_qa.py (Stage 3) — are detailed below.
conda create -n qgate python=3.12 -y
conda activate qgate
pip install -r requirements.txtNumPy must stay
< 2.0— NumPy 2.x breaks the OpenCV imports used here.
The Stage-1 producers have heavier, conflicting dependencies — keep each in its own environment, not the main qgate one (they communicate only through per-record JSON files).
Producer environments (BLIP · YOLO-World)
- Relevance (BLIP) uses LAVIS, which pins older
tokenizers/transformers/timmbuilds that do not install on Python 3.12 (thetokenizerswheel needs a Rust toolchain there) and would downgrade the fusion/QA packages. Use a separate Python 3.9 environment:Runconda create -n qgate-relevance python=3.9 -y conda activate qgate-relevance pip install salesforce-lavis
calculate_relevance_score.pyfrom this env; run everything else fromqgate. - Grounding (
score_logic) needs the external YOLO-World repo plusmmdet/mmengine/supervision/decord/seaborn/datasetsand a checkpoint, in its own env for a matchedmmcvbuild. Clone YOLO-World intoscore_producers/grounding/YOLO-World/(git-ignored).
Development (lint & tests)
The CI (.github/workflows/ci.yml) lints with ruff (pyflakes errors), byte-compiles every source, runs the unit tests, and executes the offline demo. To reproduce locally:
pip install -e ".[dev]" # pytest + ruff
ruff check --select F qgate fuse_scores.py run_qa.py prepare_data.py score_producers tests
pytest tests/ -q # covers the fusion normalization + keyframe selection primitivescp .env.example .envOPENAI_API_KEY— required for the GPT-4o gater, the GPT-4o QA backend, and the grounding producer's entity extraction (Stage 1,score_logic).OPENAI_BASE_URL— must be the full chat-completions endpoint (the code POSTs to it directly). Defaulthttps://api.openai.com/v1/chat/completions; for a proxy give the complete URL, e.g.https://your-proxy/v1/chat/completions— nothttps://your-proxy/v1.
Scripts read credentials from the environment and do not auto-load
.env— before a manual run,set -a; . ./.env; set +a.run_example.shsources.envfor you.
Local open-source backends (Qwen / InternVL via vLLM)
Serve any OpenAI-compatible server (e.g. vLLM) and point the matching env var at it:
QWEN3_32B_BASE_URL→ backendqwen3_32bINTERNVL3_BASE_URL→ backendinternvl3_8bINTERNVL26B_BASE_URL→ backendinternvl_26b
Every stage reads a unified QA-index JSON. Data/ ships one sample record per benchmark (longvideobench / videomme) documenting the format; the full indices are built with prepare_data.py:
# LongVideoBench — official validation JSON + local videos
python prepare_data.py --dataset longvideobench \
--input_file /path/to/lvb_val.json \
--video_root ./Datasets/LVBench/videos --output_dir ./Data
# Video-MME — question metadata from HuggingFace hub (needs `pip install datasets`); videos local
python prepare_data.py --dataset videomme \
--video_root ./Datasets/Video-MME/videos --output_dir ./DataSubtitle roots are overridable via LVB_SUBTITLE_ROOT / VIDEOMME_SUBTITLE_ROOT. Note the layout: LongVideoBench uses subtitles/<id>_en.json and Video-MME uses a single subtitle/<id>.srt.
Dataset layout & record schema
Datasets/
├── LVBench/
│ ├── videos/ <video_id>.mp4
│ └── subtitles/ <video_id>_en.json # LongVideoBench subtitle format
└── Video-MME/
├── videos/ <video_id>.mp4
└── subtitle/ <video_id>.srt
| Field | Type | Notes |
|---|---|---|
video_id |
str | unique clip id |
video_path |
str | path to the .mp4 (rooted at --video_root) |
question |
str | the question text |
options |
str | newline-joined, letter-prefixed: "A) ...\nB) ...\n..." |
answer |
str | correct option letter, e.g. "B" |
duration_group |
str / int | length bucket — LVB uses seconds (15/60/600/3600); Video-MME uses short/medium/long |
position |
list | optional ground-truth timestamp hints (may be empty) |
Compute the three streams in dependency order — score_logic → score_relevance → score_narrative. The score_relevance and score_narrative producers each read the previous JSON via --input_json and write an auto-suffixed file (e.g. *_blip_relevance.json, *_all-mpnet-base-v2_narrative.json) that you pass to the next stage; the score_logic (grounding) scripts take --obj_path/--kfs_path instead (see below).
| Stream | Producer | Reproducible? |
|---|---|---|
score_logic (Visual Grounding) |
score_producers/grounding/ |
mmdet/mmengine/supervision/datasets |
score_relevance (Global Matching) |
score_producers/relevance_blip/ |
✅ once LAVIS is installed (Python 3.9 env) |
score_narrative (Contextual Alignment) |
score_producers/narrative_sbert/ |
✅ in-repo (LVB / Video-MME only) |
Field handoff: the grounding keyframe script writes raw scores as
score_list, but fusion readsscore_logic. Before the relevance step, merge each record'sscore_listinto ascore_logicfield onData/<dataset>_processed.jsonand feed that merged file onward.
Per-producer commands
1. score_logic (Visual Grounding). Run from inside the producer dir so its local lib//utils/ imports resolve; it re-loads the raw benchmark annotations itself. Requires OPENAI_API_KEY — Step 1 uses GPT-4o to extract the query's key objects, and the script exits immediately without it.
cd score_producers/grounding
export OPENAI_API_KEY=... # or: set -a; . ../../.env; set +a
# Step 1 — propose grounding objects per question
python scripts/get_grounding_objects.py --dataset longvideobench \
--video_root /path/to/LVBench/videos \
--anno_path /path/to/LVBench/lvb_val.json --obj_path ./output/lvb_obj.json
# Step 2 — YOLO-World keyframe search -> per-second raw grounding scores
python scripts/get_key_frames.py \
--obj_path ./output/lvb_obj.json --kfs_path ./output/lvb_logic.jsonThe --config_path/--checkpoint_path/--video_root/--anno_path defaults assume the external YOLO-World layout; repoint them at your local paths.
2. score_relevance (BLIP image-text matching). Run from the dedicated qgate-relevance (Python 3.9) env (see Installation). --model_name blip loads the LAVIS blip_image_text_matching model (ViT-L); the score is its ITM probability.
conda activate qgate-relevance
# <merged.json> carries score_logic (see the Field handoff note above), not the raw score_list.
python score_producers/relevance_blip/calculate_relevance_score.py \
--input_json <merged.json> --output_dir ./Score/relevance \
--dataset longvideobench --model_name blip3. score_narrative (Sentence-BERT). Accepts --dataset longvideobench|videomme; its output carries all three streams and is the input to Stage 2.
python score_producers/narrative_sbert/calculate_narrative_score.py \
--input_json <relevance_scores.json> --output_dir ./Score/narrative \
--dataset longvideobenchrun_example.sh (see Quick start) reads ./Score/<dataset>_raw_scores.json — the Stage-1 output (the narrative producer's file already carries all three streams), which the script does not create. Point RAW_SCORES at that file, or copy it into place:
cp ./Score/narrative/<dataset>_processed_blip_relevance_all-mpnet-base-v2_narrative.json \
./Score/<dataset>_raw_scores.jsonTo run the stages manually, given an input JSON with all three raw streams:
# Stage 2 — query-modulated gating + fusion (GPT-4o gater, τ = 0.5)
python fuse_scores.py \
--input_json <narrative_scores.json> \
--output_dir ./Score/fusion \
--strategy llm --backend gpt4 --temperature 0.5
# Stage 3 — keyframe selection + downstream QA (LongVideoBench, GPT-4o, K = 8)
python run_qa.py \
--input_json <fused_scores.json> \
--dataset longvideobench --output_dir ./results \
--backend gpt4 --frame_num 8 \
--score_key score_fusion \
--use_subtitles --filter_with_subtitlesFlags & ablations
- Gating (
fuse_scores.py --strategy, required):llm(the paper's Query-Modulated Gating) orequal(static 1/3 weights, used by the offline demo). The LLM gater sees the candidate options, the paper's main setting. - Backends (
--backend):gpt4,qwen3_32b,internvl3_8b,internvl_26b— see Configuration.
Q-Gate downstream-QA accuracy (%) on LongVideoBench and Video-MME (paper, Table 1), across MLLM backbones and video-length splits.
| Backbone | K | LVB Long | LVB Med | LVB Short | MME Long | MME Med | MME Short |
|---|---|---|---|---|---|---|---|
| GPT-4o | 8 | 50.71 | 56.55 | 65.41 | 54.78 | 59.68 | 67.16 |
| Qwen3-VL | 32 | 59.40 | 63.11 | 70.59 | 61.19 | 66.13 | 79.41 |
Downstream-QA accuracy (%); higher is better. K = number of selected keyframes. The Qwen3-VL backbone (Qwen3-VL-32B-Instruct) is selected with --backend qwen3_32b.
See the paper for the full comparison against prior methods, ablations, and efficiency analysis.
@article{wang2026focus,
title={Where to Focus: Query-Modulated Multimodal Keyframe Selection for Long Video Understanding},
author={Wang, Shaoguang and Guo, Weiyu and Chen, Ziyang and Hu, Xuming and Xiong, Hui},
journal={arXiv preprint arXiv:2604.17422},
year={2026}
}Accepted to ACM MM 2026; this entry will be updated to the official proceedings citation once available.
Q-Gate builds on excellent open-source work, including YOLO-World, BLIP / LAVIS, Sentence-Transformers, and the AKS and T* keyframe-selection methods. We thank the authors of LongVideoBench and Video-MME for their benchmarks.
Released under the MIT License. The grounding producer under
score_producers/grounding/ is derived from the MIT-licensed
T*; see
THIRD_PARTY_LICENSES for the reproduced notices.