Buckets:
name: institutional-newspapers
description: >-
Use the Institutional Newspapers family of Hugging Face assets from the
Institutional Data Initiative: historical newspaper scans segmented into
individual crops, each enriched with OCR (Tesseract and VLM), crop-type
classification, reading order, language detection, text metrics, named
entities, subjects, and precomputed text and image embeddings. Currently
covers the Boston Public Library collection (1.47M scans, 1795-1930), plus the
three models that produced it and the production pipeline. Use when a task
involves this corpus, reading or analyzing historical newspaper text or
layout, reassembling an article from a page, finding advertisements or
illustrations, tracking entities or subjects across newspapers, segmenting or
classifying a new newspaper scan, or running the pipeline on another
collection.
Institutional Newspapers - dataset, models & pipeline
Institutional Newspapers is a growing family of Hugging Face assets from the Institutional Data Initiative: each instance takes one institution's historical newspaper collection, segments every scan into individual type-agnostic crops, and enriches each crop with OCR and analysis. New collections are added over time. Browse the Institutional Newspapers collection to see what is available; a given instance may publish a different set of repos.
This skill covers the instance published so far - the Boston Public Library collection:
| Repo | Type | Contents |
|---|---|---|
institutional/institutional-newspapers-bpl |
dataset | One row per scan. 1,473,635 scans, 83,147,041 crops |
institutional/institutional-newspapers-segmenter-yolo26x |
model | Scan -> crop bounding boxes |
institutional/institutional-newspapers-crop-classifier-image-yolo26m-cls |
model | Crop image -> crop type (7 classes) |
institutional/institutional-newspapers-crop-classifier-text-model2vec |
model | Crop text -> crop type (6 classes) |
| institutional-newspapers-pipeline | code | The 15-step pipeline that produced the dataset |
Scale: 1,473,635 public domain scans published between 1795 and 1930, 83,147,041 crops, 16.3 billion o200k_base tokens of VLM OCR text and 14.7 billion from Tesseract.
Two ideas that drive every decision:
- The
crop_*columns are parallel lists that share one index, already in reading order. One row is one scan. Everycrop_*column holds one entry per crop, and all of them are sorted by the reading order the pipeline detected for that page.zipthem to reassemble a crop. Filtering preserves reading order, because it only drops entries from lists that are already sorted. - The dataset is precomputed pipeline output. Read the field instead of re-running the model. All three models were already run over the whole collection and their results stored as columns. Only run a model on a new scan or crop that is not in the dataset.
Pick the right asset
| I need to... | Use |
|---|---|
| Crops, OCR, and all enrichment for a scan in the collection | dataset institutional-newspapers-bpl (large - stream it) |
| Metadata only, without scan images, OCR text, or embeddings | Parquet column projection (see below) |
| The crop type of a crop (already computed) | field crop_classification_gen |
| The per-modality signal behind that decision | fields crop_classification_image_only_gen / crop_classification_text_only_gen (+ _conf_gen) |
| The reading order of a page (already computed) | the order of the crop_* lists themselves |
| Entities or subjects for a crop (already computed) | crop_ner_per_gen / crop_ner_loc_gen / crop_ner_org_gen, crop_subject_gen (experimental) |
| Vector search over crops | crop_text_embeddings (256-d) / crop_image_embeddings (384-d) |
| Find crops on a new newspaper scan | model ...-segmenter-yolo26x |
| Classify a new crop | models ...-classifier-image-... + ...-classifier-text-..., then the combination rule |
| Process a new collection end to end | the pipeline |
Dataset
institutional-newspapers-bpl
One row per newspaper scan (one page of one issue). A single train split. Large, because each row embeds its scan image - always stream unless you project columns. See Auth & gating.
from datasets import load_dataset
ds = load_dataset(
"institutional/institutional-newspapers-bpl",
split="train",
streaming=True,
token=True, # gated repo: uses HF_TOKEN / cached login
)
row = next(iter(ds))
print(row["issue_id_src"], row["page_number_gen"], row["year_ext"])
print(row["scan_image"]) # a PIL image, decoded from the embedded WEBP bytes
# The crop_* columns share one index and are already in reading order.
for bbox, category, text in zip(
row["crop_bbox_gen"],
row["crop_classification_gen"],
row["crop_vlm_ocr_gen"],
):
# One value per crop, NULL where the step produced no result.
print(category or "", bbox, text or "")
Reassemble the readable text of a page by keeping only the crop types you want. The result stays in reading order:
KEEP = {"Content", "Section heading"}
article = "\n\n".join(
text
for category, text in zip(row["crop_classification_gen"], row["crop_vlm_ocr_gen"])
if category and category[0] in KEEP and text
)
Loading only specific columns
Read metadata from the Parquet shards directly with column projection. Parquet is columnar, so the bytes of the skipped columns are never transferred. This is the difference between a small download and a very large one.
import pyarrow.parquet as pq
from huggingface_hub import HfFileSystem
SKIP = {
"scan_image",
"crop_tesseract_ocr_gen",
"crop_text_embeddings",
"crop_image_embeddings",
}
fs = HfFileSystem(token=True)
shards = fs.glob("datasets/institutional/institutional-newspapers-bpl/**/*.parquet")
with fs.open(shards[0], cache_type="none") as handle:
parquet_file = pq.ParquetFile(handle, pre_buffer=True)
keep = [
field.name
for field in parquet_file.schema_arrow
if field.name not in SKIP
]
table = parquet_file.read(columns=keep)
print(table.num_rows, table.column_names)
Shards are named BPL-part-NNNNN.parquet and hold up to 250 scans each. They are compressed with zstd and written with a row-group size of 10.
Models
All under institutional/. Run them in order on a new scan; for scans already in the collection, read the dataset instead.
...-newspapers-segmenter-yolo26x (ultralytics)
A YOLO26x fine-tune. Single-class object detection: a scan in, one bounding box per crop out, regardless of crop type. Precision 0.927, recall 0.910, F1 0.918, mAP50 0.955, mAP50-95 0.901 on 153 held-out scans (2,991 bboxes).
from huggingface_hub import snapshot_download
from ultralytics import YOLO
model_path = snapshot_download("institutional/institutional-newspapers-segmenter-yolo26x")
model = YOLO(f"{model_path}/best.pt")
results = model("newspaper_scan.jpg", imgsz=960, iou=0.15, conf=0.6)
boxes = [b.xyxy.tolist()[0] for b in results[0].boxes] # crop these from the scan
Use imgsz=960, iou=0.15, conf=0.6. The confidence threshold drops low-certainty detections and the deliberately low IoU threshold suppresses heavily overlapping boxes, which is what these dense layouts need.
...-crop-classifier-image-yolo26m-cls (ultralytics)
A YOLO26m-cls fine-tune. A cropped image in, one of the seven crop types out. Top-1 accuracy 0.913. Much stronger than the text classifier on the visual categories: F1 0.84 on Photograph or illustration against 0.48, and 0.92 on Cartoon against 0.68.
model_path = snapshot_download("institutional/institutional-newspapers-crop-classifier-image-yolo26m-cls")
model = YOLO(f"{model_path}/best.pt")
r = model("crop.jpg", imgsz=768)[0]
print(r.names[r.probs.top1], float(r.probs.top1conf))
Use imgsz=768.
...-crop-classifier-text-model2vec (model2vec)
A Model2Vec fine-tune of minishlab/potion-base-32M with a classifier head - light and CPU-friendly. The OCR text of a crop in, one of six crop types out. It does not learn Empty. Accuracy 0.92.
from model2vec.inference import StaticModelPipeline
model = StaticModelPipeline.from_pretrained(
"institutional/institutional-newspapers-crop-classifier-text-model2vec"
)
print(model.predict(["CLASSIFIED ADVERTISING - Rooms for rent, furnished apartments..."],
max_length=None))
# ['Advertisement']
Pass max_length=None so the input text is not truncated.
Run both classifiers, not one. Neither visual nor textual signal alone classifies newspaper crops reliably.
How they connect
Field suffixes tell you a value's provenance:
| Suffix | Meaning |
|---|---|
_src |
From the source archive or the scan filename |
_ext |
From an external source, such as a library catalog API |
_gen |
Generated by the IDI pipeline |
_exp |
Experimental or exploratory generation |
The crop-type combination rule
crop_classification_gen is not simply the more confident of the two classifiers. The pipeline merges them like this, and you should apply the same rule when you classify a new crop yourself:
- Default: take the prediction with the higher confidence score.
- If the image classifier says
Photograph or illustrationwith confidence above 0.5, take the image classifier. - If the image classifier says
Emptywith confidence above 0.4, take the image classifier. - If the text classifier says
Empty, take the image classifier. Every crop with no OCR-able text is assignedEmptyby rule with a confidence of exactly 1.0, so this value is a marker, not a measurement. - If only one modality produced a prediction, take it.
Both raw per-modality columns ship with the dataset, so you can apply a different rule instead.
Pipeline order for a new scan
scan -> segment into crops -> OCR each crop (Tesseract + dots.mocr) -> classify each crop (text + image) -> merge the two signals -> detect reading order -> enrich (language, text metrics, NER, subjects, thesauri, embeddings).
Gotchas
crop_language_conf_genisNULLwhen the language code was inherited fromlanguage_ext. The pipeline substitutes the issue-level code when detection confidence is below 0.50, when the crop holds fewer than 30 words, or when the issue language is not supported by Lingua. A missing confidence is the marker for an inherited code, not an error.- Tesseract word boxes are relative to the crop;
crop_bbox_genis relative to the scan. Add the crop origin before drawing word boxes on a full scan. scan_imagedominates row size. Never load it unless you need the pixels.- The NER, subject, and thesauri fields are experimental. Subjects are a zero-shot soft ranking (mean top-1 confidence 0.65, and a mean gap of 0.457 to the second choice), so read
crop_subject_genas a ranking, not a label. Thesauri matches are naive keyword matches, useful as a navigational aid rather than as an interpretation of the text. - Two subject labels overlap with crop types by design (
Commercial Advertisements & Classifieds,Mastheads, Page Headers & Printer Imprints). They give the classifier somewhere to put crops that carry no topic.
Running the pipeline on a new collection
The pipeline runs 15 sequential steps over batches of issues and writes to SQLite. Every step is separate and can be re-run on its own.
git clone https://github.com/institutional/institutional-newspapers-pipeline.git
bash install.sh # installs system-level dependencies too
nano .env # corpora list and S3 credentials
uv run pipeline.py system cache-models # pull models into the local cache
uv run pipeline.py system warmup-ocr-vlm # pre-compile the OCR VLM encoder
uv run pipeline.py system build # list available issues
uv run pipeline.py orchestration prepare --corpus=CORPUS --items-per-batch=100
./run.sh <PIPELINE_RUN_ID> # execute, with logging
uv run pipeline.py orchestration status
uv run pipeline.py export CORPUS # build the releasable dataset
Every command has a --help option. Requirements: uv, Tesseract 5 with tessdata_best, sqlite, and a CUDA-capable GPU. vLLM only installs on Linux, so the VLM OCR step needs a Linux host. To add a corpus you need an S3 bucket of .tar.gz archives holding JP2, JPEG, or TIFF scans whose filenames sort alphabetically by page number; see "Adding corpora" in the pipeline README.
Auth & gating
export HF_TOKEN="hf_..." # or: huggingface-cli login
Or load it from a local .env:
from dotenv import load_dotenv
load_dotenv() # sets HF_TOKEN in the environment
Dependencies
pip install datasets huggingface_hub pyarrow pillow ultralytics model2vec python-dotenv
Xet Storage Details
- Size:
- 13.5 kB
- Xet hash:
- 275dd5c9cafdecc9f3744043d821f9dd46d180b3d5720711117f76179a837cbd
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.