dev2607 commited on
Commit
1da41bb
·
verified ·
1 Parent(s): 4ed270a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +429 -0
app.py ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import sys
4
+ import re
5
+ import numpy as np
6
+ from PIL import Image
7
+ import gradio as gr
8
+ import requests
9
+ import json
10
+ from dotenv import load_dotenv
11
+
12
+ # Attempt to install pytesseract if not found
13
+ try:
14
+ import pytesseract
15
+ except ImportError:
16
+ subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'pytesseract'])
17
+ import pytesseract
18
+
19
+ # AFTER importing pytesseract, then set the path
20
+ try:
21
+ # First try the default path
22
+ if os.path.exists('/usr/bin/tesseract'):
23
+ pytesseract.pytesseract.tesseract_cmd = '/usr/bin/tesseract'
24
+ # Try to find it on the PATH
25
+ else:
26
+ tesseract_path = subprocess.check_output(['which', 'tesseract']).decode().strip()
27
+ if tesseract_path:
28
+ pytesseract.pytesseract.tesseract_cmd = tesseract_path
29
+ except:
30
+ # If all else fails, try the default installation path
31
+ pytesseract.pytesseract.tesseract_cmd = 'tesseract'
32
+
33
+ # Load environment variables
34
+ load_dotenv()
35
+
36
+ # Mistral API Key
37
+ MISTRAL_API_KEY = "GlrVCBWyvTYjWGKl5jqtK4K41uWWJ79F"
38
+
39
+ # Import and configure Mistral API
40
+ def analyze_ingredients_with_mistral(ingredients_list, health_conditions=None):
41
+ """
42
+ Use Mistral AI to analyze ingredients and provide health insights.
43
+ """
44
+ if not ingredients_list:
45
+ return "No ingredients detected or provided."
46
+
47
+ # Prepare the list of ingredients for the prompt
48
+ ingredients_text = ", ".join(ingredients_list)
49
+
50
+ # Create a prompt for Mistral
51
+ if health_conditions and health_conditions.strip():
52
+ prompt = f"""
53
+ Analyze the following food ingredients for a person with these health conditions: {health_conditions}
54
+ Ingredients: {ingredients_text}
55
+ For each ingredient:
56
+ 1. Provide its potential health benefits
57
+ 2. Identify any potential risks
58
+ 3. Note if it may affect the specified health conditions
59
+ Then provide an overall assessment of the product's suitability for someone with the specified health conditions.
60
+ Format your response in markdown with clear headings and sections.
61
+ """
62
+ else:
63
+ prompt = f"""
64
+ Analyze the following food ingredients:
65
+ Ingredients: {ingredients_text}
66
+ For each ingredient:
67
+ 1. Provide its potential health benefits
68
+ 2. Identify any potential risks or common allergens associated with it
69
+ Then provide an overall assessment of the product's general health profile.
70
+ Format your response in markdown with clear headings and sections.
71
+ """
72
+
73
+ try:
74
+ headers = {
75
+ "Authorization": f"Bearer {MISTRAL_API_KEY}",
76
+ "Content-Type": "application/json"
77
+ }
78
+ data = {
79
+ "model": "mistral-small", # Or another suitable model
80
+ "messages": [{"role": "user", "content": prompt}],
81
+ "temperature": 0.7,
82
+ }
83
+
84
+ response = requests.post("https://api.mistral.ai/v1/chat/completions", headers=headers, json=data)
85
+
86
+ if response.status_code == 200:
87
+ analysis = response.json()['choices'][0]['message']['content']
88
+ else:
89
+ return dummy_analyze(ingredients_list, health_conditions) + f"\n\n(Using fallback analysis: Mistral API Error - {response.status_code} - {response.text})"
90
+
91
+ # Add disclaimer
92
+ disclaimer = """
93
+ ## Disclaimer
94
+ This analysis is provided for informational purposes only and should not replace professional medical advice.
95
+ Always consult with a healthcare provider regarding dietary restrictions, allergies, or health conditions.
96
+ """
97
+
98
+ return analysis + disclaimer
99
+
100
+ except Exception as e:
101
+ # Fallback to basic analysis if API call fails
102
+ return dummy_analyze(ingredients_list, health_conditions) + f"\n\n(Using fallback analysis: {str(e)})"
103
+
104
+
105
+ # Dummy analysis function for when API is not available
106
+ def dummy_analyze(ingredients_list, health_conditions=None):
107
+ ingredients_text = ", ".join(ingredients_list)
108
+
109
+ report = f"""
110
+ # Ingredient Analysis Report
111
+ ## Detected Ingredients
112
+ {", ".join([i.title() for i in ingredients_list])}
113
+ ## Overview
114
+ This is a simulated analysis since the Mistral API call failed. In the actual application,
115
+ the ingredients would be analyzed by Mistral for their health implications.
116
+ ## Health Considerations
117
+ """
118
+
119
+ if health_conditions:
120
+ report += f"""
121
+ The analysis would specifically consider these health concerns: {health_conditions}
122
+ """
123
+ else:
124
+ report += """
125
+ No specific health concerns were provided, so a general analysis would be performed.
126
+ """
127
+
128
+ report += """
129
+ ## Disclaimer
130
+ This analysis is provided for informational purposes only and should not replace professional medical advice.
131
+ Always consult with a healthcare provider regarding dietary restrictions, allergies, or health conditions.
132
+ """
133
+
134
+ return report
135
+
136
+ # Function to extract text from images using OCR
137
+ def extract_text_from_image(image):
138
+ try:
139
+ if image is None:
140
+ return "No image captured. Please try again."
141
+
142
+ # Verify Tesseract executable is accessible
143
+ try:
144
+ subprocess.run([pytesseract.pytesseract.tesseract_cmd, "--version"],
145
+ check=True, capture_output=True, text=True)
146
+ except (subprocess.SubprocessError, FileNotFoundError):
147
+ return "Tesseract OCR is not installed or not properly configured. Please check installation."
148
+
149
+ # Import necessary libraries
150
+ import cv2
151
+ import numpy as np
152
+ from PIL import Image, ImageOps, ImageEnhance
153
+
154
+ # First approach: Invert the image for light text on dark background
155
+ inverted_image = ImageOps.invert(image)
156
+
157
+ # Try OCR on inverted image
158
+ custom_config = r'--oem 3 --psm 6 -l eng --dpi 300'
159
+ inverted_text = pytesseract.image_to_string(inverted_image, config=custom_config)
160
+
161
+ # Second approach: OpenCV processing for colored backgrounds
162
+ img_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
163
+
164
+ # Convert to grayscale
165
+ gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
166
+
167
+ # Apply bilateral filter to preserve edges while reducing noise
168
+ filtered = cv2.bilateralFilter(gray, 11, 17, 17)
169
+
170
+ # Adaptive thresholding to handle varied lighting
171
+ thresh = cv2.adaptiveThreshold(filtered, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
172
+ cv2.THRESH_BINARY, 11, 2)
173
+
174
+ # Invert the image (if text is light on dark background)
175
+ inverted_thresh = cv2.bitwise_not(thresh)
176
+
177
+ # Try OCR on processed image
178
+ cv_text = pytesseract.image_to_string(
179
+ Image.fromarray(inverted_thresh),
180
+ config=custom_config
181
+ )
182
+
183
+ # Third approach: Color filtering to isolate text from colored background
184
+ # Convert to HSV color space to better isolate colors
185
+ hsv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2HSV)
186
+
187
+ # Create a mask to extract light colored text (assuming white/light text)
188
+ lower_white = np.array([0, 0, 150])
189
+ upper_white = np.array([180, 30, 255])
190
+ mask = cv2.inRange(hsv, lower_white, upper_white)
191
+
192
+ # Apply morphological operations to clean up the mask
193
+ kernel = np.ones((2, 2), np.uint8)
194
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
195
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
196
+
197
+ # Improve character connectivity
198
+ mask = cv2.dilate(mask, kernel, iterations=1)
199
+
200
+ # Try OCR on color filtered image
201
+ color_text = pytesseract.image_to_string(
202
+ Image.fromarray(mask),
203
+ config=r'--oem 3 --psm 6 -l eng --dpi 300'
204
+ )
205
+
206
+ # Fourth approach: Try directly with the image but with different configs
207
+ direct_text = pytesseract.image_to_string(
208
+ image,
209
+ config=r'--oem 3 --psm 11 -l eng --dpi 300'
210
+ )
211
+
212
+ # Compare results and select the best one
213
+ results = [inverted_text, cv_text, color_text, direct_text]
214
+
215
+ # Select the result with the most alphanumeric characters
216
+ def count_alphanumeric(text):
217
+ return sum(c.isalnum() for c in text)
218
+
219
+ best_text = max(results, key=count_alphanumeric)
220
+
221
+ # If still poor results, try with explicit text color inversion in tesseract
222
+ if count_alphanumeric(best_text) < 20:
223
+ # Try with tesseract's built-in inversion
224
+ neg_text = pytesseract.image_to_string(
225
+ image,
226
+ config=r'--oem 3 --psm 6 -c textord_heavy_nr=1 -c textord_debug_printable=0 -l eng --dpi 300'
227
+ )
228
+ if count_alphanumeric(neg_text) > count_alphanumeric(best_text):
229
+ best_text = neg_text
230
+
231
+ # Clean up the text
232
+ best_text = re.sub(r'[^\w\s,;:%.()\n\'-]', '', best_text)
233
+ best_text = best_text.replace('\n\n', '\n')
234
+
235
+ # Special case for ingredients list format
236
+ if "ingredient" in best_text.lower() or any(x in best_text.lower() for x in ["sugar", "cocoa", "milk", "contain"]):
237
+ # Specific cleaning for ingredient lists
238
+ best_text = re.sub(r'([a-z])([A-Z])', r'\1 \2', best_text) # Add space between lowercase and uppercase
239
+ best_text = re.sub(r'(\d+)([a-zA-Z])', r'\1 \2', best_text) # Add space between number and letter
240
+
241
+ if not best_text.strip():
242
+ return "No text could be extracted. Ensure image is clear and readable."
243
+
244
+ return best_text.strip()
245
+ except Exception as e:
246
+ return f"Error extracting text: {str(e)}"
247
+
248
+ # Function to parse ingredients from text
249
+ def parse_ingredients(text):
250
+ if not text:
251
+ return []
252
+
253
+ # Clean up the text
254
+ text = re.sub(r'^ingredients:?\s*', '', text.lower(), flags=re.IGNORECASE)
255
+
256
+ # Remove common OCR errors and extraneous characters
257
+ text = re.sub(r'[|\\/@#$%^&*()_+=]', '', text)
258
+
259
+ # Replace common OCR errors
260
+ text = re.sub(r'\bngredients\b', 'ingredients', text)
261
+
262
+ # Handle common OCR misreads
263
+ replacements = {
264
+ '0': 'o', 'l': 'i', '1': 'i',
265
+ '5': 's', '8': 'b', 'Q': 'g',
266
+ }
267
+
268
+ for error, correction in replacements.items():
269
+ text = text.replace(error, correction)
270
+
271
+ # Split by common ingredient separators
272
+ ingredients = re.split(r',|;|\n', text)
273
+
274
+ # Clean up each ingredient
275
+ cleaned_ingredients = []
276
+ for i in ingredients:
277
+ i = i.strip().lower()
278
+ if i and len(i) > 1: # Ignore single characters which are likely OCR errors
279
+ cleaned_ingredients.append(i)
280
+
281
+ return cleaned_ingredients
282
+
283
+
284
+ # Function to process input based on method (camera, upload, or manual entry)
285
+ def process_input(input_method, text_input, camera_input, upload_input, health_conditions):
286
+ if input_method == "Camera":
287
+ if camera_input is not None:
288
+ extracted_text = extract_text_from_image(camera_input)
289
+ # If OCR fails, inform the user they can try manual entry
290
+ if "Error" in extracted_text or "No text could be extracted" in extracted_text:
291
+ return extracted_text + "\n\nPlease try using the 'Manual Entry' option instead."
292
+
293
+ ingredients = parse_ingredients(extracted_text)
294
+ return analyze_ingredients_with_mistral(ingredients, health_conditions)
295
+ else:
296
+ return "No camera image captured. Please try again."
297
+
298
+ elif input_method == "Image Upload":
299
+ if upload_input is not None:
300
+ extracted_text = extract_text_from_image(upload_input)
301
+ # If OCR fails, inform the user they can try manual entry
302
+ if "Error" in extracted_text or "No text could be extracted" in extracted_text:
303
+ return extracted_text + "\n\nPlease try using the 'Manual Entry' option instead."
304
+
305
+ ingredients = parse_ingredients(extracted_text)
306
+ return analyze_ingredients_with_mistral(ingredients, health_conditions)
307
+ else:
308
+ return "No image uploaded. Please try again."
309
+
310
+ elif input_method == "Manual Entry":
311
+ if text_input and text_input.strip():
312
+ ingredients = parse_ingredients(text_input)
313
+ return analyze_ingredients_with_mistral(ingredients, health_conditions)
314
+ else:
315
+ return "No ingredients entered. Please try again."
316
+
317
+ return "Please provide input using one of the available methods."
318
+
319
+ # Create the Gradio interface
320
+ with gr.Blocks(title="AI Ingredient Scanner") as app:
321
+ gr.Markdown("# AI Ingredient Scanner")
322
+ gr.Markdown("Scan product ingredients and analyze them for health benefits, risks, and potential allergens.")
323
+
324
+ with gr.Row():
325
+ with gr.Column():
326
+ input_method = gr.Radio(
327
+ ["Camera", "Image Upload", "Manual Entry"],
328
+ label="Input Method",
329
+ value="Camera"
330
+ )
331
+
332
+ # Camera input
333
+ camera_input = gr.Image(label="Capture ingredients with camera", type="pil", visible=True)
334
+
335
+ # Image upload
336
+ upload_input = gr.Image(label="Upload image of ingredients label", type="pil", visible=False)
337
+
338
+ # Text input
339
+ text_input = gr.Textbox(
340
+ label="Enter ingredients list (comma separated)",
341
+ placeholder="milk, sugar, flour, eggs, vanilla extract",
342
+ lines=3,
343
+ visible=False
344
+ )
345
+
346
+ # Health conditions input - now optional and more flexible
347
+ health_conditions = gr.Textbox(
348
+ label="Enter your health concerns (optional)",
349
+ placeholder="diabetes, high blood pressure, peanut allergy, etc.",
350
+ lines=2,
351
+ info="The AI will automatically analyze ingredients for these conditions"
352
+ )
353
+
354
+ analyze_button = gr.Button("Analyze Ingredients")
355
+
356
+ with gr.Column():
357
+ output = gr.Markdown(label="Analysis Results")
358
+ extracted_text_output = gr.Textbox(label="Extracted Text (for verification)", lines=3)
359
+
360
+ # Show/hide inputs based on selection
361
+ def update_visible_inputs(choice):
362
+ return {
363
+ upload_input: gr.update(visible=(choice == "Image Upload")),
364
+ camera_input: gr.update(visible=(choice == "Camera")),
365
+ text_input: gr.update(visible=(choice == "Manual Entry"))
366
+ }
367
+
368
+ input_method.change(update_visible_inputs, input_method, [upload_input, camera_input, text_input])
369
+
370
+ # Extract and display the raw text (for verification purposes)
371
+ def show_extracted_text(input_method, text_input, camera_input, upload_input):
372
+ if input_method == "Camera" and camera_input is not None:
373
+ return extract_text_from_image(camera_input)
374
+ elif input_method == "Image Upload" and upload_input is not None:
375
+ return extract_text_from_image(upload_input)
376
+ elif input_method == "Manual Entry":
377
+ return text_input
378
+ return "No input detected"
379
+
380
+ # Set up event handlers
381
+ analyze_button.click(
382
+ fn=process_input,
383
+ inputs=[input_method, text_input, camera_input, upload_input, health_conditions],
384
+ outputs=output
385
+ )
386
+
387
+ analyze_button.click(
388
+ fn=show_extracted_text,
389
+ inputs=[input_method, text_input, camera_input, upload_input],
390
+ outputs=extracted_text_output
391
+ )
392
+
393
+ gr.Markdown("### How to use")
394
+ gr.Markdown("""
395
+ 1. Choose your input method (Camera, Image Upload, or Manual Entry)
396
+ 2. Take a photo of the ingredients label or enter ingredients manually
397
+ 3. Optionally enter your health concerns
398
+ 4. Click "Analyze Ingredients" to get your personalized analysis
399
+ The AI will automatically analyze the ingredients, their health implications, and their potential impact on your specific health concerns.
400
+ """)
401
+
402
+ gr.Markdown("### Examples of what you can ask")
403
+ gr.Markdown("""
404
+ The system can handle a wide range of health concerns, such as:
405
+ - General health goals: "trying to reduce sugar intake" or "watching sodium levels"
406
+ - Medical conditions: "diabetes" or "hypertension"
407
+ - Allergies: "peanut allergy" or "shellfish allergy"
408
+ - Dietary restrictions: "vegetarian" or "gluten-free diet"
409
+ - Multiple conditions: "diabetes, high cholesterol, and lactose intolerance"
410
+ The AI will tailor its analysis to your specific needs.
411
+ """)
412
+
413
+ gr.Markdown("### Tips for best results")
414
+ gr.Markdown("""
415
+ - Hold the camera steady and ensure good lighting
416
+ - Focus directly on the ingredients list
417
+ - Make sure the text is clear and readable
418
+ - Be specific about your health concerns for more targeted analysis
419
+ """)
420
+
421
+ gr.Markdown("### Disclaimer")
422
+ gr.Markdown("""
423
+ This tool is for informational purposes only and should not replace professional medical advice.
424
+ Always consult with a healthcare provider regarding dietary restrictions, allergies, or health conditions.
425
+ """)
426
+
427
+ # Launch the app
428
+ if __name__ == "__main__":
429
+ app.launch()