Intelligent document processing for contracts, agreements, and legal documents. Extracts text, classifies document types, detects execution status, and organizes files automatically — via CLI or REST API.
git clone https://github.com/Mo-Awadalla/LegalDocuMan.git
cd LegalDocuMan
docker compose up --buildThen upload a document:
curl -X POST http://localhost:5000/api/v1/upload \
-F "file=@/path/to/your/contract.pdf"Check processing status:
curl http://localhost:5000/api/v1/jobs/<job_id>- Document Classification — auto-detects MSA, SOW, NDA, PO, Amendment, License
- Execution Status Detection — deterministic regex + RF-DETR visual signature detection to distinguish executed (
_final) from non-executed (_supporting) documents. RF-DETR is core to the classification pipeline — the model checkpoint is downloaded at Docker build time from huggingface.co/Mo-Awadalla/legaldocuman-rfdetr (or must be provided atmodels/checkpoint_best_total.pthfor local development). - Smart Naming —
K_VendorName_documentType_001.pdforYYYYMMDD_Vendor_Original.pdf - Folder Organization —
_final(executed) vs_supporting(unsigned / draft / exhibit) - Date Extraction — effective, expiration, renewal, and review dates from content
- Contract Lifecycle Dashboard — surfaces overdue and upcoming expiration, renewal, review, and termination dates
- Full-Text Contract Search — searches filenames, vendors, and persisted extracted document text
- Vendor Matching — fuzzy match against a master vendor list
- Pluggable OCR Backends — Tesseract (default, local) or NVIDIA (NeMo / Triton / TAO, stub ready)
- Retention Category Mapping — auto-assigns retention policies (long_term, indefinite, short_term, tied_to_parent)
- REST API — upload documents, poll job status, retrieve metadata (type, vendor, execution status, dates, retention category, generated filename)
- PostgreSQL persistence — all document records stored with checksum, file size, and full metadata
- Docker-ready — single
docker compose upspins up the app + database
git clone https://github.com/Mo-Awadalla/LegalDocuMan.git
cd LegalDocuMan
pip install -r requirements.txtFor the fastest non-Docker loop, run the Flask app with SQLite:
python -m venv .venv
.venv/Scripts/python -m pip install -r requirements.txt
DATABASE_URL=sqlite:///legaldocuman.db \
UPLOAD_FOLDER=./uploads \
PROCESSED_FOLDER=./processed \
SECRET_KEY=dev-local-secret \
.venv/Scripts/python run.pyOpen http://localhost:5000. Build the React app first if you want Flask to serve the latest frontend:
cd frontend
npm ci
npm run build
cd ..SQLite is for local development only. Use PostgreSQL for shared environments and pilots.
Local development can use SQLite and local uploads/ / processed/ folders. Do not use those defaults for shared pilots or production.
Before sharing an instance, set explicit environment values:
SECRET_KEY=<strong-random-secret>
API_KEY=<private-pilot-api-key>
DOWNLOAD_TOKEN_TTL_SECONDS=300
RATE_LIMIT_ENABLED=1
RATE_LIMIT_AUTH_PER_MINUTE=10
RATE_LIMIT_UPLOAD_PER_MINUTE=30
CORS_ORIGINS=https://your-domain.example
DATABASE_URL=postgresql://user:password@db:5432/legaldocuman
UPLOAD_FOLDER=/srv/legaldocuman/uploads
PROCESSED_FOLDER=/srv/legaldocuman/processed
JOB_BACKEND=rq
REDIS_URL=redis://localhost:6379/0
AUTO_CREATE_DB=0Browser users should sign in through POST /api/v1/auth/login; the React app stores the returned bearer token and sends it on API requests. Do not embed API_KEY values in frontend builds. API_KEY is only for private server-to-server or scripted API clients that can keep the key secret and send X-API-Key: API_KEY_VALUE or Authorization: Bearer API_KEY_VALUE.
Avoid committing .env files or uploaded/processed documents.
Use database migrations instead of startup table creation, and run the web app behind a production WSGI server with the worker as a separate process:
AUTO_CREATE_DB=0 flask --app run.py db upgrade
JOB_BACKEND=rq gunicorn --bind 0.0.0.0:5000 --workers 2 --threads 4 --timeout 120 run:app
AUTO_CREATE_DB=0 python -m legaldocuman.app.workerThe Docker image defaults to gunicorn instead of Flask's development server. docker compose up --build also runs the database migration before starting gunicorn, and disables startup table creation in the app and worker containers with AUTO_CREATE_DB=0.
The app includes the first customer-grade controls beyond the private API key gate:
- User accounts with tenant scoping and roles:
admin,reviewer,user. - Bootstrap the first admin with
BOOTSTRAP_ADMIN_EMAILandBOOTSTRAP_ADMIN_PASSWORDbefore first startup. - Login endpoint:
POST /api/v1/auth/login; use the returned bearer token for API calls. - Browser downloads should mint a short-lived token with
POST /api/v1/documents/<id>/download-token, then callGET /api/v1/documents/<id>/download?download_token=.... - Basic in-memory per-client rate limits cover login and upload endpoints for single-process private pilots/tests.
- Baseline security headers are applied to every Flask response (CSP, nosniff, referrer, frame, and permissions policies).
- Manual review/correction endpoint and UI on the document detail page.
- Audit trail events for login, upload, processing, and manual updates.
- Built-in malware scanning for empty/EICAR files, plus optional
MALWARE_SCANNER=clamav. - Object storage abstraction with
STORAGE_BACKEND=s3whenboto3and AWS credentials are configured. - Redis/RQ worker process via
python -m legaldocuman.app.workerand docker-composeworkerservice.
Tesseract OCR + Poppler (required for PDF text extraction):
# macOS
brew install tesseract poppler
# Ubuntu/Debian
sudo apt-get install tesseract-ocr poppler-utils
# Windows — download from https://github.com/UB-Mannheim/tesseract/wiki
# Then set the path in .env: TESSERACT_PATH=C:\\Program Files\\Tesseract-OCR\\tesseract.exeNVIDIA OCR (interface only; implementation still required before use):
# The backend intentionally reports unavailable until image_to_text/pdf_to_text
# are implemented with a real NVIDIA client.
export OCR_BACKEND=nvidia
export NVIDIA_API_KEY=nvapi-your-key-hereCopy the example env file and fill in your values:
cp .env.example .envKey variables:
| Variable | Default | Description |
|---|---|---|
OCR_BACKEND |
tesseract |
tesseract or nvidia |
NVIDIA_API_KEY |
— | Your NVIDIA API key (required only if OCR_BACKEND=nvidia) |
NVIDIA_API_BASE_URL |
https://integrate.api.nvidia.com/v1 |
NVIDIA API endpoint |
NVIDIA_OCR_MODEL |
nvidia/nemotron-ocr-v2 |
Model identifier |
TESSERACT_PATH |
auto-detect | Override Tesseract binary path |
POPLER_PATH |
auto-detect | Override Poppler binary path (pdf2image) |
DOWNLOAD_TOKEN_TTL_SECONDS |
300 |
Seconds before signed document download tokens expire |
RATE_LIMIT_ENABLED |
1 |
Enable in-memory login/upload rate limiting |
RATE_LIMIT_AUTH_PER_MINUTE |
10 |
Login attempts allowed per client per window |
RATE_LIMIT_UPLOAD_PER_MINUTE |
30 |
Upload attempts allowed per client per window |
.env is gitignored — never commit it.
Start the server:
docker compose up # Docker
# or
python run.py # Manual — requires PostgreSQL runningUpload a document:
curl -X POST http://localhost:5000/api/v1/upload \
-H "Authorization: Bearer REPLACE_WITH_LOGIN_TOKEN" \
-F "file=@/path/to/contract.pdf"Response:
{"id": 1, "job_id": 1, "status": "pending", "job_status": "completed"}Mint a browser download token and download:
curl -s -X POST http://localhost:5000/api/v1/documents/1/download-token \
-H "Authorization: Bearer REPLACE_WITH_LOGIN_TOKEN" > download-token.json
# Copy the download_token value from download-token.json into the URL below.
curl -L "http://localhost:5000/api/v1/documents/1/download?download_token=REPLACE_WITH_DOWNLOAD_TOKEN" -o contract.pdfCheck job status:
curl http://localhost:5000/api/v1/jobs/1 \
-H "Authorization: Bearer PASTE_LOGIN_TOKEN"Response:
{
"id": 1,
"original_name": "contract.pdf",
"status": "completed",
"document_type": "MSA",
"vendor": "Acme Corp",
"execution_status": "executed",
"effective_date": "2024-01-01",
"expiration_date": "2026-12-31",
"retention_category": "long_term",
"generated_filename": "K_AcmeCorp_MasterServiceAgreement_001.pdf",
"created_at": "2026-05-24T10:00:00"
}from legaldocuman import DocumentProcessor
processor = DocumentProcessor("/path/to/contracts")
processor.process_contracts_enhanced()
processor.print_summary()from legaldocuman import DocumentProcessor
from legaldocuman.backends import NvidiaOCRBackend
ocr = NvidiaOCRBackend()
processor = DocumentProcessor("/path/to/contracts", ocr_backend=ocr)
processor.process_contracts_enhanced()Or set the env var:
export OCR_BACKEND=nvidia
export NVIDIA_API_KEY=nvapi-your-key-here
python your_script.pyprocessor = DocumentProcessor("/path/to/contracts")
processor.sort_files_by_year("/path/to/archive", year_threshold=2017)LegalDocuMan/
├── legaldocuman/ # Main package
│ ├── config.py # Centralized config (env vars, constants)
│ ├── processor.py # DocumentProcessor orchestrator
│ ├── extractors.py # TextExtractor (PDF, DOCX, TXT + OCR)
│ ├── classifiers.py # DocumentTypeClassifier + DocumentStatusClassifier
│ ├── dates.py # DateExtractor
│ ├── vendors.py # VendorExtractor
│ ├── intake.py # DocumentIntake pipeline (stateless)
│ ├── storage.py # StorageBackend abstraction
│ ├── utils.py # File ops, naming, hashing
│ ├── run.py # Flask app entry point
│ └── backends/ # Pluggable OCR backends
│ ├── base.py # OCRBackend ABC
│ ├── tesseract.py # Tesseract + pdf2image
│ ├── nvidia.py # NVIDIA OCR stub
│ └── rfdetr_signature.py # RF-DETR visual signature detector
├── legaldocuman/app/ # Flask web application
│ ├── __init__.py # create_app() factory
│ ├── config_loader.py # Env var → Flask config
│ ├── extensions.py # Flask-SQLAlchemy db instance
│ ├── models.py # Document SQLAlchemy model
│ ├── main/routes.py # Main blueprint routes
│ ├── api/routes.py # Upload + job status API
│ └── processors/
│ └── worker.py # Background document processor
├── models/ # RF-DETR checkpoint
├── tests/ # Pytest suite
├── Dockerfile # Container definition
├── docker-compose.yml # App + PostgreSQL orchestration
├── .env.example # Env var template
├── requirements.txt # Dependencies
└── README.md # This file
pytest tests/ -vCoverage report:
pytest tests/ --cov=legaldocuman --cov-report=term-missingAll tests mock heavy dependencies (Tesseract, pdfplumber, PIL) so they run fast without system installs. See Known Limitations for integration test status.
A lightweight Playwright scaffold lives under frontend/e2e. It is optional and is not wired into CI because browser installation and a running backend/frontend add setup cost.
Install the browser once:
cd frontend
npm run e2e:installRun against an already running backend and frontend:
# terminal 1: backend, for example http://127.0.0.1:5000
# terminal 2: frontend, optionally point Vite's /api proxy at the backend
cd frontend
VITE_API_PROXY_TARGET=http://127.0.0.1:5000 npm run dev -- --host 127.0.0.1
# terminal 3: Playwright
cd frontend
E2E_BASE_URL=http://127.0.0.1:5173 \
E2E_API_URL=http://127.0.0.1:5000 \
E2E_ADMIN_EMAIL=admin@example.com \
E2E_ADMIN_PASSWORD=password123 \
npm run e2eE2E_BASE_URL (or BASE_URL) controls the browser target. E2E_API_URL (or API_URL) controls direct API checks for job/detail/download assertions. The authenticated upload flow is skipped unless admin credentials are supplied; the protected-route redirect smoke still runs. If you want Playwright to start the Vite server for you, set E2E_START_FRONTEND=1.
This system was productized from a script originally built for processing large volumes of legal contracts containing privileged party names, signatures, and sensitive terms. The architecture reflects lessons learned from that domain:
-
Regex + visual ML, not either/or — Document type classification is pure regex (explainable in audits). Execution status uses both regex (fast, text-based) and RF-DETR (slow but visually detects signature strokes). The regex layer flags execution language; RF-DETR confirms presence of ink signatures on the page. Every decision is traceable: regex matches are logged, RF-DETR detections include confidence scores and bounding boxes.
-
No third-party cloud ML — Sensitive legal documents should never leave your infrastructure. All processing (OCR, classification, visual signature detection) runs locally — no external API calls to OpenAI, cloud CV services, or third-party providers.
-
Runs on commodity hardware — No GPU required. Tesseract OCR runs on CPU. RF-DETR works with CPU-only PyTorch (the Docker image ships the CPU wheel). The system is designed to work on a standard laptop or a modest cloud VM.
-
Pluggable backend architecture — The
OCRBackendabstract base class andNvidiaOCRBackendstub are wired so you can swap in a GPU-backed OCR backend without touching the pipeline. Same pattern for storage: swapLocalStorageBackendfor S3 or GCS with zero pipeline changes. -
Retention category mapping — Auto-assigns retention policies (long_term, indefinite, short_term, tied_to_parent) to support records-management and destruction-scheduling workflows.
-
Stateless intake pipeline —
DocumentIntakeis a pure function from(file_path, vendor_folder) → DocumentRecord. File movement, metadata persistence, and registry updates are the caller's responsibility. This makes the core analysis logic easy to test, replay, and compose into larger workflows.
| Type | Abbreviation | Description |
|---|---|---|
| MSA | AGMT | Master Service Agreement |
| SOW | AGMT | Statement of Work |
| NDA | AGMT | Non-Disclosure Agreement |
| PO | K | Purchase Order |
| AMD | AMD | Amendment |
| LICENSE | K | License Agreement |
| CONTRACT | K | General Contract |
| Category | Document Types | Duration |
|---|---|---|
long_term |
MSA / Contract / Agreement (with expiration) | 7+ years |
indefinite |
NDA, License | Permanent |
contracts |
SOW | Tied to parent MSA |
short_term |
PO, Invoice | 3-7 years |
tied_to_parent |
Amendment | Same as parent |
review_required |
Unknown / uncategorized | Manual review |
The following are planned enhancements not yet implemented:
-
In-file rename support — Currently the system extracts metadata and reports it via the API, but does not rename the uploaded file on disk. A post-processing step should rename the file to its generated filename (e.g.
K_VendorName_MasterServiceAgreement_001.pdf) and updatestored_pathin the database accordingly. -
Front-end refinements — The Flask web UI (upload form, job list, job detail) is functional but minimal. Areas for improvement: drag-and-drop file upload, real-time job status polling via SSE or WebSocket, batch upload with per-file status feedback, and a dashboard summary view (total processed, breakdown by document type, execution status pie chart).
-
Async / threaded batch processing — The
process_document_asyncworker runs synchronously per document. RF-DETR inference is the slowest step in the pipeline, and sequential processing means large batches (100+ files) can take minutes. Thread-based orasyncio-based concurrency — with a worker pool bounded by CPU/GPU cores or a configurable concurrency limit — would significantly improve throughput for bulk uploads.
-
Execution status detection combines regex + visual ML — The system uses two signals: (1) deterministic regex scans extracted text for execution-language keywords ("in witness whereof", signature blocks, e-signature platform markers) to classify a document as executed or supporting, and (2) RF-DETR (a fine-tuned computer vision model) detects handwritten signature strokes in document images for a second confidence layer. RF-DETR requires the checkpoint at
models/checkpoint_best_total.pth— if unavailable the system falls back to regex-only mode. The regex layer cannot distinguish between a draft that mentions signatures and a fully executed contract; visual detection helps close that gap but is not definitive on its own. -
Unit tests only, no integration tests — The test suite mocks all external dependencies (pdfplumber, Tesseract, PIL, dateparser). This means the tests verify internal logic but do not verify that the system works end-to-end with real PDFs or DOCX files on your machine. Integration tests (creating real documents and running the full pipeline) are the next step.
-
NVIDIA backend is a stub — The
NvidiaOCRBackendclass has the interface wired but the actual API calls are not implemented. Fill inimage_to_text()andpdf_to_text()with your NVIDIA API client when ready.
MIT