UCDP-AEC

Models and data for the UCDP Abstractive Event analysis Corpus paper
git clone https://esimon.eu/repos/UCDP-AEC.git
Log | Files | Refs | README | LICENSE

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)