|
| 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() |
0 commit comments