Skip to content

tarekio/pagen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pagen: Arabic Text-Detection Data Generator

Pagen generates synthetic realistic-looking and diverse document images with word-level polygon annotations for training and evaluating text-detection (and OCR) models.

It produces train and val splits in a single command, with realistic augmentation (scan textures, photo perspective warp, photometric degradation) fused into generation: one final image per document, no intermediate copies.

The output follows the doctr detection-dataset layout:

{
  "1.png": {
    "img_dimensions": [width, height],
    "img_hash": "<sha256 of the png>",
    "polygons": [[[x1, y1], [x2, y1], [x2, y2], [x1, y2]], ...],
    "labels": ["الكلمة", "الأولى", ...]
  }
}

Each polygon is a 4-point quadrilateral (TL, TR, BR, BL) wrapping a single word. Rendering uses WeasyPrint (Markdown → HTML → PDF); word labels come from WeasyPrint's layout tree so Arabic shaping and Eastern-Arabic digits are always correct. PyMuPDF rasterizes the PDF and supplies tight per-glyph geometry so polygons fit the visible ink rather than the loose em-box.

Requirements

System dependencies (Linux/Debian)

WeasyPrint needs Pango, Cairo, and friends. PyMuPDF bundles its own MuPDF; no extra system packages are needed for rasterization.

sudo apt-get install build-essential python3-dev python3-cffi \
  libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 \
  libffi-dev shared-mime-info

Python dependencies

python -m venv .venv
source .venv/bin/activate
pip install -e .
# or without editable install:
pip install -r requirements.txt

Optional extras:

pip install python-dotenv          # auto-load .env for API keys
pip install arabic-reshaper matplotlib  # pagen visualize --show

Fonts

Drop Arabic .ttf files into resources/fonts/ (or pass --fonts-dir). A good source is Google Fonts. Color fonts (COLR/SVG) are skipped automatically.

Corpus

Add word files to resources/corpora/ (one or more Arabic words per line; space-separated is fine), or point --corpus at a file or directory. Without any corpus, a tiny built-in fallback is used.

Images

Augmentation draws on two folders of your own photos. Unsplash is a good free source for both.

  • resources/images/scene/ (--scene-dir): photos of a blank sheet of paper lying in a real scene (on a desk, table, floor, with surrounding clutter). The rendered document is perspective-warped onto the paper region and composited into the photo, so the model sees documents at realistic angles against busy backgrounds. The paper's four corners are detected automatically and cached in paper_corners.json (editable via pagen corners --edit), so pick photos where the sheet is clearly visible against its surroundings. Search Unsplash for blank paper, paper on desk, empty document.
  • resources/images/textures/ (--textures-dir): flat paper / scan textures (plain paper grain, aged or stained paper, photocopier noise) used as full-frame overlays. The rendered document is multiply-blended onto the texture to mimic a scanned or photocopied page, with no perspective, just surface character. Search Unsplash for paper texture, old paper, parchment.

The --clean-prob / --textures-prob / --scene-prob flags control how often each path (clean, texture overlay, scene warp) is chosen. If a folder is empty, its path is simply skipped.

Quickstart

# Interactive wizard: prompts for counts, output dir, mode
pagen

# Or specify everything:
pagen dataset --train 500 --val 100 -o data/

The result loads directly with doctr:

from doctr.datasets import DetectionDataset
ds = DetectionDataset(img_folder="data/train/images", label_path="data/train/labels.json")

Commands

pagen dataset: generate a detection dataset

pagen dataset --train N --val N [options]
Flag Default Description
--train N 0 Number of training documents (omit to skip the split; at least one of train/val must be > 0)
--val N 0 Number of validation documents (omit to skip the split)
-o DIR output Root output directory; splits go in DIR/train and DIR/val
--pdf-only off Skip augmentation; produce clean rendered images
--keep-txt off Save plain-text word labels per page (.txt)
--keep-pdf off Save the intermediate PDF per document
--dpi 150 Raster resolution
--workers all CPUs Parallel worker processes
--seed 42 RNG seed
--templates-dir templates Directory of .md templates
--corpus auto Corpus file or directory
--fonts-dir fonts Directory of .ttf fonts

Augmentation flags (ignored with --pdf-only):

Flag Default Description
--clean-prob 0.10 Fraction of images kept clean
--textures-prob 0.45 Fraction composited onto a scan texture
--scene-prob 0.45 Fraction perspective-warped onto a background photo
--textures-dir resources/images/textures Scan texture images
--scene-dir resources/images/scene Background photo images

LLM content fillon by default, falling back to random corpus words:

Flag Default Description
--llm Require the LLM — fail if the backend is unreachable
--no-llm Use random corpus fill (skip the LLM entirely)
--llm-base-url http://localhost:11434/v1 OpenAI-compatible API endpoint
--llm-model gemma3:4b Model name
--llm-temperature 0.7 Sampling temperature for content fill
--api-key-env OPENAI_API_KEY Env var holding the API key (never a CLI flag)
--fill-variants 10 LLM fills pre-generated per template, then sampled by workers

By default the tool probes the LLM backend and uses it if it responds. If the backend is unreachable it says so and asks whether to fall back to random corpus fill (a non-interactive run falls back automatically). Use --llm to require the LLM — it then fails if the backend is unreachable instead of falling back — or --no-llm to go straight to corpus fill. When LLM fill is used, content is generated once per template (--fill-variants variants each) in a single pass before rendering, and workers sample from that pool — so LLM calls scale with templates * fill-variants, not with the number of images. If the backend is unreachable, generation also falls back to random corpus fill.

Default model — gemma3:4b @ temperature 0.7. This was chosen by evaluating five local models over 20 random templates (see eval/): gemma3:4b produced 100% valid Arabic, was the fastest (~4.2 s/doc), and is the smallest (3.3 GB) — a good fit for an 8 GB GPU. aya-expanse:8b matched its quality but is ~20% slower; qwen2.5:7b and qwen3.5:4b occasionally romanized Arabic (Latin transliteration). Fill diversity comes from having many templates (each fill varies the slot values), not from a high temperature, so 0.7 is used for stable quality. Any OpenAI-compatible model works via --llm-model / --llm-base-url. See eval/README.md to re-run the comparison.

Re-running into an existing output directory appends: new IDs continue after existing ones and labels.json is preserved.

Examples:

# 1000 augmented training docs + 200 val
# (uses the LLM if the backend is up, else asks to fall back to corpus)
pagen dataset --train 1000 --val 200 -o data/

# Skip the LLM entirely, no prompt (offline / no Ollama)
pagen dataset --train 1000 --val 200 -o data/ --no-llm

# LLM fill via Ollama, no prompt (must be running: ollama serve)
pagen dataset --train 100 --val 20 --llm --llm-model gemma3:4b

# Clean (no augmentation), save txt and pdf alongside images
pagen dataset --train 50 --val 10 --pdf-only --keep-txt --keep-pdf -o data/ --no-llm

# LLM fill via any OpenAI-compatible provider
OPENAI_API_KEY=sk-... pagen dataset --train 100 --val 20 \
  --llm --llm-base-url https://api.openai.com/v1 --llm-model gpt-4o-mini

pagen eval: generate eval images (no polygon annotations)

Produces a PNG + plain-text ground truth per document. No labels.json.

pagen eval -c 50 -o data/eval

pagen templates: generate markdown templates via LLM

Templates are generated once and reused across many dataset runs. This command is never invoked automatically by the dataset pipeline.

By default each document type yields --per-type 5 templates, produced in a single batched LLM call that assigns a distinct structural archetype per template (table, header fields, bullet list, numbered steps, prose, subsections). This gives the trained detector varied word-box layouts for the same document type, which independent single-shot calls do not: they collapse onto the model's one favored shape. Use --per-type 1 for the classic one-call-per-type behavior. The k templates for a type land as slug.md, slug_2.md, ...

# Generate 10 document types x 5 templates each (50 files) from the built-in pool
pagen templates --random 10 --llm --llm-model gemma3:4b

# One template per type (classic single-call behavior)
pagen templates --random 10 --per-type 1 --llm

# Generate specific types (5 templates each)
pagen templates "عقد عمل" "خطاب توصية" --llm

# From a file
pagen templates --file my_types.txt --llm

pagen corners: manage paper corner cache

The photo background augmentation path needs to know where the paper is in each photo. Corners are detected automatically the first time and cached in paper_corners.json. The automatic detection is not perfect, so you can inspect and fix the cache with an interactive editor.

Each entry records its provenance ("source": "auto" or "user"). The cache is reconciled append-only: only newly-added images are auto-detected and saved; existing entries are never re-detected or overwritten, so corners you fix in the editor ("source": "user") are safe across dataset runs.

# Build / refresh the cache (runs automatically during augmented dataset gen)
pagen corners --scene-dir resources/images/scene

# Launch interactive editor to fix bad auto-detections
pagen corners --scene-dir resources/images/scene --edit

# Export overlay images to inspect detections
pagen corners --scene-dir resources/images/scene --visualize --out corners_vis/

Editor controls: drag handle = move corner | n/ = next | p/ = prev | r = re-detect | f = full-frame | s = save | q/Esc = save + quit


pagen visualize: validate and overlay a dataset

pagen visualize data/train --max 20 --save data/train/debug_vis/
pagen visualize data/train --show   # requires matplotlib

Reports out-of-bounds polygons, label/polygon count mismatches, and empty labels.


API key security

API keys cannot be passed as CLI flags. Set the environment variable named by --api-key-env (default OPENAI_API_KEY):

export OPENAI_API_KEY=sk-...
# or put it in a .env file (auto-loaded if python-dotenv is installed):
echo "OPENAI_API_KEY=sk-..." > .env

Ollama requires no real key; the client sends a dummy value it ignores.

About

Simple Python script that generates single-page synthetic Arabic documents for training or evaluating OCR models.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Contributors

Languages