Utils¶
Filter Utility¶
Utility for filtering PPA full-text corpus to work with a subset of pages.
- Currently supports the following types of filtering:
List of PPA work ids (as a text file, id-per-line)
CSV file specifying work pages (by digital page number) (csv, page-per-line)
Filtering by key-value pair for either inclusion or exclusion
These filtering options can be combined, generally as a logical AND. Pages filtered by work ids or page numbers will be further filtered by the key-value logic. In cases where both work- and page-level filtering occurs, works not specified in the page filtering are included in full. Works that are specified in both will be limited to the pages specified in page-level filtering.
Filter methods can be run via command-line or python code. Filtering takes a jsonl file
(compressed or not) as input, and will produce a jsonl file (compressed or not) as output.
The input and output filenames can use any extension supported by orjsonl, with or
without compression (e.g. .jsonl, .jsonl.gz, .jsonl.bz2).
Example command line usages:
corppa-filter path/to/ppa_pages.jsonl output/ppa_subset_pages.jsonl --idfile my_ids.txt
corppa-filter path/to/ppa_pages.jsonl output/ppa_subset_pages.jsonl --pg-file pages.csv --include key=value
Path Utilities¶
General-purpose methods for working with paths, PPA identifiers, and directories
- corppa.utils.path_utils.decode_htid(encoded_htid: str) str¶
Return original HathiTrust volume identifier from encoded version:
[library id].[encoded volume id]
Specifically, the volume-portion of the id undergoes the following character replacements:
"+" --> ":", "=" --> "/", "," --> "."
- corppa.utils.path_utils.encode_htid(htid: str) str¶
Returns the “clean” version of a HathiTrust volume identifier with the form:
[library id].[volume id]
Specifically, the volume-portion of the id undergoes the following character replacements:
":" --> "+", "/" --> "=", "." --> ","
- corppa.utils.path_utils.find_relative_paths(base_dir: Path, exts: Iterable[str], follow_symlinks: bool = True, group_by_dir: bool = False) Iterator[Path] | Iterator[tuple[Path, list[Path]]]¶
This method finds files anywhere under the specified base directory that match any of the specified file extensions (case insensitive), and returns a generator of path objects with a path relative to the base directory. File extensions should include the leading period, i.e.
[".jpg", ".tiff"]rather than["jpg", "tiff"].For example, given a base directory
a/b/c/images, an extension list of.jpg, and files nested at different levels in the hierarchya/b/c/images/alpha.jpg,a/b/c/images/d/beta.jpg:a/b/c/images |-- alpha.jpg +-- d |-- beta.jpg
The result will include the two items:
alpha.jpgandd/beta.jpgWhen
group_by_dirisTrue, resulting files will be returned grouped by the parent directory. The return result is a tuple of a singlepathlib.Pathobject for the directory and a list ofpathlib.Pathobjects for the files in that directory that match the specified extensions. Given a hierarchy like this:images/vol-a/ |-- alpha.jpg |-- beta.jpg
the method would return
(vol-a, [alpha.jpg, beta.jpg]).
- corppa.utils.path_utils.get_gale_image_name(vol_id: str, page_num: int) str¶
Return the Gale/ECCO page image filename for the given volume id and page number, e.g.
CB0127060085_00050.TIF. Gale image filenames use a zero-padded page number with a trailing0.
- corppa.utils.path_utils.get_image_relpath(work_id: str, page_num: int) Path¶
Get the (relative) image path for specified PPA work page
- corppa.utils.path_utils.get_page_number(pagefile: Path) str¶
Extract and return the page number from the filename for page-level content (e.g., image or text). Returns the page number as a string with leading zeros. (Note: logic is currently specific to Gale/ECCO file naming conventions.)
- corppa.utils.path_utils.get_ppa_source(vol_id: str) str¶
For a given volume id, return the corresponding source. Assume: * Gale volume ids begin with
"CW0"or"CB0"* EEBO-TCP volume ids begin withA* Hathitrust volume ids contain a"."
- corppa.utils.path_utils.get_stub_dir(source: str, vol_id: str) Path¶
Returns the stub directory path (
pathlib.Path) for the specified volume (vol_id)For Gale, the path is formed from every third number (excluding the leading 0) of the volume identifier.
Ex. CB0127060085 --> 100
For HathiTrust, we use the Stubbytree directory specification created by HTRC. The path is composed of two directories: (1) the library portion of the volume identifier and (2) every third character of the encoded volume identifier.
Ex. mdp.39015003633594 --> mdp/31039
- corppa.utils.path_utils.get_vol_dir(vol_id: str) Path¶
Returns the volume directory (
pathlib.Path) for the specified volume (vol_id)
Generate PPA Page Set¶
Utility for generating a PPA page set.
This method takes three inputs: (1) an input csv, (2) an output csv, and (3) the size of the page set.
- The input CSV file must have the following fields:
work_id: PPA work id
page_start: Starting index for page range being considered for this work
page_end: Ending index for page range being considered for this work
poery_pages: Comma separated list of page numbers containing poetry
- The pages are selected as follows:
First, all pages with poetry are selected
Then, all remaining pages are chosen randomly (proportionately by work)
- The resulting output CSV file has the following fields:
work_id: PPA work id
page_num: Digital page number
Example usage:
python generate_page_set.py input.csv output.csv 300
Add Image (Relative) Paths¶
This script adds image relative paths to the PPA full-text corpus. Optionally,
a file extension (e.g., .jpg) can be provided to be used for all relative
image paths instead of defaulting to their source-level defaults.
Example usage:
python add_image_relpaths.py ppa_corpus.jsonl ppa_with_images.jsonl
python add_image_relpaths.py ppa_corpus.jsonl ppa_with_images.jsonl --ext=.jpg
Build Text Corpus¶
Script for building a text corpus file (JSONL) from a directory of texts.
This script converts each text (.txt) file within an input directory
(including nested files), and compiles them into a single output text corpus
where each record corresponds to a single text file with the following fields:
id: The name of the file (without prefix)
text: The text of the file (assumes UTF-8 formatting)
Note that the output file can also be written in any compressed form supported
by orjsonl. If no suffix is provided, .jsonl will be used.
Example usage:
python build_text_corpus.py input_dir out_corpus
python build_text_corpus.py input_dir out_corpus.jsonl
python build_text_corpus.py input_dir out_corpus.jsonl.gz
Dataset Preparation¶
This is a utility scripto help prep the PPA metadata/text/image dataset for publication.
It should be used to align page images with text to create a page_image.tar file and update the ppa_pages.jsonl file with image paths and optionally old text, when OCR has changed.
- Handles the following formats:
HathiTrust dataset zip files
HathiTrust manually downloaded zip files (full or excerpted) for 1930s content
Gale/ECCO image directory, using existing corppa path utils and naming conventions
For HathiTrust dataset zip files, the script checks for page alignment; if average text similarity is below our trusted threshold, does a best-effort alignment based on high-confidence, unambiguous page matches which are used to infer matches for neighboring pages.
For convenience, a slurm sbatch file is provided in scripts/dataset_prep.sbatch
Supports interruption and continuing partial work, so that alignment can be completed in multiple passes.
- corppa.utils.dataset_prep.add_image_paths_to_metadata(corpus_dir: Path, output_dir: Path) None¶
Update metadata with image_file and image_path fields for works with images.
- corppa.utils.dataset_prep.add_zip_file_to_tar(zipfile: ZipFile, zip_filename: str, tar: TarFile, tar_file_path: str) None¶
Add a single file from an open ZipFile to an open TarFile without extracting to disk. tar_file_path sets the path within the tar archive.
- corppa.utils.dataset_prep.align_pages(work_id: str, pages_df: DataFrame, zipfile: ZipFile) dict¶
Determine alignment between pages in different versions of a HathiTrust work. Returns a dict mapping each page id to a
(page_filename, new_ocr)tuple, wherenew_ocris the zip page text when it differs from the original page text andNoneotherwise. Returns an empty dict if no alignment could be determined.
- corppa.utils.dataset_prep.align_shifted_pages(pages_df: DataFrame, zip_pages_df: DataFrame, detailed: bool = False) DataFrame¶
Align corpus pages to zip page filenames when page order has shifted between versions. Shifts are determined by matching pages with sufficient text in the first dataframe to pages in the zip file using normalized indel similarity (rapidfuzz.fuzz.ratio). Short pages and pages with low-confidence matches are aligned based on the shift of the nearest preceding alignment, or nearest following alignment if no preceding alignment.
Returns a DataFrame with
id,page_filename, andnew_ocrcolumns where each row is the determined alignment; returns an empty dataframe if alignment could not be determined.When
detailedis True, the returned frame instead includes the per-page diagnostic columns useful for reviewing/visualizing an alignment:id,order(original page order),aligned_order(mapped zip order),page_filename,cdist_best_score(best rapidfuzz.cdist score, 0-100, for every scored/long page; null for short pages),shift(trusted anchor shift, null for filled pages),inferred_shift(shift actually applied), and the rawtext/zip_textfor each page. Derived review fields (is_anchor/is_matchedflags, text lengths, snippets, extra scoring) are added byreview_alignmentrather than here.
- corppa.utils.dataset_prep.find_corpus_file(corpus_dir: Path, filenames: list[str]) Path¶
Return the first existing file in
corpus_dirfromfilenames(checked in order). RaisesFileNotFoundErrorif none exist.
- corppa.utils.dataset_prep.get_ht1930_work_ids(metadata_path: Path) dict[str, str | None]¶
Load work-level metadata and return a dictionary of
work_idvalues for HathiTrust volumes published in 1930, with their corresponding digital page range (excerpts only), OR any other volumes that were added to PPA after the text/image dataset was requested.These volumes have zip files with different naming convention and structure and must be handled differently.
- corppa.utils.dataset_prep.get_ht_zipfile_path(work_id: str, image_dir: Path) Path¶
Return the expected HathiTrust zip path for a work under
image_dir. The zip is named from the (encoded) HathiTrust id without institution prefix.
- corppa.utils.dataset_prep.get_zip_image_names(zipfile: ZipFile) dict[str, str]¶
HathiTrust zip files include images in multiple formats, sometimes mixed even within a single zipfile. Use the ZipFile namelist to generate and return a dictionary of all files with supported image extensions. Dictionary key is the file basename, value is the full path within the zip.
- corppa.utils.dataset_prep.get_zip_textfiles(zipfile: ZipFile) Iterator[tuple[str, str]]¶
Return a generator of text files from an open zip archive. Returns tuples of (file_id, content) where file_id is the stem of the filename.
- corppa.utils.dataset_prep.get_zipfile_pages(zipfile) DataFrame¶
Load text files from a HathiTrust zipfile into a polars dataframe.
Returned dataframe has the following columns:
page_filenametext: full text contents of the pagepage_id: numeric portion extracted frompage_filenameorder: numeric version ofpage_idtext_len: number of characters intext
- corppa.utils.dataset_prep.longest_increasing_subseq(values: ndarray) ndarray¶
Return the indices (into
values) of the longest strictly increasing subsequence. When several subsequences tie for longest, one is returned deterministically (same input always yields the same result); which of the tied subsequences is chosen is not part of the contract, since downstream alignment depends only on the derived shifts and the rapidfuzz-based dedup, not on the specific anchor set.Used by align_shifted_pages to filter high-confidence mappings down to a set of “anchor” matches that enforce sequential alignment while allowing for gaps on either side.
We use this to reduce a set of candidate page matches down to a monotonic, conflict-free set of “anchors”. Each confident match pairs an original page with a zip page (identified by its
order, the digital page sequence). Reading those zip page orders in original-page order, a valid alignment must be strictly increasing: later original pages can only map to later zip pages, and each zip page may be claimed at most once (strict increase => no repeats). Any confident match that would break that ordering - e.g. repeated boilerplate pages that all matched the same zip page, or a match that jumps backwards - is dropped by keeping only the longest increasing run.This is the classic O(n log n) patience-sorting algorithm.
- corppa.utils.dataset_prep.main()¶
Parse command-line arguments and prepare a full-text dataset.
- corppa.utils.dataset_prep.open_ht_zipfile(zipfile_path: Path | None) Iterator[ZipFile | None]¶
Open a HathiTrust image zip file, yielding the open
ZipFile. YieldsNone(without raising) whenzipfile_pathisNoneor does not exist, so callers can uniformly skip works whose zip is missing.
- corppa.utils.dataset_prep.plot_alignment(review_df: DataFrame)¶
Visualize a review frame from
review_alignmentas an Altair chart.Original pages are drawn on one row and their mapped zip pages on a second row, both positioned by page order along x. A line connects each original page to the zip page it maps to, so the horizontal offset of that line is the shift; parallel lines mean a consistent shift, and crossings/gaps stand out. Points are colored by alignment status (anchor / inferred / unmatched) and sized by match score. Hover shows a compact mapping/score line (e.g. “page 24 -> zip 12 | best 100 | aligned 100”) and a text snippet.
Expects the derived columns added by
review_alignment(snippets,match_score). Requiresaltair(install thenotebooksextra).
- corppa.utils.dataset_prep.process_gale_work(work_id: str, pages: list[dict], image_dir: Path, tar: TarFile) Iterator[dict]¶
Add available Gale page images to a work and yield its page records.
- corppa.utils.dataset_prep.process_ht1930_work(work_id: str, pages: list[dict], image_dir: Path, tar: TarFile, digital_page_range: str | None = None) Iterator[dict]¶
Images for HathiTrust works published in 1930 and imported manually in 2026 were downloaded manually from the HT web interface. The zip files only contain images (no text files, no nested directories) and were downloaded close to when the works were imported, so they do not require page re-alignment. Pass digital_page_range for excerpts to find the correct zipfile, which includes digital page range.
Find and load the zip file, and map images to pages based on filename.
- corppa.utils.dataset_prep.process_ht_work(work_id: str, pages: list[dict], image_dir: Path, tar: TarFile) Iterator[dict]¶
Align HathiTrust pages with images and yield the resulting page records.
- corppa.utils.dataset_prep.process_work(work_id: str, pages: list[dict], image_dir: Path, tar: TarFile | None, ht1930_work_ids: dict[str, str | None] | None = None) Iterator[dict]¶
Process one work using the source-specific image handling strategy.
- corppa.utils.dataset_prep.review_alignment(work_id: str, pages: DataFrame | list[dict], zipfile_path: Path | str) DataFrame¶
Run the shifted-page alignment for a single work and return a per-page frame for review/visualization. Intended for notebook use.
Builds on
align_shifted_pages(detailed=True)and adds derived review columns:is_anchor/is_matched(flags inferred fromshift/page_filename),text_len/zip_text_len(character counts),text_snippet/zip_text_snippet(first-line hover snippets), andmatch_score(the rapidfuzz.fuzz.ratio, 0-100, between each matched page and its actual aligned zip page) so it can be compared againstcdist_best_score(the best score against any zip page).pagesmay be a page DataFrame (withid,order/textcolumns) or a list of page dicts;zipfile_pathis a path to the work’s zip file.
- corppa.utils.dataset_prep.zip_image_filenames(zipfile: ZipFile) dict[int, str]¶
Map numeric digital page sequence number to image filename within a HathiTrust zip.
Used for image-only volume zip files. Images are TIF or JPG only; base filename is numeric with leading zeros. Files with a non-numeric stem (e.g. stray cover/metadata images) are skipped.
Dataset Metadata Refinement¶
Utility script for PPA full-text dataset publication prep.
Takes a manually cleaned version of PPA metadata in CSV format, and propagates refined author and pub_place fields to CSV and JSON metadata in the specified ppa_corpus folder.
- corppa.utils.dataset_refine.main()¶
Parse command-line arguments and refine a PPA corpus metadata directory.