ids_to_documents.py (6662B)
1 from typing import Any, Iterator 2 import argparse 3 import bisect 4 import collections 5 import io 6 import json 7 import operator 8 import pathlib 9 import struct 10 import sys 11 import urllib.request 12 13 import tqdm 14 import zstandard 15 16 17 CACHE_URL: str = "https://recurrent.network/AEC/2025.cache" 18 HPLT_URL_PREFIX: str = "https://data.hplt-project.org/two" 19 DOWNLOAD_CHUNK_SIZE: int = 8192 20 21 22 def parse_command_argument() -> argparse.Namespace: 23 """ Parse command line arguments. """ 24 parser = argparse.ArgumentParser(description="Transform a UCDP-AEC-ids dataset into UCDP-AEC by replacing HPLT ids by with source documents.") 25 parser.add_argument("output", type=pathlib.Path, help="Path to the output dataset splits.") 26 parser.add_argument("input", type=pathlib.Path, help="Path to the input dataset splits.") 27 parser.add_argument("-C", "--no-cache", action="store_true", help="Do not use the cached subset (WARNING: requires a lot of bandwidth).") 28 parser.add_argument("-J", "--jsonl-only", action="store_true", help="Only transform the jsonl data, do not process the huggingface dataset (do not require the library to be installed)") 29 30 args: argparse.Namespace = parser.parse_args() 31 return args 32 33 34 def process_hf_split(output_path: pathlib.Path, split_path: pathlib.Path, hplt_data: dict[str, str]) -> None: 35 import datasets 36 datasets.load_from_disk(str(split_path))\ 37 .map(input_columns=["source_article"], load_from_cache_file=False, 38 function = lambda source_article: {"source_article": hplt_data[source_article]})\ 39 .save_to_disk(str(output_path)) 40 41 42 def process_jsonl_split(output_path: pathlib.Path, split_path: pathlib.Path, hplt_data: dict[str, str]) -> None: 43 with output_path.open("w") as output_file, split_path.open("r") as input_file: 44 for line in input_file: 45 sample = json.loads(line) 46 sample["source_article"] = hplt_data[sample["source_article"]] 47 print(json.dumps(sample), file=output_file) 48 49 50 def hplt_build_index(url: str) -> list[tuple[int, int]]: 51 with urllib.request.urlopen(url) as file: 52 data: bytes = file.read() 53 54 filename_length: int = data.find(b'\0') 55 header_length: int = filename_length + 5 56 number_of_frames: int = struct.unpack("<I", data[filename_length+1:header_length])[0] 57 58 cumulative_sum: tuple[int, int] = (0, 0) 59 index: list[tuple[int, int]] = [cumulative_sum] 60 for i in range(number_of_frames): 61 start: int = header_length + i*8 62 newlines, compressed_size = struct.unpack("<2I", data[start:start+8]) 63 cumulative_sum = (cumulative_sum[0] + newlines, cumulative_sum[1] + compressed_size) 64 index.append(cumulative_sum) 65 return index 66 67 68 def hplt_get_line(url: str, index: list[tuple[str, str]], line_number: int) -> str: 69 right = bisect.bisect_right(index, line_number, key=operator.itemgetter(0)) 70 left = right - 1 71 while left > 0 and index[left][0] == line_number: 72 left -= 1 73 newlines_offset = index[left][0] 74 75 start = index[left][1] 76 end = index[right][1] 77 78 with urllib.request.urlopen( 79 urllib.request.Request(url, 80 headers={"Range": f"bytes={start}-{end-1}"})) as file: 81 data: bytes = file.read() 82 assert(len(data) == end - start) 83 84 decompressor = zstandard.ZstdDecompressor() 85 reader = decompressor.stream_reader(io.BytesIO(data), read_across_frames=True) 86 return reader.readall().split(b'\n')[line_number - newlines_offset].decode() 87 88 89 def hplt_download_lines(filename: str, line_numbers: list[int]) -> dict[str, str]: 90 indexable_path: str = f"{HPLT_URL_PREFIX}/{filename[:-4]}.1M.zst" 91 index_path: str = f"{indexable_path}.zindex" 92 93 index: list[tuple[int, int]] = hplt_build_index(index_path) 94 95 data: dict[str, str] = {} 96 line_number: int 97 for line_number in set(line_numbers): 98 line: str = hplt_get_line(indexable_path, index, line_number - 1) 99 sample: dict[str, Any] = json.loads(line) 100 data[f"{filename}:{line_number}"] = sample["text"] 101 return data 102 103 104 def get_hplt_data(hplt_ids: list[str]) -> dict[str, str]: 105 files: dict[str, list[int]] = collections.defaultdict(list) 106 for hplt_id in hplt_ids: 107 file, line_number = hplt_id.split(":") 108 files[file].append(int(line_number)) 109 data: dict[str, str] = {} 110 for file, line_numbers in tqdm.tqdm(files.items(), desc="Downloading from HPLT"): 111 data.update(hplt_download_lines(file, line_numbers)) 112 return data 113 114 115 def get_hplt_cache(hplt_ids: list[str]) -> dict[str, str]: 116 cache: dict[str, str] = {} 117 try: 118 with urllib.request.urlopen(CACHE_URL) as cache_file: 119 for line in cache_file: 120 cached_document: dict[str, str] = json.loads(line) 121 cache[cached_document["id"]] = cached_document["text"] 122 except urllib.error.HTTPError as error: 123 if error.code == 404: 124 print("ERROR: The cache data was invalidated, try updating the repository or using the cacheless downloader (--no-cache).", file=sys.stderr) 125 sys.exit(1) 126 else: 127 raise 128 return cache 129 130 131 def main(output_path: pathlib.Path, input_path: pathlib.Path, cacheless: bool, jsonl_only: bool) -> None: 132 if not jsonl_only: 133 import datasets 134 135 print("Reading IDs…", flush=True, end=" ") 136 hplt_ids: list[str] = [] 137 for split_path in input_path.iterdir(): 138 if split_path.suffix == ".jsonl": 139 with split_path.open("r") as split_file: 140 for line in split_file: 141 sample: dict[str, Any] = json.loads(line) 142 hplt_ids.append(sample["source_article"]) 143 elif not jsonl_only and split_path.is_dir(): 144 split = datasets.load_from_disk(split_path) 145 hplt_ids.extend(split["source_article"]) 146 print("done") 147 148 output_path.mkdir(parents=True) 149 print("Downloading data…", flush=True, end=" ") 150 hplt_data: dict[str, str] = get_hplt_data(hplt_ids) if cacheless else get_hplt_cache(hplt_ids) 151 print("done") 152 153 print("Transforming dataset…", flush=True, end=" ") 154 for split_path in input_path.iterdir(): 155 if split_path.is_dir() and not jsonl_only: 156 process_hf_split(output_path / split_path.name, split_path, hplt_data) 157 elif split_path.suffix == ".jsonl": 158 process_jsonl_split(output_path / split_path.name, split_path, hplt_data) 159 print("done") 160 161 162 if __name__ == "__main__": 163 args: argparse.Namespace = parse_command_argument() 164 main(output_path=args.output, input_path=args.input, cacheless=args.no_cache, jsonl_only=args.jsonl_only)