180 lines
5.9 KiB
Markdown
180 lines
5.9 KiB
Markdown
# Telugu Article Extractor
|
||
|
||
A local web app that takes a Telugu newspaper PDF and produces one PNG +
|
||
one TXT file per article.
|
||
|
||
## Pipeline
|
||
|
||
```
|
||
PDF
|
||
│ Stage 1 PyMuPDF → page_001.png (target: 4200×7400 px, fits aspect)
|
||
▼
|
||
Pages
|
||
│ Stage 2 PaddleOCR PP-Structure → regions.json
|
||
│ (title / text / figure rectangles with bboxes)
|
||
▼
|
||
Regions
|
||
│ Stage 3 Article grouping → articles.json
|
||
│ Primary: Claude API (spatial reasoning over regions,
|
||
│ sees page thumbnail + region JSON)
|
||
│ Fallback: column-aware + dateline-anchored Python rules
|
||
│ • title region starts a new article
|
||
│ • "<place>, మే <day> (ఆంధ్రజ్యోతి):" dateline
|
||
│ also starts a new article
|
||
│ • regions partitioned by column first to avoid
|
||
│ cross-column contamination
|
||
▼
|
||
Articles (bboxes)
|
||
│ Stage 4 PIL crop → article.png per article
|
||
▼
|
||
Article PNGs
|
||
│ Stage 5 Tesseract (lang=tel) → article.txt per article
|
||
▼
|
||
Done. Browse in the UI, download all as zip.
|
||
```
|
||
|
||
## Setup
|
||
|
||
### 1. System dependencies
|
||
|
||
```bash
|
||
# Ubuntu / Debian
|
||
sudo apt update
|
||
sudo apt install -y tesseract-ocr tesseract-ocr-tel libgl1
|
||
```
|
||
|
||
`tesseract-ocr-tel` is the Telugu language data — required for the per-article
|
||
OCR step.
|
||
|
||
`libgl1` is needed by paddlepaddle on headless Linux.
|
||
|
||
### 2. Python environment
|
||
|
||
```bash
|
||
cd telugu_extractor
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
First run of paddlepaddle/paddleocr will download model weights (~400 MB)
|
||
into `~/.paddleocr/`. This happens once.
|
||
|
||
### 3. (Optional) Enable Claude grouping
|
||
|
||
Stage 3 (article grouping) works two ways:
|
||
|
||
- **Without an API key**: falls back to column-aware + dateline Python
|
||
rules. Works fine for tidy pages; struggles on banner headlines and
|
||
cross-column layouts.
|
||
- **With an API key**: Claude looks at a thumbnail of the page plus
|
||
the regions JSON and does the spatial reasoning. Much better on
|
||
complex layouts. Costs ~$0.01–0.05 per page.
|
||
|
||
To enable, set the environment variable before running:
|
||
|
||
```bash
|
||
export ANTHROPIC_API_KEY="your-api-key-here"
|
||
```
|
||
|
||
Get a key at https://console.anthropic.com/. If the variable isn't set
|
||
or the API call fails, the app silently falls back to the Python rules.
|
||
|
||
### 4. Run
|
||
|
||
```bash
|
||
python app.py
|
||
```
|
||
|
||
Then open <http://localhost:5000>.
|
||
|
||
## How to use
|
||
|
||
1. On the homepage, choose a PDF and pick a target render size.
|
||
Defaults: 4200 × 7400 px (≈ 600 DPI for a tabloid newspaper page).
|
||
Each page is scaled to fit *inside* this box preserving aspect.
|
||
|
||
2. Click "Start extraction". A background job kicks off and the page
|
||
redirects to a job view that polls for status.
|
||
|
||
3. When the job finishes, every article is shown as a card with:
|
||
- The cropped image (`article.png`)
|
||
- A link to its OCR'd text (`article.txt`)
|
||
- The detected dateline (e.g. `చిన్నగూడూరు, మే 15 (ఆంధ్రజ్యోతి):`)
|
||
if the grouper found one
|
||
- The first non-empty line of OCR text as a headline preview
|
||
|
||
4. Click "Download all (zip)" to grab everything in one file.
|
||
|
||
## What's in each job folder
|
||
|
||
```
|
||
output/<timestamp>/
|
||
├── input.pdf the uploaded PDF
|
||
├── meta.json job metadata
|
||
├── pages/
|
||
│ ├── page_001.png rendered page
|
||
│ ├── page_001.regions.json raw PaddleOCR layout output
|
||
│ └── page_001.articles.json grouping result
|
||
└── articles/
|
||
└── p001_a001/
|
||
├── article.png per-article crop
|
||
├── article.txt per-article OCR
|
||
└── info.json bbox + dateline + member regions
|
||
```
|
||
|
||
Everything is on disk and inspectable. If a crop looks wrong, you can
|
||
look at `pages/page_NNN.articles.json` to see exactly which regions
|
||
were grouped together.
|
||
|
||
## Tuning the grouping
|
||
|
||
Stage 3 has two implementations in `extractor.py`:
|
||
|
||
### Path A: Claude API (`_group_with_claude`)
|
||
|
||
The system prompt is in `CLAUDE_GROUPING_PROMPT`. It tells Claude:
|
||
- Use `title` regions as article starts
|
||
- Use the `dateline` field as a strong "this is an article body start" signal
|
||
- Respect column boundaries
|
||
- Handle banner headlines that span columns
|
||
- Pull quotes inside another article's bbox belong to that article
|
||
|
||
To tune, edit the prompt. To debug, look at `pages/page_NNN.articles.json`
|
||
and check the `grouped_by` field — `"claude"` means the API call worked,
|
||
`"rules"` means it fell back.
|
||
|
||
### Path B: Python rules (`_group_with_rules`)
|
||
|
||
Used when no API key is set or the API call fails.
|
||
|
||
1. **Column partition** — regions clustered by `left` coordinate.
|
||
2. **Within each column**: `title` region OR dateline match starts a
|
||
new article; everything else attaches to the current one.
|
||
|
||
To tune the rules:
|
||
- Adjust `DATELINE_RE` for different paper conventions.
|
||
- Adjust the column-clustering threshold in `_assign_columns`.
|
||
|
||
## Known limitations
|
||
|
||
- **L-shaped articles**: an article whose body wraps around a photo into
|
||
a non-rectangular shape can't be captured by one bbox. The current
|
||
pipeline produces the smallest rect that covers everything, which may
|
||
include a sliver of a neighboring article. You'd need a polygon-based
|
||
crop to fix that — out of scope here.
|
||
|
||
- **Cross-page continuations** (`మిగతా 3వ పేజీలో`): not stitched. Each
|
||
page is processed independently.
|
||
|
||
- **PaddleOCR layout model is trained mostly on English/Chinese
|
||
documents.** Expect 80–95% of articles to be bounded correctly out of
|
||
the box. Edge cases are why the per-job folder keeps the raw
|
||
`regions.json` — so you can audit and tune.
|
||
|
||
## License & data
|
||
|
||
All processing is local. The app never sends your PDF or anything
|
||
extracted from it to a remote service. The PaddleOCR model weights are
|
||
downloaded once from PaddlePaddle's CDN on first run.
|