Serverless PDF symbol detection powered by YOLO, RabbitMQ, and Modal. Processes construction blueprint PDFs, detects engineering symbols, and returns aggregated counts β locally or in the cloud.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EstimaX Backend β
β β
β User uploads PDF βββΊ Stores in Supabase S3 βββΊ Publishes to RabbitMQ β
β β β
β Result βββ Consumes from result_queue βββββββββββββββΌββββββββββ β
β Notification βββ Consumes from notification_queue βββΌβββββ β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββΌβββββΌβββββΌβββββββ
β β β
βββββββββββΌβββββ΄βββββ΄βββββββ
β AI Service (this) β
β β
β RabbitMQ Consumer β
β β β
β βΌ β
β process_pdf_service β
β β β
β βββΊ Local (YOLO) β
β β β
β βββΊ Modal Cloud β
β ββ GPU (T4) β
β ββ CPU β
β β
β Publishes results to: β
β β’ result_queue β
β β’ notification_queue β
ββββββββββββββββββββββββββββ
Ai_service/
βββ modal_worker.py # Modal serverless functions (GPU + CPU)
βββ requirements.txt
βββ Dockerfile
β
βββ app/
βββ main.py # FastAPI app + RabbitMQ consumer thread
β
βββ core/
β βββ config.py # Environment config (RabbitMQ, Modal, GPU)
β
βββ inference/
β βββ pdf_reader.py # YOLO sliding-window detection on PDF pages
β
βββ services/
β βββ process_pdf_service.py # Orchestrator: Modal vs Local routing
β
βββ utils/
β βββ rabbitmq_client.py # Thread-safe RabbitMQ consumer + publisher
β
βββ models/
β βββ best.pt # YOLO model weights
β
βββ detections/ # Local output: per-page detection images
βββ .env.dev # Environment variables
βββ .env.example # Template
The backend publishes a message to the ai_jobs queue:
{
"user_id": "12345",
"job_id": "c77c5e69-a262-4322-b172-fcd052221591",
"file_path": "https://storage.supabase.co/.../blueprint.pdf?token=..."
}main.py runs a daemon consumer thread that listens on ai_jobs. When a message arrives:
Consumer Thread βββΊ rabbitmq_client.process_message()
β
βββ Parses payload (user_id, job_id, file_path)
βββ Calls process_pdf_service.process_pdf()
βββ Publishes result to result_queue
βββ Publishes notification to notification_queue
βββ ACKs the original message
Controlled by two env vars:
| Variable | Value | Behavior |
|---|---|---|
USE_MODAL |
true |
Offload to Modal serverless |
USE_MODAL |
false |
Process locally with YOLO |
MODAL_GPU |
auto |
Try GPU β fallback to CPU (Modal only) |
MODAL_GPU |
gpu |
GPU only β fail if unavailable |
MODAL_GPU |
cpu |
CPU only β faster cold-start |
- Downloads PDF if it's a URL
- Loads YOLO model from
app/models/best.pt - Converts each PDF page to an image (200 DPI)
- Runs sliding-window detection (640Γ640 windows, stride 512)
- Applies NMS to remove duplicate detections
- Returns aggregated symbol counts
- Resolves the Modal function (
process_pdf_job_gpuorprocess_pdf_job_cpu) - Calls
.remote(file_path)β Modal handles container provisioning - Inside the Modal container: same YOLO pipeline runs with the bundled model
- Automatic retry (2 attempts) on transient gRPC
ConnectionError
Two messages are published back:
Result Queue (result_queue):
{
"user_id": "12345",
"job_id": "c77c5e69-a262-4322-b172-fcd052221591",
"status": "success",
"result": "{\"valve\": 12, \"pump\": 3, \"motor\": 7}",
"created_at": "2026-03-11T22:57:54"
}Notification Queue (notification_queue):
{
"user_id": "12345",
"job_id": "c77c5e69-a262-4322-b172-fcd052221591",
"message": "PDF processed successfully",
"status": "success",
"created_at": "2026-03-11T22:57:54"
}The
resultfield is a JSON-encoded string of symbol name β count.
On failure, status is "error" and result contains:
{
"error": "Description of what went wrong"
}cd Ai_service
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtcp app/.env.example app/.env.dev
# Edit app/.env.dev with your credentialscd app
uvicorn main:app --host 0.0.0.0 --port 8001 --reloadmodal setup # First time only β authenticate
modal deploy modal_worker.pyThen set in .env.dev:
USE_MODAL=true
MODAL_GPU=cpu # or gpu / auto
docker build -t estimax-ai .
docker run -p 8000:8000 --env-file app/.env.dev estimax-ai| Variable | Default | Description |
|---|---|---|
MQ_HOST |
localhost |
RabbitMQ host |
MQ_PORT |
5672 |
RabbitMQ port |
MQ_USER |
guest |
RabbitMQ username |
MQ_PASSWORD |
guest |
RabbitMQ password |
MQ_VHOST |
/ |
RabbitMQ virtual host |
PDF_QUEUE |
ai_jobs |
Queue to consume jobs from |
RESULT_QUEUE |
result_queue |
Queue to publish results to |
NOTIFICATION_QUEUE |
notification_queue |
Queue to publish notifications to |
USE_MODAL |
false |
Enable Modal serverless inference |
MODAL_GPU |
auto |
GPU mode: auto / gpu / cpu |
The detection engine uses a sliding-window approach optimized for large blueprint images:
PDF Page (200 DPI)
β
βΌ
Rasterize via PyMuPDF
β
βΌ
Sliding Window (640Γ640, stride 512)
β
βΌ
YOLO Inference per window
β
βΌ
Aggregate all detections
β
βΌ
Non-Maximum Suppression (NMS)
β
βΌ
Symbol counts per class
Parameters:
- Window size:
640Γ640 - Stride:
512(128px overlap for edge detection) - Confidence threshold:
0.3 - IoU threshold:
0.45 - DPI:
200
| Method | Path | Description |
|---|---|---|
GET/HEAD |
/ |
Service info |
GET/HEAD |
/health |
Health check (consumer alive?) |
GET |
/status |
Detailed consumer thread status |
ai_jobs β Backend publishes jobs
result_queue β AI publishes processing results
notification_queue β AI publishes status notifications
All queues are durable with persistent messages and manual ACK.
| Function | Image | GPU | Description |
|---|---|---|---|
process_pdf_job_gpu |
CUDA torch | T4 | Faster inference |
process_pdf_job_cpu |
CPU torch | None | Faster cold-start |
Both share the same detection logic. The GPU image includes CUDA-enabled PyTorch, while the CPU image uses the lightweight CPU-only build for faster container startup.
Backend: PENDING βββΊ QUEUED βββββββββββββββββββββββββββΊ COMPLETED / FAILED
β β²
βΌ β
AI Service: CONSUMING βββΊ PROCESSING βββΊ PUBLISHING βββββ
β
βββ Local (YOLO)
βββ Modal (GPU / CPU)