rongo1 commited on
Commit
29d41e1
Β·
0 Parent(s):

feat: init

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