rongo1
commited on
Commit
Β·
29d41e1
0
Parent(s):
feat: init
Browse files- .gitignore +44 -0
- QUICK_START.md +53 -0
- README.md +118 -0
- app.py +602 -0
- business_cards/.gitkeep +3 -0
- env.example +10 -0
- prompts/prompt.txt +38 -0
- prompts/system_prompt.txt +1 -0
- requirements.txt +5 -0
- setup_hf_space.md +60 -0
.gitignore
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
*.so
|
6 |
+
.Python
|
7 |
+
env/
|
8 |
+
venv/
|
9 |
+
.venv/
|
10 |
+
|
11 |
+
# Environment variables
|
12 |
+
.env
|
13 |
+
|
14 |
+
# IDE
|
15 |
+
.vscode/
|
16 |
+
.idea/
|
17 |
+
*.swp
|
18 |
+
*.swo
|
19 |
+
|
20 |
+
# Output files
|
21 |
+
business_card_exports/
|
22 |
+
*.xlsx
|
23 |
+
*.xls
|
24 |
+
|
25 |
+
# Business card images (keep folder structure but ignore images)
|
26 |
+
business_cards/*.jpg
|
27 |
+
business_cards/*.jpeg
|
28 |
+
business_cards/*.png
|
29 |
+
business_cards/*.gif
|
30 |
+
business_cards/*.bmp
|
31 |
+
business_cards/*.webp
|
32 |
+
# Keep the .gitkeep file
|
33 |
+
!business_cards/.gitkeep
|
34 |
+
|
35 |
+
# Test files
|
36 |
+
test_business_cards/
|
37 |
+
|
38 |
+
# OS
|
39 |
+
.DS_Store
|
40 |
+
Thumbs.db
|
41 |
+
|
42 |
+
# Logs
|
43 |
+
*.log
|
44 |
+
business_card_extractor.log
|
QUICK_START.md
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Quick Start Guide
|
2 |
+
|
3 |
+
## 1. Install Dependencies
|
4 |
+
|
5 |
+
```bash
|
6 |
+
pip install -r requirements.txt
|
7 |
+
```
|
8 |
+
|
9 |
+
## 2. Run the Application
|
10 |
+
|
11 |
+
```bash
|
12 |
+
python app.py
|
13 |
+
```
|
14 |
+
|
15 |
+
## 3. Use the Web Interface
|
16 |
+
|
17 |
+
1. Open your browser to http://localhost:7860
|
18 |
+
2. Click "Upload Business Cards" and select one or more business card images
|
19 |
+
3. Choose your AI model: Flash (faster) or Pro (more accurate)
|
20 |
+
4. Choose whether to save images (enabled by default)
|
21 |
+
5. Click "Process Business Cards"
|
22 |
+
6. Download both generated Excel files:
|
23 |
+
- π **Current Run**: Just the cards you processed
|
24 |
+
- π **Total Database**: All cards ever processed
|
25 |
+
|
26 |
+
## What Gets Extracted?
|
27 |
+
|
28 |
+
- **Names**: Full name, first name, last name
|
29 |
+
- **Contact Info**: Multiple emails and phone numbers
|
30 |
+
- **Professional Info**: Job title, company, department
|
31 |
+
- **Location**: Full address, street, city, state, zip, country
|
32 |
+
- **Online**: Website, LinkedIn profile
|
33 |
+
- **Other**: Any additional information on the card
|
34 |
+
|
35 |
+
## Output Format
|
36 |
+
|
37 |
+
- Each business card = 1 row in Excel
|
38 |
+
- Two files: Current run + Cumulative database
|
39 |
+
- Multiple emails/phones are combined with commas
|
40 |
+
- Phone types (mobile/landline) are combined into one column
|
41 |
+
- Street and address are combined into one field
|
42 |
+
- Filename, processing date, and image path included for reference
|
43 |
+
- Images optionally saved to business_cards folder with timestamps
|
44 |
+
- Excel files are auto-formatted with proper column widths
|
45 |
+
|
46 |
+
## Tips
|
47 |
+
|
48 |
+
- Upload multiple cards at once (processed in batches of 5 for efficiency)
|
49 |
+
- Supported formats: JPG, PNG, JPEG
|
50 |
+
- Higher quality images = better extraction
|
51 |
+
- Batch processing reduces API calls and costs
|
52 |
+
- Enable image saving to keep a visual record of processed cards
|
53 |
+
- Check the preview table before downloading
|
README.md
ADDED
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Business Card Data Extractor
|
2 |
+
|
3 |
+
A Gradio-based application that extracts contact information from business card images using Google's Gemini API and exports the data to Excel.
|
4 |
+
|
5 |
+
## Features
|
6 |
+
|
7 |
+
- **Efficient Batch Processing**: Upload multiple cards, processed 5 at a time per API call
|
8 |
+
- **Model Selection**: Choose between Gemini 2.5 Flash (fast) or Pro (accurate)
|
9 |
+
- **Image Storage**: Optionally save business card images with timestamped filenames
|
10 |
+
- **AI-Powered Extraction**: Uses Gemini AI to extract:
|
11 |
+
- Names (full name, first name, last name)
|
12 |
+
- Job titles and departments
|
13 |
+
- Company information
|
14 |
+
- Email addresses (multiple supported)
|
15 |
+
- Phone numbers (multiple supported)
|
16 |
+
- Addresses
|
17 |
+
- Websites and social media links
|
18 |
+
- Additional information
|
19 |
+
- **Excel Export**: Automatically creates formatted Excel files
|
20 |
+
- **Data Consolidation**: Multiple emails/phones are combined with commas in a single cell
|
21 |
+
|
22 |
+
## Installation
|
23 |
+
|
24 |
+
1. Clone this repository
|
25 |
+
2. Install dependencies:
|
26 |
+
```bash
|
27 |
+
pip install -r requirements.txt
|
28 |
+
```
|
29 |
+
|
30 |
+
## Usage
|
31 |
+
|
32 |
+
1. Run the application:
|
33 |
+
```bash
|
34 |
+
python app.py
|
35 |
+
```
|
36 |
+
|
37 |
+
2. Open your browser to the provided URL (typically http://localhost:7860)
|
38 |
+
|
39 |
+
3. Upload one or more business card images
|
40 |
+
|
41 |
+
4. Click "Process Business Cards"
|
42 |
+
|
43 |
+
5. Download the generated Excel file
|
44 |
+
|
45 |
+
## Output Format
|
46 |
+
|
47 |
+
**Two Excel files are generated:**
|
48 |
+
|
49 |
+
1. **Current Run File**: Contains only the cards from the current session
|
50 |
+
2. **Total Database File**: Contains ALL cards ever processed (cumulative)
|
51 |
+
|
52 |
+
Each business card creates one row in the Excel file with columns for:
|
53 |
+
- filename
|
54 |
+
- processed_date
|
55 |
+
- method (AI model used: gemini-2.5-flash or gemini-2.5-pro)
|
56 |
+
- saved_image_path (path to saved image file, if image saving is enabled)
|
57 |
+
- full_name, first_name, last_name
|
58 |
+
- job_title, company, department
|
59 |
+
- emails (comma-separated if multiple)
|
60 |
+
- phones (all types combined, comma-separated if multiple)
|
61 |
+
- address (street and full address combined)
|
62 |
+
- city, state, postal_code, country
|
63 |
+
- website, linkedin
|
64 |
+
- And more...
|
65 |
+
|
66 |
+
## Configuration
|
67 |
+
|
68 |
+
### Environment Variables
|
69 |
+
|
70 |
+
Set the following environment variable:
|
71 |
+
- `Gemini_API`: Your Google Gemini API key
|
72 |
+
|
73 |
+
#### For Hugging Face Spaces:
|
74 |
+
1. Go to your Space settings
|
75 |
+
2. Add a new secret named `Gemini_API`
|
76 |
+
3. Set the value to your Google Gemini API key
|
77 |
+
|
78 |
+
#### For Local Development:
|
79 |
+
```bash
|
80 |
+
export Gemini_API="your_api_key_here"
|
81 |
+
```
|
82 |
+
|
83 |
+
Or create a `.env` file:
|
84 |
+
```bash
|
85 |
+
# Copy the example file
|
86 |
+
cp env.example .env
|
87 |
+
# Then edit .env with your actual API key
|
88 |
+
```
|
89 |
+
|
90 |
+
## Logging
|
91 |
+
|
92 |
+
The application includes comprehensive logging:
|
93 |
+
- **Log File**: `business_card_extractor.log` (created automatically)
|
94 |
+
- **Console Output**: Real-time logging to terminal
|
95 |
+
- **Log Levels**: INFO for general progress, DEBUG for detailed operations
|
96 |
+
- **Coverage**: Every processing step, API calls, file operations, and errors
|
97 |
+
|
98 |
+
Logs help with:
|
99 |
+
- Debugging extraction issues
|
100 |
+
- Monitoring API usage
|
101 |
+
- Tracking processing performance
|
102 |
+
- Identifying problematic business cards
|
103 |
+
|
104 |
+
## File Structure
|
105 |
+
|
106 |
+
```
|
107 |
+
Business_Cards_analyzer/
|
108 |
+
βββ app.py # Main Gradio application
|
109 |
+
βββ requirements.txt # Python dependencies
|
110 |
+
βββ env.example # Environment variables template
|
111 |
+
βββ setup_hf_space.md # Hugging Face deployment guide
|
112 |
+
βββ prompts/ # AI prompts for data extraction
|
113 |
+
β βββ prompt.txt
|
114 |
+
β βββ system_prompt.txt
|
115 |
+
βββ business_card_exports/ # Output Excel files
|
116 |
+
βββ business_cards/ # Saved business card images (optional)
|
117 |
+
βββ .gitkeep # Ensures directory exists
|
118 |
+
```
|
app.py
ADDED
@@ -0,0 +1,602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import google.generativeai as genai
|
3 |
+
import json
|
4 |
+
import pandas as pd
|
5 |
+
from datetime import datetime
|
6 |
+
import os
|
7 |
+
from pathlib import Path
|
8 |
+
from PIL import Image
|
9 |
+
import io
|
10 |
+
import base64
|
11 |
+
import logging
|
12 |
+
import sys
|
13 |
+
import shutil
|
14 |
+
|
15 |
+
# Configure logging
|
16 |
+
logging.basicConfig(
|
17 |
+
level=logging.INFO,
|
18 |
+
format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
|
19 |
+
handlers=[
|
20 |
+
logging.FileHandler('business_card_extractor.log'),
|
21 |
+
logging.StreamHandler(sys.stdout)
|
22 |
+
]
|
23 |
+
)
|
24 |
+
logger = logging.getLogger(__name__)
|
25 |
+
|
26 |
+
# Configure Gemini API
|
27 |
+
logger.info("Configuring Gemini API")
|
28 |
+
gemini_api_key = os.getenv("Gemini_API")
|
29 |
+
if not gemini_api_key:
|
30 |
+
logger.error("Gemini_API environment variable not found!")
|
31 |
+
logger.error("Please set the Gemini_API environment variable with your Google Gemini API key")
|
32 |
+
logger.error("For Hugging Face Spaces: Add it as a Repository Secret in Space Settings")
|
33 |
+
raise ValueError("β Gemini_API environment variable is required. Please set it in your environment or Hugging Face Space secrets.")
|
34 |
+
|
35 |
+
genai.configure(api_key=gemini_api_key)
|
36 |
+
logger.info("Gemini API configured successfully")
|
37 |
+
|
38 |
+
# Create output directories
|
39 |
+
logger.info("Setting up output directories")
|
40 |
+
output_dir = Path("business_card_exports")
|
41 |
+
images_dir = Path("business_cards")
|
42 |
+
output_dir.mkdir(exist_ok=True)
|
43 |
+
images_dir.mkdir(exist_ok=True)
|
44 |
+
logger.info(f"Export directory created/verified: {output_dir}")
|
45 |
+
logger.info(f"Images directory created/verified: {images_dir}")
|
46 |
+
|
47 |
+
# Log startup
|
48 |
+
logger.info("Business Card Data Extractor starting up")
|
49 |
+
logger.info(f"Working directory: {os.getcwd()}")
|
50 |
+
logger.info(f"Export directory: {output_dir.absolute()}")
|
51 |
+
logger.info(f"Images directory: {images_dir.absolute()}")
|
52 |
+
|
53 |
+
def extract_business_card_data_batch(images, filenames, model_name="gemini-2.5-flash"):
|
54 |
+
"""Extract data from multiple business card images in a single API call"""
|
55 |
+
|
56 |
+
logger.info(f"Starting batch extraction for {len(images)} images using model: {model_name}")
|
57 |
+
logger.debug(f"Filenames in batch: {filenames}")
|
58 |
+
|
59 |
+
# Load prompts
|
60 |
+
logger.debug("Loading prompt templates")
|
61 |
+
try:
|
62 |
+
with open("prompts/prompt.txt", "r", encoding="utf-8") as f:
|
63 |
+
prompt_template = f.read()
|
64 |
+
logger.debug(f"Loaded prompt template ({len(prompt_template)} characters)")
|
65 |
+
|
66 |
+
with open("prompts/system_prompt.txt", "r", encoding="utf-8") as f:
|
67 |
+
system_prompt = f.read()
|
68 |
+
logger.debug(f"Loaded system prompt ({len(system_prompt)} characters)")
|
69 |
+
except FileNotFoundError as e:
|
70 |
+
logger.error(f"Failed to load prompt files: {e}")
|
71 |
+
raise
|
72 |
+
|
73 |
+
# Configure model
|
74 |
+
logger.debug(f"Configuring Gemini model: {model_name}")
|
75 |
+
generation_config = {
|
76 |
+
"temperature": 0.1,
|
77 |
+
"response_mime_type": "application/json"
|
78 |
+
}
|
79 |
+
|
80 |
+
try:
|
81 |
+
model = genai.GenerativeModel(
|
82 |
+
model_name=model_name,
|
83 |
+
generation_config=generation_config,
|
84 |
+
system_instruction=system_prompt
|
85 |
+
)
|
86 |
+
logger.debug("Gemini model configured successfully")
|
87 |
+
except Exception as e:
|
88 |
+
logger.error(f"Failed to configure Gemini model: {e}")
|
89 |
+
raise
|
90 |
+
|
91 |
+
# Prepare multiple images for the model
|
92 |
+
logger.debug("Preparing content parts for API request")
|
93 |
+
content_parts = []
|
94 |
+
|
95 |
+
# Add the prompt first
|
96 |
+
batch_prompt = f"""
|
97 |
+
{prompt_template}
|
98 |
+
|
99 |
+
I'm sending you {len(images)} business card images. Please extract the data from each card and return a JSON array with {len(images)} objects. Each object should contain the extracted data for one business card in the same order as the images.
|
100 |
+
|
101 |
+
Return format: [card1_data, card2_data, card3_data, ...]
|
102 |
+
"""
|
103 |
+
content_parts.append(batch_prompt)
|
104 |
+
logger.debug(f"Added batch prompt ({len(batch_prompt)} characters)")
|
105 |
+
|
106 |
+
# Add each image
|
107 |
+
logger.debug("Converting and adding images to request")
|
108 |
+
for i, image in enumerate(images):
|
109 |
+
try:
|
110 |
+
buffered = io.BytesIO()
|
111 |
+
image.save(buffered, format="PNG")
|
112 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode()
|
113 |
+
|
114 |
+
image_part = {
|
115 |
+
"mime_type": "image/png",
|
116 |
+
"data": img_base64
|
117 |
+
}
|
118 |
+
content_parts.append(f"Business Card {i+1}:")
|
119 |
+
content_parts.append(image_part)
|
120 |
+
logger.debug(f"Added image {i+1} ({len(img_base64)} base64 characters)")
|
121 |
+
except Exception as e:
|
122 |
+
logger.error(f"Failed to process image {i+1} ({filenames[i] if i < len(filenames) else 'unknown'}): {e}")
|
123 |
+
raise
|
124 |
+
|
125 |
+
# Generate content
|
126 |
+
logger.info(f"Making API call to {model_name} with {len(content_parts)} content parts")
|
127 |
+
try:
|
128 |
+
response = model.generate_content(content_parts)
|
129 |
+
logger.info(f"API call successful. Response length: {len(response.text) if response.text else 0} characters")
|
130 |
+
logger.debug(f"Raw response: {response.text[:500]}..." if len(response.text) > 500 else f"Raw response: {response.text}")
|
131 |
+
except Exception as e:
|
132 |
+
logger.error(f"API call failed: {e}")
|
133 |
+
raise
|
134 |
+
|
135 |
+
# Parse response
|
136 |
+
logger.debug("Parsing JSON response")
|
137 |
+
try:
|
138 |
+
# Parse JSON response
|
139 |
+
response_data = json.loads(response.text)
|
140 |
+
logger.info(f"Successfully parsed JSON response")
|
141 |
+
|
142 |
+
# Ensure we got an array
|
143 |
+
if not isinstance(response_data, list):
|
144 |
+
logger.debug("Response is not an array, converting to array")
|
145 |
+
response_data = [response_data]
|
146 |
+
|
147 |
+
logger.info(f"Response contains {len(response_data)} extracted card data objects")
|
148 |
+
|
149 |
+
# Add metadata to each card's data
|
150 |
+
logger.debug("Adding metadata to extracted data")
|
151 |
+
for i, data in enumerate(response_data):
|
152 |
+
data['method'] = model_name
|
153 |
+
if i < len(filenames):
|
154 |
+
data['filename'] = filenames[i]
|
155 |
+
logger.debug(f"Added metadata to card {i+1}: {filenames[i]}")
|
156 |
+
|
157 |
+
logger.info(f"Batch extraction completed successfully for {len(response_data)} cards")
|
158 |
+
return response_data
|
159 |
+
|
160 |
+
except json.JSONDecodeError as e:
|
161 |
+
logger.warning(f"Initial JSON parsing failed: {e}. Attempting to clean response.")
|
162 |
+
# Try to clean the response
|
163 |
+
text = response.text.strip()
|
164 |
+
if text.startswith("```json"):
|
165 |
+
text = text[7:]
|
166 |
+
logger.debug("Removed ```json prefix")
|
167 |
+
if text.endswith("```"):
|
168 |
+
text = text[:-3]
|
169 |
+
logger.debug("Removed ``` suffix")
|
170 |
+
|
171 |
+
try:
|
172 |
+
response_data = json.loads(text.strip())
|
173 |
+
logger.info("Successfully parsed cleaned JSON response")
|
174 |
+
|
175 |
+
# Ensure we got an array
|
176 |
+
if not isinstance(response_data, list):
|
177 |
+
logger.debug("Cleaned response is not an array, converting to array")
|
178 |
+
response_data = [response_data]
|
179 |
+
|
180 |
+
logger.info(f"Cleaned response contains {len(response_data)} extracted card data objects")
|
181 |
+
|
182 |
+
# Add metadata to each card's data
|
183 |
+
logger.debug("Adding metadata to cleaned extracted data")
|
184 |
+
for i, data in enumerate(response_data):
|
185 |
+
data['method'] = model_name
|
186 |
+
if i < len(filenames):
|
187 |
+
data['filename'] = filenames[i]
|
188 |
+
logger.debug(f"Added metadata to cleaned card {i+1}: {filenames[i]}")
|
189 |
+
|
190 |
+
logger.info(f"Batch extraction completed successfully after cleaning for {len(response_data)} cards")
|
191 |
+
return response_data
|
192 |
+
except json.JSONDecodeError as e2:
|
193 |
+
logger.error(f"Failed to parse even cleaned JSON response: {e2}")
|
194 |
+
logger.error(f"Cleaned text: {text[:1000]}...")
|
195 |
+
raise
|
196 |
+
|
197 |
+
def extract_business_card_data(image, model_name="gemini-2.5-flash"):
|
198 |
+
"""Extract data from single business card image - legacy function"""
|
199 |
+
logger.debug(f"Single card extraction called with model: {model_name}")
|
200 |
+
result = extract_business_card_data_batch([image], ["single_card"], model_name)
|
201 |
+
if result:
|
202 |
+
logger.debug("Single card extraction successful")
|
203 |
+
return result[0]
|
204 |
+
else:
|
205 |
+
logger.warning("Single card extraction returned no results")
|
206 |
+
return None
|
207 |
+
|
208 |
+
def process_business_cards(images, model_name="gemini-2.5-flash", save_images=True):
|
209 |
+
"""Process multiple business card images and create both current run and cumulative Excel files"""
|
210 |
+
|
211 |
+
logger.info(f"Starting business card processing session")
|
212 |
+
logger.info(f"Number of images received: {len(images) if images else 0}")
|
213 |
+
logger.info(f"Model selected: {model_name}")
|
214 |
+
logger.info(f"Save images option: {save_images}")
|
215 |
+
|
216 |
+
if not images:
|
217 |
+
logger.warning("No images provided for processing")
|
218 |
+
return None, None, "Please upload at least one business card image.", None
|
219 |
+
|
220 |
+
all_data = []
|
221 |
+
errors = []
|
222 |
+
|
223 |
+
# Prepare images for batch processing
|
224 |
+
logger.info("Preparing images for batch processing")
|
225 |
+
image_batches = []
|
226 |
+
filename_batches = []
|
227 |
+
batch_size = 5
|
228 |
+
logger.debug(f"Using batch size: {batch_size}")
|
229 |
+
|
230 |
+
# Load and group images into batches of 5
|
231 |
+
loaded_images = []
|
232 |
+
filenames = []
|
233 |
+
|
234 |
+
logger.info(f"Loading {len(images)} images")
|
235 |
+
for idx, image_path in enumerate(images):
|
236 |
+
try:
|
237 |
+
# Load image
|
238 |
+
if isinstance(image_path, str):
|
239 |
+
logger.debug(f"Loading image {idx+1}: {image_path}")
|
240 |
+
image = Image.open(image_path)
|
241 |
+
filename = os.path.basename(image_path)
|
242 |
+
else:
|
243 |
+
logger.debug(f"Using direct image object {idx+1}")
|
244 |
+
image = image_path
|
245 |
+
filename = f"image_{idx+1}.png"
|
246 |
+
|
247 |
+
loaded_images.append(image)
|
248 |
+
filenames.append(filename)
|
249 |
+
logger.debug(f"Successfully loaded image {idx+1}: {filename} (size: {image.size})")
|
250 |
+
|
251 |
+
except Exception as e:
|
252 |
+
error_msg = f"Error loading {image_path}: {str(e)}"
|
253 |
+
logger.error(error_msg)
|
254 |
+
errors.append(error_msg)
|
255 |
+
|
256 |
+
logger.info(f"Successfully loaded {len(loaded_images)} out of {len(images)} images")
|
257 |
+
|
258 |
+
# Save images if requested
|
259 |
+
saved_image_paths = []
|
260 |
+
if save_images and loaded_images:
|
261 |
+
logger.info(f"Saving {len(loaded_images)} images to business_cards directory")
|
262 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
263 |
+
|
264 |
+
for i, (image, filename) in enumerate(zip(loaded_images, filenames)):
|
265 |
+
try:
|
266 |
+
# Create unique filename with timestamp
|
267 |
+
name, ext = os.path.splitext(filename)
|
268 |
+
if not ext:
|
269 |
+
ext = '.png'
|
270 |
+
unique_filename = f"{timestamp}_{i+1:03d}_{name}{ext}"
|
271 |
+
image_path = images_dir / unique_filename
|
272 |
+
|
273 |
+
# Save the image
|
274 |
+
image.save(image_path)
|
275 |
+
saved_image_paths.append(str(image_path))
|
276 |
+
logger.debug(f"Saved image {i+1}: {unique_filename}")
|
277 |
+
|
278 |
+
except Exception as e:
|
279 |
+
logger.error(f"Failed to save image {filename}: {e}")
|
280 |
+
|
281 |
+
logger.info(f"Successfully saved {len(saved_image_paths)} images")
|
282 |
+
|
283 |
+
# Group into batches
|
284 |
+
logger.info(f"Grouping {len(loaded_images)} images into batches of {batch_size}")
|
285 |
+
for i in range(0, len(loaded_images), batch_size):
|
286 |
+
batch_images = loaded_images[i:i + batch_size]
|
287 |
+
batch_filenames = filenames[i:i + batch_size]
|
288 |
+
image_batches.append(batch_images)
|
289 |
+
filename_batches.append(batch_filenames)
|
290 |
+
logger.debug(f"Created batch {len(image_batches)} with {len(batch_images)} images: {batch_filenames}")
|
291 |
+
|
292 |
+
logger.info(f"Created {len(image_batches)} batches for processing")
|
293 |
+
|
294 |
+
# Process each batch
|
295 |
+
logger.info(f"Starting processing of {len(image_batches)} batches")
|
296 |
+
for batch_idx, (batch_images, batch_filenames) in enumerate(zip(image_batches, filename_batches)):
|
297 |
+
try:
|
298 |
+
logger.info(f"Processing batch {batch_idx + 1}/{len(image_batches)} ({len(batch_images)} cards)")
|
299 |
+
print(f"Processing batch {batch_idx + 1}/{len(image_batches)} ({len(batch_images)} cards)")
|
300 |
+
|
301 |
+
# Extract data for the entire batch
|
302 |
+
logger.debug(f"Calling batch extraction for batch {batch_idx + 1}")
|
303 |
+
batch_data = extract_business_card_data_batch(batch_images, batch_filenames, model_name)
|
304 |
+
logger.info(f"Batch {batch_idx + 1} extraction completed, got {len(batch_data)} results")
|
305 |
+
|
306 |
+
# Process each card's data in the batch
|
307 |
+
logger.debug(f"Processing individual card data for batch {batch_idx + 1}")
|
308 |
+
for i, data in enumerate(batch_data):
|
309 |
+
card_filename = batch_filenames[i] if i < len(batch_filenames) else f"card_{i+1}"
|
310 |
+
logger.debug(f"Processing card data for: {card_filename}")
|
311 |
+
|
312 |
+
# Add timestamp to data
|
313 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
314 |
+
data['processed_date'] = timestamp
|
315 |
+
logger.debug(f"Added timestamp {timestamp} to {card_filename}")
|
316 |
+
|
317 |
+
# Add saved image path if images were saved
|
318 |
+
global_index = batch_idx * batch_size + i
|
319 |
+
if save_images and global_index < len(saved_image_paths):
|
320 |
+
data['saved_image_path'] = saved_image_paths[global_index]
|
321 |
+
logger.debug(f"Added saved image path for {card_filename}: {saved_image_paths[global_index]}")
|
322 |
+
else:
|
323 |
+
data['saved_image_path'] = None
|
324 |
+
|
325 |
+
# Handle multiple values (emails, phones) by joining with commas
|
326 |
+
list_fields_processed = []
|
327 |
+
for key, value in data.items():
|
328 |
+
if isinstance(value, list):
|
329 |
+
original_count = len(value)
|
330 |
+
data[key] = ', '.join(str(v) for v in value)
|
331 |
+
list_fields_processed.append(f"{key}({original_count})")
|
332 |
+
logger.debug(f"Combined {original_count} {key} values for {card_filename}")
|
333 |
+
|
334 |
+
if list_fields_processed:
|
335 |
+
logger.debug(f"List fields processed for {card_filename}: {list_fields_processed}")
|
336 |
+
|
337 |
+
# Combine phone fields if they exist separately
|
338 |
+
if 'mobile_phones' in data and data['mobile_phones']:
|
339 |
+
logger.debug(f"Combining phone fields for {card_filename}")
|
340 |
+
if data.get('phones'):
|
341 |
+
# Combine mobile and regular phones
|
342 |
+
existing_phones = str(data['phones']) if data['phones'] else ""
|
343 |
+
mobile_phones = str(data['mobile_phones']) if data['mobile_phones'] else ""
|
344 |
+
combined = [p for p in [existing_phones, mobile_phones] if p and p != 'null']
|
345 |
+
data['phones'] = ', '.join(combined)
|
346 |
+
logger.debug(f"Combined phones for {card_filename}: {data['phones']}")
|
347 |
+
else:
|
348 |
+
data['phones'] = data['mobile_phones']
|
349 |
+
logger.debug(f"Used mobile phones as phones for {card_filename}: {data['phones']}")
|
350 |
+
del data['mobile_phones'] # Remove separate mobile field
|
351 |
+
|
352 |
+
# Combine address fields if they exist separately
|
353 |
+
if 'street' in data and data['street']:
|
354 |
+
logger.debug(f"Combining address fields for {card_filename}")
|
355 |
+
if data.get('address'):
|
356 |
+
# If both exist, combine them
|
357 |
+
if str(data['street']) != str(data['address']) and data['street'] != 'null':
|
358 |
+
original_address = data['address']
|
359 |
+
data['address'] = f"{data['street']}, {data['address']}"
|
360 |
+
logger.debug(f"Combined address for {card_filename}: '{data['street']}' + '{original_address}' = '{data['address']}'")
|
361 |
+
else:
|
362 |
+
data['address'] = data['street']
|
363 |
+
logger.debug(f"Used street as address for {card_filename}: {data['address']}")
|
364 |
+
del data['street'] # Remove separate street field
|
365 |
+
|
366 |
+
all_data.append(data)
|
367 |
+
logger.debug(f"Added processed data for {card_filename} to results (total: {len(all_data)})")
|
368 |
+
|
369 |
+
logger.info(f"Completed processing batch {batch_idx + 1}, total cards processed so far: {len(all_data)}")
|
370 |
+
|
371 |
+
except Exception as e:
|
372 |
+
batch_filenames_str = ', '.join(batch_filenames)
|
373 |
+
error_msg = f"Error processing batch {batch_idx + 1} ({batch_filenames_str}): {str(e)}"
|
374 |
+
logger.error(error_msg)
|
375 |
+
errors.append(error_msg)
|
376 |
+
|
377 |
+
if not all_data:
|
378 |
+
logger.warning("No data could be extracted from any images")
|
379 |
+
error_summary = "No data could be extracted from the images.\n" + "\n".join(errors)
|
380 |
+
return None, None, error_summary, None
|
381 |
+
|
382 |
+
logger.info(f"Successfully extracted data from {len(all_data)} business cards")
|
383 |
+
|
384 |
+
# Create DataFrame for current run
|
385 |
+
logger.info("Creating DataFrame for current run")
|
386 |
+
current_df = pd.DataFrame(all_data)
|
387 |
+
logger.debug(f"Current run DataFrame created with {len(current_df)} rows and {len(current_df.columns)} columns")
|
388 |
+
logger.debug(f"Columns: {list(current_df.columns)}")
|
389 |
+
|
390 |
+
# Generate timestamp
|
391 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
392 |
+
logger.debug(f"Generated timestamp: {timestamp}")
|
393 |
+
|
394 |
+
# Create current run file
|
395 |
+
current_filename = output_dir / f"current_run_{timestamp}.xlsx"
|
396 |
+
logger.info(f"Current run file will be saved as: {current_filename}")
|
397 |
+
|
398 |
+
# Load existing cumulative data if it exists
|
399 |
+
cumulative_filename = output_dir / "all_business_cards_total.xlsx"
|
400 |
+
logger.info(f"Checking for existing cumulative file: {cumulative_filename}")
|
401 |
+
|
402 |
+
if cumulative_filename.exists():
|
403 |
+
logger.info("Existing cumulative file found, loading and merging data")
|
404 |
+
try:
|
405 |
+
existing_df = pd.read_excel(cumulative_filename)
|
406 |
+
logger.info(f"Loaded existing data: {len(existing_df)} rows")
|
407 |
+
# Append new data to existing
|
408 |
+
cumulative_df = pd.concat([existing_df, current_df], ignore_index=True)
|
409 |
+
logger.info(f"Merged data: {len(cumulative_df)} total rows ({len(existing_df)} existing + {len(current_df)} new)")
|
410 |
+
except Exception as e:
|
411 |
+
error_msg = f"Warning: Could not load existing data: {e}"
|
412 |
+
logger.warning(error_msg)
|
413 |
+
print(error_msg)
|
414 |
+
cumulative_df = current_df
|
415 |
+
logger.info("Using current data only for cumulative file")
|
416 |
+
else:
|
417 |
+
logger.info("No existing cumulative file found, using current data only")
|
418 |
+
cumulative_df = current_df
|
419 |
+
|
420 |
+
# Write current run Excel file
|
421 |
+
logger.info(f"Writing current run Excel file: {current_filename}")
|
422 |
+
try:
|
423 |
+
with pd.ExcelWriter(current_filename, engine='openpyxl') as writer:
|
424 |
+
current_df.to_excel(writer, index=False, sheet_name='Current Run')
|
425 |
+
logger.debug(f"Written {len(current_df)} rows to 'Current Run' sheet")
|
426 |
+
|
427 |
+
# Auto-adjust column widths
|
428 |
+
logger.debug("Auto-adjusting column widths for current run file")
|
429 |
+
worksheet = writer.sheets['Current Run']
|
430 |
+
adjusted_columns = []
|
431 |
+
for column in current_df:
|
432 |
+
column_length = max(current_df[column].astype(str).map(len).max(), len(column))
|
433 |
+
col_idx = current_df.columns.get_loc(column)
|
434 |
+
final_width = min(column_length + 2, 50)
|
435 |
+
worksheet.column_dimensions[chr(65 + col_idx)].width = final_width
|
436 |
+
adjusted_columns.append(f"{column}:{final_width}")
|
437 |
+
logger.debug(f"Adjusted column widths: {adjusted_columns}")
|
438 |
+
|
439 |
+
logger.info(f"Current run Excel file saved successfully: {current_filename}")
|
440 |
+
except Exception as e:
|
441 |
+
logger.error(f"Failed to write current run Excel file: {e}")
|
442 |
+
raise
|
443 |
+
|
444 |
+
# Write cumulative Excel file
|
445 |
+
logger.info(f"Writing cumulative Excel file: {cumulative_filename}")
|
446 |
+
try:
|
447 |
+
with pd.ExcelWriter(cumulative_filename, engine='openpyxl') as writer:
|
448 |
+
cumulative_df.to_excel(writer, index=False, sheet_name='All Business Cards')
|
449 |
+
logger.debug(f"Written {len(cumulative_df)} rows to 'All Business Cards' sheet")
|
450 |
+
|
451 |
+
# Auto-adjust column widths
|
452 |
+
logger.debug("Auto-adjusting column widths for cumulative file")
|
453 |
+
worksheet = writer.sheets['All Business Cards']
|
454 |
+
adjusted_columns = []
|
455 |
+
for column in cumulative_df:
|
456 |
+
column_length = max(cumulative_df[column].astype(str).map(len).max(), len(column))
|
457 |
+
col_idx = cumulative_df.columns.get_loc(column)
|
458 |
+
final_width = min(column_length + 2, 50)
|
459 |
+
worksheet.column_dimensions[chr(65 + col_idx)].width = final_width
|
460 |
+
adjusted_columns.append(f"{column}:{final_width}")
|
461 |
+
logger.debug(f"Adjusted column widths: {adjusted_columns}")
|
462 |
+
|
463 |
+
logger.info(f"Cumulative Excel file saved successfully: {cumulative_filename}")
|
464 |
+
except Exception as e:
|
465 |
+
logger.error(f"Failed to write cumulative Excel file: {e}")
|
466 |
+
raise
|
467 |
+
|
468 |
+
# Create summary message
|
469 |
+
logger.info("Creating summary message")
|
470 |
+
num_batches = len(image_batches) if 'image_batches' in locals() else 1
|
471 |
+
summary = f"Successfully processed {len(all_data)} business card(s) in {num_batches} batch(es) of up to 5 cards.\n"
|
472 |
+
summary += f"π€ AI Model used: {model_name}\n"
|
473 |
+
summary += f"β‘ API calls made: {num_batches} (instead of {len(all_data)})\n"
|
474 |
+
|
475 |
+
if save_images:
|
476 |
+
num_saved = len(saved_image_paths) if 'saved_image_paths' in locals() else 0
|
477 |
+
summary += f"πΎ Images saved: {num_saved} cards saved to business_cards folder\n\n"
|
478 |
+
else:
|
479 |
+
summary += f"πΎ Images saved: No (save option was disabled)\n\n"
|
480 |
+
|
481 |
+
summary += f"π Current run file: {current_filename.name}\n"
|
482 |
+
summary += f"π Total cumulative file: {cumulative_filename.name}\n"
|
483 |
+
summary += f"π Total cards in database: {len(cumulative_df)}\n\n"
|
484 |
+
|
485 |
+
if errors:
|
486 |
+
logger.warning(f"Encountered {len(errors)} errors during processing")
|
487 |
+
summary += "Errors encountered:\n" + "\n".join(errors)
|
488 |
+
for error in errors:
|
489 |
+
logger.warning(f"Processing error: {error}")
|
490 |
+
else:
|
491 |
+
logger.info("No errors encountered during processing")
|
492 |
+
|
493 |
+
# Display preview of current run
|
494 |
+
logger.debug("Creating preview DataFrame")
|
495 |
+
preview_df = current_df.head(10)
|
496 |
+
logger.debug(f"Preview contains {len(preview_df)} rows")
|
497 |
+
|
498 |
+
logger.info("Business card processing session completed successfully")
|
499 |
+
logger.info(f"Session summary - Cards: {len(all_data)}, Batches: {num_batches}, API calls: {num_batches}, Total DB size: {len(cumulative_df)}")
|
500 |
+
|
501 |
+
return str(current_filename), str(cumulative_filename), summary, preview_df
|
502 |
+
|
503 |
+
# Create Gradio interface
|
504 |
+
logger.info("Creating Gradio interface")
|
505 |
+
with gr.Blocks(title="Business Card Data Extractor") as demo:
|
506 |
+
gr.Markdown(
|
507 |
+
"""
|
508 |
+
# Business Card Data Extractor
|
509 |
+
|
510 |
+
Upload business card images to extract contact information and export to Excel.
|
511 |
+
Cards are processed in batches of 5 for efficiency (fewer API calls, lower cost).
|
512 |
+
|
513 |
+
**Two files are generated:**
|
514 |
+
- π **Current Run**: Contains only the cards you just processed
|
515 |
+
- π **Total Database**: Contains ALL cards ever processed (cumulative)
|
516 |
+
|
517 |
+
**Image Storage:**
|
518 |
+
- πΎ **Optional**: Save uploaded images to business_cards folder
|
519 |
+
- π **Tracking**: Image file paths included in Excel database
|
520 |
+
"""
|
521 |
+
)
|
522 |
+
|
523 |
+
with gr.Row():
|
524 |
+
with gr.Column():
|
525 |
+
image_input = gr.File(
|
526 |
+
label="Upload Business Cards",
|
527 |
+
file_count="multiple",
|
528 |
+
file_types=["image"],
|
529 |
+
type="filepath"
|
530 |
+
)
|
531 |
+
|
532 |
+
model_selector = gr.Dropdown(
|
533 |
+
choices=[
|
534 |
+
("Gemini 2.5 Pro (Higher Quality, More Accurate)", "gemini-2.5-pro"),
|
535 |
+
("Gemini 2.5 Flash (Faster, Cost-effective)", "gemini-2.5-flash")
|
536 |
+
],
|
537 |
+
value="gemini-2.5-flash",
|
538 |
+
label="π€ AI Model Selection",
|
539 |
+
info="Choose between speed (Flash) or accuracy (Pro)"
|
540 |
+
)
|
541 |
+
|
542 |
+
save_images_checkbox = gr.Checkbox(
|
543 |
+
value=True,
|
544 |
+
label="πΎ Save Business Card Images",
|
545 |
+
info="Save uploaded images to business_cards folder and include paths in database"
|
546 |
+
)
|
547 |
+
|
548 |
+
process_btn = gr.Button("Process Business Cards", variant="primary")
|
549 |
+
|
550 |
+
with gr.Column():
|
551 |
+
current_file = gr.File(label="π Download Current Run")
|
552 |
+
total_file = gr.File(label="π Download Total Database")
|
553 |
+
status_output = gr.Textbox(label="Processing Status", lines=5)
|
554 |
+
|
555 |
+
preview_output = gr.Dataframe(label="Data Preview (Current Run)", interactive=False)
|
556 |
+
|
557 |
+
# Wrapper function for better error handling and logging
|
558 |
+
def process_with_logging(images, model_name, save_images):
|
559 |
+
"""Wrapper function to add error handling and logging to the main process"""
|
560 |
+
try:
|
561 |
+
logger.info(f"Gradio interface initiated processing request")
|
562 |
+
logger.debug(f"Request parameters - Images: {len(images) if images else 0}, Model: {model_name}, Save Images: {save_images}")
|
563 |
+
return process_business_cards(images, model_name, save_images)
|
564 |
+
except Exception as e:
|
565 |
+
logger.error(f"Unexpected error in Gradio processing: {e}")
|
566 |
+
error_msg = f"An unexpected error occurred: {str(e)}\nPlease check the logs for more details."
|
567 |
+
return None, None, error_msg, None
|
568 |
+
|
569 |
+
# Handle processing
|
570 |
+
process_btn.click(
|
571 |
+
fn=process_with_logging,
|
572 |
+
inputs=[image_input, model_selector, save_images_checkbox],
|
573 |
+
outputs=[current_file, total_file, status_output, preview_output]
|
574 |
+
)
|
575 |
+
|
576 |
+
gr.Markdown(
|
577 |
+
"""
|
578 |
+
## Features:
|
579 |
+
- π€ **Model Selection**: Choose between Gemini 2.5 Flash (fast) or Pro (accurate)
|
580 |
+
- β‘ **Batch Processing**: Processes 5 cards per API call for efficiency
|
581 |
+
- π **Data Extraction**: Names, emails, phone numbers, addresses, and more
|
582 |
+
- π **Smart Combination**: Multiple emails/phones combined with commas
|
583 |
+
- π **Address Merging**: All phone types and address fields combined
|
584 |
+
- πΎ **Image Storage**: Optionally save images to business_cards folder
|
585 |
+
- π **Dual Output**: Current run + cumulative database files
|
586 |
+
- π **Full Tracking**: Processing date, filename, image path, and AI model used
|
587 |
+
- π― **One Row Per Card**: Each business card becomes one spreadsheet row
|
588 |
+
"""
|
589 |
+
)
|
590 |
+
|
591 |
+
if __name__ == "__main__":
|
592 |
+
logger.info("Starting Gradio demo")
|
593 |
+
logger.info("Application will be available at http://localhost:7860")
|
594 |
+
try:
|
595 |
+
demo.launch()
|
596 |
+
except KeyboardInterrupt:
|
597 |
+
logger.info("Application stopped by user (Ctrl+C)")
|
598 |
+
except Exception as e:
|
599 |
+
logger.error(f"Application crashed: {e}")
|
600 |
+
raise
|
601 |
+
finally:
|
602 |
+
logger.info("Application shutdown complete")
|
business_cards/.gitkeep
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# This file ensures the business_cards directory is created in the repository
|
2 |
+
# Business card images will be saved here when the "Save Images" option is enabled
|
3 |
+
# Files in this directory are ignored by git (see .gitignore) except for this .gitkeep file
|
env.example
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Environment Variables for Business Card Data Extractor
|
2 |
+
# Copy this file to .env and replace with your actual values
|
3 |
+
|
4 |
+
# Google Gemini API Key (Required)
|
5 |
+
# Get your key from: https://aistudio.google.com/
|
6 |
+
# For Hugging Face Spaces: Add this as a Repository Secret named "Gemini_API"
|
7 |
+
Gemini_API=your_gemini_api_key_here
|
8 |
+
|
9 |
+
# Example:
|
10 |
+
# Gemini_API=AIzaSyBxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
prompts/prompt.txt
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Extract all contact information from the business card image(s).
|
2 |
+
|
3 |
+
Output Format:
|
4 |
+
- If processing ONE card: Return a single JSON object
|
5 |
+
- If processing MULTIPLE cards: Return a JSON array with one object per card
|
6 |
+
|
7 |
+
Each JSON object must contain all the extracted information from one business card.
|
8 |
+
|
9 |
+
Important Instructions:
|
10 |
+
- If a field has multiple values (like multiple email addresses or phone numbers), return them as an array
|
11 |
+
- If a field is not found on the card, set its value to null
|
12 |
+
- Extract ALL information visible on the card, even if it doesn't fit the standard fields
|
13 |
+
- Preserve the exact formatting of phone numbers as shown on the card
|
14 |
+
- For names, try to identify first name, last name, and full name separately
|
15 |
+
|
16 |
+
Standard Fields to Extract:
|
17 |
+
|
18 |
+
{
|
19 |
+
"full_name": "The complete name as displayed",
|
20 |
+
"first_name": "First/given name only",
|
21 |
+
"last_name": "Last/family name only",
|
22 |
+
"job_title": "Professional title or position",
|
23 |
+
"company": "Company or organization name",
|
24 |
+
"department": "Department or division if specified",
|
25 |
+
"emails": ["Array of all email addresses found"],
|
26 |
+
"phones": ["Array of all phone numbers found (include both mobile and landline)"],
|
27 |
+
"fax": "Fax number if present",
|
28 |
+
"website": "Company or personal website URL",
|
29 |
+
"linkedin": "LinkedIn profile URL if present",
|
30 |
+
"address": "Complete address as displayed (combine street and full address)",
|
31 |
+
"city": "City name",
|
32 |
+
"state": "State or province",
|
33 |
+
"postal_code": "ZIP or postal code",
|
34 |
+
"country": "Country if specified",
|
35 |
+
"additional_info": "Any other relevant information not covered above"
|
36 |
+
}
|
37 |
+
|
38 |
+
Return ONLY the JSON object, no additional text or formatting.
|
prompts/system_prompt.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
You are a highly accurate business card data extraction AI. Your task is to analyze business card images and extract all contact information, formatting the output as structured JSON. When processing multiple cards, return a JSON array with one object per card in the same order as the images. You must identify and extract every piece of information visible on each card, including names, titles, contact details, addresses, and any additional information. Be thorough and precise in your extraction.
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==4.44.1
|
2 |
+
google-generativeai==0.8.0
|
3 |
+
pandas==2.1.4
|
4 |
+
openpyxl==3.1.2
|
5 |
+
Pillow==10.2.0
|
setup_hf_space.md
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Hugging Face Spaces Setup Guide
|
2 |
+
|
3 |
+
## Quick Deployment to Hugging Face Spaces
|
4 |
+
|
5 |
+
### 1. Create a New Space
|
6 |
+
1. Go to [Hugging Face Spaces](https://huggingface.co/spaces)
|
7 |
+
2. Click "Create new Space"
|
8 |
+
3. Choose:
|
9 |
+
- **Space name**: `business-card-extractor`
|
10 |
+
- **License**: `mit`
|
11 |
+
- **Space SDK**: `gradio`
|
12 |
+
- **Visibility**: `public` or `private`
|
13 |
+
|
14 |
+
### 2. Upload Files
|
15 |
+
Upload these files to your space:
|
16 |
+
```
|
17 |
+
app.py
|
18 |
+
requirements.txt
|
19 |
+
prompts/prompt.txt
|
20 |
+
prompts/system_prompt.txt
|
21 |
+
README.md
|
22 |
+
business_cards/.gitkeep
|
23 |
+
```
|
24 |
+
|
25 |
+
**Note**: The `business_cards/` and `business_card_exports/` directories will be created automatically.
|
26 |
+
|
27 |
+
### 3. Set Environment Variables
|
28 |
+
1. Go to your Space **Settings**
|
29 |
+
2. Scroll to **Repository secrets**
|
30 |
+
3. Click **Add a new secret**
|
31 |
+
4. Set:
|
32 |
+
- **Name**: `Gemini_API`
|
33 |
+
- **Value**: Your Google Gemini API key
|
34 |
+
|
35 |
+
### 4. Get Your Gemini API Key
|
36 |
+
1. Go to [Google AI Studio](https://aistudio.google.com/)
|
37 |
+
2. Click "Get API key"
|
38 |
+
3. Create a new API key
|
39 |
+
4. Copy the key for use in step 3
|
40 |
+
|
41 |
+
### 5. Your Space is Ready!
|
42 |
+
- The space will automatically build and deploy
|
43 |
+
- It will be available at: `https://huggingface.co/spaces/YOUR_USERNAME/business-card-extractor`
|
44 |
+
- All business card images and Excel files will be saved in the space
|
45 |
+
|
46 |
+
## Features Available in Hugging Face Spaces
|
47 |
+
β
**Full functionality**: All features work in Hugging Face Spaces
|
48 |
+
β
**Image storage**: Business cards saved to `business_cards/` folder
|
49 |
+
β
**Excel exports**: Download both current run and cumulative files
|
50 |
+
β
**Persistent storage**: All data preserved between sessions
|
51 |
+
β
**Batch processing**: Efficient 5-cards-per-API-call processing
|
52 |
+
|
53 |
+
## Environment Variables Required
|
54 |
+
- `Gemini_API`: Your Google Gemini API key (required)
|
55 |
+
|
56 |
+
## Notes
|
57 |
+
- The space will create necessary directories automatically
|
58 |
+
- Logs are available in the space's terminal/logs
|
59 |
+
- All uploaded images are processed and optionally saved
|
60 |
+
- Excel files accumulate over time in the cumulative database
|