Nusantara Corpus PDF Extractor
AI-agent skills and sub-agents that extract structured parallel-corpus entries from local/low-resource-language dictionary PDFs (scanned or digital) into a quality-checked JSONL corpus. Language-agnostic by design; Bahasa Indonesia is the default pivot language.
This repo ships two ways:
- An agent skill (
SKILL.md) + agent specs (agents/) that you install into your AI coding harness (opencode, Claude Code, etc.) with a singlenpx skills addcommand. - A Python package (
nusantara-corpus-extractor) on PyPI that provides the extraction pipeline (scripts/) as an installable CLI.
What's in the box
| Kind | Location | Purpose |
|---|---|---|
| Skill | SKILL.md |
The umbrella extraction skill: full pipeline loop, language setup, quality loop |
| Skill | .opencode/skills/conventions-management/ |
Guides the conventions agent: pattern detection, phonology updates |
| Skill | .opencode/skills/linguistic-correction/ |
Guides the correction agent: translation validation, homonym resolution |
| Agent | agents/extraction-agent.md |
Orchestrates the full extraction loop |
| Agent | agents/conventions-agent.md |
Learns book structure, updates phonology ref |
| Agent | agents/correction-agent.md |
Validates meaning, resolves ambiguity |
| Pipeline | scripts/*.py |
The Python extraction/quality-loop engine |
Installing the skill on your harness
The repo ships as a single agent skill — nusantara-corpus-pdf-extractor
(the root SKILL.md with YAML frontmatter) — and installs with the
open agent skills CLI:
# List what's available
npx skills add masdevid/nusantara-corpus-extractor --list
# Install the skill into your harness (opencode, Claude Code, etc.)
npx skills add masdevid/nusantara-corpus-extractor
# Or target a specific harness / skill
npx skills add masdevid/nusantara-corpus-extractor -a opencode -a claude-code
npx skills add masdevid/nusantara-corpus-extractor --skill nusantara-corpus-pdf-extractor
The agent specs (agents/*.md) are plain Markdown — copy them into your
harness's agent directory (e.g. agents/ or .opencode/agents/ for
subagents).
Note: The skill references the Python pipeline in
scripts/. Install the Python package (below) or keep this repo checked out so the skill's commands can findscripts/.
Installing the Python pipeline
The extraction engine is published to PyPI:
pip install nusantara-corpus-extractor
This installs the nusantara-corpus-extractor CLI (extract / merge) plus all
pipeline modules. For scanned PDFs you'll also need Tesseract OCR:
brew install tesseract
Verify the install
# Python pipeline
nusantara-corpus-extractor --help
# Skills (after npx skills add)
npx skills list
Quick Start
Prerequisites
# Install the pipeline (see "Installing the Python pipeline" above)
pip install nusantara-corpus-extractor
# Tesseract OCR (for scanned PDFs)
brew install tesseract
First Extraction
# 1. Set up language (copy + fill in phonology reference)
cp references/phonology_template.md references/sentani_phonology.md
# Edit sentani_phonology.md with orthography rules and OCR confusion pairs
# 2. Extract a single book
nusantara-corpus-extractor extract \
--pdf "dictionaries/Set-Kamus-Sentani-Indonesia-Inggris-2.pdf" \
--book-id set \
--lang-code shj --lang-name Sentani --lang-family "Trans-New Guinea" \
--phonology references/sentani_phonology.md
# 3. Check output
cat out/shj/books/set/entries.jsonl
cat out/shj/books/set/book_profile.md
cat out/shj/books/set/flagged_terms.md
Merge Multiple Books
# After extracting multiple books for the same language:
nusantara-corpus-extractor extract --pdf "Kamus Bahasa Sentani.pdf" --book-id kamus ...
nusantara-corpus-extractor merge --lang-code shj
# Check merged corpus
cat out/shj/corpus_shj.jsonl
cat out/shj/cross_book_conflicts.md
Architecture
The pipeline runs a loop, not a single pass — it iterates until convergence (zero new flags) or a maximum iteration count is hit.
Pipeline Overview
flowchart TD
A["PDF / Scan"] --> B["0. PROFILE\nBookProfiler"]
B --> B5["0.5 CONVENTIONS\nConventions Agent"]
B5 --> C["1. PARSE\nPDFParser"]
C --> D["2. EXTRACT\nEntryExtractor"]
D --> E["3. CORRECT\nTypoCorrector"]
E --> F["4. CROSSCHECK\nMeaningCrossChecker"]
F --> G["5. SPOT\nPatternSpotter"]
G --> G5["5.5 CORRECTION\nCorrection Agent"]
G5 --> H{"6. CONVERGED?"}
H -->|"new_flags == 0\nor max_iterations"| I["8. WRITE\nCorpusWriter"]
H -->|"flags remain"| J["7. WEB VERIFY\n(optional)"]
J --> C
I --> K["corpus.jsonl"]
I --> L["flagged_terms.md"]
I --> M["pattern_insights.md"]
I --> N["quality_report_N.md"]
style B5 fill:#e1f5fe,stroke:#0288d1
style G5 fill:#e1f5fe,stroke:#0288d1
style H fill:#fff3e0,stroke:#f57c00
style I fill:#e8f5e9,stroke:#388e3c
The Loop
0. PROFILE classify book kind, detect zones, suggest settings
0.5 CONVENTIONS analyze entry layout, update phonology ref (sub-agent)
1. PARSE extract text from digital/OCR pages
2. EXTRACT parse raw text into DictionaryEntry objects
3. CORRECT fix OCR confusions, validate orthography
4. CROSSCHECK cross-check glosses against corpus + pivot
5. SPOT spot systematic issues across flags
5.5 CORRECTION validate translations, resolve homonyms (sub-agent)
6. REPORT track convergence, write quality report
7. WEB VERIFY (optional) search for genuinely ambiguous flags
8. WRITE output JSONL corpus + markdown reports
Three Agents
flowchart LR
subgraph EA["Extraction Agent"]
direction TB
E1["Orchestrates full loop"] --> E2["Tracks convergence"]
E2 --> E3["Writes reports"]
end
subgraph CA["Conventions Agent"]
direction TB
C1["Analyzes book structure"] --> C2["Updates phonology ref"]
C2 --> C3["Writes conventions file"]
end
subgraph CO["Correction Agent"]
direction TB
CO1["Validates translations"] --> CO2["Resolves homonyms"]
CO2 --> CO3["Checks morphology"]
end
EA -->|"step 0.5"| CA
EA -->|"step 5.5"| CO
CA -->|"updated config"| EA
CO -->|"corrected entries"| EA
style EA fill:#e8f5e9,stroke:#388e3c
style CA fill:#e1f5fe,stroke:#0288d1
style CO fill:#e1f5fe,stroke:#0288d1
| Agent | Role | Runs |
|---|---|---|
| Extraction Agent | Orchestrator — runs the full loop | Every pass |
| Conventions Agent | Memory — learns book structure, updates config | After profiling (step 0.5) |
| Correction Agent | Linguist — validates meaning, resolves ambiguity | After crosscheck (step 5.5) |
Detailed Diagrams
Note: The Mermaid diagrams below render when viewed directly on GitHub. Click each link to see the full diagram.
- Pipeline Workflow — full extraction loop with step-by-step script references
- Multi-Book Workflow — extracting multiple dictionaries, merging, conventions accumulation
- Agent System — how the three agents interact, data sharing between agents
- Data Flow — input/output file flow, model relationships (class diagram), output directory structure
Configuration
Language Setup
Each language needs:
- A
Languageconfig (code, name, family, pivot_code, pivot_name) - A phonology reference file (
references/<lang>_phonology.md)
from models import Language
sentani = Language(
code="shj",
name="Sentani",
family="Trans-New Guinea",
pivot_code="ind", # tesseract lang code for gloss language
pivot_name="Bahasa Indonesia",
)
Phonology Reference
Copy the template and fill in orthography rules:
cp references/phonology_template.md references/sentani_phonology.md
Key sections:
- Entry splitting — regex pattern to cut entries in the text layer
- Entry pattern — regex to match a valid headword + gloss
- Valid characters — character set for headwords
- OCR confusion pairs — known OCR errors for this language
- Morphology rules — reduplication, affixes, verb forms
Pivot Language
Bahasa Indonesia is the default pivot. Override with:
--pivot-code eng --pivot-name "English"
Multi-Book Workflow
When extracting multiple dictionaries for the same language:
Directory Structure
out/
shj/ # Language level
sentani_phonology.md # Shared orthography reference
conventions_shj.md # Cumulative conventions
corpus_shj.jsonl # Merged corpus from all books
cross_book_conflicts.md # Conflicting headwords
books/
set/ # Book: "Set Kamus Sentani"
entries.jsonl
book_profile.md
flagged_terms.md
conventions_set.md
sentani_kamus/ # Book: "Kamus Bahasa Sentani"
entries.jsonl
book_profile.md
conventions_sentani_kamus.md
Step-by-Step
# 1. Extract each book with --book-id
nusantara-corpus-extractor extract \
--pdf "dictionaries/Set-Kamus-Sentani.pdf" \
--book-id set \
--lang-code shj --lang-name Sentani \
--phonology references/sentani_phonology.md
nusantara-corpus-extractor extract \
--pdf "dictionaries/Kamus Bahasa Sentani.pdf" \
--book-id kamus \
--lang-code shj --lang-name Sentani \
--phonology references/sentani_phonology.md
# 2. Merge into single language corpus
nusantara-corpus-extractor merge --lang-code shj
# 3. Resolve cross-book conflicts (if any)
# Edit out/shj/cross_book_conflicts.md, then re-run merge
Merge Rules
flowchart TD
subgraph Input["Input: entries from multiple books"]
A["entries_A.jsonl"] --> M{"corpus_merger.py"}
B["entries_B.jsonl"] --> M
end
subgraph Rules["Merge Rules"]
M -->|"same headword\nsame gloss"| S1["Keep one\n(higher confidence)"]
M -->|"same headword\nsimilar gloss"| S2["Merge\n(keep longer gloss)"]
M -->|"same headword\ndifferent gloss"| S3["Multi-sense entry\n(1) gloss_A; (2) gloss_B"]
M -->|"same headword\nconflicting gloss"| S4["Flag conflict\nfor human review"]
end
S1 --> OUT["corpus.jsonl"]
S2 --> OUT
S3 --> OUT
S4 --> CBC["cross_book_conflicts.md"]
style M fill:#fff3e0,stroke:#f57c00
style OUT fill:#e8f5e9,stroke:#388e3c
style CBC fill:#ffebee,stroke:#d32f2f
| Scenario | Action |
|---|---|
| Same headword, same gloss | Keep one (higher confidence wins) |
| Same headword, similar gloss (>80% overlap) | Merge, keep longer gloss |
| Same headword, different gloss | Merge into multi-sense entry |
| Same headword, conflicting glosses | Flag in cross_book_conflicts.md |
Conventions System
Per-book (books/<book_id>/conventions_<book_id>.md):
- Snapshot of what THIS book looks like
- Read-only after creation (preserves original format)
Cumulative (out/<lang>/conventions_<lang>.md):
- Patterns observed across ALL books for this language
- Updated after each book extraction
- Informs the conventions agent when extracting a new book
Multi-Language Projects
Directory Structure
out/
shj/ # Sentani
corpus_shj.jsonl
books/
set/
kamus/
bhw/ # Biak
corpus_bhw.jsonl
books/
kamus_biak/
lni/ # Lani
corpus_lni.jsonl
books/
kamus_lani_wone/
kamus_lengkap_lani/
Cross-Language Considerations
- Each language has its own phonology reference, conventions, and corpus
- The phonology reference is per-language (different orthography rules)
- The pivot language is configurable per language (Indonesian, English, etc.)
- There is no cross-language merging — each language produces an independent corpus
Output Format
JSONL Corpus
Each line is a dictionary entry:
{
"id": "a1b2c3d4",
"headword": "bo",
"pos": "n",
"gloss_pivot": "(1) pohon; (2) kayu",
"examples": ["bo fau ...", "bo siro ..."],
"page_ref": 42,
"confidence": 0.95,
"source_language": "shj",
"source_book": "set",
"source_page": 42
}
Report Files
| File | Content |
|---|---|
book_profile.md |
Book kind, page zones, conventions detected |
flagged_terms.md |
Open/resolved flags with web evidence |
pattern_insights.md |
Systematic issues across flags |
quality_report_N.md |
Per-pass stats (entries in/out, flags, convergence) |
cross_book_conflicts.md |
Conflicting headwords across books |
Reference
Scripts
| Script | Purpose |
|---|---|
scripts/models.py |
Domain model (Language, DictionaryEntry, FlaggedTerm, etc.) |
scripts/pdf_parser.py |
PDF parsing, digital/OCR page detection |
scripts/book_profiler.py |
Book classification, zone splitting |
scripts/conventions_extractor.py |
Entry layout analysis, pattern detection |
scripts/morphology_rules.py |
Reduplication, affix detection, root finding |
scripts/translation_checker.py |
Gloss validation, example checking |
scripts/homonym_resolver.py |
Homonym/variant classification |
scripts/entry_extractor.py |
Raw text → DictionaryEntry objects |
scripts/typo_corrector.py |
OCR confusion fixing |
scripts/meaning_crosscheck.py |
Gloss cross-checking |
scripts/pattern_spotter.py |
Systematic issue detection |
scripts/web_verification.py |
Web search queue management |
scripts/quality_loop.py |
Loop orchestrator, convergence logic |
scripts/corpus_writer.py |
JSONL + markdown output |
scripts/corpus_merger.py |
Multi-book corpus merging |
scripts/cli.py |
CLI entry point (extract/merge) |
Agent Specifications
| Agent | File | Purpose |
|---|---|---|
| Extraction Agent | agents/extraction-agent.md |
Full loop orchestration |
| Conventions Agent | agents/conventions-agent.md |
Book structure analysis |
| Correction Agent | agents/correction-agent.md |
Linguistic validation |
Skills
| Skill | File | Purpose |
|---|---|---|
| Corpus Extractor | SKILL.md |
Umbrella extraction skill: pipeline loop, language setup, quality loop |
| Conventions Management | .opencode/skills/conventions-management/SKILL.md |
Pattern detection, conventions workflow |
| Linguistic Correction | .opencode/skills/linguistic-correction/SKILL.md |
Translation validation, homonym resolution |
Publishing a release
The Python package is published to PyPI automatically by the
.github/workflows/publish.yml workflow whenever you push a v* tag (or
publish a GitHub Release). It builds the sdist + wheel, runs twine check,
then uploads using the PYPI_API_TOKEN repository secret.
Only the repo owner can publish. The workflow is gated so it runs only
when triggered by the owner (github.actor == github.repository_owner);
collaborators pushing a tag or publishing a release will have the publish
job skipped.
# Bump the version in pyproject.toml, then tag and push (as the owner)
git tag v0.2.0
git push origin v0.2.0
The workflow uploads dist/* to PyPI; the nusantara-corpus-extractor
package becomes available for pip install shortly after.
Extending
Adding a New Language
- Create a
Languageinstance:Language(code="xyz", name="XYZ", family="Austronesian", pivot_code="ind", pivot_name="Bahasa Indonesia")
- Copy and fill in
references/phonology_template.md→references/<lang>_phonology.md - Run extraction with
--lang-code xyz
Adding a New Dictionary
- Place the PDF in
dictionaries/ - Run extraction with
--book-id <descriptive_id> - Merge into the language corpus
Customizing Morphology Rules
Edit the conventions file or phonology reference to add language-specific affix patterns, reduplication rules, or verb conjugation patterns.
Contributing
See docs/diagrams/ for architecture diagrams, AGENTS.md for agent guidelines.
Metadata
Release files for nusantara-corpus-extractor 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| nusantara_corpus_extractor-0.1.0.tar.gz | 64.4 kB | Without provenance | Details for nusantara_corpus_extractor-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| nusantara_corpus_extractor-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for nusantara_corpus_extractor-0.1.0-py3-none-any.whl |
Total release size: 121.3 kB
Release files / nusantara_corpus_extractor-0.1.0.tar.gz
| Download URL | nusantara_corpus_extractor-0.1.0.tar.gz |
|---|---|
| Size | 64.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3375ce702a467c0c266281b74a9f91c259f7c6f64e83dfd33dae6f06cd99f101
|
|
BLAKE2b-256 checksum How to use checksums |
b7358f227385234f6f4a759058473e41a0726af1c564e0aad959ae9336e680f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / nusantara_corpus_extractor-0.1.0-py3-none-any.whl
| Download URL | nusantara_corpus_extractor-0.1.0-py3-none-any.whl |
|---|---|
| Size | 56.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
de01858d803d3363e00ee5f5aa05cf8b445b9c1868ad711789df57d9360ca0ad
|
|
BLAKE2b-256 checksum How to use checksums |
7fac225f3f5614e63625315cd8ce4ed1a5d635221e5c9d4fd06d57abd270b599
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for nusantara-corpus-extractor 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.