OCR Text Extraction

This commit is contained in:
2026-01-24 21:24:07 +05:30
parent c76d533783
commit 71d8f0fab5
41 changed files with 1414 additions and 33 deletions

View File

@@ -2,12 +2,12 @@
# Connecting to existing 'postgres-db' container in 'arbit-app_arbit-network' # Connecting to existing 'postgres-db' container in 'arbit-app_arbit-network'
DB_USER=postgres DB_USER=postgres
DB_PASSWORD='M@tr!x#149@dm!N' DB_PASSWORD='M@tr!x#149@dm!N'
DB_NAME=ocr_db DB_NAME=ocr
DB_HOST=postgres-db DB_HOST=postgres-db
DB_PORT=5432 DB_PORT=5432
# Mail Configuration (IMAP) # Mail Configuration (IMAP)
MAIL_SERVER=imap.gmail.com MAIL_SERVER=imap.gmail.com
MAIL_USERNAME=matrixinfotech.it@gmail.com
MAIL_PASSWORD=qtxsthbxbisgcmqu
MAIL_PORT=993 MAIL_PORT=993
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password

View File

@@ -9,9 +9,8 @@ This guide describes how to deploy the OCR application on a Linux host (e.g., Ub
- **Reverse Proxy**: Host Nginx proxies requests to Frontend (Static) and Backend (API). - **Reverse Proxy**: Host Nginx proxies requests to Frontend (Static) and Backend (API).
## Prerequisites ## Prerequisites
- Docker & Docker Compose installed on the host. - **Deployment Host**: Docker & Docker Compose, Nginx.
- Nginx installed on the host. - **Build Machine**: Node.js & NPM (to run the package script).
- Node.js & NPM (for building Angular).
--- ---
@@ -26,9 +25,9 @@ The app connects to your **existing Postgres container** (`postgres-db`) in the
docker exec -it postgres-db psql -U postgres -c "CREATE DATABASE ocr_db;" docker exec -it postgres-db psql -U postgres -c "CREATE DATABASE ocr_db;"
``` ```
2. **Navigate to project root**: 2. **Navigate to the packaged directory**:
```bash ```bash
cd /path/to/OCR cd ocr_build
``` ```
3. **Verify Environment**: 3. **Verify Environment**:
@@ -42,7 +41,7 @@ The app connects to your **existing Postgres container** (`postgres-db`) in the
This will start `ocr_backend` and `ocr_frontend`. This will start `ocr_backend` and `ocr_frontend`.
```bash ```bash
# Build and start in detached mode # Build and start in detached mode
docker-compose --env-file .env.prod up -d --build docker-compose up -d --build
``` ```
5. **Verify Status**: 5. **Verify Status**:
@@ -60,7 +59,7 @@ Since the Frontend is now running in a container on port 8080 (serving `/ocrf/`)
1. **Create Config File**: 1. **Create Config File**:
Copy the provided config to `/etc/nginx/sites-available/ocr`. Copy the provided config to `/etc/nginx/sites-available/ocr`.
```bash ```bash
sudo cp deployment/nginx.conf /etc/nginx/sites-available/ocr sudo cp nginx_host.conf /etc/nginx/sites-available/ocr
``` ```
2. **Enable Site**: 2. **Enable Site**:

View File

@@ -1,5 +1,7 @@
import os import os
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey, LargeBinary import urllib.parse
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey, LargeBinary, Enum as SqlEnum
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.orm import sessionmaker, relationship
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -7,22 +9,11 @@ from dotenv import load_dotenv
# Load environment variables # Load environment variables
load_dotenv() load_dotenv()
DB_USER = os.getenv("DB_USER") DB_USER = os.getenv("DB_USER", "postgres")
DB_PASSWORD = os.getenv("DB_PASSWORD") DB_PASSWORD = os.getenv("DB_PASSWORD", "password")
DB_HOST = os.getenv("DB_HOST") DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT") DB_PORT = os.getenv("DB_PORT", "5432")
import urllib.parse DB_NAME = os.getenv("DB_NAME", "ocr_db")
# ... (imports)
# Load environment variables
load_dotenv()
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_HOST = os.getenv("DB_HOST")
DB_PORT = os.getenv("DB_PORT")
DB_NAME = os.getenv("DB_NAME")
encoded_user = urllib.parse.quote_plus(DB_USER) encoded_user = urllib.parse.quote_plus(DB_USER)
encoded_password = urllib.parse.quote_plus(DB_PASSWORD) encoded_password = urllib.parse.quote_plus(DB_PASSWORD)
@@ -35,6 +26,26 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base() Base = declarative_base()
class Vendor(Base):
__tablename__ = "vendors"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
default_model = Column(String) # 'text' or 'vision'
created_at = Column(DateTime)
class Document(Base):
__tablename__ = "documents"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=True)
filename = Column(String)
upload_date = Column(DateTime)
status = Column(String) # 'pending', 'verified'
processed_data = Column(JSONB) # Store the verified extraction results
vendor = relationship("Vendor")
class Email(Base): class Email(Base):
__tablename__ = "emails" __tablename__ = "emails"
@@ -54,7 +65,7 @@ class Attachment(Base):
email_id = Column(Integer, ForeignKey("emails.id")) email_id = Column(Integer, ForeignKey("emails.id"))
filename = Column(String) filename = Column(String)
content_type = Column(String) content_type = Column(String)
file_content = Column(LargeBinary) # Storing content directly in DB as requested file_content = Column(LargeBinary)
email = relationship("Email", back_populates="attachments") email = relationship("Email", back_populates="attachments")

View File

@@ -85,6 +85,12 @@ class LoginResponse(BaseModel):
token: str token: str
message: str message: str
@app.post("/api/login", response_model=LoginResponse)
def login(request: LoginRequest):
if request.username == "admin" and request.password == "admin":
return LoginResponse(token="fake-super-secret-token", message="Success")
raise HTTPException(status_code=401, detail="Invalid username or password")
class NERResponse(BaseModel): class NERResponse(BaseModel):
text: str text: str

View File

@@ -31,7 +31,7 @@ services:
container_name: ocr_frontend container_name: ocr_frontend
restart: always restart: always
ports: ports:
- "8080:80" - "8075:80"
networks: networks:
- arbit-network - arbit-network

View File

@@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test" "test": "ng test"

10
frontend/proxy.conf.json Normal file
View File

@@ -0,0 +1,10 @@
{
"/ocrb": {
"target": "http://localhost:8000",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/ocrb": ""
}
}
}

View File

@@ -26,16 +26,16 @@ import { ToastModule } from 'primeng/toast';
template: ` template: `
<div class="login-container"> <div class="login-container">
<p-card header="OCR Admin Login" [style]="{width: '360px'}" styleClass="p-card-shadow"> <p-card header="OCR Admin Login" [style]="{width: '360px'}" styleClass="p-card-shadow">
<div class="field"> <div class="field mt-4">
<span class="p-float-label"> <span class="p-float-label">
<input id="username" type="text" pInputText [(ngModel)]="username" class="w-full"> <input id="username" type="text" pInputText [(ngModel)]="username" class="w-full">
<label htmlFor="username">Username</label> <label for="username">Username</label>
</span> </span>
</div> </div>
<div class="field mt-4"> <div class="field mt-4">
<span class="p-float-label"> <span class="p-float-label">
<input id="password" type="password" pInputText [(ngModel)]="password" class="w-full"> <input id="password" type="password" pInputText [(ngModel)]="password" class="w-full">
<label htmlFor="password">Password</label> <label for="password">Password</label>
</span> </span>
</div> </div>
<div class="mt-4"> <div class="mt-4">
@@ -55,6 +55,7 @@ import { ToastModule } from 'primeng/toast';
} }
.w-full { width: 100%; } .w-full { width: 100%; }
.mt-4 { margin-top: 1.5rem; } .mt-4 { margin-top: 1.5rem; }
/* PrimeNG handles the rest */
`] `]
}) })
export class LoginComponent { export class LoginComponent {

13
ocr_build/.env Normal file
View File

@@ -0,0 +1,13 @@
# Database Configuration
# Connecting to existing 'postgres-db' container in 'arbit-app_arbit-network'
DB_USER=postgres
DB_PASSWORD='M@tr!x#149@dm!N'
DB_NAME=ocr
DB_HOST=postgres-db
DB_PORT=5432
# Mail Configuration (IMAP)
MAIL_SERVER=imap.gmail.com
MAIL_USERNAME=matrixinfotech.it@gmail.com
MAIL_PASSWORD=qtxsthbxbisgcmqu
MAIL_PORT=993

95
ocr_build/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,95 @@
# OCR Application Deployment Guide
This guide describes how to deploy the OCR application on a Linux host (e.g., Ubuntu/Debian).
## Architecture
- **Backend**: Containerized (FastAPI, Python 3.9).
- **Database**: Containerized (PostgreSQL 15).
- **Frontend**: Static files (Angular) served by Host Nginx.
- **Reverse Proxy**: Host Nginx proxies requests to Frontend (Static) and Backend (API).
## Prerequisites
- **Deployment Host**: Docker & Docker Compose, Nginx.
- **Build Machine**: Node.js & NPM (to run the package script).
---
## 1. Full Stack Deployment (Docker)
The app connects to your **existing Postgres container** (`postgres-db`) in the `arbit-app_arbit-network`.
1. **Create the Database**:
Since we are using an existing postgres instance, we must manually create the `ocr_db`.
```bash
# Run this on your host to create the DB inside the existing container
docker exec -it postgres-db psql -U postgres -c "CREATE DATABASE ocr_db;"
```
2. **Navigate to the packaged directory**:
```bash
cd ocr_build
```
3. **Verify Environment**:
Ensure `.env.prod` exists and points to `DB_HOST=postgres-db`.
**Also configure your Email credentials** in `.env.prod` if you want the Mailbox feature to work (Gmail requires an App Password).
```bash
cat .env.prod
```
4. **Start App Containers**:
This will start `ocr_backend` and `ocr_frontend`.
```bash
# Build and start in detached mode
docker-compose up -d --build
```
5. **Verify Status**:
```bash
docker-compose ps
```
You should see `ocr_backend` and `ocr_frontend` running.
---
## 2. Nginx Configuration (Host Reverse Proxy)
Since the Frontend is now running in a container on port 8080 (serving `/ocrf/`), we configure the Host Nginx to proxy traffic to it.
1. **Create Config File**:
Copy the provided config to `/etc/nginx/sites-available/ocr`.
```bash
sudo cp nginx_host.conf /etc/nginx/sites-available/ocr
```
2. **Enable Site**:
```bash
sudo ln -s /etc/nginx/sites-available/ocr /etc/nginx/sites-enabled/
```
3. **Test & Reload**:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
---
## 3. Verification
- **Url**: `http://app.technobeesolutions.in/ocrf/` (Proxies to Frontend Container)
- **API**: `http://app.technobeesolutions.in/ocrb/` (Proxies to Backend Container)
---
## 4. Troubleshooting
- **Logs**:
```bash
docker-compose logs -f backend
```
- **Database**:
Connect via the existing container:
```bash
docker exec -it postgres-db psql -U postgres -d ocr_db
```

11
ocr_build/backend/.env Normal file
View File

@@ -0,0 +1,11 @@
DB_USER=postgres
DB_PASSWORD=M@tr!x#149@dm!N
DB_HOST=192.168.0.111
DB_PORT=7925
DB_NAME=ocr
# Mail Configuration (Gmail)
MAIL_SERVER=imap.gmail.com
MAIL_USERNAME=matrixinfotech.it@gmail.com
MAIL_PASSWORD=qtxsthbxbisgcmqu
MAIL_PORT=993

View File

@@ -0,0 +1,30 @@
# Use official lightweight Python image
FROM python:3.9-slim
# Install system dependencies
# tesseract-ocr: for pytesseract
# poppler-utils: for pdf2image
# libtesseract-dev: development headers
RUN apt-get update && apt-get install -y \
tesseract-ocr \
poppler-utils \
libtesseract-dev \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy requirements first to leverage Docker cache
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code
COPY . .
# Expose port (default for Uvicorn)
EXPOSE 8000
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1,12 @@
from database import engine, Base
def create_tables():
print("Creating tables in database...")
try:
Base.metadata.create_all(bind=engine)
print("Tables created successfully!")
except Exception as e:
print(f"Error creating tables: {e}")
if __name__ == "__main__":
create_tables()

View File

@@ -0,0 +1,77 @@
import os
import urllib.parse
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey, LargeBinary, Enum as SqlEnum
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
DB_USER = os.getenv("DB_USER", "postgres")
DB_PASSWORD = os.getenv("DB_PASSWORD", "password")
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "ocr_db")
encoded_user = urllib.parse.quote_plus(DB_USER)
encoded_password = urllib.parse.quote_plus(DB_PASSWORD)
# SQLAlchemy Database URL
DATABASE_URL = f"postgresql://{encoded_user}:{encoded_password}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class Vendor(Base):
__tablename__ = "vendors"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
default_model = Column(String) # 'text' or 'vision'
created_at = Column(DateTime)
class Document(Base):
__tablename__ = "documents"
id = Column(Integer, primary_key=True, index=True)
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=True)
filename = Column(String)
upload_date = Column(DateTime)
status = Column(String) # 'pending', 'verified'
processed_data = Column(JSONB) # Store the verified extraction results
vendor = relationship("Vendor")
class Email(Base):
__tablename__ = "emails"
id = Column(Integer, primary_key=True, index=True)
subject = Column(String, index=True)
sender = Column(String, index=True)
body = Column(Text)
received_date = Column(DateTime)
is_read = Column(Boolean, default=False)
attachments = relationship("Attachment", back_populates="email")
class Attachment(Base):
__tablename__ = "attachments"
id = Column(Integer, primary_key=True, index=True)
email_id = Column(Integer, ForeignKey("emails.id"))
filename = Column(String)
content_type = Column(String)
file_content = Column(LargeBinary)
email = relationship("Email", back_populates="attachments")
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,20 @@
from reportlab.pdfgen import canvas
from PIL import Image, ImageDraw
def create_pdf(filename, text):
c = canvas.Canvas(filename)
c.drawString(100, 750, text)
c.save()
print(f"Created {filename}")
def create_image(filename, text):
img = Image.new('RGB', (400, 100), color = (255, 255, 255))
d = ImageDraw.Draw(img)
# Default font is usually tiny, but readable by tesseract
d.text((10,10), text, fill=(0,0,0))
img.save(filename)
print(f"Created {filename}")
if __name__ == "__main__":
create_pdf("test_ocr.pdf", "Hello World PDF OCR")
create_image("test_ocr.png", "Hello World Image OCR")

View File

@@ -0,0 +1,95 @@
import os
import datetime
import mimetypes
import time
from imap_tools import MailBox, AND
from sqlalchemy.orm import Session
from database import SessionLocal, Email, Attachment
from dotenv import load_dotenv
load_dotenv()
MAIL_SERVER = os.getenv("MAIL_SERVER")
MAIL_USERNAME = os.getenv("MAIL_USERNAME")
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
MAIL_PORT = int(os.getenv("MAIL_PORT", 993))
def fetch_and_store_emails():
"""
Connects to Gmail, fetches UNREAD emails, stores them in DB, and marks as READ.
"""
print(f"[{datetime.datetime.now()}] Checking for new emails...")
if not MAIL_USERNAME or not MAIL_PASSWORD or MAIL_USERNAME == "your-email@gmail.com":
print("Skipping Email Sync: Credentials not configured.")
return {"status": "skipped", "message": "Credentials not configured"}
db: Session = SessionLocal()
count = 0
try:
# Connect to Inbox
# imap-tools MailBox defaults to SSL (port 993) if not specified, but we make it explicit here.
with MailBox(MAIL_SERVER, port=MAIL_PORT).login(MAIL_USERNAME, MAIL_PASSWORD, initial_folder='INBOX') as mailbox:
# Fetch UNREAD emails
for msg in mailbox.fetch(AND(seen=False)):
print(f"Processing Email: {msg.subject}")
# Check if already exists (optional, but good practice if marking read fails)
# For now using simplistic approach: if we fetch it, we save it.
new_email = Email(
subject=msg.subject,
sender=msg.from_,
body=msg.text or msg.html,
received_date=msg.date,
is_read=False # Consumed by app, but 'seen' on server
)
db.add(new_email)
db.commit() # Commit to get ID
db.refresh(new_email)
# Process Attachments
for i, att in enumerate(msg.attachments):
filename = att.filename
if not filename:
# Fallback for missing filename
ext = mimetypes.guess_extension(att.content_type) or ".bin"
timestamp = int(time.time())
filename = f"attachment_{timestamp}_{i}{ext}"
print(f" - Attachment: {filename}")
new_attachment = Attachment(
email_id=new_email.id,
filename=filename,
content_type=att.content_type,
file_content=att.payload # raw bytes
)
db.add(new_attachment)
db.commit()
# Mark as Read on Server (Auto-done by fetch unless bulk=True or mark_seen=False)
# imap-tools fetches are auto-seen by default.
count += 1
print(f"Sync Complete. {count} new emails.")
return {"status": "success", "count": count}
except Exception as e:
error_msg = str(e)
print(f"Error fetching emails: {error_msg}")
if "AUTHENTICATIONFAILED" in error_msg or "Invalid credentials" in error_msg:
return {
"status": "error",
"message": f"Authentication Failed: {error_msg}. \n[HINT]: If using Gmail, you MUST use an 'App Password', not your login password. Also ensure IMAP is enabled in Gmail Settings."
}
return {"status": "error", "message": error_msg}
finally:
db.close()
if __name__ == "__main__":
fetch_and_store_emails()

263
ocr_build/backend/main.py Normal file
View File

@@ -0,0 +1,263 @@
import io
import datetime
from typing import Optional, List
from contextlib import asynccontextmanager
from fastapi import FastAPI, File, UploadFile, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from pypdf import PdfReader
import pytesseract
from PIL import Image
from pdf2image import convert_from_bytes
import pdfplumber
from sqlalchemy.orm import Session
# 2. OCR Module
def extract_text_from_pdf(file_bytes: bytes) -> str:
# Try pdfplumber first for layout preservation
try:
with pdfplumber.open(io.BytesIO(file_bytes)) as pdf:
text = ""
for page in pdf.pages:
page_text = page.extract_text(layout=True)
if page_text:
text += page_text + "\n"
if text.strip():
return text.strip()
except Exception as e:
print(f"pdfplumber error: {e}")
# Fallback to pypdf
try:
reader = PdfReader(io.BytesIO(file_bytes))
text = ""
for page in reader.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
return text.strip()
except Exception as e:
print(f"Error reading PDF: {e}")
return ""
# Internal modules
from database import get_db, Email
from scheduler import start_scheduler, stop_scheduler
from mail_service import fetch_and_store_emails
# Lifespan for Scheduler
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
start_scheduler()
yield
# Shutdown
stop_scheduler()
app = FastAPI(lifespan=lifespan)
# CORS configuration
origins = [
"http://localhost",
"http://localhost:3000",
"http://localhost:5173", # Vite default
"http://localhost:4200", # Angular default
"http://127.0.0.1:4200",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Models
class LoginRequest(BaseModel):
username: str
password: str
class LoginResponse(BaseModel):
token: str
message: str
@app.post("/api/login", response_model=LoginResponse)
def login(request: LoginRequest):
if request.username == "admin" and request.password == "admin":
return LoginResponse(token="fake-super-secret-token", message="Success")
raise HTTPException(status_code=401, detail="Invalid username or password")
class NERResponse(BaseModel):
text: str
def extract_text_from_image(file_bytes: bytes) -> str:
try:
image = Image.open(io.BytesIO(file_bytes))
text = pytesseract.image_to_string(image)
return text.strip()
except Exception as e:
print(f"Error reading Image: {e}")
return ""
@app.post("/api/ocr/extract", response_model=NERResponse)
async def extract_text(file: UploadFile = File(...)):
content = await file.read()
filename = file.filename.lower()
extracted_text = ""
if filename.endswith(".pdf"):
# Try text extraction first
extracted_text = extract_text_from_pdf(content)
# If text is empty, it might be a scanned PDF.
if not extracted_text.strip():
try:
images = convert_from_bytes(content)
for i, image in enumerate(images):
page_text = pytesseract.image_to_string(image)
extracted_text += f"\n--- Page {i+1} ---\n{page_text}"
except Exception as e:
extracted_text = f"Error processing Scanned PDF: {str(e)}\n\n(Hint: Ensure 'poppler' is installed on your system. Run 'brew install poppler')"
elif filename.endswith((".png", ".jpg", ".jpeg", ".tiff", ".bmp")):
extracted_text = extract_text_from_image(content)
else:
raise HTTPException(status_code=400, detail="Unsupported file type")
return NERResponse(text=extracted_text)
import zipfile
import mimetypes
from fastapi.responses import StreamingResponse
class EmailDTO(BaseModel):
id: int
subject: Optional[str]
sender: Optional[str]
received_date: Optional[datetime.datetime]
is_read: bool
has_attachments: bool = False
class AttachmentDTO(BaseModel):
id: int
filename: str
content_type: str
class EmailListResponse(BaseModel):
items: List[EmailDTO]
total: int
class EmailDetailDTO(EmailDTO):
body: Optional[str]
attachments: List[AttachmentDTO]
@app.get("/api/emails", response_model=EmailListResponse)
def get_emails(skip: int = 0, limit: int = 50, db: Session = Depends(get_db)):
total = db.query(Email).count()
emails = db.query(Email).order_by(Email.received_date.desc()).offset(skip).limit(limit).all()
items = [
EmailDTO(
id=e.id,
subject=e.subject,
sender=e.sender,
received_date=e.received_date,
is_read=e.is_read,
has_attachments=bool(e.attachments) # Check if list is not empty
) for e in emails
]
return EmailListResponse(items=items, total=total)
# ...
@app.get("/api/emails/{email_id}", response_model=EmailDetailDTO)
def get_email_details(email_id: int, db: Session = Depends(get_db)):
email = db.query(Email).filter(Email.id == email_id).first()
if not email:
raise HTTPException(status_code=404, detail="Email not found")
# Mark as read if opened
if not email.is_read:
email.is_read = True
db.commit()
# Safely handle missing content_type and filename
attachments = []
for a in email.attachments:
fname = a.filename
if not fname or fname == "unknown":
ext = mimetypes.guess_extension(a.content_type or "") or ".bin"
fname = f"attachment_{a.id}{ext}"
attachments.append(
AttachmentDTO(
id=a.id,
filename=fname,
content_type=a.content_type or "application/octet-stream"
)
)
return EmailDetailDTO(
id=email.id,
subject=email.subject,
sender=email.sender,
received_date=email.received_date,
is_read=email.is_read,
has_attachments=bool(email.attachments),
body=email.body,
attachments=attachments
)
@app.get("/api/emails/{email_id}/download-all")
def download_all_attachments(email_id: int, db: Session = Depends(get_db)):
email = db.query(Email).filter(Email.id == email_id).first()
if not email:
raise HTTPException(status_code=404, detail="Email not found")
if not email.attachments:
raise HTTPException(status_code=400, detail="No attachments found")
# Create a zip file in memory
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
for att in email.attachments:
# Use filename from DB, ensure unique names if duplicates exist (simple append here)
zip_file.writestr(att.filename, att.file_content)
zip_buffer.seek(0)
return StreamingResponse(
zip_buffer,
media_type="application/zip",
headers={"Content-Disposition": f"attachment; filename=email-{email_id}-attachments.zip"}
)
@app.get("/api/attachments/{attachment_id}")
def download_attachment(attachment_id: int, view: bool = False, db: Session = Depends(get_db)):
attachment = db.query(DBAttachment).filter(DBAttachment.id == attachment_id).first()
if not attachment:
raise HTTPException(status_code=404, detail="Attachment not found")
disposition = "inline" if view else "attachment"
return StreamingResponse(
io.BytesIO(attachment.file_content),
media_type=attachment.content_type,
headers={"Content-Disposition": f"{disposition}; filename={attachment.filename}"}
)
@app.post("/api/emails/sync")
def sync_emails():
result = fetch_and_store_emails()
return result
@app.get("/")
def read_root():
return {"message": "OCR Backend API is running"}

View File

@@ -0,0 +1,13 @@
fastapi
uvicorn
python-multipart
pypdf
pytesseract
Pillow
sqlalchemy
psycopg2-binary
imap-tools
apscheduler
python-dotenv
pdfplumber
pdf2image

View File

@@ -0,0 +1,14 @@
from apscheduler.schedulers.background import BackgroundScheduler
from mail_service import fetch_and_store_emails
scheduler = BackgroundScheduler()
def start_scheduler():
# Add job to run every 10 seconds
scheduler.add_job(fetch_and_store_emails, 'interval', seconds=10, id='mail_sync_job', replace_existing=True)
scheduler.start()
print("Mail Scheduler Started (Interval: 10s)")
def stop_scheduler():
scheduler.shutdown()
print("Mail Scheduler Stopped")

View File

@@ -0,0 +1 @@
This is a test PDF content

View File

@@ -0,0 +1,68 @@
%PDF-1.3
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 7 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 6 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
>>
endobj
5 0 obj
<<
/Author (anonymous) /CreationDate (D:20260121205329+05'00') /Creator (anonymous) /Keywords () /ModDate (D:20260121205329+05'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
6 0 obj
<<
/Count 1 /Kids [ 3 0 R ] /Type /Pages
>>
endobj
7 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 115
>>
stream
GapQh0E=F,0U\H3T\pNYT^QKk?tc>IP,;W#U1^23ihPEM_?CW4KISi90MjG^2,FS#<RC5+c,n(/#gY0H8Pob4EDu@N%an;^a\iDl!bkQj!W^(m'ef~>endstream
endobj
xref
0 8
0000000000 65535 f
0000000061 00000 n
0000000092 00000 n
0000000199 00000 n
0000000402 00000 n
0000000470 00000 n
0000000731 00000 n
0000000790 00000 n
trailer
<<
/ID
[<6da718d498e92fec48910ff2674542d2><6da718d498e92fec48910ff2674542d2>]
% ReportLab generated PDF document -- digest (opensource)
/Info 5 0 R
/Root 4 0 R
/Size 8
>>
startxref
995
%%EOF

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,41 @@
services:
# Backend API
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: ocr_backend
restart: always
environment:
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_NAME: ${DB_NAME}
# Mail Config
MAIL_SERVER: ${MAIL_SERVER}
MAIL_PORT: ${MAIL_PORT}
MAIL_USERNAME: ${MAIL_USERNAME}
MAIL_PASSWORD: ${MAIL_PASSWORD}
ports:
- "8000:8000"
networks:
- arbit-network
# Frontend Angular App
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: ocr_frontend
restart: always
ports:
- "8075:80"
networks:
- arbit-network
networks:
arbit-network:
external: true
name: arbit-app_arbit-network

View File

@@ -0,0 +1,18 @@
FROM nginx:alpine
# Remove default nginx static assets
RUN rm -rf /usr/share/nginx/html/*
# Create target directory
RUN mkdir -p /usr/share/nginx/html/ocrf
# Copy Pre-built assets (from local dist)
COPY dist /usr/share/nginx/html/ocrf
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
ocr_build/frontend/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

18
ocr_build/frontend/dist/index.html vendored Normal file
View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en" data-beasties-container>
<head>
<meta charset="utf-8">
<title>Frontend</title>
<base href="/ocrf/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- PrimeNG Theme -->
<link id="theme-css" rel="stylesheet" type="text/css" href="https://unpkg.com/primeng@17.18.0/resources/themes/lara-light-blue/theme.css">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/primeng@17.18.0/resources/primeng.min.css">
<!-- PrimeFlex -->
<link rel="stylesheet" href="https://unpkg.com/primeflex@3.3.1/primeflex.css">
<style>@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}html,body{margin:0;font-family:var(--font-family);background-color:var(--surface-ground);height:100%}</style><link rel="stylesheet" href="styles-4O3ZUZOP.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-4O3ZUZOP.css"></noscript></head>
<body>
<app-root></app-root>
<link rel="modulepreload" href="chunk-IMH4GOJN.js"><script src="polyfills-6ISPNSXF.js" type="module"></script><script src="main-ZCNRUEIP.js" type="module"></script></body>
</html>

109
ocr_build/frontend/dist/main-ZCNRUEIP.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html/ocrf;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Optional: Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires 1y;
add_header Cache-Control "public, no-transform";
}
}

22
ocr_build/nginx_host.conf Normal file
View File

@@ -0,0 +1,22 @@
server {
listen 80;
server_name app.technobeesolutions.in;
# Proxy Angular Frontend Container
location /ocrf/ {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy API requests to the Docker Backend
location /ocrb/ {
proxy_pass http://localhost:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

65
package_release.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
# Exit on error
set -e
ECHO "📦 Starting Release Package Process..."
# 1. Clean previous build
rm -rf ocr_build
mkdir -p ocr_build
# 2. Build Frontend Locally
echo "🏗️ Building Angular Frontend (Base Href: /ocrf/)..."
cd frontend
npm install --legacy-peer-deps
npm run build -- --configuration production --base-href /ocrf/
cd ..
# 3. Prepare Frontend Artifacts
echo "📂 Preparing Frontend Packaging..."
mkdir -p ocr_build/frontend
# Copy the compiled 'browser' folder to ocr_build/frontend/dist
cp -r frontend/dist/frontend/browser ocr_build/frontend/dist
# Copy the internal nginx config
cp frontend/nginx.conf ocr_build/frontend/
# Create a lightweight Production Dockerfile for Frontend
# This replaces the multi-stage build with a single stage serving pre-built files
cat <<EOF > ocr_build/frontend/Dockerfile
FROM nginx:alpine
# Remove default nginx static assets
RUN rm -rf /usr/share/nginx/html/*
# Create target directory
RUN mkdir -p /usr/share/nginx/html/ocrf
# Copy Pre-built assets (from local dist)
COPY dist /usr/share/nginx/html/ocrf
# Copy custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
EOF
# 4. Prepare Backend Artifacts
echo "🐍 Preparing Backend Packaging..."
mkdir -p ocr_build/backend
# Copy Backend Source (Excluding venv and pycache)
rsync -av --progress backend/ ocr_build/backend/ --exclude venv --exclude __pycache__ --exclude .pytest_cache --exclude uploads
# 5. Copy Root Configurations
echo "⚙️ Copying Configuration files..."
cp docker-compose.yaml ocr_build/
cp .env.prod ocr_build/.env
cp deployment/nginx.conf ocr_build/nginx_host.conf
cp DEPLOYMENT.md ocr_build/
echo "✅ Release Package Created: ./ocr_build"
echo " Transfer the 'ocr_build' folder to your Linux server and run:"
echo " cd ocr_build && docker-compose --env-file .env.prod up -d --build"