TextStruct is a fully local, open-source pipeline for converting scanned textbooks into structured, LLM-ready data.
No APIs. No cloud services. Runs entirely offline after model download.
- Converts scanned PDFs into images
- Runs layout-aware OCR locally (DocTR)
- Intelligent text cleanup (headers/footers, OCR artifacts)
- Section and subsection detection
- Semantic chunking with embeddings
- NEW: Pedagogical role classification (definitions, examples, procedures, etc.)
- Outputs structured, RAG-ready JSON
- Designed as a foundation for:
- Educational knowledge extraction
- Intent-aware RAG systems
- Test generation
- Study material creation
- ❌ Chat UI
- ❌ Cloud OCR
- ❌ External APIs
- Docker
- NVIDIA GPU + drivers (optional for acceleration)
- nvidia-container-toolkit (for GPU support)
- Python 3.9+ (if running without Docker)
TextStruct runs on CPU out of the box — no GPU required.
docker compose build
docker compose run --rm textstruct \
python src/main.py --batch data/input --infer-chaptersGPU acceleration is opt-in via an override file (requires the
nvidia-container-toolkit on the host). Layer docker-compose.gpu.yml on top of
the base compose file:
docker compose -f docker-compose.yml -f docker-compose.gpu.yml run --rm textstruct \
python src/main.py --batch data/input --infer-chapters --fastTests run from the repo root (pyproject.toml puts both the root and src/ on
the path):
pip install -r requirements.txt -r requirements-dev.txt
pytestOr inside Docker:
docker compose run --rm textstruct pytestTextStruct supports GPU-batched OCR for significantly faster processing.
- NVIDIA GPU with ≥16 GB VRAM
- CUDA-enabled Docker runtime
python src/main.py --batch data/input --infer-chapters --fast- CPU: 4+ cores recommended
- RAM: 4-8 GB minimum, 16 GB recommended for large PDFs
- Disk: 2-5 GB for models + cache
- Network: Optional (only for LLM features)
| Pipeline Stage | CPU | GPU | RAM | Network | CPU-Only? | Optimization Available |
|---|---|---|---|---|---|---|
| PDF → Images | ✓ | ✗ | 500MB-2GB | ✗ | ✅ Yes | DPI reduction |
| OCR (default) | ✓ | ✗ | 2-4GB | ✗ | ✅ Yes | Model selection |
OCR (--fast) |
partial | ✓ | 4-8GB | ✗ | ✅ Yes (fallback) | GPU acceleration |
| Embeddings (auto) | ✓ | auto | 2-4GB | ✗ | ✅ Yes | GPU if available |
| Semantic Merge | ✓ | ✗ | 500MB | ✗ | ✅ Yes | Batch processing |
| Role Classification | ✓ | ✗ | 500MB | ✗ | ✅ Yes | 3-tier cascade |
| Role LLM Fallback | ✗ | ✗ | 500MB | ✓ | Requires Ollama | |
| Math Cleanup | ✓ | ✗ | 300MB | ✗ | ✅ Yes | Regex patterns |
| Math LLM Cleanup | ✗ | ✗ | 300MB | ✓ | Requires Ollama | |
| Vector Store | ✓ | ✗ | 1-3GB | ✗ | ✅ Yes | NumPy operations |
Legend:
- ✓ = Used
- ✗ = Not used
- auto = Auto-detected
- partial = Light usage
- ✅ Yes = Fully functional without GPU
⚠️ Optional = Feature can be disabled
python src/main.py input.pdf --refine --semantic-chunk- Speed: ~2-5 min per 100-page PDF
- Hardware: CPU + 4GB RAM
- Network: Not required
- Accuracy: Good (heuristic role classification, regex math cleanup)
python src/main.py input.pdf --fast --refine --semantic-chunk- Speed: ~30-60 sec per 100-page PDF (3-5× faster OCR)
- Hardware: GPU + 8GB RAM
- Network: Not required
- Accuracy: Same as default
python src/main.py input.pdf --refine --semantic-chunk --classify-roles-llm --fix-math-ollama- Speed: ~5-15 min per 100-page PDF (LLM overhead)
- Hardware: CPU + 4GB RAM
- Network: Ollama required
- Accuracy: Highest (LLM-based role classification and math cleanup)
python src/main.py input.pdf --fast --refine --semantic-chunk --classify-roles-llm --fix-math-ollama- Speed: ~1-3 min per 100-page PDF
- Hardware: GPU + 8GB RAM + Ollama
- Network: Ollama required
- Accuracy: Highest
TextStruct implements aggressive caching to speed up repeated operations:
-
Embedding Cache (
.doctr_cache/embeddings/)- Stores all computed embeddings as hash-keyed
.npyfiles - Cache hit = instant retrieval (no recomputation)
- Shared across all pipeline runs
- Stores all computed embeddings as hash-keyed
-
Model Cache (
~/.cache/huggingface/)- Sentence-transformers model (~420MB)
- DocTR OCR models (~100-300MB)
- Downloaded once, cached forever
-
Ollama Model Cache
- LLM model stays warm in Ollama container
- First request: ~1-2s (model load)
- Subsequent requests: ~50-200ms
-
OCR Results (Future improvement)
- Currently NOT cached
- Re-OCR on every run (even if PDF unchanged)
| Flag | Effect | Default | Hardware Impact |
|---|---|---|---|
--fast |
Enable GPU for OCR + faster models | OFF | GPU used if available, CPU fallback |
--force-cpu |
Force CPU-only execution | OFF | Disables GPU even if available |
--classify-roles-llm |
Use LLM for uncertain role classification | OFF | Network required (Ollama) |
--fix-math-ollama |
Use LLM for complex math cleanup | OFF | Network required (Ollama) |
--quiet |
Suppress info logs | OFF | No hardware impact |
--verbose |
Show debug logs | OFF | No hardware impact |
| Bottleneck | Impact | Workaround | Future Fix |
|---|---|---|---|
| Sequential OCR | 5-10 min for 100 pages | Use --fast flag |
Batch OCR processing |
| Sequential LLM requests | 1-3s per uncertain chunk | Disable --classify-roles-llm |
Batch LLM API |
| Embedding recomputation | 30-60s for 500 chunks | Cache hits automatic | Semantic deduplication |
| Linear vector search | Slow for >10k chunks | Use smaller PDFs | ANN indexing (FAISS) |
For Development/Testing:
python src/main.py sample.pdf --fast --refine --semantic-chunk --quietFor Production (Accuracy Priority):
python src/main.py input.pdf --refine --semantic-chunk --classify-roles-llm --build-indexFor Production (Speed Priority):
python src/main.py input.pdf --fast --refine --semantic-chunk --build-indexFor Batch Processing:
python src/main.py --batch data/input/ --fast --refine --semantic-chunk --quietTextStruct can classify chunks into 10 pedagogical roles for intent-aware RAG:
definition- Formal definitionsexplanation- Why/how something worksprocedure- Step-by-step instructionsexample- Worked examplesquestion- Practice questions/exercisesresult- Theorems, conclusionsreference- Tables, formulas, lookup dataevidence- Citations, data-backed claimsvisual_proxy- Figure/diagram descriptionscontext- Background, history, motivation
docker compose run --rm textstruct \
python src/main.py data/input/textbook.pdf \
--refine \
--semantic-chunk \
--classify-roles- ~10-15s for 100 chunks on CPU
- 75-85% accuracy
- Uses pattern matching + embedding similarity
docker compose run --rm textstruct \
python src/main.py data/input/textbook.pdf \
--refine \
--semantic-chunk \
--classify-roles-llm- ~2-5 minutes for 100 chunks
- 85-95% accuracy
- Requires Ollama running on llm-net network
- Uses LLM for uncertain cases only
{
"id": "chunk_sem_00042",
"text": "A prime number is...",
"metadata": {
"pedagogical_role": "definition",
"role_confidence": 0.92,
"semantic_coherence_score": 0.87,
...
}
}--refine- Clean OCR artifacts and separate pedagogical callouts--semantic-chunk- Merge similar chunks using embeddings (requires--refine)--classify-roles- Fast role classification (requires--semantic-chunk)--classify-roles-llm- High-precision LLM classification (requires Ollama)
--fast- GPU-accelerated OCR (requires ≥16GB VRAM)--layout-aware- Handle multi-column layouts
--debug-clean- Save before/after cleanup text--debug-layout- Save annotated images showing bboxes, reading order, column boundaries (requires--layout-aware)
docker compose run --rm textstruct \
python src/main.py data/input/textbook.pdf \
--layout-aware \
--refine \
--semantic-chunk \
--classify-roles \
--fastTextStruct now supports LanceDB for high-performance vector storage and retrieval, replacing the legacy pickle-based in-memory store.
- 10-100x faster search on large collections (ANN indexing with IVF-PQ)
- SQL-like filtering with WHERE clauses, range queries, OR logic
- Parquet-backed persistence (no serialization overhead)
- Scalable to millions of vectors with efficient disk-based storage
- Backward compatible with existing pickle indexes
LanceDB support requires additional dependencies:
pip install lancedb>=0.3.0 pyarrow>=14.0.0Or using Docker (already included in Dockerfile):
docker compose buildpython src/main.py input.pdf \
--semantic-chunk \
--build-index \
--classify-rolesThis creates data/output/<pdf_name>/vector_store.lancedb/ directory.
LanceDB enables advanced filtering beyond simple key-value matching:
python src/query.py data/output/textbook/vector_store.lancedb \
--query "triangle theorems" \
--sql-filter "page_start >= 10 AND page_start <= 20" \
--top-k 5python src/query.py data/output/textbook/vector_store.lancedb \
--query "area formulas" \
--sql-filter "pedagogical_role = 'definition' AND role_confidence > 0.8"python src/query.py data/output/textbook/vector_store.lancedb \
--query "worked examples" \
--sql-filter "pedagogical_role = 'example' OR pedagogical_role = 'procedure'"python src/query.py data/output/textbook/vector_store.lancedb --interactive
> triangle area sql:page_start >= 5 AND page_start <= 15
> examples sql:role_confidence > 0.85| Dataset Size | Pickle (Linear) | LanceDB (No Index) | LanceDB (IVF-PQ) |
|---|---|---|---|
| 40 chunks | 1-2 ms | 2-3 ms | 3-5 ms |
| 400 chunks | 25-50 ms | 30-60 ms | 5-10 ms |
| 4,000 chunks | 250-500 ms | 300-600 ms | 15-30 ms |
| 40,000 chunks | 2.5-5 s | 3-6 s | 50-100 ms |
Indexing time: ~5-15s for 1,000 chunks (one-time cost)
Migrate existing pickle indexes:
python src/migrate_to_lancedb.py \
--pickle data/output/textbook/vector_store.pkl \
--lancedb data/output/textbook/vector_store.lancedb \
--create-index \
--verifyBatch migration:
python src/migrate_to_lancedb.py \
--batch-dir data/output/ \
--create-indexThe migration tool:
- Converts all metadata to LanceDB schema
- Creates IVF-PQ index automatically
- Verifies data integrity with random sampling
- Preserves all semantic chunk metadata
TextStruct maintains full backward compatibility:
- Automatic fallback: If LanceDB not installed, uses pickle backend
- Dual format support: Can read both
.pkland.lancedbstores - Query interface: All existing query code works unchanged
- Migration path: Easy upgrade with verification
LanceDB stores flattened metadata for SQL queries:
{
"id": "chunk_sem_00001",
"text": "A prime number is...",
"vector": [0.123, -0.456, ...], # 768-dim text embedding
# Pedagogical metadata (flat for SQL)
"pedagogical_role": "definition",
"role_confidence": 0.92,
"section": "1.2 Prime Numbers",
"page_start": 15,
"page_end": 15,
# Vision metadata (Phase 5.v)
"has_visual_content": true,
"image_embedding": [0.789, -0.234, ...], # 512-dim CLIP
"image_path": "data/output/.../rois/roi_page0015_block0003_figure.png",
"visual_type": "figure",
# Full metadata as JSON (for complex nested structures)
"metadata_json": "{...}"
}| Flag | Description | Default |
|---|---|---|
--build-index |
Build vector store after chunking | OFF |
--sql-filter <WHERE> |
SQL filtering (query mode only) | None |
--vector-backend lancedb|inmemory |
Choose backend explicitly | lancedb |
TextStruct can now extract visual content (figures, tables, diagrams) and enable hybrid text + vision retrieval using CLIP embeddings.
- Semantic image search: Find diagrams by text description
- Hybrid retrieval: Combine text and visual similarity
- Figure-aware chunking: Automatically link text to related visuals
- Cross-modal queries: Text→image and image→image search
Vision-RAG requires additional dependencies:
pip install transformers>=4.30.0 open-clip-torch>=2.20.0Or using Docker (already included):
docker compose buildEnable vision extraction during pipeline:
python src/main.py input.pdf \
--layout-aware \
--detect-tables \
--detect-figures \
--semantic-chunk \
--build-index \
--extract-visualsWhat it does:
- Detects figure/table regions using Phase 4.x layout analysis
- Crops ROIs with 10px padding and saves as separate images
- Generates CLIP embeddings (512-dim) for each ROI
- Links ROIs to semantic chunks based on page proximity
- Stores both text (768-dim) and vision (512-dim) embeddings in LanceDB
Output structure:
data/output/<pdf_name>/
├── rois/
│ ├── roi_page0012_block0005_figure.png
│ ├── roi_page0015_block0002_table.png
│ └── ...
├── chunks_semantic.json # Includes visual metadata
└── vector_store.lancedb/ # Hybrid text+vision index
python src/query.py data/output/textbook/vector_store.lancedb \
--query "triangle area formula"python src/query.py data/output/textbook/vector_store.lancedb \
--query-image path/to/diagram.pngpython src/query.py data/output/textbook/vector_store.lancedb \
--query "geometric proof" \
--query-image sample_diagram.png \
--text-weight 0.6 \
--vision-weight 0.4Fusion formula:
score = (text_weight × text_similarity) + (vision_weight × vision_similarity)
Chunks with linked visual content include:
{
"id": "chunk_sem_00042",
"text": "Figure 3.2 shows the relationship between...",
"metadata": {
"pedagogical_role": "visual_proxy",
"has_visual_content": true,
"image_path": "data/output/.../rois/roi_page0015_block0003_figure.png",
"visual_type": "figure",
"bbox": [120, 450, 480, 720],
"page_start": 15,
...
}
}TextStruct supports two vision encoders:
- Model: OpenAI CLIP ViT-B/32
- Embedding dim: 512
- Use case: General figures, diagrams, photos
- Speed: ~50-100ms per image (GPU), ~200-400ms (CPU)
- Quality: Excellent for semantic visual search
python src/main.py input.pdf \
--extract-visuals \
--vision-encoder clip- Specialized for: Dense documents, tables, multi-column layouts
- Fallback: Uses CLIP if ColPali not available
- Use case: Complex tables, multi-column text regions
python src/main.py input.pdf \
--extract-visuals \
--vision-encoder colpaliVision Extraction:
- ~50-100ms per ROI (GPU)
- ~200-400ms per ROI (CPU)
- Cached embeddings reused on subsequent runs
Hybrid Search:
- Text-only: ~5-50ms (depends on index size)
- Vision-only: ~10-80ms
- Hybrid: ~15-100ms
Storage Overhead:
- ~100-500 KB per extracted ROI image (PNG)
- ~2 KB per vision embedding (.npy)
- Example: 100-page textbook with 50 figures ≈ 25-50 MB
Vision embeddings are cached to disk (same as text embeddings):
data/output/.doctr_cache/vision_embeddings/
├── cache_manifest.json
├── <hash1>.npy # CLIP embedding for image 1
├── <hash2>.npy
└── ...
Cache hits: Instant retrieval (no recomputation)
| Flag | Description | Default |
|---|---|---|
--extract-visuals |
Extract figure/table ROIs and generate embeddings | OFF |
--vision-encoder clip|colpali |
Choose vision encoder | clip |
--query-image <path> |
Image query for retrieval | None |
--text-weight <0-1> |
Text similarity weight in hybrid search | 0.7 |
--vision-weight <0-1> |
Vision similarity weight in hybrid search | 0.3 |
- Minimum:
--layout-aware --semantic-chunkmust be enabled - Recommended:
--detect-tables --detect-figuresfor better ROI detection - GPU: Optional but recommended for faster vision encoding
python src/main.py data/input/geometry_textbook.pdf \
--layout-aware \
--detect-tables \
--detect-figures \
--refine \
--semantic-chunk \
--classify-roles \
--build-index \
--extract-visuals \
--fastOutput:
- Structured semantic chunks with pedagogical roles
- LanceDB vector store with text + vision embeddings
- Extracted ROI images in
rois/directory - Hybrid search support for text and image queries
python src/query.py data/output/textbook/vector_store.lancedb \
--query "prime factorization" \
--sql-filter "pedagogical_role = 'definition' AND role_confidence > 0.85 AND page_start >= 20 AND page_start <= 40" \
--top-k 3python src/query.py data/output/textbook/vector_store.lancedb \
--query "area calculation" \
--sql-filter "(pedagogical_role = 'example' OR pedagogical_role = 'procedure') AND has_visual_content = true"python src/query.py data/output/textbook/vector_store.lancedb \
--query "triangle congruence proof" \
--query-image reference_diagram.png \
--text-weight 0.5 \
--vision-weight 0.5 \
--top-k 5python src/query.py data/output/textbook/vector_store.lancedb --interactive
> prime numbers
> definitions sql:role_confidence > 0.9
> examples sql:page_start >= 10 AND page_start <= 20
> exitComplete output directory structure with all features enabled:
data/output/<pdf_name>/
├── images/
│ ├── page_0001.png
│ ├── page_0002.png
│ └── ...
├── rois/ # Phase 5.v visual content
│ ├── roi_page0012_block0005_figure.png
│ ├── roi_page0015_block0002_table.png
│ └── ...
├── chunks_raw.json # Raw OCR blocks
├── chunks_clean.json # After cleanup
├── chunks_semantic.json # After semantic merging + visual linking
├── vector_store.lancedb/ # LanceDB index
│ ├── data.lance
│ ├── _versions/
│ └── ...
└── .doctr_cache/
├── embeddings/ # Text embeddings
│ ├── cache_manifest.json
│ └── *.npy
└── vision_embeddings/ # Vision embeddings
├── cache_manifest.json
└── *.npy
--refine- Clean OCR artifacts, separate pedagogical callouts--semantic-chunk- Merge similar chunks using embeddings--classify-roles- Fast role classification (heuristics + embeddings)--classify-roles-llm- High-precision LLM classification (requires Ollama)--build-index- Build LanceDB vector store (enables retrieval)
--layout-aware- Handle multi-column layouts, reading order--detect-tables- Detect table regions--detect-figures- Detect figure/diagram regions--extract-visuals- Extract ROIs and generate vision embeddings (Phase 5.v)--vision-encoder clip|colpali- Choose vision encoder (default: clip)
--fast- GPU-accelerated OCR (requires ≥16GB VRAM)--force-cpu- Force CPU-only execution
--query <text>- Text query--query-image <path>- Image query (vision-only or hybrid)--sql-filter <WHERE>- SQL filtering (LanceDB only)--text-weight <0-1>- Text weight in hybrid search (default: 0.7)--vision-weight <0-1>- Vision weight in hybrid search (default: 0.3)--top-k <N>- Number of results (default: 5)--interactive- Interactive query mode
--debug-clean- Save before/after cleanup text--debug-layout- Save annotated layout images--quiet- Suppress info logs--verbose- Show debug logs
python src/main.py input.pdf \
--layout-aware \
--detect-tables \
--detect-figures \
--refine \
--semantic-chunk \
--classify-roles-llm \
--build-index \
--extract-visualsUse case: Extracting high-quality structured data from textbooks for RAG systems, test generation, study guides
python src/main.py --batch data/input/ \
--fast \
--refine \
--semantic-chunk \
--classify-roles \
--build-index \
--quietUse case: Processing large document collections quickly
python src/main.py input.pdf \
--layout-aware \
--detect-figures \
--semantic-chunk \
--build-index \
--extract-visuals \
--vision-encoder clipUse case: Math, physics, engineering textbooks with heavy visual content
python src/main.py input.pdf \
--refine \
--semantic-chunk \
--force-cpu \
--quietUse case: Quick local processing without GPU or network dependencies
- Phase 3.y: Pedagogical role classification with LLM fallback
- Phase 4.x: Layout-aware OCR, table/figure detection
- Phase 5.x: LanceDB vector store with SQL filtering
- Phase 5.v: Vision-RAG integration with CLIP/ColPali
- Future (Phase 5.x+): Pedagogical bundling (definitions + examples)
See ToDo.md for detailed implementation notes and roadmap