Buckets:
| """Convert the four released ThinkMorph datasets to the BAGEL SFT contract.""" | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import io | |
| import json | |
| import os | |
| import shutil | |
| import uuid | |
| from pathlib import Path | |
| import pyarrow as pa | |
| import pyarrow.parquet as pq | |
| from PIL import Image | |
| TASKS = { | |
| "Chart_Refocus": ("chart_refocus", 6000, 2), | |
| "Jigsaw_Assembly": ("jigsaw_assembly", 6000, 3), | |
| "Spatial_Navigation": ("spatial_navigation", 6000, 1), | |
| "Visual_Search": ("visual_search", 6990, 3), | |
| } | |
| INTERLEAVED_THINK_SYSTEM_PROMPT = """ | |
| Let's think step by step to answer the question. For text-based thinking, enclose the process within <think> </think>, e.g. <think> thinking process here </think>. For visual thinking, enclose the content within <image_start> </image_end>, e.g. <image_start> thinking image here </image_end>. Finally conclude with the final answer wrapped in <answer></answer> tags, i.e.<answer> answer here </answer>. | |
| """ | |
| SOURCE_COLUMNS = ( | |
| "pid", | |
| "question", | |
| "answer", | |
| "problem_image_0", | |
| "resoning_thought_0", | |
| "reasoning_image_0", | |
| "resoning_thought_1", | |
| "task", | |
| ) | |
| OUTPUT_SCHEMA = pa.schema( | |
| [ | |
| ("image_list", pa.list_(pa.binary())), | |
| ("instruction_list", pa.list_(pa.string())), | |
| ("output_text_list", pa.list_(pa.string())), | |
| ] | |
| ) | |
| def _sha256(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def _image_bytes(value: object, source_file: Path) -> bytes: | |
| if isinstance(value, dict): | |
| payload = value.get("bytes") | |
| if payload is not None: | |
| return bytes(payload) | |
| image_path = value.get("path") | |
| if image_path: | |
| path = Path(image_path) | |
| if not path.is_absolute(): | |
| path = source_file.parent / path | |
| return path.read_bytes() | |
| if isinstance(value, (bytes, bytearray, memoryview)): | |
| return bytes(value) | |
| raise ValueError(f"image payload is missing in {source_file}") | |
| def _image_png(value: object, source_file: Path) -> bytes: | |
| with Image.open(io.BytesIO(_image_bytes(value, source_file))) as image: | |
| image.load() | |
| buffer = io.BytesIO() | |
| image.save(buffer, format="PNG") | |
| return buffer.getvalue() | |
| def convert_row(row: dict, source_file: Path) -> dict: | |
| missing = [name for name in SOURCE_COLUMNS if row.get(name) is None] | |
| if missing: | |
| raise ValueError(f"missing {missing} in {source_file}") | |
| thought_0 = str(row["resoning_thought_0"]) | |
| thought_1 = str(row["resoning_thought_1"]) | |
| answer = str(row["answer"]) | |
| return { | |
| "image_list": [ | |
| _image_png(row["problem_image_0"], source_file), | |
| _image_png(row["reasoning_image_0"], source_file), | |
| ], | |
| "instruction_list": [ | |
| INTERLEAVED_THINK_SYSTEM_PROMPT + str(row["question"]) | |
| ], | |
| "output_text_list": [ | |
| f"<think>{thought_0}</think><image_start>", | |
| f"<image_end><think>{thought_1}</think><answer>{answer}</answer>", | |
| ], | |
| } | |
| def _write_chunk(rows: list[dict], path: Path, row_group_size: int) -> None: | |
| pq.write_table( | |
| pa.Table.from_pylist(rows, schema=OUTPUT_SCHEMA), | |
| path, | |
| compression="zstd", | |
| row_group_size=row_group_size, | |
| ) | |
| def convert_corpus( | |
| source_root: Path, | |
| output_root: Path, | |
| row_group_size: int, | |
| rows_per_file: int, | |
| ) -> dict: | |
| if output_root.exists(): | |
| raise FileExistsError(output_root) | |
| output_root.parent.mkdir(parents=True, exist_ok=True) | |
| staging = output_root.parent / f".{output_root.name}.tmp-{uuid.uuid4().hex}" | |
| staging.mkdir() | |
| source_manifest = [] | |
| output_manifest = [] | |
| parquet_info = {} | |
| task_counts = {} | |
| try: | |
| for task, (slug, expected_rows, expected_files) in TASKS.items(): | |
| sources = sorted((source_root / task / "data").glob("*.parquet")) | |
| if len(sources) != expected_files: | |
| raise ValueError( | |
| f"{task} has {len(sources)} shards, expected {expected_files}" | |
| ) | |
| destination_dir = staging / slug | |
| destination_dir.mkdir() | |
| task_count = 0 | |
| output_count = 0 | |
| pending = [] | |
| for source in sources: | |
| parquet = pq.ParquetFile(source) | |
| source_manifest.append( | |
| { | |
| "task": task, | |
| "path": str(source), | |
| "sha256": _sha256(source), | |
| "rows": parquet.metadata.num_rows, | |
| } | |
| ) | |
| for batch in parquet.iter_batches( | |
| batch_size=rows_per_file, | |
| columns=list(SOURCE_COLUMNS), | |
| ): | |
| rows = batch.to_pylist() | |
| mismatched = [row["task"] for row in rows if row["task"] != task] | |
| if mismatched: | |
| raise ValueError(f"task mismatch in {source}: {mismatched[0]}") | |
| pending.extend(convert_row(row, source) for row in rows) | |
| while len(pending) >= rows_per_file: | |
| chunk, pending = pending[:rows_per_file], pending[rows_per_file:] | |
| destination = ( | |
| destination_dir / f"chunk_{output_count:04d}.parquet" | |
| ) | |
| _write_chunk(chunk, destination, row_group_size) | |
| output_count += 1 | |
| task_count += len(chunk) | |
| if pending: | |
| destination = destination_dir / f"chunk_{output_count:04d}.parquet" | |
| _write_chunk(pending, destination, row_group_size) | |
| output_count += 1 | |
| task_count += len(pending) | |
| if task_count != expected_rows: | |
| raise ValueError(f"{task} has {task_count} rows, expected {expected_rows}") | |
| task_counts[task] = task_count | |
| for destination in sorted(destination_dir.glob("*.parquet")): | |
| parquet = pq.ParquetFile(destination) | |
| final_path = output_root / slug / destination.name | |
| parquet_info[str(final_path)] = { | |
| "num_row_groups": parquet.num_row_groups, | |
| "num_rows": parquet.metadata.num_rows, | |
| } | |
| output_manifest.append( | |
| { | |
| "task": task, | |
| "path": str(final_path), | |
| "sha256": _sha256(destination), | |
| "rows": parquet.metadata.num_rows, | |
| } | |
| ) | |
| print( | |
| f"CONVERT_TASK_COMPLETE task={task} rows={task_count} " | |
| f"files={output_count}", | |
| flush=True, | |
| ) | |
| (staging / "parquet_info.json").write_text( | |
| json.dumps(parquet_info, indent=2, sort_keys=True) + "\n", | |
| encoding="utf-8", | |
| ) | |
| summary = { | |
| "schema": "thinkmorph_bagel_sft_v1", | |
| "source_root": str(source_root), | |
| "rows": sum(task_counts.values()), | |
| "tasks": task_counts, | |
| "row_group_size": row_group_size, | |
| "rows_per_file": rows_per_file, | |
| "source_files": source_manifest, | |
| "output_files": output_manifest, | |
| } | |
| (staging / "manifest.json").write_text( | |
| json.dumps(summary, indent=2, sort_keys=True) + "\n", | |
| encoding="utf-8", | |
| ) | |
| (staging / "READY").write_text("complete\n", encoding="utf-8") | |
| os.replace(staging, output_root) | |
| return summary | |
| except BaseException: | |
| shutil.rmtree(staging, ignore_errors=True) | |
| raise | |
| def main() -> None: | |
| project_root = Path( | |
| os.environ.get( | |
| "SPATIAL_MMCOT_PROJECT_ROOT", | |
| Path(__file__).resolve().parents[3], | |
| ) | |
| ) | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument( | |
| "--source-root", | |
| type=Path, | |
| default=project_root / "data/ThinkMorph", | |
| ) | |
| parser.add_argument( | |
| "--output-root", | |
| type=Path, | |
| default=project_root / "data/ThinkMorph/Bagel-SFT", | |
| ) | |
| parser.add_argument("--row-group-size", type=int, default=10) | |
| parser.add_argument("--rows-per-file", type=int, default=100) | |
| args = parser.parse_args() | |
| if ( | |
| args.row_group_size <= 0 | |
| or args.rows_per_file <= 0 | |
| or args.rows_per_file % args.row_group_size | |
| ): | |
| raise ValueError("rows-per-file must be a positive multiple of row-group-size") | |
| summary = convert_corpus( | |
| args.source_root.resolve(), | |
| args.output_root.resolve(), | |
| args.row_group_size, | |
| args.rows_per_file, | |
| ) | |
| print(json.dumps({key: value for key, value in summary.items() if not key.endswith("_files")})) | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 9.15 kB
- Xet hash:
- e4b3b31772b6836651e56863d0230bc7ee185be2250884c2f81aea6bf774fbdc
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.