rongo1 commited on
Commit
0b03a29
·
verified ·
1 Parent(s): fd3316e

Upload folder using huggingface_hub

Browse files
.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