github-actions[bot] commited on
Commit
060dc2a
·
1 Parent(s): a571c24

Sync from GitHub: 922d61677657398e61342f3cabff773c13c26de4

Browse files
.gitignore CHANGED
@@ -27,6 +27,10 @@ htmlcov/
27
  .mypy_cache/
28
  .ipynb_checkpoints/
29
 
 
 
 
 
30
  *.md
31
  !README_git.md
32
  !README.md
 
27
  .mypy_cache/
28
  .ipynb_checkpoints/
29
 
30
+ # Frontend development files
31
+ frontend/node_modules/
32
+ frontend/.env.local
33
+
34
  *.md
35
  !README_git.md
36
  !README.md
Dockerfile CHANGED
@@ -2,7 +2,7 @@ FROM python:3.10-slim
2
 
3
  WORKDIR /app
4
 
5
- # Install system dependencies for OpenCV and other libraries
6
  RUN apt-get update && apt-get install -y \
7
  git \
8
  libgl1 \
@@ -11,6 +11,9 @@ RUN apt-get update && apt-get install -y \
11
  libxext6 \
12
  libxrender-dev \
13
  libgomp1 \
 
 
 
14
  && rm -rf /var/lib/apt/lists/*
15
 
16
  # Copy requirements first for better caching
@@ -19,7 +22,15 @@ COPY requirements.txt .
19
  # Install Python dependencies
20
  RUN pip install --no-cache-dir -r requirements.txt
21
 
22
- # Copy application files
 
 
 
 
 
 
 
 
23
  COPY config.py .
24
  COPY model_manager.py .
25
  COPY inference.py .
 
2
 
3
  WORKDIR /app
4
 
5
+ # Install system dependencies including Node.js
6
  RUN apt-get update && apt-get install -y \
7
  git \
8
  libgl1 \
 
11
  libxext6 \
12
  libxrender-dev \
13
  libgomp1 \
14
+ curl \
15
+ && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
16
+ && apt-get install -y nodejs \
17
  && rm -rf /var/lib/apt/lists/*
18
 
19
  # Copy requirements first for better caching
 
22
  # Install Python dependencies
23
  RUN pip install --no-cache-dir -r requirements.txt
24
 
25
+ # Copy frontend files and build
26
+ COPY frontend/package*.json frontend/
27
+ WORKDIR /app/frontend
28
+ RUN npm install
29
+ COPY frontend/ .
30
+ RUN npm run build
31
+
32
+ # Copy backend application files
33
+ WORKDIR /app
34
  COPY config.py .
35
  COPY model_manager.py .
36
  COPY inference.py .
README.md CHANGED
@@ -9,42 +9,164 @@ license: mit
9
  app_port: 7860
10
  ---
11
 
12
- # Invoice Information Extractor
13
 
14
- AI-powered REST API for extracting structured information from Indian tractor invoices.
15
 
16
- ## Features
17
 
18
- - 🔍 **Text Extraction:** Dealer name, model name, horse power, asset cost
19
- - 🖊️ **Signature Detection:** Automatic signature detection with bounding boxes
20
- - 📋 **Stamp Detection:** Stamp detection with confidence scores
21
- - 🎯 **Validation:** Smart validation and error correction
22
- - **Fast Processing:** ~3-5 seconds per invoice
 
 
23
 
24
- ## Models
 
 
 
 
 
 
25
 
26
- - **YOLO**: Custom-trained signature/stamp detector
27
- - **Qwen2.5-VL-7B**: Vision-language model for text extraction (4-bit quantized)
28
 
29
- ## API Documentation
30
 
31
- Visit `/docs` for interactive API documentation.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- ## Quick Start
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- ### Python
36
- ```python
37
- import requests
38
 
39
- url = "https://YOUR_USERNAME-invoice-extractor.hf.space/extract"
40
- files = {"file": open("invoice.png", "rb")}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- response = requests.post(url, files=files)
43
- print(response.json())
 
 
 
 
 
 
 
44
  ```
45
 
46
- ### cURL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  ```bash
48
- curl -X POST "https://YOUR_USERNAME-invoice-extractor.hf.space/extract" \
49
- -F "file=@invoice.png"
50
  ```
 
 
 
 
 
 
 
 
 
 
 
9
  app_port: 7860
10
  ---
11
 
12
+ # Invoice Information Extractor 🧾
13
 
14
+ A complete full-stack application for extracting information from invoices using AI. Features a modern React frontend and powerful FastAPI backend with YOLOv8 and PaddleOCR.
15
 
16
+ ## Features
17
 
18
+ ### Frontend (React + Vite)
19
+ - 📁 **Drag-and-drop file upload** with multi-file support
20
+ - 📄 **PDF to Image conversion** (automatically converts multi-page PDFs)
21
+ - 🔄 **Batch processing** with real-time progress tracking
22
+ - 🎨 **Visual detection** of signatures and stamps with bounding boxes
23
+ - 📱 **Responsive design** that works on all devices
24
+ - ⚡ **Fast and modern** UI with Tailwind CSS
25
 
26
+ ### Backend (FastAPI + AI Models)
27
+ - 🤖 **YOLOv8 object detection** for signatures and stamps
28
+ - 📝 **PaddleOCR** for text extraction
29
+ - 🚀 **High-performance API** with async support
30
+ - 📊 **Batch processing** capabilities
31
+ - 🔒 **CORS enabled** for frontend integration
32
+ - 📚 **Interactive API docs** at /docs
33
 
34
+ ## 🎯 Quick Start
 
35
 
36
+ ### Option 1: Automated Setup (Windows)
37
 
38
+ Run the automated setup script:
39
+ ```bash
40
+ setup.bat
41
+ ```
42
+
43
+ Then start both servers:
44
+ ```bash
45
+ start.bat
46
+ ```
47
+
48
+ ### Option 2: Manual Setup
49
+
50
+ #### Backend Setup
51
+ ```bash
52
+ # Install dependencies
53
+ pip install -r requirements.txt
54
+
55
+ # Start the server
56
+ python app.py
57
+ ```
58
+ Backend runs on: http://localhost:7860
59
 
60
+ #### Frontend Setup
61
+ ```bash
62
+ # Navigate to frontend
63
+ cd frontend
64
+
65
+ # Install dependencies
66
+ npm install
67
+
68
+ # Create environment file
69
+ cp .env.example .env
70
+
71
+ # Start development server
72
+ npm run dev
73
+ ```
74
+ Frontend runs on: http://localhost:3000
75
 
76
+ ## 🖥️ Usage
 
 
77
 
78
+ 1. **Open the application** at http://localhost:3000
79
+ 2. **Upload files** by dragging and dropping or clicking to browse
80
+ - Supports: JPEG, PNG, GIF, WEBP images and PDF files
81
+ 3. **Click "Process"** to start extraction
82
+ 4. **View results** with:
83
+ - Extracted text from the invoice
84
+ - Visual detection of signatures (red boxes)
85
+ - Visual detection of stamps (blue boxes)
86
+ - Coordinates and metadata
87
+
88
+ ## 🔌 API Endpoints
89
+
90
+ ### POST /process-invoice
91
+ Process a single invoice image
92
+
93
+ **Request:**
94
+ ```bash
95
+ curl -X POST "http://localhost:7860/process-invoice" \
96
+ -F "file=@invoice.jpg"
97
+ ```
98
 
99
+ **Response:**
100
+ ```json
101
+ {
102
+ "extracted_text": "Invoice details...",
103
+ "signature_coords": [[x1, y1, x2, y2]],
104
+ "stamp_coords": [[x1, y1, x2, y2]],
105
+ "doc_id": "invoice",
106
+ "processing_time": 2.5
107
+ }
108
  ```
109
 
110
+ ### GET /docs
111
+ Interactive API documentation (Swagger UI)
112
+
113
+ ### GET /health
114
+ Health check endpoint
115
+
116
+ ## 🛠️ Technology Stack
117
+
118
+ ### Frontend
119
+ - **React 18** - Modern UI library
120
+ - **Vite** - Lightning-fast build tool
121
+ - **Tailwind CSS** - Utility-first CSS
122
+ - **PDF.js** - PDF rendering
123
+ - **Axios** - HTTP client
124
+
125
+ ### Backend
126
+ - **FastAPI** - Modern Python web framework
127
+ - **YOLOv8** - State-of-the-art object detection
128
+ - **PaddleOCR** - Multilingual OCR
129
+ - **Uvicorn** - ASGI server
130
+
131
+ ## 📖 Documentation
132
+
133
+ - [Setup Guide](SETUP_GUIDE.md) - Detailed setup instructions
134
+ - [Frontend Architecture](frontend/ARCHITECTURE.md) - Frontend technical details
135
+ - [API Documentation](http://localhost:7860/docs) - Interactive API docs (when running)
136
+
137
+ ## 🚀 Deployment
138
+
139
+ ### Hugging Face Spaces
140
+
141
+ The application is fully configured for deployment to Hugging Face Spaces:
142
+
143
+ 1. **Build frontend**:
144
+ ```bash
145
+ build-frontend.bat
146
+ ```
147
+
148
+ 2. **Push to GitHub** (auto-deploys via GitHub Actions):
149
+ ```bash
150
+ git add .
151
+ git commit -m "Deploy to HF"
152
+ git push origin main
153
+ ```
154
+
155
+ See [HF_DEPLOYMENT_READY.md](HF_DEPLOYMENT_READY.md) and [DEPLOYMENT.md](DEPLOYMENT.md) for detailed instructions.
156
+
157
+ ### Docker
158
+
159
  ```bash
160
+ docker build -t invoice-extractor .
161
+ docker run -p 7860:7860 invoice-extractor
162
  ```
163
+
164
+ The Dockerfile automatically builds the frontend during the build process.
165
+
166
+ ## 📄 License
167
+
168
+ This project is licensed under the MIT License.
169
+
170
+ ---
171
+
172
+ **Made with ❤️ using AI and modern web technologies**
app.py CHANGED
@@ -4,7 +4,8 @@ Provides REST API for invoice processing
4
  """
5
 
6
  from fastapi import FastAPI, File, UploadFile, HTTPException, Form
7
- from fastapi.responses import JSONResponse
 
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from contextlib import asynccontextmanager
10
  from typing import Optional
@@ -56,10 +57,21 @@ app.add_middleware(
56
  allow_headers=["*"],
57
  )
58
 
 
 
 
 
 
 
59
 
60
  @app.get("/")
61
  async def root():
62
- """Root endpoint - API information"""
 
 
 
 
 
63
  return {
64
  "name": API_TITLE,
65
  "version": API_VERSION,
@@ -67,6 +79,7 @@ async def root():
67
  "models_loaded": model_manager.is_loaded(),
68
  "endpoints": {
69
  "health": "/health",
 
70
  "extract": "/extract (POST)",
71
  "docs": "/docs"
72
  }
@@ -184,6 +197,76 @@ async def extract_invoice(
184
  file.file.close()
185
 
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  @app.post("/extract_batch")
188
  async def extract_batch(
189
  files: list[UploadFile] = File(..., description="Multiple invoice images")
 
4
  """
5
 
6
  from fastapi import FastAPI, File, UploadFile, HTTPException, Form
7
+ from fastapi.responses import JSONResponse, FileResponse
8
+ from fastapi.staticfiles import StaticFiles
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from contextlib import asynccontextmanager
11
  from typing import Optional
 
57
  allow_headers=["*"],
58
  )
59
 
60
+ # Mount frontend static files if they exist
61
+ frontend_dist = os.path.join(os.path.dirname(__file__), "frontend", "dist")
62
+ if os.path.exists(frontend_dist):
63
+ app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dist, "assets")), name="assets")
64
+ print(f"📂 Serving frontend from: {frontend_dist}")
65
+
66
 
67
  @app.get("/")
68
  async def root():
69
+ """Root endpoint - Serve frontend or API information"""
70
+ frontend_index = os.path.join(os.path.dirname(__file__), "frontend", "dist", "index.html")
71
+ if os.path.exists(frontend_index):
72
+ return FileResponse(frontend_index)
73
+
74
+ # Fallback to API information
75
  return {
76
  "name": API_TITLE,
77
  "version": API_VERSION,
 
79
  "models_loaded": model_manager.is_loaded(),
80
  "endpoints": {
81
  "health": "/health",
82
+ "process": "/process-invoice (POST)",
83
  "extract": "/extract (POST)",
84
  "docs": "/docs"
85
  }
 
197
  file.file.close()
198
 
199
 
200
+ @app.post("/process-invoice")
201
+ async def process_invoice(
202
+ file: UploadFile = File(..., description="Invoice image file")
203
+ ):
204
+ """
205
+ Process a single invoice and return extracted information
206
+ Simplified endpoint for frontend integration
207
+
208
+ **Parameters:**
209
+ - **file**: Invoice image file (required)
210
+
211
+ **Returns:**
212
+ - JSON with extracted_text, signature_coords, stamp_coords
213
+ """
214
+
215
+ # Validate file type
216
+ if file.content_type and not file.content_type.startswith("image/"):
217
+ raise HTTPException(
218
+ status_code=400,
219
+ detail="File must be an image"
220
+ )
221
+
222
+ # Check if models are loaded
223
+ if not model_manager.is_loaded():
224
+ raise HTTPException(
225
+ status_code=503,
226
+ detail="Models not loaded. Please wait for server initialization."
227
+ )
228
+
229
+ temp_file = None
230
+ try:
231
+ # Save uploaded file to temporary location
232
+ suffix = os.path.splitext(file.filename)[1] if file.filename else '.jpg'
233
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp:
234
+ temp_file = temp.name
235
+ shutil.copyfileobj(file.file, temp)
236
+
237
+ # Use filename as doc_id
238
+ doc_id = os.path.splitext(file.filename)[0] if file.filename else "invoice"
239
+
240
+ # Process invoice
241
+ result = InferenceProcessor.process_invoice(temp_file, doc_id)
242
+
243
+ # Return simplified response matching frontend expectations
244
+ return JSONResponse(content={
245
+ "extracted_text": result.get("extracted_text", ""),
246
+ "signature_coords": result.get("signature_coords", []),
247
+ "stamp_coords": result.get("stamp_coords", []),
248
+ "doc_id": result.get("doc_id", doc_id),
249
+ "processing_time": result.get("processing_time_sec", 0)
250
+ }, media_type="application/json; charset=utf-8")
251
+
252
+ except Exception as e:
253
+ raise HTTPException(
254
+ status_code=500,
255
+ detail=f"Error processing invoice: {str(e)}"
256
+ )
257
+
258
+ finally:
259
+ # Clean up temporary file
260
+ if temp_file and os.path.exists(temp_file):
261
+ try:
262
+ os.unlink(temp_file)
263
+ except:
264
+ pass
265
+
266
+ # Close uploaded file
267
+ file.file.close()
268
+
269
+
270
  @app.post("/extract_batch")
271
  async def extract_batch(
272
  files: list[UploadFile] = File(..., description="Multiple invoice images")
build-frontend.bat ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ============================================
3
+ echo Building Frontend for Production
4
+ echo ============================================
5
+ echo.
6
+
7
+ cd frontend
8
+
9
+ echo [1/2] Installing dependencies...
10
+ call npm install
11
+ if errorlevel 1 (
12
+ echo ERROR: Failed to install dependencies
13
+ pause
14
+ exit /b 1
15
+ )
16
+ echo ✓ Dependencies installed
17
+ echo.
18
+
19
+ echo [2/2] Building frontend...
20
+ call npm run build
21
+ if errorlevel 1 (
22
+ echo ERROR: Build failed
23
+ pause
24
+ exit /b 1
25
+ )
26
+ echo ✓ Frontend built successfully
27
+ echo.
28
+
29
+ cd ..
30
+
31
+ echo ============================================
32
+ echo Build Complete!
33
+ echo ============================================
34
+ echo.
35
+ echo Frontend built to: frontend/dist/
36
+ echo.
37
+ echo Ready to push to Hugging Face!
38
+ echo.
39
+ echo Next steps:
40
+ echo 1. Commit changes: git add . && git commit -m "Build frontend"
41
+ echo 2. Push to GitHub: git push origin main
42
+ echo 3. GitHub Actions will auto-deploy to HF
43
+ echo.
44
+ echo Or push directly to HF:
45
+ echo git push hf main
46
+ echo ============================================
47
+ pause
build-frontend.sh ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pre-deployment Build Script
2
+
3
+ echo "============================================"
4
+ echo "Building Frontend for Production"
5
+ echo "============================================"
6
+ echo ""
7
+
8
+ cd frontend
9
+
10
+ echo "[1/2] Installing dependencies..."
11
+ npm install
12
+ if [ $? -ne 0 ]; then
13
+ echo "ERROR: Failed to install dependencies"
14
+ exit 1
15
+ fi
16
+ echo "✓ Dependencies installed"
17
+ echo ""
18
+
19
+ echo "[2/2] Building frontend..."
20
+ npm run build
21
+ if [ $? -ne 0 ]; then
22
+ echo "ERROR: Build failed"
23
+ exit 1
24
+ fi
25
+ echo "✓ Frontend built successfully"
26
+ echo ""
27
+
28
+ cd ..
29
+
30
+ echo "============================================"
31
+ echo "Build Complete!"
32
+ echo "============================================"
33
+ echo ""
34
+ echo "Frontend built to: frontend/dist/"
35
+ echo ""
36
+ echo "Ready to push to Hugging Face!"
37
+ echo ""
38
+ echo "Next steps:"
39
+ echo " 1. Commit changes: git add . && git commit -m 'Build frontend'"
40
+ echo " 2. Push to GitHub: git push origin main"
41
+ echo " 3. GitHub Actions will auto-deploy to HF"
42
+ echo ""
43
+ echo "Or push directly to HF:"
44
+ echo " git push hf main"
45
+ echo "============================================"
config.py CHANGED
@@ -34,7 +34,7 @@ HP_VALID_RANGE = (20, 120)
34
  ASSET_COST_VALID_RANGE = (100_000, 3_000_000)
35
 
36
  # Cost calculation
37
- COST_PER_GPU_HOUR = 0.5 # USD
38
 
39
  # API settings
40
  API_TITLE = "Invoice Information Extractor API"
 
34
  ASSET_COST_VALID_RANGE = (100_000, 3_000_000)
35
 
36
  # Cost calculation
37
+ COST_PER_GPU_HOUR = 0.6 # USD
38
 
39
  # API settings
40
  API_TITLE = "Invoice Information Extractor API"
frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build outputs
5
+ dist/
6
+ build/
7
+
8
+ # Environment files
9
+ .env
10
+ .env.local
11
+ .env.*.local
12
+
13
+ # Logs
14
+ npm-debug.log*
15
+ yarn-debug.log*
16
+ yarn-error.log*
17
+ pnpm-debug.log*
18
+
19
+ # Editor directories and files
20
+ .vscode/*
21
+ !.vscode/extensions.json
22
+ .idea
23
+ *.suo
24
+ *.ntvs*
25
+ *.njsproj
26
+ *.sln
27
+ *.sw?
28
+
29
+ # OS files
30
+ .DS_Store
31
+ Thumbs.db
32
+
33
+ # Testing
34
+ coverage/
35
+ .nyc_output/
36
+
37
+ # Cache
38
+ .cache/
39
+ .parcel-cache/
40
+ .env.example
41
+ .env.production
frontend/README.md ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Invoice Extractor Frontend
2
+
3
+ A modern, interactive React frontend for the Invoice Information Extractor application.
4
+
5
+ ## Features
6
+
7
+ - 📁 **Multiple File Upload**: Support for images (JPEG, PNG, GIF, WEBP) and PDF files
8
+ - 🔄 **PDF Conversion**: Automatic PDF to image conversion in the browser
9
+ - 📊 **Batch Processing**: Process multiple files sequentially with real-time progress
10
+ - 🎨 **Visual Detection**: Display bounding boxes for detected signatures and stamps
11
+ - 📱 **Responsive Design**: Works seamlessly on desktop and mobile devices
12
+ - ⚡ **Real-time Updates**: See results as they are processed
13
+
14
+ ## Installation
15
+
16
+ 1. Install dependencies:
17
+ ```bash
18
+ cd frontend
19
+ npm install
20
+ ```
21
+
22
+ 2. Create environment file:
23
+ ```bash
24
+ cp .env.example .env
25
+ ```
26
+
27
+ 3. Update `.env` with your backend API URL (default: http://localhost:8000)
28
+
29
+ ## Development
30
+
31
+ Start the development server:
32
+ ```bash
33
+ npm run dev
34
+ ```
35
+
36
+ The app will be available at `http://localhost:3000`
37
+
38
+ ## Building for Production
39
+
40
+ ```bash
41
+ npm run build
42
+ ```
43
+
44
+ The built files will be in the `dist` directory.
45
+
46
+ ## Preview Production Build
47
+
48
+ ```bash
49
+ npm run preview
50
+ ```
51
+
52
+ ## Technology Stack
53
+
54
+ - **React 18** - UI framework
55
+ - **Vite** - Build tool and dev server
56
+ - **Tailwind CSS** - Styling
57
+ - **PDF.js** - PDF rendering and conversion
58
+ - **Axios** - HTTP client
59
+ - **Lucide React** - Icon library
60
+
61
+ ## Usage
62
+
63
+ 1. **Upload Files**: Drag and drop or click to select images/PDFs
64
+ 2. **Process**: Click "Process" button to send files to the backend
65
+ 3. **View Results**: See extracted text, signatures, and stamps with visual indicators
66
+ 4. **Batch Processing**: Multiple files are processed one by one with progress tracking
67
+
68
+ ## API Integration
69
+
70
+ The frontend expects the following backend endpoints:
71
+
72
+ - `POST /process-invoice` - Process a single invoice image
73
+ - Request: FormData with 'file' field
74
+ - Response: JSON with extracted_text, signature_coords, stamp_coords
75
+
76
+ ## Features in Detail
77
+
78
+ ### PDF to Image Conversion
79
+ - Converts multi-page PDFs to individual images
80
+ - Each page is processed separately
81
+ - High-quality conversion using PDF.js
82
+
83
+ ### Bounding Box Visualization
84
+ - Red boxes for signatures
85
+ - Blue boxes for stamps
86
+ - Drawn directly on the canvas over the image
87
+
88
+ ### Batch Processing
89
+ - Sequential processing to avoid overwhelming the backend
90
+ - Real-time progress updates
91
+ - Individual error handling per file
92
+
93
+ ## Customization
94
+
95
+ ### Styling
96
+ Edit `tailwind.config.js` to customize colors and theme.
97
+
98
+ ### API Endpoint
99
+ Update `VITE_API_URL` in `.env` file.
100
+
101
+ ### Supported File Types
102
+ Modify the `accept` attribute in FileUpload.jsx and validation logic.
103
+
104
+ ## Troubleshooting
105
+
106
+ **CORS Issues**: Make sure your backend allows requests from the frontend origin.
107
+
108
+ **PDF Not Loading**: Check that PDF.js worker is properly loaded (check browser console).
109
+
110
+ **API Errors**: Verify backend is running and accessible at the configured URL.
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Invoice Information Extractor</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "invoice-extractor-frontend",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0",
14
+ "axios": "^1.6.2",
15
+ "pdfjs-dist": "^3.11.174",
16
+ "lucide-react": "^0.294.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^18.2.43",
20
+ "@types/react-dom": "^18.2.17",
21
+ "@vitejs/plugin-react": "^4.2.1",
22
+ "autoprefixer": "^10.4.16",
23
+ "postcss": "^8.4.32",
24
+ "tailwindcss": "^3.3.6",
25
+ "vite": "^5.0.8"
26
+ }
27
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { FileText, AlertCircle } from 'lucide-react';
3
+ import FileUpload from './components/FileUpload';
4
+ import ProgressIndicator from './components/ProgressIndicator';
5
+ import ResultCard from './components/ResultCard';
6
+ import { convertFileToImages, dataUrlToBlob } from './utils/fileConverter';
7
+ import { processBatchInvoices } from './utils/api';
8
+
9
+ function App() {
10
+ const [processing, setProcessing] = useState(false);
11
+ const [results, setResults] = useState([]);
12
+ const [progress, setProgress] = useState({ total: 0, completed: 0, current: '' });
13
+ const [error, setError] = useState(null);
14
+ const [imageDataMap, setImageDataMap] = useState({});
15
+
16
+ const handleFilesSelected = async (files) => {
17
+ setProcessing(true);
18
+ setResults([]);
19
+ setError(null);
20
+ setImageDataMap({});
21
+
22
+ try {
23
+ // Step 1: Convert all files to images
24
+ const allImages = [];
25
+ const imageData = {};
26
+
27
+ for (const file of files) {
28
+ try {
29
+ const images = await convertFileToImages(file);
30
+ images.forEach((img, idx) => {
31
+ const key = `${file.name}_${idx}`;
32
+ allImages.push({
33
+ blob: dataUrlToBlob(img.dataUrl),
34
+ filename: img.filename,
35
+ originalFile: img.originalFile,
36
+ pageNumber: img.pageNumber,
37
+ key: key
38
+ });
39
+ imageData[key] = img.dataUrl;
40
+ });
41
+ } catch (err) {
42
+ console.error(`Error converting ${file.name}:`, err);
43
+ setError(`Failed to convert ${file.name}: ${err.message}`);
44
+ }
45
+ }
46
+
47
+ setImageDataMap(imageData);
48
+ setProgress({ total: allImages.length, completed: 0, current: '' });
49
+
50
+ // Step 2: Process all images through the API
51
+ await processBatchInvoices(allImages, (index, result) => {
52
+ // Update progress
53
+ setProgress(prev => ({
54
+ ...prev,
55
+ completed: index + 1,
56
+ current: index < allImages.length - 1 ? allImages[index + 1].filename : ''
57
+ }));
58
+
59
+ // Add result
60
+ setResults(prev => [...prev, result]);
61
+ });
62
+
63
+ } catch (err) {
64
+ console.error('Processing error:', err);
65
+ setError(`Processing failed: ${err.message}`);
66
+ } finally {
67
+ setProcessing(false);
68
+ setProgress(prev => ({ ...prev, current: '' }));
69
+ }
70
+ };
71
+
72
+ return (
73
+ <div className="min-h-screen py-8 px-4 sm:px-6 lg:px-8">
74
+ <div className="max-w-7xl mx-auto">
75
+ {/* Header */}
76
+ <div className="text-center mb-8">
77
+ <div className="flex items-center justify-center mb-4">
78
+ <div className="p-3 bg-white rounded-2xl shadow-lg">
79
+ <FileText className="w-12 h-12 text-primary-600" />
80
+ </div>
81
+ </div>
82
+ <h1 className="text-4xl font-bold text-white mb-2 drop-shadow-lg">
83
+ Invoice Information Extractor
84
+ </h1>
85
+ <p className="text-lg text-white/90 drop-shadow">
86
+ Extract text, detect signatures and stamps from invoices using AI
87
+ </p>
88
+ </div>
89
+
90
+ {/* Main Content */}
91
+ <div className="space-y-6">
92
+ {/* Upload Section */}
93
+ <div className="glass-morphism p-6">
94
+ <FileUpload onFilesSelected={handleFilesSelected} disabled={processing} />
95
+ </div>
96
+
97
+ {/* Error Display */}
98
+ {error && (
99
+ <div className="bg-red-50 border-2 border-red-200 rounded-xl p-4 flex items-start">
100
+ <AlertCircle className="w-6 h-6 text-red-600 mr-3 flex-shrink-0 mt-0.5" />
101
+ <div>
102
+ <h3 className="text-red-800 font-semibold mb-1">Error</h3>
103
+ <p className="text-red-700 text-sm">{error}</p>
104
+ </div>
105
+ </div>
106
+ )}
107
+
108
+ {/* Progress Indicator */}
109
+ {processing && (
110
+ <ProgressIndicator
111
+ total={progress.total}
112
+ completed={progress.completed}
113
+ current={progress.current}
114
+ results={results}
115
+ />
116
+ )}
117
+
118
+ {/* Results Section */}
119
+ {results.length > 0 && (
120
+ <div className="space-y-4">
121
+ <div className="flex items-center justify-between bg-white rounded-xl p-4 shadow-md">
122
+ <h2 className="text-2xl font-bold text-gray-800 flex items-center">
123
+ <FileText className="w-6 h-6 mr-2 text-primary-600" />
124
+ Processing Results
125
+ </h2>
126
+ <span className="text-sm font-medium text-gray-600 bg-primary-50 px-3 py-1 rounded-full">
127
+ {results.length} {results.length === 1 ? 'Document' : 'Documents'}
128
+ </span>
129
+ </div>
130
+
131
+ {results.map((result, index) => {
132
+ const imageKey = `${result.originalFile}_${index}`;
133
+ return (
134
+ <ResultCard
135
+ key={index}
136
+ result={result}
137
+ imageData={imageDataMap[imageKey] || imageDataMap[Object.keys(imageDataMap)[index]]}
138
+ />
139
+ );
140
+ })}
141
+ </div>
142
+ )}
143
+
144
+ {/* Info Cards */}
145
+ {!processing && results.length === 0 && (
146
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
147
+ <div className="glass-morphism p-6 text-center">
148
+ <div className="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-3">
149
+ <FileText className="w-6 h-6 text-primary-600" />
150
+ </div>
151
+ <h3 className="font-semibold text-gray-800 mb-2">Multiple Formats</h3>
152
+ <p className="text-sm text-gray-600">
153
+ Upload images or PDFs. PDFs are automatically converted to images.
154
+ </p>
155
+ </div>
156
+
157
+ <div className="glass-morphism p-6 text-center">
158
+ <div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
159
+ <svg className="w-6 h-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
160
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
161
+ </svg>
162
+ </div>
163
+ <h3 className="font-semibold text-gray-800 mb-2">Batch Processing</h3>
164
+ <p className="text-sm text-gray-600">
165
+ Process multiple documents at once and see results one by one.
166
+ </p>
167
+ </div>
168
+
169
+ <div className="glass-morphism p-6 text-center">
170
+ <div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3">
171
+ <svg className="w-6 h-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
172
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
173
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
174
+ </svg>
175
+ </div>
176
+ <h3 className="font-semibold text-gray-800 mb-2">Visual Detection</h3>
177
+ <p className="text-sm text-gray-600">
178
+ Automatically detect and highlight signatures and stamps on documents.
179
+ </p>
180
+ </div>
181
+ </div>
182
+ )}
183
+ </div>
184
+
185
+ {/* Footer */}
186
+ <div className="mt-12 text-center text-white/80 text-sm">
187
+ <p>Powered by AI • Secure Processing • Fast & Accurate</p>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ );
192
+ }
193
+
194
+ export default App;
frontend/src/components/FileUpload.jsx ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useState } from 'react';
2
+ import { Upload, FileText, Image as ImageIcon, X } from 'lucide-react';
3
+
4
+ const FileUpload = ({ onFilesSelected, disabled }) => {
5
+ const [dragActive, setDragActive] = useState(false);
6
+ const [selectedFiles, setSelectedFiles] = useState([]);
7
+
8
+ const handleDrag = useCallback((e) => {
9
+ e.preventDefault();
10
+ e.stopPropagation();
11
+ if (e.type === 'dragenter' || e.type === 'dragover') {
12
+ setDragActive(true);
13
+ } else if (e.type === 'dragleave') {
14
+ setDragActive(false);
15
+ }
16
+ }, []);
17
+
18
+ const handleDrop = useCallback((e) => {
19
+ e.preventDefault();
20
+ e.stopPropagation();
21
+ setDragActive(false);
22
+
23
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
24
+ handleFiles(Array.from(e.dataTransfer.files));
25
+ }
26
+ }, []);
27
+
28
+ const handleFiles = (files) => {
29
+ const validFiles = files.filter(file => {
30
+ const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
31
+ return validTypes.includes(file.type);
32
+ });
33
+
34
+ if (validFiles.length !== files.length) {
35
+ alert('Some files were skipped. Only images (JPEG, PNG, GIF, WEBP) and PDF files are supported.');
36
+ }
37
+
38
+ setSelectedFiles(prev => [...prev, ...validFiles]);
39
+ };
40
+
41
+ const handleFileInput = (e) => {
42
+ if (e.target.files && e.target.files.length > 0) {
43
+ handleFiles(Array.from(e.target.files));
44
+ }
45
+ };
46
+
47
+ const removeFile = (index) => {
48
+ setSelectedFiles(prev => prev.filter((_, i) => i !== index));
49
+ };
50
+
51
+ const handleProcess = () => {
52
+ if (selectedFiles.length > 0) {
53
+ onFilesSelected(selectedFiles);
54
+ setSelectedFiles([]);
55
+ }
56
+ };
57
+
58
+ const formatFileSize = (bytes) => {
59
+ if (bytes === 0) return '0 Bytes';
60
+ const k = 1024;
61
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
62
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
63
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
64
+ };
65
+
66
+ return (
67
+ <div className="w-full space-y-4">
68
+ {/* Upload Zone */}
69
+ <div
70
+ className={`upload-zone relative border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
71
+ dragActive ? 'drag-active' : ''
72
+ } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
73
+ onDragEnter={handleDrag}
74
+ onDragLeave={handleDrag}
75
+ onDragOver={handleDrag}
76
+ onDrop={handleDrop}
77
+ onClick={() => !disabled && document.getElementById('fileInput').click()}
78
+ >
79
+ <input
80
+ id="fileInput"
81
+ type="file"
82
+ multiple
83
+ accept="image/*,.pdf"
84
+ onChange={handleFileInput}
85
+ className="hidden"
86
+ disabled={disabled}
87
+ />
88
+
89
+ <div className="flex flex-col items-center justify-center space-y-4">
90
+ <div className="p-4 bg-primary-50 rounded-full">
91
+ <Upload className="w-12 h-12 text-primary-500" />
92
+ </div>
93
+
94
+ <div>
95
+ <p className="text-lg font-semibold text-gray-700 mb-1">
96
+ Drop your files here or click to browse
97
+ </p>
98
+ <p className="text-sm text-gray-500">
99
+ Support for images (JPEG, PNG, GIF, WEBP) and PDF files
100
+ </p>
101
+ </div>
102
+
103
+ <div className="flex items-center gap-2 text-xs text-gray-400">
104
+ <ImageIcon className="w-4 h-4" />
105
+ <span>Multiple files supported</span>
106
+ <span>•</span>
107
+ <FileText className="w-4 h-4" />
108
+ <span>PDF to image conversion</span>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ {/* Selected Files List */}
114
+ {selectedFiles.length > 0 && (
115
+ <div className="space-y-3">
116
+ <div className="flex items-center justify-between">
117
+ <h3 className="text-sm font-semibold text-gray-700">
118
+ Selected Files ({selectedFiles.length})
119
+ </h3>
120
+ <button
121
+ onClick={() => setSelectedFiles([])}
122
+ className="text-xs text-red-500 hover:text-red-700 font-medium"
123
+ >
124
+ Clear All
125
+ </button>
126
+ </div>
127
+
128
+ <div className="space-y-2 max-h-64 overflow-y-auto">
129
+ {selectedFiles.map((file, index) => (
130
+ <div
131
+ key={index}
132
+ className="flex items-center justify-between bg-white p-3 rounded-lg border border-gray-200 hover:border-primary-300 transition-colors"
133
+ >
134
+ <div className="flex items-center space-x-3 flex-1 min-w-0">
135
+ <div className="flex-shrink-0">
136
+ {file.type === 'application/pdf' ? (
137
+ <FileText className="w-8 h-8 text-red-500" />
138
+ ) : (
139
+ <ImageIcon className="w-8 h-8 text-blue-500" />
140
+ )}
141
+ </div>
142
+ <div className="flex-1 min-w-0">
143
+ <p className="text-sm font-medium text-gray-900 truncate">
144
+ {file.name}
145
+ </p>
146
+ <p className="text-xs text-gray-500">
147
+ {formatFileSize(file.size)}
148
+ {file.type === 'application/pdf' && ' • Will be converted to images'}
149
+ </p>
150
+ </div>
151
+ </div>
152
+ <button
153
+ onClick={() => removeFile(index)}
154
+ className="ml-2 p-1 hover:bg-red-50 rounded-full transition-colors"
155
+ title="Remove file"
156
+ >
157
+ <X className="w-5 h-5 text-red-500" />
158
+ </button>
159
+ </div>
160
+ ))}
161
+ </div>
162
+
163
+ <button
164
+ onClick={handleProcess}
165
+ disabled={disabled}
166
+ className="w-full bg-gradient-to-r from-primary-500 to-primary-600 text-white font-semibold py-3 px-6 rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
167
+ >
168
+ <Upload className="w-5 h-5" />
169
+ <span>Process {selectedFiles.length} File{selectedFiles.length > 1 ? 's' : ''}</span>
170
+ </button>
171
+ </div>
172
+ )}
173
+ </div>
174
+ );
175
+ };
176
+
177
+ export default FileUpload;
frontend/src/components/ProgressIndicator.jsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Loader2, CheckCircle, XCircle } from 'lucide-react';
3
+
4
+ const ProgressIndicator = ({ total, completed, current, results }) => {
5
+ const progress = total > 0 ? (completed / total) * 100 : 0;
6
+ const successCount = results.filter(r => r.success).length;
7
+ const errorCount = results.filter(r => !r.success).length;
8
+
9
+ return (
10
+ <div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
11
+ <div className="space-y-4">
12
+ <div className="flex items-center justify-between">
13
+ <h3 className="text-lg font-semibold text-gray-800 flex items-center">
14
+ <Loader2 className="w-5 h-5 mr-2 text-primary-500 animate-spin" />
15
+ Processing Files
16
+ </h3>
17
+ <span className="text-sm font-medium text-gray-600">
18
+ {completed} / {total}
19
+ </span>
20
+ </div>
21
+
22
+ {/* Progress Bar */}
23
+ <div className="relative">
24
+ <div className="overflow-hidden h-3 text-xs flex rounded-full bg-gray-200">
25
+ <div
26
+ style={{ width: `${progress}%` }}
27
+ className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-gradient-to-r from-primary-500 to-primary-600 transition-all duration-500"
28
+ />
29
+ </div>
30
+ <div className="mt-2 text-xs text-gray-500 text-center">
31
+ {Math.round(progress)}% Complete
32
+ </div>
33
+ </div>
34
+
35
+ {/* Current Processing */}
36
+ {current && (
37
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
38
+ <p className="text-sm text-blue-800 flex items-center">
39
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
40
+ Currently processing: <span className="font-medium ml-1">{current}</span>
41
+ </p>
42
+ </div>
43
+ )}
44
+
45
+ {/* Statistics */}
46
+ <div className="grid grid-cols-3 gap-3 pt-2">
47
+ <div className="text-center">
48
+ <div className="text-2xl font-bold text-gray-800">{total}</div>
49
+ <div className="text-xs text-gray-500">Total</div>
50
+ </div>
51
+ <div className="text-center">
52
+ <div className="text-2xl font-bold text-green-600 flex items-center justify-center">
53
+ {successCount}
54
+ {successCount > 0 && <CheckCircle className="w-5 h-5 ml-1" />}
55
+ </div>
56
+ <div className="text-xs text-gray-500">Success</div>
57
+ </div>
58
+ <div className="text-center">
59
+ <div className="text-2xl font-bold text-red-600 flex items-center justify-center">
60
+ {errorCount}
61
+ {errorCount > 0 && <XCircle className="w-5 h-5 ml-1" />}
62
+ </div>
63
+ <div className="text-xs text-gray-500">Errors</div>
64
+ </div>
65
+ </div>
66
+
67
+ {/* Results List */}
68
+ {results.length > 0 && (
69
+ <div className="border-t pt-4 mt-4">
70
+ <h4 className="text-sm font-semibold text-gray-700 mb-3">Processing Status</h4>
71
+ <div className="space-y-2 max-h-48 overflow-y-auto">
72
+ {results.map((result, index) => (
73
+ <div
74
+ key={index}
75
+ className={`flex items-center justify-between p-2 rounded ${
76
+ result.success ? 'bg-green-50' : 'bg-red-50'
77
+ }`}
78
+ >
79
+ <span className="text-sm text-gray-700 truncate flex-1">
80
+ {result.filename}
81
+ </span>
82
+ {result.success ? (
83
+ <CheckCircle className="w-4 h-4 text-green-600 flex-shrink-0 ml-2" />
84
+ ) : (
85
+ <XCircle className="w-4 h-4 text-red-600 flex-shrink-0 ml-2" />
86
+ )}
87
+ </div>
88
+ ))}
89
+ </div>
90
+ </div>
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ };
96
+
97
+ export default ProgressIndicator;
frontend/src/components/ResultCard.jsx ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef, useEffect, useState } from 'react';
2
+
3
+ const ResultCard = ({ result, imageData }) => {
4
+ const canvasRef = useRef(null);
5
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
6
+
7
+ useEffect(() => {
8
+ if (!imageData || !canvasRef.current) return;
9
+
10
+ const canvas = canvasRef.current;
11
+ const ctx = canvas.getContext('2d');
12
+ const img = new Image();
13
+
14
+ img.onload = () => {
15
+ // Set canvas size to match container while maintaining aspect ratio
16
+ const maxWidth = 800;
17
+ const maxHeight = 600;
18
+ let width = img.width;
19
+ let height = img.height;
20
+
21
+ if (width > maxWidth) {
22
+ height = (height * maxWidth) / width;
23
+ width = maxWidth;
24
+ }
25
+ if (height > maxHeight) {
26
+ width = (width * maxHeight) / height;
27
+ height = maxHeight;
28
+ }
29
+
30
+ canvas.width = width;
31
+ canvas.height = height;
32
+ setDimensions({ width, height });
33
+
34
+ // Draw image
35
+ ctx.drawImage(img, 0, 0, width, height);
36
+
37
+ // Calculate scale factors
38
+ const scaleX = width / img.width;
39
+ const scaleY = height / img.height;
40
+
41
+ // Draw bounding boxes for signature
42
+ if (result.signature_coords && result.signature_coords.length > 0) {
43
+ ctx.strokeStyle = '#ef4444';
44
+ ctx.lineWidth = 3;
45
+ ctx.setLineDash([5, 5]);
46
+
47
+ result.signature_coords.forEach(coords => {
48
+ const [x1, y1, x2, y2] = coords;
49
+ ctx.strokeRect(
50
+ x1 * scaleX,
51
+ y1 * scaleY,
52
+ (x2 - x1) * scaleX,
53
+ (y2 - y1) * scaleY
54
+ );
55
+ });
56
+
57
+ // Add label
58
+ ctx.fillStyle = '#ef4444';
59
+ ctx.font = 'bold 14px Arial';
60
+ ctx.fillText('Signature', result.signature_coords[0][0] * scaleX, result.signature_coords[0][1] * scaleY - 5);
61
+ }
62
+
63
+ // Draw bounding boxes for stamp
64
+ if (result.stamp_coords && result.stamp_coords.length > 0) {
65
+ ctx.strokeStyle = '#3b82f6';
66
+ ctx.lineWidth = 3;
67
+ ctx.setLineDash([5, 5]);
68
+
69
+ result.stamp_coords.forEach(coords => {
70
+ const [x1, y1, x2, y2] = coords;
71
+ ctx.strokeRect(
72
+ x1 * scaleX,
73
+ y1 * scaleY,
74
+ (x2 - x1) * scaleX,
75
+ (y2 - y1) * scaleY
76
+ );
77
+ });
78
+
79
+ // Add label
80
+ ctx.fillStyle = '#3b82f6';
81
+ ctx.font = 'bold 14px Arial';
82
+ ctx.fillText('Stamp', result.stamp_coords[0][0] * scaleX, result.stamp_coords[0][1] * scaleY - 5);
83
+ }
84
+ };
85
+
86
+ img.src = imageData;
87
+ }, [imageData, result]);
88
+
89
+ if (!result.success) {
90
+ return (
91
+ <div className="bg-red-50 border-2 border-red-200 rounded-xl p-6 mb-4">
92
+ <div className="flex items-start">
93
+ <div className="flex-shrink-0">
94
+ <svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
95
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
96
+ </svg>
97
+ </div>
98
+ <div className="ml-3">
99
+ <h3 className="text-sm font-medium text-red-800">Processing Error</h3>
100
+ <div className="mt-2 text-sm text-red-700">
101
+ <p><strong>File:</strong> {result.filename}</p>
102
+ <p><strong>Error:</strong> {result.error}</p>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <div className="bg-white rounded-xl shadow-lg overflow-hidden mb-6 border border-gray-200">
112
+ {/* Header */}
113
+ <div className="bg-gradient-to-r from-primary-500 to-primary-600 px-6 py-4">
114
+ <h3 className="text-lg font-semibold text-white flex items-center">
115
+ <svg className="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
116
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
117
+ </svg>
118
+ {result.filename}
119
+ </h3>
120
+ {result.pageNumber && (
121
+ <p className="text-primary-100 text-sm mt-1">Page {result.pageNumber}</p>
122
+ )}
123
+ </div>
124
+
125
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
126
+ {/* Image with bounding boxes */}
127
+ <div className="space-y-4">
128
+ <h4 className="text-md font-semibold text-gray-700 flex items-center">
129
+ <svg className="w-5 h-5 mr-2 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
130
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
131
+ </svg>
132
+ Document Preview
133
+ </h4>
134
+ <div className="relative bg-gray-50 rounded-lg p-4 flex justify-center items-center">
135
+ <canvas ref={canvasRef} className="max-w-full h-auto rounded shadow-md" />
136
+ </div>
137
+ <div className="flex gap-4 text-sm">
138
+ {result.signature_coords && result.signature_coords.length > 0 && (
139
+ <div className="flex items-center">
140
+ <div className="w-4 h-4 border-2 border-red-500 mr-2"></div>
141
+ <span className="text-gray-600">Signature Detected</span>
142
+ </div>
143
+ )}
144
+ {result.stamp_coords && result.stamp_coords.length > 0 && (
145
+ <div className="flex items-center">
146
+ <div className="w-4 h-4 border-2 border-blue-500 mr-2"></div>
147
+ <span className="text-gray-600">Stamp Detected</span>
148
+ </div>
149
+ )}
150
+ </div>
151
+ </div>
152
+
153
+ {/* Extracted Information */}
154
+ <div className="space-y-4">
155
+ <h4 className="text-md font-semibold text-gray-700 flex items-center">
156
+ <svg className="w-5 h-5 mr-2 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
157
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
158
+ </svg>
159
+ Extracted Information
160
+ </h4>
161
+
162
+ <div className="bg-gray-50 rounded-lg p-4 space-y-3">
163
+ <div className="bg-white rounded-lg p-4 shadow-sm">
164
+ <h5 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">Extracted Text</h5>
165
+ <div className="text-sm text-gray-800 whitespace-pre-wrap max-h-96 overflow-y-auto font-mono bg-gray-50 p-3 rounded border border-gray-200">
166
+ {result.extracted_text || 'No text extracted'}
167
+ </div>
168
+ </div>
169
+
170
+ {/* Detection Status */}
171
+ <div className="grid grid-cols-2 gap-3">
172
+ <div className="bg-white rounded-lg p-4 shadow-sm">
173
+ <div className="flex items-center justify-between">
174
+ <span className="text-sm font-medium text-gray-600">Signature</span>
175
+ {result.signature_coords && result.signature_coords.length > 0 ? (
176
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
177
+ <svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
178
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
179
+ </svg>
180
+ Detected
181
+ </span>
182
+ ) : (
183
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
184
+ Not Found
185
+ </span>
186
+ )}
187
+ </div>
188
+ </div>
189
+
190
+ <div className="bg-white rounded-lg p-4 shadow-sm">
191
+ <div className="flex items-center justify-between">
192
+ <span className="text-sm font-medium text-gray-600">Stamp</span>
193
+ {result.stamp_coords && result.stamp_coords.length > 0 ? (
194
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
195
+ <svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
196
+ <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
197
+ </svg>
198
+ Detected
199
+ </span>
200
+ ) : (
201
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
202
+ Not Found
203
+ </span>
204
+ )}
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ {/* Coordinates Info */}
210
+ {(result.signature_coords?.length > 0 || result.stamp_coords?.length > 0) && (
211
+ <div className="bg-white rounded-lg p-4 shadow-sm">
212
+ <h5 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">Coordinates</h5>
213
+ <div className="text-xs space-y-2">
214
+ {result.signature_coords?.length > 0 && (
215
+ <div>
216
+ <span className="font-medium text-red-600">Signature:</span>
217
+ <code className="ml-2 text-gray-700">
218
+ {JSON.stringify(result.signature_coords)}
219
+ </code>
220
+ </div>
221
+ )}
222
+ {result.stamp_coords?.length > 0 && (
223
+ <div>
224
+ <span className="font-medium text-blue-600">Stamp:</span>
225
+ <code className="ml-2 text-gray-700">
226
+ {JSON.stringify(result.stamp_coords)}
227
+ </code>
228
+ </div>
229
+ )}
230
+ </div>
231
+ </div>
232
+ )}
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ );
238
+ };
239
+
240
+ export default ResultCard;
frontend/src/index.css ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ body {
6
+ margin: 0;
7
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
8
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
9
+ sans-serif;
10
+ -webkit-font-smoothing: antialiased;
11
+ -moz-osx-font-smoothing: grayscale;
12
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
13
+ min-height: 100vh;
14
+ }
15
+
16
+ .glass-morphism {
17
+ background: rgba(255, 255, 255, 0.95);
18
+ backdrop-filter: blur(10px);
19
+ border-radius: 20px;
20
+ border: 1px solid rgba(255, 255, 255, 0.18);
21
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
22
+ }
23
+
24
+ .upload-zone {
25
+ border: 2px dashed #cbd5e1;
26
+ transition: all 0.3s ease;
27
+ }
28
+
29
+ .upload-zone:hover {
30
+ border-color: #0ea5e9;
31
+ background-color: #f0f9ff;
32
+ }
33
+
34
+ .upload-zone.drag-active {
35
+ border-color: #0ea5e9;
36
+ background-color: #e0f2fe;
37
+ transform: scale(1.02);
38
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
frontend/src/utils/api.js ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+
3
+ // Use same origin in production (when deployed), localhost in development
4
+ const API_BASE_URL = import.meta.env.VITE_API_URL || window.location.origin;
5
+
6
+ /**
7
+ * Process a single invoice image
8
+ * @param {Blob} imageBlob - Image blob
9
+ * @param {string} filename - Original filename
10
+ * @returns {Promise<Object>} Processed result
11
+ */
12
+ export async function processSingleInvoice(imageBlob, filename) {
13
+ const formData = new FormData();
14
+ formData.append('file', imageBlob, filename);
15
+
16
+ const response = await axios.post(`${API_BASE_URL}/process-invoice`, formData, {
17
+ headers: {
18
+ 'Content-Type': 'multipart/form-data',
19
+ },
20
+ });
21
+
22
+ return response.data;
23
+ }
24
+
25
+ /**
26
+ * Process multiple invoices in batch
27
+ * @param {Array} images - Array of {blob, filename} objects
28
+ * @param {Function} onProgress - Progress callback (index, result)
29
+ * @returns {Promise<Array>} Array of results
30
+ */
31
+ export async function processBatchInvoices(images, onProgress) {
32
+ const results = [];
33
+
34
+ for (let i = 0; i < images.length; i++) {
35
+ try {
36
+ const result = await processSingleInvoice(images[i].blob, images[i].filename);
37
+ const resultWithMetadata = {
38
+ ...result,
39
+ filename: images[i].filename,
40
+ originalFile: images[i].originalFile,
41
+ pageNumber: images[i].pageNumber,
42
+ index: i,
43
+ success: true
44
+ };
45
+ results.push(resultWithMetadata);
46
+
47
+ if (onProgress) {
48
+ onProgress(i, resultWithMetadata);
49
+ }
50
+ } catch (error) {
51
+ const errorResult = {
52
+ filename: images[i].filename,
53
+ originalFile: images[i].originalFile,
54
+ pageNumber: images[i].pageNumber,
55
+ index: i,
56
+ success: false,
57
+ error: error.response?.data?.detail || error.message
58
+ };
59
+ results.push(errorResult);
60
+
61
+ if (onProgress) {
62
+ onProgress(i, errorResult);
63
+ }
64
+ }
65
+ }
66
+
67
+ return results;
68
+ }
frontend/src/utils/fileConverter.js ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as pdfjsLib from 'pdfjs-dist';
2
+
3
+ // Configure PDF.js worker
4
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;
5
+
6
+ /**
7
+ * Convert PDF to images
8
+ * @param {File} file - PDF file
9
+ * @returns {Promise<Array>} Array of image data URLs
10
+ */
11
+ export async function pdfToImages(file) {
12
+ const images = [];
13
+ const arrayBuffer = await file.arrayBuffer();
14
+ const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
15
+
16
+ for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
17
+ const page = await pdf.getPage(pageNum);
18
+ const viewport = page.getViewport({ scale: 2.0 });
19
+
20
+ const canvas = document.createElement('canvas');
21
+ const context = canvas.getContext('2d');
22
+ canvas.height = viewport.height;
23
+ canvas.width = viewport.width;
24
+
25
+ await page.render({
26
+ canvasContext: context,
27
+ viewport: viewport
28
+ }).promise;
29
+
30
+ images.push({
31
+ dataUrl: canvas.toDataURL('image/jpeg', 0.95),
32
+ pageNumber: pageNum
33
+ });
34
+ }
35
+
36
+ return images;
37
+ }
38
+
39
+ /**
40
+ * Convert image file to data URL
41
+ * @param {File} file - Image file
42
+ * @returns {Promise<string>} Image data URL
43
+ */
44
+ export function imageToDataUrl(file) {
45
+ return new Promise((resolve, reject) => {
46
+ const reader = new FileReader();
47
+ reader.onload = (e) => resolve(e.target.result);
48
+ reader.onerror = reject;
49
+ reader.readAsDataURL(file);
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Convert file to processable format
55
+ * @param {File} file - Input file
56
+ * @returns {Promise<Array>} Array of {dataUrl, filename, pageNumber} objects
57
+ */
58
+ export async function convertFileToImages(file) {
59
+ const fileType = file.type;
60
+
61
+ if (fileType === 'application/pdf') {
62
+ const pdfImages = await pdfToImages(file);
63
+ return pdfImages.map(img => ({
64
+ dataUrl: img.dataUrl,
65
+ filename: `${file.name}_page_${img.pageNumber}`,
66
+ pageNumber: img.pageNumber,
67
+ originalFile: file.name
68
+ }));
69
+ } else if (fileType.startsWith('image/')) {
70
+ const dataUrl = await imageToDataUrl(file);
71
+ return [{
72
+ dataUrl,
73
+ filename: file.name,
74
+ pageNumber: 1,
75
+ originalFile: file.name
76
+ }];
77
+ } else {
78
+ throw new Error('Unsupported file type. Please upload an image or PDF file.');
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Data URL to Blob conversion
84
+ * @param {string} dataUrl - Data URL
85
+ * @returns {Blob} Blob object
86
+ */
87
+ export function dataUrlToBlob(dataUrl) {
88
+ const arr = dataUrl.split(',');
89
+ const mime = arr[0].match(/:(.*?);/)[1];
90
+ const bstr = atob(arr[1]);
91
+ let n = bstr.length;
92
+ const u8arr = new Uint8Array(n);
93
+ while (n--) {
94
+ u8arr[n] = bstr.charCodeAt(n);
95
+ }
96
+ return new Blob([u8arr], { type: mime });
97
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ primary: {
11
+ 50: '#f0f9ff',
12
+ 100: '#e0f2fe',
13
+ 200: '#bae6fd',
14
+ 300: '#7dd3fc',
15
+ 400: '#38bdf8',
16
+ 500: '#0ea5e9',
17
+ 600: '#0284c7',
18
+ 700: '#0369a1',
19
+ 800: '#075985',
20
+ 900: '#0c4a6e',
21
+ }
22
+ }
23
+ },
24
+ },
25
+ plugins: [],
26
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 3000,
8
+ proxy: {
9
+ '/process-invoice': {
10
+ target: 'http://localhost:8000',
11
+ changeOrigin: true,
12
+ },
13
+ '/batch-process': {
14
+ target: 'http://localhost:8000',
15
+ changeOrigin: true,
16
+ }
17
+ }
18
+ }
19
+ })
setup.bat ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ============================================
3
+ echo Invoice Extractor - Complete Setup
4
+ echo ============================================
5
+ echo.
6
+
7
+ echo [1/4] Installing Backend Dependencies...
8
+ pip install -r requirements.txt
9
+ if errorlevel 1 (
10
+ echo ERROR: Failed to install backend dependencies
11
+ pause
12
+ exit /b 1
13
+ )
14
+ echo ✓ Backend dependencies installed
15
+ echo.
16
+
17
+ echo [2/4] Installing Frontend Dependencies...
18
+ cd frontend
19
+ call npm install
20
+ if errorlevel 1 (
21
+ echo ERROR: Failed to install frontend dependencies
22
+ pause
23
+ exit /b 1
24
+ )
25
+ echo ✓ Frontend dependencies installed
26
+ echo.
27
+
28
+ echo [3/4] Setting up Frontend Environment...
29
+ if not exist .env (
30
+ copy .env.example .env
31
+ echo ✓ Created .env file
32
+ ) else (
33
+ echo ✓ .env file already exists
34
+ )
35
+ cd ..
36
+ echo.
37
+
38
+ echo [4/4] Setup Complete!
39
+ echo.
40
+ echo ============================================
41
+ echo Ready to start the application!
42
+ echo ============================================
43
+ echo.
44
+ echo To start the backend:
45
+ echo python app.py
46
+ echo.
47
+ echo To start the frontend:
48
+ echo cd frontend
49
+ echo npm run dev
50
+ echo.
51
+ echo Then open http://localhost:3000 in your browser
52
+ echo ============================================
53
+ pause
start.bat ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ============================================
3
+ echo Starting Invoice Extractor Application
4
+ echo ============================================
5
+ echo.
6
+
7
+ echo Starting Backend Server...
8
+ start "Backend Server" cmd /k "python app.py"
9
+ timeout /t 5 /nobreak > nul
10
+
11
+ echo Starting Frontend Development Server...
12
+ start "Frontend Server" cmd /k "cd frontend && npm run dev"
13
+
14
+ echo.
15
+ echo ============================================
16
+ echo Both servers are starting!
17
+ echo ============================================
18
+ echo.
19
+ echo Backend: http://localhost:7860
20
+ echo Frontend: http://localhost:3000
21
+ echo.
22
+ echo Press any key to stop both servers...
23
+ pause > nul
24
+
25
+ taskkill /FI "WindowTitle eq Backend Server*" /T /F
26
+ taskkill /FI "WindowTitle eq Frontend Server*" /T /F