Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitignore +48 -0
- QUICK_START.md +53 -0
- README.md +112 -0
- app.py +675 -0
- business_card_exports/.gitkeep +2 -0
- business_cards/.gitkeep +2 -0
- env.example +28 -0
- google_funcs.py +195 -0
- prompts/prompt.txt +38 -0
- prompts/system_prompt.txt +1 -0
- requirements.txt +9 -0
- setup_hf_space.md +60 -0
.gitignore
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
# Google Drive authentication files
|
15 |
+
token.pickle
|
16 |
+
google_token_base64.txt
|
17 |
+
|
18 |
+
# IDE
|
19 |
+
.vscode/
|
20 |
+
.idea/
|
21 |
+
*.swp
|
22 |
+
*.swo
|
23 |
+
|
24 |
+
# Output files
|
25 |
+
!business_card_exports/.gitkeep
|
26 |
+
*.xlsx
|
27 |
+
*.xls
|
28 |
+
|
29 |
+
# Business card images (keep folder structure but ignore images)
|
30 |
+
business_cards/*.jpg
|
31 |
+
business_cards/*.jpeg
|
32 |
+
business_cards/*.png
|
33 |
+
business_cards/*.gif
|
34 |
+
business_cards/*.bmp
|
35 |
+
business_cards/*.webp
|
36 |
+
# Keep the .gitkeep file
|
37 |
+
!business_cards/.gitkeep
|
38 |
+
|
39 |
+
|
40 |
+
# OS
|
41 |
+
.DS_Store
|
42 |
+
Thumbs.db
|
43 |
+
|
44 |
+
# Logs
|
45 |
+
*.log
|
46 |
+
business_card_extractor.log
|
47 |
+
|
48 |
+
convert_token_to_base64.py
|
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,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Business Card Data Extractor
|
3 |
+
emoji: 💼
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: purple
|
6 |
+
sdk: gradio
|
7 |
+
sdk_version: "4.44.1"
|
8 |
+
app_file: app.py
|
9 |
+
pinned: false
|
10 |
+
---
|
11 |
+
|
12 |
+
# Business Card Data Extractor 💼
|
13 |
+
|
14 |
+
An AI-powered tool that extracts structured data from business card images using Google's Gemini AI. Upload business card images and get organized data exported to Excel files with automatic Google Drive storage.
|
15 |
+
|
16 |
+
## Features
|
17 |
+
|
18 |
+
- **Batch Processing**: Process multiple business cards at once (up to 5 per batch)
|
19 |
+
- **AI Model Selection**: Choose between Gemini 2.5 Flash (fast) or Gemini 2.5 Pro (accuracy)
|
20 |
+
- **Google Drive Storage**: Automatic upload to organized Drive folders
|
21 |
+
- **Excel Export**: Get data in two formats:
|
22 |
+
- Current session results
|
23 |
+
- Cumulative database (appends across sessions)
|
24 |
+
- **Smart Data Extraction**: Extracts name, company, title, emails, phones, address, website
|
25 |
+
- **Direct Links**: Access files directly through Google Drive URLs
|
26 |
+
|
27 |
+
## How to Use
|
28 |
+
|
29 |
+
1. **Setup**: Complete the setup process below (one-time)
|
30 |
+
2. **Upload Images**: Select up to 5 business card images
|
31 |
+
3. **Choose Model**: Select Gemini model (Flash for speed, Pro for accuracy)
|
32 |
+
4. **Process**: Click "Extract Business Card Data"
|
33 |
+
5. **Access Files**: Download temporary copies or access permanent files via Google Drive links
|
34 |
+
|
35 |
+
## Supported Data Fields
|
36 |
+
|
37 |
+
- **Name**: Full name from business card
|
38 |
+
- **Company**: Company/organization name
|
39 |
+
- **Title**: Job title/position
|
40 |
+
- **Emails**: Email addresses (comma-separated if multiple)
|
41 |
+
- **Phones**: Phone numbers (comma-separated if multiple)
|
42 |
+
- **Address**: Full address information
|
43 |
+
- **Website**: Company website URL
|
44 |
+
- **Processing Info**: Timestamp, model used, filename
|
45 |
+
|
46 |
+
## Setup Instructions
|
47 |
+
|
48 |
+
### 1. Google Gemini API
|
49 |
+
- Get your API key from: https://aistudio.google.com/
|
50 |
+
- Set as environment variable: `Gemini_API`
|
51 |
+
|
52 |
+
### 2. Google Drive API Setup
|
53 |
+
1. **Create Google Cloud Project**:
|
54 |
+
- Go to https://console.cloud.google.com/
|
55 |
+
- Create a new project or select an existing one
|
56 |
+
|
57 |
+
2. **Enable Google Drive API**:
|
58 |
+
- In the Google Cloud Console, go to "APIs & Services" > "Library"
|
59 |
+
- Search for "Google Drive API" and enable it
|
60 |
+
|
61 |
+
3. **Create OAuth 2.0 Credentials**:
|
62 |
+
- Go to "APIs & Services" > "Credentials"
|
63 |
+
- Click "+ CREATE CREDENTIALS" > "OAuth client ID"
|
64 |
+
- Select "Desktop application"
|
65 |
+
- Download the JSON file
|
66 |
+
- Extract `client_id` and `client_secret` from the JSON
|
67 |
+
|
68 |
+
4. **Set Environment Variables**:
|
69 |
+
```bash
|
70 |
+
GOOGLE_CLIENT_ID=your_client_id_here
|
71 |
+
GOOGLE_CLIENT_SECRET=your_client_secret_here
|
72 |
+
```
|
73 |
+
|
74 |
+
### 3. Local Development Setup
|
75 |
+
1. **Install Dependencies**:
|
76 |
+
```bash
|
77 |
+
pip install -r requirements.txt
|
78 |
+
```
|
79 |
+
|
80 |
+
2. **Run Locally First**:
|
81 |
+
```bash
|
82 |
+
python app.py
|
83 |
+
```
|
84 |
+
- Complete the OAuth flow in your browser
|
85 |
+
- This creates `token.pickle` file
|
86 |
+
|
87 |
+
### 4. Deployment Setup (Hugging Face Spaces, etc.)
|
88 |
+
1. **Generate Token for Deployment**:
|
89 |
+
```bash
|
90 |
+
python convert_token_to_base64.py
|
91 |
+
```
|
92 |
+
- This converts `token.pickle` to a base64 string
|
93 |
+
|
94 |
+
2. **Set Environment Variables** in your deployment platform:
|
95 |
+
```bash
|
96 |
+
Gemini_API=your_gemini_api_key
|
97 |
+
GOOGLE_CLIENT_ID=your_google_client_id
|
98 |
+
GOOGLE_CLIENT_SECRET=your_google_client_secret
|
99 |
+
GOOGLE_TOKEN_BASE64=your_base64_encoded_token
|
100 |
+
```
|
101 |
+
|
102 |
+
## Google Drive Folders
|
103 |
+
- **📁 Exports**: https://drive.google.com/drive/folders/1k5iP4egzLrGJwnHkMhxt9bAkaCiieojO
|
104 |
+
- **🖼️ Images**: https://drive.google.com/drive/folders/1gd280IqcAzpAFTPeYsZjoBUOU9S7Zx3c
|
105 |
+
|
106 |
+
## Technical Details
|
107 |
+
|
108 |
+
- **Image Formats**: JPG, JPEG, PNG, WEBP, BMP
|
109 |
+
- **Maximum File Size**: 10MB per image
|
110 |
+
- **Batch Processing**: Up to 5 cards per API call
|
111 |
+
- **Storage**: Automatic upload to Google Drive
|
112 |
+
- **Models**: Gemini 2.5 Flash (fast) / Pro (accurate)
|
app.py
ADDED
@@ -0,0 +1,675 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 tempfile
|
14 |
+
|
15 |
+
# Import Google Drive functionality
|
16 |
+
from google_funcs import get_drive_service, upload_excel_to_exports_folder, upload_image_to_images_folder, list_files_in_folder
|
17 |
+
|
18 |
+
# Configure logging
|
19 |
+
logging.basicConfig(
|
20 |
+
level=logging.INFO,
|
21 |
+
format='%(asctime)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s',
|
22 |
+
handlers=[
|
23 |
+
logging.StreamHandler(sys.stdout)
|
24 |
+
]
|
25 |
+
)
|
26 |
+
logger = logging.getLogger(__name__)
|
27 |
+
|
28 |
+
# Configure Gemini API
|
29 |
+
logger.info("Configuring Gemini API")
|
30 |
+
gemini_api_key = os.getenv("Gemini_API")
|
31 |
+
if not gemini_api_key:
|
32 |
+
logger.error("Gemini_API environment variable not found!")
|
33 |
+
logger.error("Please set the Gemini_API environment variable with your Google Gemini API key")
|
34 |
+
raise ValueError("❌ Gemini_API environment variable is required. Please set it in your environment.")
|
35 |
+
|
36 |
+
genai.configure(api_key=gemini_api_key)
|
37 |
+
logger.info("Gemini API configured successfully")
|
38 |
+
|
39 |
+
# Initialize Google Drive service
|
40 |
+
logger.info("Initializing Google Drive service")
|
41 |
+
try:
|
42 |
+
drive_service = get_drive_service()
|
43 |
+
logger.info("Google Drive service initialized successfully")
|
44 |
+
except Exception as e:
|
45 |
+
logger.error(f"Failed to initialize Google Drive service: {e}")
|
46 |
+
logger.error("Please ensure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables are set")
|
47 |
+
raise ValueError("❌ Google Drive credentials are required. Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables.")
|
48 |
+
|
49 |
+
# Log startup
|
50 |
+
logger.info("Business Card Data Extractor starting up with Google Drive storage")
|
51 |
+
|
52 |
+
def upload_to_google_drive(file_path, is_excel=False, filename=None):
|
53 |
+
"""Upload a file to Google Drive"""
|
54 |
+
try:
|
55 |
+
if is_excel:
|
56 |
+
logger.info(f"Uploading Excel file to Google Drive: {filename or file_path}")
|
57 |
+
result = upload_excel_to_exports_folder(drive_service, file_path=file_path, filename=filename)
|
58 |
+
else:
|
59 |
+
logger.info(f"Uploading image file to Google Drive: {filename or file_path}")
|
60 |
+
result = upload_image_to_images_folder(drive_service, file_path=file_path, filename=filename)
|
61 |
+
|
62 |
+
if result:
|
63 |
+
logger.info(f"Successfully uploaded to Google Drive: {result['webViewLink']}")
|
64 |
+
return result
|
65 |
+
else:
|
66 |
+
logger.error("Failed to upload to Google Drive")
|
67 |
+
return None
|
68 |
+
except Exception as e:
|
69 |
+
logger.error(f"Failed to upload to Google Drive: {e}")
|
70 |
+
return None
|
71 |
+
|
72 |
+
def upload_bytes_to_google_drive(file_data, filename, is_excel=False):
|
73 |
+
"""Upload file data (bytes) to Google Drive"""
|
74 |
+
try:
|
75 |
+
if is_excel:
|
76 |
+
logger.info(f"Uploading Excel data to Google Drive: {filename}")
|
77 |
+
result = upload_excel_to_exports_folder(drive_service, file_data=file_data, filename=filename)
|
78 |
+
else:
|
79 |
+
logger.info(f"Uploading image data to Google Drive: {filename}")
|
80 |
+
result = upload_image_to_images_folder(drive_service, file_data=file_data, filename=filename)
|
81 |
+
|
82 |
+
if result:
|
83 |
+
logger.info(f"Successfully uploaded to Google Drive: {result['webViewLink']}")
|
84 |
+
return result
|
85 |
+
else:
|
86 |
+
logger.error("Failed to upload to Google Drive")
|
87 |
+
return None
|
88 |
+
except Exception as e:
|
89 |
+
logger.error(f"Failed to upload to Google Drive: {e}")
|
90 |
+
return None
|
91 |
+
|
92 |
+
def extract_business_card_data_batch(images, filenames, model_name="gemini-2.5-flash"):
|
93 |
+
"""Extract data from multiple business card images in a single API call"""
|
94 |
+
|
95 |
+
logger.info(f"Starting batch extraction for {len(images)} images using model: {model_name}")
|
96 |
+
logger.debug(f"Filenames in batch: {filenames}")
|
97 |
+
|
98 |
+
# Load prompts
|
99 |
+
logger.debug("Loading prompt templates")
|
100 |
+
try:
|
101 |
+
with open("prompts/prompt.txt", "r", encoding="utf-8") as f:
|
102 |
+
prompt_template = f.read()
|
103 |
+
logger.debug(f"Loaded prompt template ({len(prompt_template)} characters)")
|
104 |
+
|
105 |
+
with open("prompts/system_prompt.txt", "r", encoding="utf-8") as f:
|
106 |
+
system_prompt = f.read()
|
107 |
+
logger.debug(f"Loaded system prompt ({len(system_prompt)} characters)")
|
108 |
+
except FileNotFoundError as e:
|
109 |
+
logger.error(f"Failed to load prompt files: {e}")
|
110 |
+
raise
|
111 |
+
|
112 |
+
# Configure model
|
113 |
+
logger.debug(f"Configuring Gemini model: {model_name}")
|
114 |
+
generation_config = {
|
115 |
+
"temperature": 0.1,
|
116 |
+
"response_mime_type": "application/json"
|
117 |
+
}
|
118 |
+
|
119 |
+
try:
|
120 |
+
model = genai.GenerativeModel(
|
121 |
+
model_name=model_name,
|
122 |
+
generation_config=generation_config,
|
123 |
+
system_instruction=system_prompt
|
124 |
+
)
|
125 |
+
logger.debug("Gemini model configured successfully")
|
126 |
+
except Exception as e:
|
127 |
+
logger.error(f"Failed to configure Gemini model: {e}")
|
128 |
+
raise
|
129 |
+
|
130 |
+
# Prepare multiple images for the model
|
131 |
+
logger.debug("Preparing content parts for API request")
|
132 |
+
content_parts = []
|
133 |
+
|
134 |
+
# Add the prompt first
|
135 |
+
batch_prompt = f"""
|
136 |
+
{prompt_template}
|
137 |
+
|
138 |
+
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.
|
139 |
+
|
140 |
+
Return format: [card1_data, card2_data, card3_data, ...]
|
141 |
+
"""
|
142 |
+
content_parts.append(batch_prompt)
|
143 |
+
logger.debug(f"Added batch prompt ({len(batch_prompt)} characters)")
|
144 |
+
|
145 |
+
# Add each image
|
146 |
+
logger.debug("Converting and adding images to request")
|
147 |
+
for i, image in enumerate(images):
|
148 |
+
try:
|
149 |
+
buffered = io.BytesIO()
|
150 |
+
image.save(buffered, format="PNG")
|
151 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode()
|
152 |
+
|
153 |
+
image_part = {
|
154 |
+
"mime_type": "image/png",
|
155 |
+
"data": img_base64
|
156 |
+
}
|
157 |
+
content_parts.append(f"Business Card {i+1}:")
|
158 |
+
content_parts.append(image_part)
|
159 |
+
logger.debug(f"Added image {i+1} ({len(img_base64)} base64 characters)")
|
160 |
+
except Exception as e:
|
161 |
+
logger.error(f"Failed to process image {i+1} ({filenames[i] if i < len(filenames) else 'unknown'}): {e}")
|
162 |
+
raise
|
163 |
+
|
164 |
+
# Generate content
|
165 |
+
logger.info(f"Making API call to {model_name} with {len(content_parts)} content parts")
|
166 |
+
try:
|
167 |
+
response = model.generate_content(content_parts)
|
168 |
+
logger.info(f"API call successful. Response length: {len(response.text) if response.text else 0} characters")
|
169 |
+
logger.debug(f"Raw response: {response.text[:500]}..." if len(response.text) > 500 else f"Raw response: {response.text}")
|
170 |
+
except Exception as e:
|
171 |
+
logger.error(f"API call failed: {e}")
|
172 |
+
raise
|
173 |
+
|
174 |
+
# Parse response
|
175 |
+
logger.debug("Parsing JSON response")
|
176 |
+
try:
|
177 |
+
# Parse JSON response
|
178 |
+
response_data = json.loads(response.text)
|
179 |
+
logger.info(f"Successfully parsed JSON response")
|
180 |
+
|
181 |
+
# Ensure we got an array
|
182 |
+
if not isinstance(response_data, list):
|
183 |
+
logger.debug("Response is not an array, converting to array")
|
184 |
+
response_data = [response_data]
|
185 |
+
|
186 |
+
logger.info(f"Response contains {len(response_data)} extracted card data objects")
|
187 |
+
|
188 |
+
# Add metadata to each card's data
|
189 |
+
logger.debug("Adding metadata to extracted data")
|
190 |
+
for i, data in enumerate(response_data):
|
191 |
+
data['method'] = model_name
|
192 |
+
if i < len(filenames):
|
193 |
+
data['filename'] = filenames[i]
|
194 |
+
logger.debug(f"Added metadata to card {i+1}: {filenames[i]}")
|
195 |
+
|
196 |
+
logger.info(f"Batch extraction completed successfully for {len(response_data)} cards")
|
197 |
+
return response_data
|
198 |
+
|
199 |
+
except json.JSONDecodeError as e:
|
200 |
+
logger.warning(f"Initial JSON parsing failed: {e}. Attempting to clean response.")
|
201 |
+
# Try to clean the response
|
202 |
+
text = response.text.strip()
|
203 |
+
if text.startswith("```json"):
|
204 |
+
text = text[7:]
|
205 |
+
logger.debug("Removed ```json prefix")
|
206 |
+
if text.endswith("```"):
|
207 |
+
text = text[:-3]
|
208 |
+
logger.debug("Removed ``` suffix")
|
209 |
+
|
210 |
+
try:
|
211 |
+
response_data = json.loads(text.strip())
|
212 |
+
logger.info("Successfully parsed cleaned JSON response")
|
213 |
+
|
214 |
+
# Ensure we got an array
|
215 |
+
if not isinstance(response_data, list):
|
216 |
+
logger.debug("Cleaned response is not an array, converting to array")
|
217 |
+
response_data = [response_data]
|
218 |
+
|
219 |
+
logger.info(f"Cleaned response contains {len(response_data)} extracted card data objects")
|
220 |
+
|
221 |
+
# Add metadata to each card's data
|
222 |
+
logger.debug("Adding metadata to cleaned extracted data")
|
223 |
+
for i, data in enumerate(response_data):
|
224 |
+
data['method'] = model_name
|
225 |
+
if i < len(filenames):
|
226 |
+
data['filename'] = filenames[i]
|
227 |
+
logger.debug(f"Added metadata to cleaned card {i+1}: {filenames[i]}")
|
228 |
+
|
229 |
+
logger.info(f"Batch extraction completed successfully after cleaning for {len(response_data)} cards")
|
230 |
+
return response_data
|
231 |
+
except json.JSONDecodeError as e2:
|
232 |
+
logger.error(f"Failed to parse even cleaned JSON response: {e2}")
|
233 |
+
logger.error(f"Cleaned text: {text[:1000]}...")
|
234 |
+
raise
|
235 |
+
|
236 |
+
def extract_business_card_data(image, model_name="gemini-2.5-flash"):
|
237 |
+
"""Extract data from single business card image - legacy function"""
|
238 |
+
logger.debug(f"Single card extraction called with model: {model_name}")
|
239 |
+
result = extract_business_card_data_batch([image], ["single_card"], model_name)
|
240 |
+
if result:
|
241 |
+
logger.debug("Single card extraction successful")
|
242 |
+
return result[0]
|
243 |
+
else:
|
244 |
+
logger.warning("Single card extraction returned no results")
|
245 |
+
return None
|
246 |
+
|
247 |
+
def process_business_cards(images, model_name="gemini-2.5-flash", save_images=True):
|
248 |
+
"""Process multiple business card images and create both current run and cumulative Excel files"""
|
249 |
+
|
250 |
+
logger.info(f"Starting business card processing session")
|
251 |
+
logger.info(f"Number of images received: {len(images) if images else 0}")
|
252 |
+
logger.info(f"Model selected: {model_name}")
|
253 |
+
logger.info(f"Save images option: {save_images}")
|
254 |
+
|
255 |
+
if not images:
|
256 |
+
logger.warning("No images provided for processing")
|
257 |
+
return None, None, "Please upload at least one business card image.", None
|
258 |
+
|
259 |
+
all_data = []
|
260 |
+
errors = []
|
261 |
+
|
262 |
+
# Prepare images for batch processing
|
263 |
+
logger.info("Preparing images for batch processing")
|
264 |
+
image_batches = []
|
265 |
+
filename_batches = []
|
266 |
+
batch_size = 5
|
267 |
+
logger.debug(f"Using batch size: {batch_size}")
|
268 |
+
|
269 |
+
# Load and group images into batches of 5
|
270 |
+
loaded_images = []
|
271 |
+
filenames = []
|
272 |
+
uploaded_image_links = []
|
273 |
+
|
274 |
+
logger.info(f"Loading {len(images)} images")
|
275 |
+
for idx, image_path in enumerate(images):
|
276 |
+
try:
|
277 |
+
# Load image
|
278 |
+
if isinstance(image_path, str):
|
279 |
+
logger.debug(f"Loading image {idx+1}: {image_path}")
|
280 |
+
image = Image.open(image_path)
|
281 |
+
filename = os.path.basename(image_path)
|
282 |
+
else:
|
283 |
+
logger.debug(f"Using direct image object {idx+1}")
|
284 |
+
image = image_path
|
285 |
+
filename = f"image_{idx+1}.png"
|
286 |
+
|
287 |
+
loaded_images.append(image)
|
288 |
+
filenames.append(filename)
|
289 |
+
logger.debug(f"Successfully loaded image {idx+1}: {filename} (size: {image.size})")
|
290 |
+
|
291 |
+
except Exception as e:
|
292 |
+
error_msg = f"Error loading {image_path}: {str(e)}"
|
293 |
+
logger.error(error_msg)
|
294 |
+
errors.append(error_msg)
|
295 |
+
|
296 |
+
logger.info(f"Successfully loaded {len(loaded_images)} out of {len(images)} images")
|
297 |
+
|
298 |
+
# Save images to Google Drive if requested
|
299 |
+
if save_images and loaded_images:
|
300 |
+
logger.info(f"Saving {len(loaded_images)} images to Google Drive")
|
301 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
302 |
+
|
303 |
+
for i, (image, filename) in enumerate(zip(loaded_images, filenames)):
|
304 |
+
try:
|
305 |
+
# Create unique filename with timestamp
|
306 |
+
name, ext = os.path.splitext(filename)
|
307 |
+
if not ext:
|
308 |
+
ext = '.png'
|
309 |
+
unique_filename = f"{timestamp}_{i+1:03d}_{name}{ext}"
|
310 |
+
|
311 |
+
# Convert image to bytes
|
312 |
+
img_buffer = io.BytesIO()
|
313 |
+
image.save(img_buffer, format='PNG')
|
314 |
+
img_bytes = img_buffer.getvalue()
|
315 |
+
|
316 |
+
# Upload to Google Drive
|
317 |
+
result = upload_bytes_to_google_drive(img_bytes, unique_filename, is_excel=False)
|
318 |
+
|
319 |
+
if result:
|
320 |
+
uploaded_image_links.append(result['webViewLink'])
|
321 |
+
logger.debug(f"Saved image {i+1}: {unique_filename}")
|
322 |
+
else:
|
323 |
+
uploaded_image_links.append(None)
|
324 |
+
logger.error(f"Failed to upload image {unique_filename}")
|
325 |
+
|
326 |
+
except Exception as e:
|
327 |
+
logger.error(f"Failed to save image {filename}: {e}")
|
328 |
+
uploaded_image_links.append(None)
|
329 |
+
|
330 |
+
logger.info(f"Successfully uploaded {sum(1 for link in uploaded_image_links if link)} images to Google Drive")
|
331 |
+
|
332 |
+
# Group into batches
|
333 |
+
logger.info(f"Grouping {len(loaded_images)} images into batches of {batch_size}")
|
334 |
+
for i in range(0, len(loaded_images), batch_size):
|
335 |
+
batch_images = loaded_images[i:i + batch_size]
|
336 |
+
batch_filenames = filenames[i:i + batch_size]
|
337 |
+
image_batches.append(batch_images)
|
338 |
+
filename_batches.append(batch_filenames)
|
339 |
+
logger.debug(f"Created batch {len(image_batches)} with {len(batch_images)} images: {batch_filenames}")
|
340 |
+
|
341 |
+
logger.info(f"Created {len(image_batches)} batches for processing")
|
342 |
+
|
343 |
+
# Process each batch
|
344 |
+
logger.info(f"Starting processing of {len(image_batches)} batches")
|
345 |
+
for batch_idx, (batch_images, batch_filenames) in enumerate(zip(image_batches, filename_batches)):
|
346 |
+
try:
|
347 |
+
logger.info(f"Processing batch {batch_idx + 1}/{len(image_batches)} ({len(batch_images)} cards)")
|
348 |
+
print(f"Processing batch {batch_idx + 1}/{len(image_batches)} ({len(batch_images)} cards)")
|
349 |
+
|
350 |
+
# Extract data for the entire batch
|
351 |
+
logger.debug(f"Calling batch extraction for batch {batch_idx + 1}")
|
352 |
+
batch_data = extract_business_card_data_batch(batch_images, batch_filenames, model_name)
|
353 |
+
logger.info(f"Batch {batch_idx + 1} extraction completed, got {len(batch_data)} results")
|
354 |
+
|
355 |
+
# Process each card's data in the batch
|
356 |
+
logger.debug(f"Processing individual card data for batch {batch_idx + 1}")
|
357 |
+
for i, data in enumerate(batch_data):
|
358 |
+
card_filename = batch_filenames[i] if i < len(batch_filenames) else f"card_{i+1}"
|
359 |
+
logger.debug(f"Processing card data for: {card_filename}")
|
360 |
+
|
361 |
+
# Add timestamp to data
|
362 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
363 |
+
data['processed_date'] = timestamp
|
364 |
+
logger.debug(f"Added timestamp {timestamp} to {card_filename}")
|
365 |
+
|
366 |
+
# Add Google Drive image link if images were saved
|
367 |
+
global_index = batch_idx * batch_size + i
|
368 |
+
if save_images and global_index < len(uploaded_image_links) and uploaded_image_links[global_index]:
|
369 |
+
data['google_drive_image_link'] = uploaded_image_links[global_index]
|
370 |
+
logger.debug(f"Added Google Drive image link for {card_filename}: {uploaded_image_links[global_index]}")
|
371 |
+
else:
|
372 |
+
data['google_drive_image_link'] = None
|
373 |
+
|
374 |
+
# Handle multiple values (emails, phones) by joining with commas
|
375 |
+
list_fields_processed = []
|
376 |
+
for key, value in data.items():
|
377 |
+
if isinstance(value, list):
|
378 |
+
original_count = len(value)
|
379 |
+
data[key] = ', '.join(str(v) for v in value)
|
380 |
+
list_fields_processed.append(f"{key}({original_count})")
|
381 |
+
logger.debug(f"Combined {original_count} {key} values for {card_filename}")
|
382 |
+
|
383 |
+
if list_fields_processed:
|
384 |
+
logger.debug(f"List fields processed for {card_filename}: {list_fields_processed}")
|
385 |
+
|
386 |
+
# Combine phone fields if they exist separately
|
387 |
+
if 'mobile_phones' in data and data['mobile_phones']:
|
388 |
+
logger.debug(f"Combining phone fields for {card_filename}")
|
389 |
+
if data.get('phones'):
|
390 |
+
# Combine mobile and regular phones
|
391 |
+
existing_phones = str(data['phones']) if data['phones'] else ""
|
392 |
+
mobile_phones = str(data['mobile_phones']) if data['mobile_phones'] else ""
|
393 |
+
combined = [p for p in [existing_phones, mobile_phones] if p and p != 'null']
|
394 |
+
data['phones'] = ', '.join(combined)
|
395 |
+
logger.debug(f"Combined phones for {card_filename}: {data['phones']}")
|
396 |
+
else:
|
397 |
+
data['phones'] = data['mobile_phones']
|
398 |
+
logger.debug(f"Used mobile phones as phones for {card_filename}: {data['phones']}")
|
399 |
+
del data['mobile_phones'] # Remove separate mobile field
|
400 |
+
|
401 |
+
# Combine address fields if they exist separately
|
402 |
+
if 'street' in data and data['street']:
|
403 |
+
logger.debug(f"Combining address fields for {card_filename}")
|
404 |
+
if data.get('address'):
|
405 |
+
# If both exist, combine them
|
406 |
+
if str(data['street']) != str(data['address']) and data['street'] != 'null':
|
407 |
+
original_address = data['address']
|
408 |
+
data['address'] = f"{data['street']}, {data['address']}"
|
409 |
+
logger.debug(f"Combined address for {card_filename}: '{data['street']}' + '{original_address}' = '{data['address']}'")
|
410 |
+
else:
|
411 |
+
data['address'] = data['street']
|
412 |
+
logger.debug(f"Used street as address for {card_filename}: {data['address']}")
|
413 |
+
del data['street'] # Remove separate street field
|
414 |
+
|
415 |
+
all_data.append(data)
|
416 |
+
logger.debug(f"Added processed data for {card_filename} to results (total: {len(all_data)})")
|
417 |
+
|
418 |
+
logger.info(f"Completed processing batch {batch_idx + 1}, total cards processed so far: {len(all_data)}")
|
419 |
+
|
420 |
+
except Exception as e:
|
421 |
+
batch_filenames_str = ', '.join(batch_filenames)
|
422 |
+
error_msg = f"Error processing batch {batch_idx + 1} ({batch_filenames_str}): {str(e)}"
|
423 |
+
logger.error(error_msg)
|
424 |
+
errors.append(error_msg)
|
425 |
+
|
426 |
+
if not all_data:
|
427 |
+
logger.warning("No data could be extracted from any images")
|
428 |
+
error_summary = "No data could be extracted from the images.\n" + "\n".join(errors)
|
429 |
+
return None, None, error_summary, None
|
430 |
+
|
431 |
+
logger.info(f"Successfully extracted data from {len(all_data)} business cards")
|
432 |
+
|
433 |
+
# Create DataFrame for current run
|
434 |
+
logger.info("Creating DataFrame for current run")
|
435 |
+
current_df = pd.DataFrame(all_data)
|
436 |
+
logger.debug(f"Current run DataFrame created with {len(current_df)} rows and {len(current_df.columns)} columns")
|
437 |
+
logger.debug(f"Columns: {list(current_df.columns)}")
|
438 |
+
|
439 |
+
# Generate timestamp
|
440 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
441 |
+
logger.debug(f"Generated timestamp: {timestamp}")
|
442 |
+
|
443 |
+
# Create temporary files for Excel generation
|
444 |
+
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as current_temp:
|
445 |
+
current_temp_path = current_temp.name
|
446 |
+
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as cumulative_temp:
|
447 |
+
cumulative_temp_path = cumulative_temp.name
|
448 |
+
|
449 |
+
current_filename = f"current_run_{timestamp}.xlsx"
|
450 |
+
cumulative_filename = "all_business_cards_total.xlsx"
|
451 |
+
|
452 |
+
# Try to download existing cumulative data from Google Drive
|
453 |
+
logger.info("Checking for existing cumulative file in Google Drive")
|
454 |
+
try:
|
455 |
+
# List files in exports folder to find existing cumulative file
|
456 |
+
exports_files = list_files_in_folder(drive_service, "1k5iP4egzLrGJwnHkMhxt9bAkaCiieojO")
|
457 |
+
cumulative_file = None
|
458 |
+
for file in exports_files:
|
459 |
+
if file['name'] == 'all_business_cards_total.xlsx':
|
460 |
+
cumulative_file = file
|
461 |
+
break
|
462 |
+
|
463 |
+
if cumulative_file:
|
464 |
+
logger.info("Existing cumulative file found in Google Drive")
|
465 |
+
# For now, we'll just use current data since downloading and merging is complex
|
466 |
+
# In production, you'd want to implement Google Drive file download
|
467 |
+
cumulative_df = current_df
|
468 |
+
logger.info("Using current data only (Google Drive download not implemented yet)")
|
469 |
+
else:
|
470 |
+
logger.info("No existing cumulative file found, using current data only")
|
471 |
+
cumulative_df = current_df
|
472 |
+
except Exception as e:
|
473 |
+
logger.warning(f"Could not check for existing data in Google Drive: {e}")
|
474 |
+
cumulative_df = current_df
|
475 |
+
|
476 |
+
# Write current run Excel file
|
477 |
+
logger.info(f"Creating current run Excel file: {current_filename}")
|
478 |
+
try:
|
479 |
+
with pd.ExcelWriter(current_temp_path, engine='openpyxl') as writer:
|
480 |
+
current_df.to_excel(writer, index=False, sheet_name='Current Run')
|
481 |
+
logger.debug(f"Written {len(current_df)} rows to 'Current Run' sheet")
|
482 |
+
|
483 |
+
# Auto-adjust column widths
|
484 |
+
logger.debug("Auto-adjusting column widths for current run file")
|
485 |
+
worksheet = writer.sheets['Current Run']
|
486 |
+
for column in current_df:
|
487 |
+
column_length = max(current_df[column].astype(str).map(len).max(), len(column))
|
488 |
+
col_idx = current_df.columns.get_loc(column)
|
489 |
+
final_width = min(column_length + 2, 50)
|
490 |
+
worksheet.column_dimensions[chr(65 + col_idx)].width = final_width
|
491 |
+
|
492 |
+
logger.info(f"Current run Excel file created locally")
|
493 |
+
|
494 |
+
# Upload current run file to Google Drive
|
495 |
+
current_result = upload_to_google_drive(current_temp_path, is_excel=True, filename=current_filename)
|
496 |
+
if current_result:
|
497 |
+
logger.info(f"Current run file uploaded to Google Drive: {current_result['webViewLink']}")
|
498 |
+
|
499 |
+
except Exception as e:
|
500 |
+
logger.error(f"Failed to create current run Excel file: {e}")
|
501 |
+
raise
|
502 |
+
|
503 |
+
# Write cumulative Excel file
|
504 |
+
logger.info(f"Creating cumulative Excel file: {cumulative_filename}")
|
505 |
+
try:
|
506 |
+
with pd.ExcelWriter(cumulative_temp_path, engine='openpyxl') as writer:
|
507 |
+
cumulative_df.to_excel(writer, index=False, sheet_name='All Business Cards')
|
508 |
+
logger.debug(f"Written {len(cumulative_df)} rows to 'All Business Cards' sheet")
|
509 |
+
|
510 |
+
# Auto-adjust column widths
|
511 |
+
logger.debug("Auto-adjusting column widths for cumulative file")
|
512 |
+
worksheet = writer.sheets['All Business Cards']
|
513 |
+
for column in cumulative_df:
|
514 |
+
column_length = max(cumulative_df[column].astype(str).map(len).max(), len(column))
|
515 |
+
col_idx = cumulative_df.columns.get_loc(column)
|
516 |
+
final_width = min(column_length + 2, 50)
|
517 |
+
worksheet.column_dimensions[chr(65 + col_idx)].width = final_width
|
518 |
+
|
519 |
+
logger.info(f"Cumulative Excel file created locally")
|
520 |
+
|
521 |
+
# Upload cumulative file to Google Drive
|
522 |
+
cumulative_result = upload_to_google_drive(cumulative_temp_path, is_excel=True, filename=cumulative_filename)
|
523 |
+
if cumulative_result:
|
524 |
+
logger.info(f"Cumulative file uploaded to Google Drive: {cumulative_result['webViewLink']}")
|
525 |
+
|
526 |
+
except Exception as e:
|
527 |
+
logger.error(f"Failed to create cumulative Excel file: {e}")
|
528 |
+
raise
|
529 |
+
|
530 |
+
# Note: Don't delete temp files here - Gradio needs them for download
|
531 |
+
# Gradio will handle cleanup automatically
|
532 |
+
|
533 |
+
# Create summary message
|
534 |
+
logger.info("Creating summary message")
|
535 |
+
num_batches = len(image_batches) if 'image_batches' in locals() else 1
|
536 |
+
summary = f"Successfully processed {len(all_data)} business card(s) in {num_batches} batch(es) of up to 5 cards.\n"
|
537 |
+
summary += f"🤖 AI Model used: {model_name}\n"
|
538 |
+
summary += f"⚡ API calls made: {num_batches} (instead of {len(all_data)})\n"
|
539 |
+
|
540 |
+
if save_images:
|
541 |
+
num_uploaded = sum(1 for link in uploaded_image_links if link) if 'uploaded_image_links' in locals() else 0
|
542 |
+
summary += f"💾 Images uploaded to Google Drive: {num_uploaded} cards\n\n"
|
543 |
+
else:
|
544 |
+
summary += f"💾 Images uploaded to Google Drive: No (save option was disabled)\n\n"
|
545 |
+
|
546 |
+
summary += f"📁 Current run file: {current_filename} (uploaded to Google Drive)\n"
|
547 |
+
summary += f"📊 Total cumulative file: {cumulative_filename} (uploaded to Google Drive)\n"
|
548 |
+
summary += f"📊 Total cards in database: {len(cumulative_df)}\n\n"
|
549 |
+
|
550 |
+
# Add Google Drive links
|
551 |
+
summary += "🔗 Google Drive Links:\n"
|
552 |
+
if 'current_result' in locals() and current_result:
|
553 |
+
summary += f" 📄 Current Run: {current_result['webViewLink']}\n"
|
554 |
+
if 'cumulative_result' in locals() and cumulative_result:
|
555 |
+
summary += f" 📊 Total Database: {cumulative_result['webViewLink']}\n"
|
556 |
+
summary += f" 📁 Exports Folder: https://drive.google.com/drive/folders/1k5iP4egzLrGJwnHkMhxt9bAkaCiieojO\n"
|
557 |
+
summary += f" 🖼️ Images Folder: https://drive.google.com/drive/folders/1gd280IqcAzpAFTPeYsZjoBUOU9S7Zx3c\n\n"
|
558 |
+
|
559 |
+
if errors:
|
560 |
+
logger.warning(f"Encountered {len(errors)} errors during processing")
|
561 |
+
summary += "Errors encountered:\n" + "\n".join(errors)
|
562 |
+
for error in errors:
|
563 |
+
logger.warning(f"Processing error: {error}")
|
564 |
+
else:
|
565 |
+
logger.info("No errors encountered during processing")
|
566 |
+
|
567 |
+
# Display preview of current run
|
568 |
+
logger.debug("Creating preview DataFrame")
|
569 |
+
preview_df = current_df.head(10)
|
570 |
+
logger.debug(f"Preview contains {len(preview_df)} rows")
|
571 |
+
|
572 |
+
logger.info("Business card processing session completed successfully")
|
573 |
+
logger.info(f"Session summary - Cards: {len(all_data)}, Batches: {num_batches}, API calls: {num_batches}, Total DB size: {len(cumulative_df)}")
|
574 |
+
|
575 |
+
# Return the temporary file paths for download (Gradio will handle the download)
|
576 |
+
return current_temp_path, cumulative_temp_path, summary, preview_df
|
577 |
+
|
578 |
+
# Create Gradio interface
|
579 |
+
logger.info("Creating Gradio interface")
|
580 |
+
with gr.Blocks(title="Business Card Data Extractor") as demo:
|
581 |
+
gr.Markdown(
|
582 |
+
"""
|
583 |
+
# Business Card Data Extractor
|
584 |
+
|
585 |
+
Upload business card images to extract contact information and export to Excel.
|
586 |
+
Cards are processed in batches of 5 for efficiency (fewer API calls, lower cost).
|
587 |
+
|
588 |
+
**Two files are generated:**
|
589 |
+
- 📁 **Current Run**: Contains only the cards you just processed
|
590 |
+
- 📊 **Total Database**: Contains ALL cards ever processed (cumulative)
|
591 |
+
|
592 |
+
**☁️ Google Drive Storage:**
|
593 |
+
- 📂 Excel files: Automatically uploaded to Google Drive exports folder
|
594 |
+
- 🖼️ Images: Uploaded to Google Drive images folder (if save option enabled)
|
595 |
+
- 🔗 **Direct Links**: Access files directly through provided Google Drive links
|
596 |
+
- 📁 **Organized Folders**: Separate folders for exports and images
|
597 |
+
|
598 |
+
**📌 File Access:**
|
599 |
+
- ⬇️ Download directly from interface buttons (temporary copies)
|
600 |
+
- 🔗 Access permanent files via Google Drive links in results
|
601 |
+
- 📁 **Exports Folder**: https://drive.google.com/drive/folders/1k5iP4egzLrGJwnHkMhxt9bAkaCiieojO
|
602 |
+
- 🖼️ **Images Folder**: https://drive.google.com/drive/folders/1gd280IqcAzpAFTPeYsZjoBUOU9S7Zx3c
|
603 |
+
|
604 |
+
**⚙️ Google Drive Integration:**
|
605 |
+
- Requires `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` environment variables
|
606 |
+
- Files are automatically uploaded and organized in predefined folders
|
607 |
+
"""
|
608 |
+
)
|
609 |
+
|
610 |
+
with gr.Row():
|
611 |
+
with gr.Column():
|
612 |
+
image_input = gr.File(
|
613 |
+
label="Upload Business Cards",
|
614 |
+
file_count="multiple",
|
615 |
+
file_types=[".jpg", ".jpeg", ".png", ".webp", ".bmp"]
|
616 |
+
)
|
617 |
+
|
618 |
+
model_selector = gr.Dropdown(
|
619 |
+
choices=["gemini-2.5-pro", "gemini-2.5-flash"],
|
620 |
+
value="gemini-2.5-pro",
|
621 |
+
label="AI Model Selection"
|
622 |
+
)
|
623 |
+
|
624 |
+
save_images_checkbox = gr.Checkbox(
|
625 |
+
value=True,
|
626 |
+
label="Save Business Card Images"
|
627 |
+
)
|
628 |
+
|
629 |
+
process_btn = gr.Button("Process Business Cards", variant="primary")
|
630 |
+
|
631 |
+
with gr.Column():
|
632 |
+
current_file = gr.File(label="📁 Download Current Run")
|
633 |
+
total_file = gr.File(label="📊 Download Total Database")
|
634 |
+
status_output = gr.Textbox(label="Processing Status", lines=5)
|
635 |
+
|
636 |
+
preview_output = gr.Dataframe(label="Data Preview (Current Run)")
|
637 |
+
|
638 |
+
# Wrapper function for better error handling and logging
|
639 |
+
def process_with_logging(images, model_name, save_images):
|
640 |
+
"""Wrapper function to add error handling and logging to the main process"""
|
641 |
+
try:
|
642 |
+
logger.info(f"Gradio interface initiated processing request")
|
643 |
+
logger.debug(f"Request parameters - Images: {len(images) if images else 0}, Model: {model_name}, Save Images: {save_images}")
|
644 |
+
return process_business_cards(images, model_name, save_images)
|
645 |
+
except Exception as e:
|
646 |
+
logger.error(f"Unexpected error in Gradio processing: {e}")
|
647 |
+
error_msg = f"An unexpected error occurred: {str(e)}\nPlease check the logs for more details."
|
648 |
+
return None, None, error_msg, None
|
649 |
+
|
650 |
+
# Handle processing
|
651 |
+
process_btn.click(
|
652 |
+
fn=process_with_logging,
|
653 |
+
inputs=[image_input, model_selector, save_images_checkbox],
|
654 |
+
outputs=[current_file, total_file, status_output, preview_output]
|
655 |
+
)
|
656 |
+
|
657 |
+
gr.Markdown(
|
658 |
+
"""
|
659 |
+
## Features:
|
660 |
+
- 🤖 **Model Selection**: Choose between Gemini 2.5 Flash (fast) or Pro (accurate)
|
661 |
+
- ⚡ **Batch Processing**: Processes 5 cards per API call for efficiency
|
662 |
+
- 📄 **Data Extraction**: Names, emails, phone numbers, addresses, and more
|
663 |
+
- 📞 **Smart Combination**: Multiple emails/phones combined with commas
|
664 |
+
- 🏠 **Address Merging**: All phone types and address fields combined
|
665 |
+
- ☁️ **Google Drive Storage**: Automatic upload to organized Drive folders
|
666 |
+
- 🔗 **Direct Links**: Instant access to files via Google Drive URLs
|
667 |
+
- 📊 **Dual Output**: Current run + cumulative database files
|
668 |
+
- 📝 **Full Tracking**: Processing date, filename, Google Drive links, and AI model used
|
669 |
+
- 🎯 **One Row Per Card**: Each business card becomes one spreadsheet row
|
670 |
+
"""
|
671 |
+
)
|
672 |
+
|
673 |
+
# Launch for Hugging Face Spaces deployment
|
674 |
+
logger.info("Starting Gradio demo")
|
675 |
+
demo.launch()
|
business_card_exports/.gitkeep
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
# This file ensures the business_card_exports directory is created and tracked by git
|
2 |
+
# Excel files with extracted business card data will be saved here
|
business_cards/.gitkeep
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
# This file ensures the business_cards directory is created and tracked by git
|
2 |
+
# Business card images will be saved here when the app runs
|
env.example
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 deployment: Add this as an environment variable named "Gemini_API"
|
7 |
+
Gemini_API=your_gemini_api_key_here
|
8 |
+
|
9 |
+
# Google Drive API Credentials (Required - for file storage)
|
10 |
+
# Get these from Google Cloud Console:
|
11 |
+
# 1. Create a project at https://console.cloud.google.com/
|
12 |
+
# 2. Enable Google Drive API
|
13 |
+
# 3. Create OAuth 2.0 credentials (Desktop application)
|
14 |
+
# 4. Download the JSON and extract client_id and client_secret
|
15 |
+
GOOGLE_CLIENT_ID=your_google_client_id_here
|
16 |
+
GOOGLE_CLIENT_SECRET=your_google_client_secret_here
|
17 |
+
|
18 |
+
# Google Drive Token (Required for deployment environments)
|
19 |
+
# Generate this by running the app locally first, then use convert_token_to_base64.py
|
20 |
+
# For local development: Leave this empty (token.pickle will be created automatically)
|
21 |
+
# For deployment: Set this to the base64 encoded token string
|
22 |
+
GOOGLE_TOKEN_BASE64=your_base64_encoded_token_here
|
23 |
+
|
24 |
+
# Examples:
|
25 |
+
# Gemini_API=AIzaSyBxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
26 |
+
# GOOGLE_CLIENT_ID=1234567890-abcdefghijklmnopqrstuvwxyz.apps.googleusercontent.com
|
27 |
+
# GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx
|
28 |
+
# GOOGLE_TOKEN_BASE64=gASVxwAAAAAAAAB9cQAoWBYAAABhY2Nlc3NfdG9rZW4... (very long string)
|
google_funcs.py
ADDED
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import pickle
|
3 |
+
import base64
|
4 |
+
from google.auth.transport.requests import Request
|
5 |
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
6 |
+
from googleapiclient.discovery import build
|
7 |
+
from googleapiclient.http import MediaFileUpload, MediaIoBaseUpload
|
8 |
+
import io
|
9 |
+
from pathlib import Path
|
10 |
+
import logging
|
11 |
+
|
12 |
+
logger = logging.getLogger(__name__)
|
13 |
+
|
14 |
+
# --- CONFIGURATION ---
|
15 |
+
# Get credentials from environment variables
|
16 |
+
CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
|
17 |
+
CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
|
18 |
+
|
19 |
+
# Google Drive folder IDs
|
20 |
+
EXPORTS_FOLDER_ID = "1k5iP4egzLrGJwnHkMhxt9bAkaCiieojO" # For Excel exports
|
21 |
+
IMAGES_FOLDER_ID = "1gd280IqcAzpAFTPeYsZjoBUOU9S7Zx3c" # For business card images
|
22 |
+
|
23 |
+
# Scopes define the level of access you are requesting.
|
24 |
+
SCOPES = ['https://www.googleapis.com/auth/drive.file']
|
25 |
+
TOKEN_PICKLE_FILE = 'token.pickle'
|
26 |
+
|
27 |
+
def get_drive_service():
|
28 |
+
"""Authenticates with Google and returns a Drive service object."""
|
29 |
+
creds = None
|
30 |
+
|
31 |
+
# --- NEW CODE FOR DEPLOYMENT ENVIRONMENTS ---
|
32 |
+
# If token file doesn't exist, try to create it from environment variable
|
33 |
+
if not os.path.exists(TOKEN_PICKLE_FILE):
|
34 |
+
encoded_token = os.environ.get('GOOGLE_TOKEN_BASE64')
|
35 |
+
if encoded_token:
|
36 |
+
logger.info("Found token in environment variable. Recreating token.pickle file.")
|
37 |
+
try:
|
38 |
+
decoded_token = base64.b64decode(encoded_token)
|
39 |
+
with open(TOKEN_PICKLE_FILE, "wb") as token_file:
|
40 |
+
token_file.write(decoded_token)
|
41 |
+
logger.info("Successfully recreated token.pickle from environment variable")
|
42 |
+
except Exception as e:
|
43 |
+
logger.error(f"Failed to decode token from environment variable: {e}")
|
44 |
+
# --- END OF NEW CODE ---
|
45 |
+
|
46 |
+
# The file token.pickle stores the user's access and refresh tokens.
|
47 |
+
if os.path.exists(TOKEN_PICKLE_FILE):
|
48 |
+
with open(TOKEN_PICKLE_FILE, 'rb') as token:
|
49 |
+
creds = pickle.load(token)
|
50 |
+
|
51 |
+
# If there are no (valid) credentials available, let the user log in.
|
52 |
+
if not creds or not creds.valid:
|
53 |
+
if creds and creds.expired and creds.refresh_token:
|
54 |
+
logger.info("Refreshing expired credentials")
|
55 |
+
creds.refresh(Request())
|
56 |
+
else:
|
57 |
+
if not CLIENT_ID or not CLIENT_SECRET:
|
58 |
+
raise ValueError("GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables are required")
|
59 |
+
|
60 |
+
logger.info("Starting OAuth flow for new credentials")
|
61 |
+
# Use client_config dictionary instead of a client_secret.json file
|
62 |
+
client_config = {
|
63 |
+
"installed": {
|
64 |
+
"client_id": CLIENT_ID,
|
65 |
+
"client_secret": CLIENT_SECRET,
|
66 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
67 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
68 |
+
"redirect_uris": ["http://localhost"]
|
69 |
+
}
|
70 |
+
}
|
71 |
+
flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
|
72 |
+
creds = flow.run_local_server(port=0)
|
73 |
+
|
74 |
+
# Save the credentials for the next run
|
75 |
+
with open(TOKEN_PICKLE_FILE, 'wb') as token:
|
76 |
+
pickle.dump(creds, token)
|
77 |
+
logger.info("Saved new credentials to token.pickle")
|
78 |
+
|
79 |
+
return build('drive', 'v3', credentials=creds)
|
80 |
+
|
81 |
+
def upload_file_to_drive(service, file_path=None, file_data=None, filename=None, folder_id=None, mimetype='application/octet-stream'):
|
82 |
+
"""
|
83 |
+
Uploads a file to a specific folder in Google Drive.
|
84 |
+
|
85 |
+
Args:
|
86 |
+
service: Google Drive service object
|
87 |
+
file_path: Path to local file (for file uploads)
|
88 |
+
file_data: Bytes data (for in-memory uploads)
|
89 |
+
filename: Name for the file in Drive
|
90 |
+
folder_id: ID of the target folder
|
91 |
+
mimetype: MIME type of the file
|
92 |
+
|
93 |
+
Returns:
|
94 |
+
dict: File information (id, webViewLink) or None if failed
|
95 |
+
"""
|
96 |
+
try:
|
97 |
+
if file_path and os.path.exists(file_path):
|
98 |
+
# Upload from local file
|
99 |
+
if not filename:
|
100 |
+
filename = os.path.basename(file_path)
|
101 |
+
media = MediaFileUpload(file_path, mimetype=mimetype, resumable=True)
|
102 |
+
logger.info(f"Uploading file from path: {file_path}")
|
103 |
+
elif file_data and filename:
|
104 |
+
# Upload from bytes data
|
105 |
+
file_io = io.BytesIO(file_data)
|
106 |
+
media = MediaIoBaseUpload(file_io, mimetype=mimetype, resumable=True)
|
107 |
+
logger.info(f"Uploading file from memory: {filename}")
|
108 |
+
else:
|
109 |
+
logger.error("Either file_path or (file_data + filename) must be provided")
|
110 |
+
return None
|
111 |
+
|
112 |
+
# Define the file's metadata
|
113 |
+
file_metadata = {
|
114 |
+
'name': filename,
|
115 |
+
'parents': [folder_id] if folder_id else []
|
116 |
+
}
|
117 |
+
|
118 |
+
logger.info(f"Uploading '{filename}' to Google Drive folder {folder_id}")
|
119 |
+
|
120 |
+
# Execute the upload request
|
121 |
+
file = service.files().create(
|
122 |
+
body=file_metadata,
|
123 |
+
media_body=media,
|
124 |
+
fields='id, webViewLink, name'
|
125 |
+
).execute()
|
126 |
+
|
127 |
+
logger.info(f"✅ File uploaded successfully!")
|
128 |
+
logger.info(f" File ID: {file.get('id')}")
|
129 |
+
logger.info(f" File Name: {file.get('name')}")
|
130 |
+
logger.info(f" View Link: {file.get('webViewLink')}")
|
131 |
+
|
132 |
+
return {
|
133 |
+
'id': file.get('id'),
|
134 |
+
'name': file.get('name'),
|
135 |
+
'webViewLink': file.get('webViewLink')
|
136 |
+
}
|
137 |
+
|
138 |
+
except Exception as e:
|
139 |
+
logger.error(f"Failed to upload file to Google Drive: {e}")
|
140 |
+
return None
|
141 |
+
|
142 |
+
def upload_excel_to_exports_folder(service, file_path=None, file_data=None, filename=None):
|
143 |
+
"""Upload Excel file to the exports folder."""
|
144 |
+
return upload_file_to_drive(
|
145 |
+
service,
|
146 |
+
file_path=file_path,
|
147 |
+
file_data=file_data,
|
148 |
+
filename=filename,
|
149 |
+
folder_id=EXPORTS_FOLDER_ID,
|
150 |
+
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
151 |
+
)
|
152 |
+
|
153 |
+
def upload_image_to_images_folder(service, file_path=None, file_data=None, filename=None, mimetype='image/png'):
|
154 |
+
"""Upload image file to the images folder."""
|
155 |
+
return upload_file_to_drive(
|
156 |
+
service,
|
157 |
+
file_path=file_path,
|
158 |
+
file_data=file_data,
|
159 |
+
filename=filename,
|
160 |
+
folder_id=IMAGES_FOLDER_ID,
|
161 |
+
mimetype=mimetype
|
162 |
+
)
|
163 |
+
|
164 |
+
def list_files_in_folder(service, folder_id, max_results=100):
|
165 |
+
"""List files in a specific Google Drive folder."""
|
166 |
+
try:
|
167 |
+
query = f"'{folder_id}' in parents"
|
168 |
+
results = service.files().list(
|
169 |
+
q=query,
|
170 |
+
maxResults=max_results,
|
171 |
+
fields="files(id, name, size, createdTime, webViewLink)"
|
172 |
+
).execute()
|
173 |
+
|
174 |
+
files = results.get('files', [])
|
175 |
+
logger.info(f"Found {len(files)} files in folder {folder_id}")
|
176 |
+
return files
|
177 |
+
except Exception as e:
|
178 |
+
logger.error(f"Failed to list files in folder {folder_id}: {e}")
|
179 |
+
return []
|
180 |
+
|
181 |
+
if __name__ == '__main__':
|
182 |
+
# Test the Google Drive connection
|
183 |
+
try:
|
184 |
+
drive_service = get_drive_service()
|
185 |
+
logger.info("Google Drive service initialized successfully")
|
186 |
+
|
187 |
+
# List files in both folders to verify access
|
188 |
+
exports_files = list_files_in_folder(drive_service, EXPORTS_FOLDER_ID)
|
189 |
+
images_files = list_files_in_folder(drive_service, IMAGES_FOLDER_ID)
|
190 |
+
|
191 |
+
print(f"Exports folder contains {len(exports_files)} files")
|
192 |
+
print(f"Images folder contains {len(images_files)} files")
|
193 |
+
|
194 |
+
except Exception as e:
|
195 |
+
logger.error(f"Failed to initialize Google Drive: {e}")
|
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,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==4.44.1
|
2 |
+
pydantic==2.10.6
|
3 |
+
google-generativeai==0.8.0
|
4 |
+
pandas==2.1.4
|
5 |
+
openpyxl==3.1.2
|
6 |
+
Pillow==10.2.0
|
7 |
+
google-auth==2.23.4
|
8 |
+
google-auth-oauthlib==1.1.0
|
9 |
+
google-api-python-client==2.108.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
|