Skip to content

Commit 3fb3b5b

Browse files
zhaowei-wang-nlpcursoragentkennymckormick
authored
[Benchmark] Add support for MemLens (#1594)
* [Benchmark] Add support for MemLens Co-authored-by: Cursor <cursoragent@cursor.com> * Fix lint errors for MemLens benchmark --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: kenny.alan <kenny.alan@bytedance.com> Co-authored-by: Haodong Duan <dhd@pku.edu.cn>
1 parent 944ee12 commit 3fb3b5b

7 files changed

Lines changed: 1411 additions & 11 deletions

File tree

scripts/build_memlens_tsv.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
#!/usr/bin/env python3
2+
"""Convert MemLens dataset_*.json files to VLMEvalKit TSV format.
3+
4+
Usage:
5+
python scripts/build_memlens_tsv.py \
6+
--output_root /path/to/output_tsv_dir
7+
8+
By default, the source JSON files are downloaded from Hugging Face
9+
(`xiyuRenBill/MEMLENS`). Pass --data-root to reuse local dataset_*.json files.
10+
Image paths stored in the TSV are the same relative paths used by MemLens.
11+
"""
12+
import argparse
13+
import csv
14+
import json
15+
import os
16+
17+
csv.field_size_limit(1 << 30)
18+
19+
REPO_ID = 'xiyuRenBill/MEMLENS'
20+
INSTRUCTION_DEFAULT = 'Directly output the answer with no extra output.'
21+
22+
USER_TEMPLATE = (
23+
'Provide answers based on the given conversation history. '
24+
'If the question cannot be answered based on the given conversation, '
25+
'respond with "Insufficient information".\n'
26+
'Conversation:\n{context}\n\n'
27+
'{instruction}\n'
28+
'Question Date: {question_date}\n'
29+
'Question: {question}\n'
30+
)
31+
32+
DATASET_SPLITS = {
33+
'MemLens_32K': 'dataset_32k.json',
34+
'MemLens_64K': 'dataset_64k.json',
35+
'MemLens_128K': 'dataset_128k.json',
36+
'MemLens_256K': 'dataset_256k.json',
37+
}
38+
39+
40+
def hf_download(filename, download_dir, token=None):
41+
try:
42+
from huggingface_hub import hf_hub_download
43+
except ImportError as err:
44+
raise ImportError(
45+
'huggingface_hub is required to download MemLens assets. '
46+
'Install it with `pip install huggingface_hub`, or pass --data-root.'
47+
) from err
48+
49+
return hf_hub_download(
50+
repo_id=REPO_ID,
51+
filename=filename,
52+
repo_type='dataset',
53+
local_dir=download_dir,
54+
token=token,
55+
)
56+
57+
58+
def prepare_json_file(filename, data_root, download_dir, token=None, skip_existing=False):
59+
if data_root:
60+
path = os.path.join(data_root, filename)
61+
if not os.path.exists(path):
62+
raise FileNotFoundError(f'Missing MemLens source JSON: {path}')
63+
return path
64+
65+
os.makedirs(download_dir, exist_ok=True)
66+
local_path = os.path.join(download_dir, filename)
67+
if skip_existing and os.path.exists(local_path):
68+
return local_path
69+
print(f'downloading {filename} from {REPO_ID} ...')
70+
return hf_download(filename, download_dir, token=token)
71+
72+
73+
def extract_image_path(img_info):
74+
"""Extract the relative image path using the same keys as MemLens utils.py."""
75+
if isinstance(img_info, str):
76+
return img_info
77+
if not isinstance(img_info, dict):
78+
return ''
79+
img_path = (
80+
img_info.get('file')
81+
or img_info.get('path')
82+
or img_info.get('file_path')
83+
or img_info.get('img_file')
84+
)
85+
if isinstance(img_path, list):
86+
img_path = img_path[0] if img_path else ''
87+
return img_path or ''
88+
89+
90+
def build_context(item):
91+
"""Flatten haystack_sessions into (context_text, image_path_list).
92+
93+
context_text has <image> tokens in-place; image_path_list contains the
94+
corresponding relative paths (under release_images/) in order.
95+
"""
96+
parts = []
97+
images = []
98+
99+
sessions = item.get('haystack_sessions', [])
100+
dates = item.get('haystack_dates', [])
101+
102+
for i, session in enumerate(sessions, 1):
103+
if isinstance(session, dict):
104+
date_str = session.get('date', 'unknown')
105+
turns = session.get('session', [])
106+
else:
107+
date_str = dates[i - 1] if i - 1 < len(dates) else 'unknown'
108+
turns = session
109+
110+
parts.append(f'\n=== Session {i} (Date: {date_str}) ===\n')
111+
112+
for turn in turns:
113+
role = '[User]: ' if turn.get('role') == 'user' else '[Assistant]: '
114+
parts.append(role)
115+
116+
text = turn.get('content', '')
117+
turn_images = turn.get('images', [])
118+
119+
resolved_paths = [p for p in (extract_image_path(img) for img in turn_images) if p]
120+
121+
if resolved_paths:
122+
if text.count('<image>') > 0:
123+
# <image> tokens already embedded in content — keep in place
124+
images.extend(resolved_paths)
125+
else:
126+
# No tokens in text — prepend one token per image
127+
for path in resolved_paths:
128+
parts.append('<image> ')
129+
images.append(path)
130+
else:
131+
text = text.replace('<image>', '')
132+
133+
text = text.strip()
134+
if text:
135+
parts.append(text)
136+
parts.append('\n')
137+
138+
return ''.join(parts), images
139+
140+
141+
def build_row(item, row_id):
142+
context, image_list = build_context(item)
143+
question_text = USER_TEMPLATE.format(
144+
context=context,
145+
instruction=INSTRUCTION_DEFAULT,
146+
question_date=item.get('question_date', 'unknown'),
147+
question=item.get('question', ''),
148+
)
149+
return {
150+
'index': row_id,
151+
'question_id': item.get('question_id', ''),
152+
'question': question_text,
153+
'answer': item.get('answer', ''),
154+
'image_path': json.dumps(image_list, ensure_ascii=False),
155+
'question_type': item.get('question_type', ''),
156+
'question_date': item.get('question_date', ''),
157+
}
158+
159+
160+
def convert(input_json, output_tsv, dataset_name):
161+
with open(input_json, 'r', encoding='utf-8') as f:
162+
raw = json.load(f)
163+
data = raw.get('data', raw) if isinstance(raw, dict) else raw
164+
165+
fieldnames = ['index', 'question_id', 'question', 'answer',
166+
'image_path', 'question_type', 'question_date']
167+
168+
os.makedirs(os.path.dirname(output_tsv) or '.', exist_ok=True)
169+
with open(output_tsv, 'w', newline='', encoding='utf-8') as f:
170+
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter='\t')
171+
writer.writeheader()
172+
for i, item in enumerate(data):
173+
writer.writerow(build_row(item, i))
174+
175+
print(f'[{dataset_name}] wrote {len(data)} rows -> {output_tsv}')
176+
177+
178+
def main():
179+
parser = argparse.ArgumentParser(description='Build VLMEvalKit TSVs for MemLens.')
180+
parser.add_argument(
181+
'--data-root',
182+
default=None,
183+
help='Optional directory containing dataset_32k.json etc. '
184+
'If omitted, files are downloaded from Hugging Face.',
185+
)
186+
parser.add_argument(
187+
'--download-dir',
188+
default=None,
189+
help='Where to store downloaded JSON files. Defaults to <output-root>/downloads.',
190+
)
191+
parser.add_argument(
192+
'--output-root',
193+
required=True,
194+
help='Directory where MemLens_*.tsv files will be written.',
195+
)
196+
parser.add_argument(
197+
'--token',
198+
default=os.environ.get('HF_TOKEN'),
199+
help='Optional Hugging Face token. Defaults to HF_TOKEN.',
200+
)
201+
parser.add_argument(
202+
'--skip-existing',
203+
action='store_true',
204+
help='Reuse downloaded JSON files when present.',
205+
)
206+
parser.add_argument('--splits', nargs='+', default=list(DATASET_SPLITS.keys()),
207+
choices=list(DATASET_SPLITS.keys()),
208+
help='Which splits to convert (default: all four).')
209+
args = parser.parse_args()
210+
211+
download_dir = args.download_dir or os.path.join(args.output_root, 'downloads')
212+
for split in args.splits:
213+
json_file = DATASET_SPLITS[split]
214+
input_path = prepare_json_file(
215+
json_file,
216+
data_root=args.data_root,
217+
download_dir=download_dir,
218+
token=args.token,
219+
skip_existing=args.skip_existing,
220+
)
221+
output_path = os.path.join(args.output_root, f'{split}.tsv')
222+
convert(input_path, output_path, split)
223+
224+
225+
if __name__ == '__main__':
226+
main()

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ per-file-ignores =
55
run.py: E402
66
vlmeval/vlm/__init__.py: F401, E402
77
vlmeval/dataset/video_dataset_config.py: F405, F403
8+
vlmeval/dataset/utils/memlens_utils.py: E501
89

910
[yapf]
1011
based_on_style = pep8

vlmeval/dataset/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
from .medqbench_paired_description import MedqbenchPairedDescriptionDataset
7575
# Add by EASI team
7676
from .megabench import MEGABench
77+
from .memlens import MemLens
7778
from .miabench import MIABench
7879
from .mindcubebench import MindCubeBench
7980
from .mlvu import MLVU, MLVU_MCQ, MLVU_OpenEnded
@@ -278,7 +279,7 @@ def evaluate(self, eval_file, **judge_kwargs):
278279
IMAGE_DATASET = [
279280
ImageCaptionDataset, ImageYORNDataset, ImageMCQDataset, ImageVQADataset,
280281
MathVision, LENS, MMMUDataset, OCRBench, MathVista, LLaVABench, LLaVABench_KO, VGRPBench, MMVet, # noqa: E501
281-
MTVQADataset, TableVQABench, MMLongBench, MMLongBenchDoc, VCRDataset, MMDUDataset, DUDE,
282+
MTVQADataset, TableVQABench, MMLongBench, MemLens, MMLongBenchDoc, VCRDataset, MMDUDataset, DUDE,
282283
SlideVQA, MUIRDataset, CCOCRDataset, GMAIMMBenchDataset, MMERealWorld,
283284
HRBenchDataset, CRPE, MathVerse, NaturalBenchDataset, MIABench,
284285
OlympiadBench, SeePhys, WildVision, MMMath, QSpatial, Dynamath, GSM8KVDataset, MMGenBench, VizWiz, # noqa: E501

0 commit comments

Comments
 (0)