Create recipe_generator.py

#1
by kovermier - opened
Files changed (1) hide show
  1. recipe_generator.py +729 -729
recipe_generator.py CHANGED
@@ -1,729 +1,729 @@
1
- import os
2
- import base64
3
- import logging
4
- import time
5
- import random
6
- from typing import Optional, Tuple
7
- import json
8
-
9
- import gradio as gr
10
- from groq import Groq
11
- from PIL import Image
12
- import re
13
- import tempfile
14
- from functools import partial
15
- import time
16
-
17
- # Configure logging
18
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
19
- logger = logging.getLogger(__name__)
20
-
21
- # Ensure GROQ_API_KEY is set
22
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
23
- if not GROQ_API_KEY:
24
- raise EnvironmentError("GROQ_API_KEY environment variable not set.")
25
-
26
- # Initialize Groq client
27
- client = Groq(api_key=GROQ_API_KEY)
28
-
29
- # Constants
30
- MAX_RETRIES = 5
31
- IMAGE_RESIZE_FACTOR = 1
32
- IMAGE_QUALITY = 85
33
- MAX_TOKENS = {
34
- 'analysis': 750,
35
- 'recipe': 2000,
36
- 'nutrition': 1000,
37
- 'menu': 300,
38
- 'cost': 1000
39
- }
40
-
41
- def process_and_encode_image(image: Image.Image) -> str:
42
- """
43
- Processes and encodes the image to base64 format suitable for API usage.
44
- """
45
- try:
46
- with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file:
47
- temp_path = temp_file.name
48
- image = image.convert('RGB')
49
- image = image.resize(
50
- (
51
- int(image.width * IMAGE_RESIZE_FACTOR),
52
- int(image.height * IMAGE_RESIZE_FACTOR)
53
- )
54
- )
55
- image.save(temp_path, format="JPEG", optimize=True, quality=IMAGE_QUALITY)
56
-
57
- with open(temp_path, "rb") as img_file:
58
- base64_image = base64.b64encode(img_file.read()).decode('utf-8')
59
- os.remove(temp_path)
60
- logger.info("Image processed and encoded successfully.")
61
- return base64_image
62
- except Exception as e:
63
- logger.error(f"Error processing image: {e}")
64
- raise
65
-
66
- def groq_api_call(model: str, messages: list, max_tokens: int, json_mode: bool = False) -> str:
67
- """
68
- Makes an API call to the Groq model with retry logic.
69
- """
70
- for attempt in range(MAX_RETRIES):
71
- try:
72
- response = client.chat.completions.create(
73
- model=model,
74
- messages=messages,
75
- max_tokens=max_tokens,
76
- response_format={"type": "json_object"} if json_mode else None
77
- )
78
- content = response.choices[0].message.content
79
- logger.info(f"API call successful using model {model}.")
80
- return content
81
- except Exception as e:
82
- logger.warning(f"API call failed on attempt {attempt + 1}: {e}")
83
- if attempt < MAX_RETRIES - 1:
84
- sleep_time = (2 ** attempt) + random.random()
85
- logger.info(f"Retrying in {sleep_time:.2f} seconds...")
86
- time.sleep(sleep_time)
87
- else:
88
- logger.error("Max retries reached. Failing the API call.")
89
- raise
90
-
91
- def analyze_image(base64_image: str) -> str:
92
- """
93
- Analyzes the image using the vision model and returns a detailed description.
94
- """
95
- prompt = "Our goal is to analyze images of food with as much detail as possible. Analyze the following image of a dish and provide a detailed description including ingredients, cooking techniques, presentation, textures, colors, and any unique elements. The analysis: "
96
-
97
- messages = [
98
- {
99
- "role": "user",
100
- "content": [
101
- {"type": "text", "text": prompt},
102
- {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
103
- ]
104
- }
105
- ]
106
-
107
- analysis = groq_api_call(
108
- model="llama-3.2-11b-vision-preview",
109
- messages=messages,
110
- max_tokens=MAX_TOKENS['analysis']
111
- )
112
- return analysis
113
-
114
- def generate_recipe(analysis: str) -> str:
115
- """
116
- Generates a recipe based on the analysis.
117
- """
118
- prompt = f"""Based on the following analysis, generate a detailed recipe including:
119
-
120
- 1. Recipe Name
121
- 2. List of ingredients with approximate quantities
122
- 3. Step-by-step instructions
123
- 4. Variations or substitutions
124
- 5. Serving suggestions
125
- 6. Menu description suitable for a restaurant
126
-
127
- **Format the recipe exactly as follows, using the same section headings and Markdown syntax:**
128
-
129
- # **[Recipe Name]**
130
-
131
- ## **Ingredients**
132
- - [Ingredient 1]
133
- - [Ingredient 2]
134
- ...
135
-
136
- ## **Instructions**
137
- 1. [Step 1]
138
- 2. [Step 2]
139
- ...
140
-
141
- ## **Variations**
142
- - [Variation 1]
143
- - [Variation 2]
144
- ...
145
-
146
- ## **Serving Suggestions**
147
- - [Suggestion 1]
148
- - [Suggestion 2]
149
- ...
150
-
151
- ## **Menu Description**
152
- [Menu description here]
153
-
154
- Analysis:
155
- {analysis}
156
- """
157
-
158
- messages = [
159
- {"role": "user", "content": prompt}
160
- ]
161
-
162
- recipe = groq_api_call(
163
- model="mixtral-8x7b-32768",
164
- messages=messages,
165
- max_tokens=MAX_TOKENS['recipe']
166
- )
167
- return recipe
168
-
169
- def parse_recipe(recipe_markdown: str) -> dict:
170
- """
171
- Parses the recipe Markdown into its components.
172
- """
173
- recipe_parts = {}
174
-
175
- # Extract recipe name
176
- recipe_name_match = re.search(r"# \*\*(.+?)\*\*", recipe_markdown)
177
- recipe_parts['recipe_name'] = recipe_name_match.group(1).strip() if recipe_name_match else "Unknown Recipe"
178
-
179
- # Sections to extract
180
- sections = ['Ingredients', 'Instructions', 'Variations', 'Serving Suggestions', 'Menu Description']
181
-
182
- for i, section in enumerate(sections):
183
- pattern = rf"## \*\*{section}\*\*\n(.*?)(?=## \*\*{'|'.join(sections[i+1:])}\*\*|$)"
184
- match = re.search(pattern, recipe_markdown, re.DOTALL)
185
- recipe_parts[section.lower().replace(' ', '_')] = match.group(1).strip() if match else ""
186
- return recipe_parts
187
-
188
- def get_nutritional_info(ingredients: str) -> str:
189
- """
190
- Provides nutritional analysis for the given ingredients in a nutrition label format.
191
- """
192
- if not ingredients.strip():
193
- return "Ingredients not provided."
194
-
195
- prompt = f"""You are a nutrition expert API that provides nutritional analysis for given ingredients. Respond with a JSON object containing the nutritional information for the entire dish.
196
-
197
- Analyze the following ingredients and provide nutritional information for the complete dish:
198
-
199
- {ingredients}
200
-
201
- The JSON response should have the following structure:
202
- {{
203
- "serving_size": "X g",
204
- "calories": int,
205
- "total_fat": "X g",
206
- "saturated_fat": "X g",
207
- "trans_fat": "X g",
208
- "cholesterol": "X mg",
209
- "sodium": "X mg",
210
- "total_carbohydrate": "X g",
211
- "dietary_fiber": "X g",
212
- "total_sugars": "X g",
213
- "protein": "X g",
214
- "vitamin_d": "X mcg",
215
- "calcium": "X mg",
216
- "iron": "X mg",
217
- "potassium": "X mg"
218
- }}
219
-
220
- Provide a single set of nutritional information for the entire dish, not individual ingredients. If you cannot provide exact values, use reasonable estimates based on similar dishes. Ensure all fields are present in the response, using "0" or "0 g" for any nutrients that are not applicable or present in negligible amounts.
221
- """
222
-
223
- messages = [
224
- {"role": "system", "content": "You are a nutrition expert API that responds in JSON format."},
225
- {"role": "user", "content": prompt}
226
- ]
227
-
228
- try:
229
- nutrition_response = groq_api_call(
230
- model="llama3-8b-8192",
231
- messages=messages,
232
- max_tokens=MAX_TOKENS['nutrition'],
233
- json_mode=True
234
- )
235
-
236
- nutrition_data = json.loads(nutrition_response)
237
- logger.info("Successfully parsed nutrition data JSON.")
238
-
239
- # Ensure we have a single set of nutritional information
240
- if isinstance(nutrition_data, dict) and 'serving_size' in nutrition_data:
241
- nutrition_data = {'dish': nutrition_data}
242
- elif not any('serving_size' in item for item in nutrition_data.values()):
243
- raise ValueError("Invalid nutrition data format")
244
-
245
- # Combine multiple ingredients if necessary
246
- combined_nutrition = combine_nutrition_data(nutrition_data)
247
-
248
- nutrition_label = generate_nutrition_label(combined_nutrition)
249
- return nutrition_label
250
- except Exception as e:
251
- logger.error(f"Error in get_nutritional_info: {e}")
252
- return f"Error parsing nutritional information: {e}"
253
-
254
- def combine_nutrition_data(nutrition_data: dict) -> dict:
255
- """
256
- Combines nutritional information from multiple ingredients into a single set of data.
257
- """
258
- combined = {
259
- "serving_size": "1 serving",
260
- "calories": 0,
261
- "total_fat": 0,
262
- "saturated_fat": 0,
263
- "trans_fat": 0,
264
- "cholesterol": 0,
265
- "sodium": 0,
266
- "total_carbohydrate": 0,
267
- "dietary_fiber": 0,
268
- "total_sugars": 0,
269
- "protein": 0,
270
- "vitamin_d": 0,
271
- "calcium": 0,
272
- "iron": 0,
273
- "potassium": 0
274
- }
275
-
276
- for item in nutrition_data.values():
277
- for key, value in item.items():
278
- if key == "serving_size":
279
- continue
280
- if isinstance(value, str):
281
- # Remove 'g' or other units before conversion
282
- value = float(re.sub(r'[^\d.]+', '', value))
283
- combined[key] += value
284
-
285
- # Convert back to strings with units
286
- for key, value in combined.items():
287
- if key == "calories":
288
- combined[key] = int(value)
289
- elif key != "serving_size":
290
- unit = next((item[key].split()[-1] for item in nutrition_data.values() if key in item), "g")
291
- combined[key] = f"{value:.1f} {unit}"
292
-
293
- return combined
294
-
295
- def generate_nutrition_label(nutrition_data: dict) -> str:
296
- """
297
- Generates an HTML nutrition label from the given nutritional data.
298
- """
299
- nutrition_label = f"""
300
- <div class="nutrition-label">
301
- <p class="nutrition-footnote">Please Note that these are estimations.</p
302
- <h2 class="nutrition-title">Nutrition Facts</h2>
303
- <p class="nutrition-serving">Serving Size {nutrition_data['serving_size']}</p>
304
- <div class="nutrition-calories">
305
- <span class="left">Calories</span>
306
- <span class="right">{nutrition_data['calories']}</span>
307
- </div>
308
- <div class="nutrition-percent">% Daily Value*</div>
309
- <div class="nutrition-item">
310
- <span class="left"><strong>Total Fat</strong> {nutrition_data['total_fat']}</span>
311
- <span class="right">{calculate_daily_value(nutrition_data['total_fat'], 78)}%</span>
312
- </div>
313
- <div class="nutrition-sub-item">
314
- <span class="left">Saturated Fat {nutrition_data['saturated_fat']}</span>
315
- <span class="right">{calculate_daily_value(nutrition_data['saturated_fat'], 20)}%</span>
316
- </div>
317
- <div class="nutrition-sub-item">
318
- <span class="left"><em>Trans</em> Fat {nutrition_data['trans_fat']}</span>
319
- </div>
320
- <div class="nutrition-item">
321
- <span class="left"><strong>Cholesterol</strong> {nutrition_data['cholesterol']}</span>
322
- <span class="right">{calculate_daily_value(nutrition_data['cholesterol'], 300)}%</span>
323
- </div>
324
- <div class="nutrition-item">
325
- <span class="left"><strong>Sodium</strong> {nutrition_data['sodium']}</span>
326
- <span class="right">{calculate_daily_value(nutrition_data['sodium'], 2300)}%</span>
327
- </div>
328
- <div class="nutrition-item">
329
- <span class="left"><strong>Total Carbohydrate</strong> {nutrition_data['total_carbohydrate']}</span>
330
- <span class="right">{calculate_daily_value(nutrition_data['total_carbohydrate'], 275)}%</span>
331
- </div>
332
- <div class="nutrition-sub-item">
333
- <span class="left">Dietary Fiber {nutrition_data['dietary_fiber']}</span>
334
- <span class="right">{calculate_daily_value(nutrition_data['dietary_fiber'], 28)}%</span>
335
- </div>
336
- <div class="nutrition-sub-item">
337
- <span class="left">Total Sugars {nutrition_data['total_sugars']}</span>
338
- </div>
339
- <div class="nutrition-sub-item">
340
- <span class="left">Includes 0g Added Sugars</span>
341
- <span class="right">0%</span>
342
- </div>
343
- <div class="nutrition-item">
344
- <span class="left"><strong>Protein</strong> {nutrition_data['protein']}</span>
345
- <span class="right">{calculate_daily_value(nutrition_data['protein'], 50)}%</span>
346
- </div>
347
- <div class="nutrition-item">
348
- <span class="left">Vitamin D {nutrition_data['vitamin_d']}</span>
349
- <span class="right">{calculate_daily_value(nutrition_data['vitamin_d'], 20)}%</span>
350
- </div>
351
- <div class="nutrition-item">
352
- <span class="left">Calcium {nutrition_data['calcium']}</span>
353
- <span class="right">{calculate_daily_value(nutrition_data['calcium'], 1300)}%</span>
354
- </div>
355
- <div class="nutrition-item">
356
- <span class="left">Iron {nutrition_data['iron']}</span>
357
- <span class="right">{calculate_daily_value(nutrition_data['iron'], 18)}%</span>
358
- </div>
359
- <div class="nutrition-item">
360
- <span class="left">Potassium {nutrition_data['potassium']}</span>
361
- <span class="right">{calculate_daily_value(nutrition_data['potassium'], 4700)}%</span>
362
- </div>
363
- <p class="nutrition-footnote">* The % Daily Value (DV) tells you how much a nutrient in a serving of food contributes to a daily diet. 2,000 calories a day is used for general nutrition advice.</p>
364
- </div>
365
- """
366
- return nutrition_label
367
-
368
- def calculate_daily_value(value_str: str, daily_value: int) -> int:
369
- """
370
- Calculate the percentage of daily value for a nutrient.
371
- """
372
- try:
373
- # Extract numeric value using regex
374
- numeric_value = re.search(r'\d+\.?\d*', value_str)
375
- if numeric_value:
376
- value = float(numeric_value.group())
377
- return round((value / daily_value) * 100)
378
- else:
379
- return 0
380
- except Exception as e:
381
- logger.error(f"Error calculating daily value: {e}")
382
- return 0
383
-
384
- def estimate_cost(ingredients: str) -> str:
385
- """
386
- Estimates the cost of the ingredients.
387
- """
388
- if not ingredients.strip():
389
- return "Ingredients not provided."
390
-
391
- prompt = f"""You are a personal shopper and restaurant logistician. Estimate the total cost to produce the dish using the following ingredients. Provide a succint summary breakdown of costs for the recipe in question.
392
-
393
- Ingredients:
394
- {ingredients}
395
-
396
- Assume average market prices.
397
- """
398
-
399
- messages = [
400
- {"role": "user", "content": prompt}
401
- ]
402
-
403
- cost = groq_api_call(
404
- model="llama-3.1-8b-instant",
405
- messages=messages,
406
- max_tokens=MAX_TOKENS['cost']
407
- )
408
- return cost
409
-
410
- def create_menu_description(recipe: str) -> str:
411
- """
412
- Creates an enticing menu description based on the recipe.
413
- """
414
- prompt = f"""You are a restauranteur and influencer; Based on the following recipe, create an enticing menu description suitable for a restaurant.
415
-
416
- Recipe:
417
- {recipe}
418
-
419
- The description should be 2-3 sentences.
420
- """
421
-
422
- messages = [
423
- {"role": "user", "content": prompt}
424
- ]
425
-
426
- menu_description = groq_api_call(
427
- model="llama-3.1-8b-instant",
428
- messages=messages,
429
- max_tokens=MAX_TOKENS['menu']
430
- )
431
- return menu_description
432
-
433
- def process_image_and_generate_recipe(image: Optional[Image.Image]) -> Tuple[str, str, str, str, str, str, str]:
434
- """
435
- Main function that processes the image and generates all outputs.
436
- """
437
- if image is None:
438
- return ("No image uploaded.", "", "", "", "", "", "")
439
- try:
440
- logger.info("Starting the recipe generation process.")
441
- base64_image = process_and_encode_image(image)
442
- analysis = analyze_image(base64_image)
443
- recipe_markdown = generate_recipe(analysis)
444
- logger.info(f"Generated Recipe:\n{recipe_markdown}")
445
- recipe_parts = parse_recipe(recipe_markdown)
446
- ingredients = recipe_parts.get('ingredients', '')
447
- logger.info(f"Extracted Ingredients:\n{ingredients}")
448
-
449
- try:
450
- nutrition = get_nutritional_info(ingredients)
451
- except Exception as e:
452
- logger.error(f"Error getting nutritional info: {e}")
453
- nutrition = "Error retrieving nutritional information."
454
-
455
- try:
456
- cost = estimate_cost(ingredients)
457
- except Exception as e:
458
- logger.error(f"Error estimating cost: {e}")
459
- cost = "Error estimating cost."
460
-
461
- menu_description = recipe_parts.get('menu_description', '')
462
- instructions = recipe_parts.get('instructions', '')
463
-
464
- return (
465
- analysis,
466
- recipe_markdown,
467
- nutrition,
468
- cost,
469
- menu_description,
470
- ingredients,
471
- instructions
472
- )
473
- except Exception as e:
474
- error_message = f"An error occurred: {e}"
475
- logger.error(error_message)
476
- return (error_message, "", "", "", "", "", "")
477
-
478
- # Custom CSS for nutrition label only
479
- custom_css = """
480
- .nutrition-label {
481
- border: 2px solid #333;
482
- padding: 15px;
483
- width: 100%;
484
- max-width: 400px;
485
- font-family: 'Arial', sans-serif;
486
- background-color: white;
487
- box-sizing: border-box;
488
- color: black !important;
489
- margin: 0 auto;
490
- }
491
- .nutrition-label * {
492
- color: black !important;
493
- }
494
- .nutrition-title {
495
- font-size: 24px;
496
- font-weight: bold;
497
- margin-bottom: 10px;
498
- border-bottom: 10px solid #333;
499
- padding-bottom: 10px;
500
- }
501
- .nutrition-serving {
502
- font-size: 14px;
503
- font-weight: bold;
504
- margin-bottom: 15px;
505
- border-bottom: 1px solid #333;
506
- padding-bottom: 10px;
507
- }
508
- .nutrition-calories {
509
- font-size: 32px;
510
- font-weight: bold;
511
- border-bottom: 10px solid #333;
512
- padding: 10px 0;
513
- margin-bottom: 10px;
514
- }
515
- .nutrition-percent {
516
- font-size: 14px;
517
- font-weight: bold;
518
- text-align: right;
519
- border-bottom: 1px solid #333;
520
- padding-bottom: 5px;
521
- margin-bottom: 10px;
522
- }
523
- .nutrition-item, .nutrition-sub-item {
524
- font-size: 14px;
525
- border-bottom: 1px solid #333;
526
- padding: 5px 0;
527
- display: flex;
528
- justify-content: space-between;
529
- flex-wrap: wrap;
530
- }
531
- .nutrition-item {
532
- font-weight: bold;
533
- }
534
- .nutrition-sub-item {
535
- padding-left: 20px;
536
- }
537
- .left {
538
- flex: 1;
539
- margin-right: 10px;
540
- }
541
- .right {
542
- text-align: right;
543
- }
544
- .nutrition-footnote {
545
- font-size: 10px;
546
- margin-top: 15px;
547
- line-height: 1.4;
548
- }
549
- """
550
-
551
- # Add this before the Gradio interface definition
552
- def validate_api_key(api_key: str) -> bool:
553
- """Validates Groq API key format"""
554
- if not api_key:
555
- return False
556
- return api_key.startswith("gsk_") and len(api_key) >= 40
557
-
558
- # Modify the process function to handle state
559
- def process_with_key(image: Optional[Image.Image],
560
- api_key: str,
561
- progress: gr.Progress = gr.Progress()) -> Tuple[str, str, str, str, str, str, str, str]:
562
- """Processes image with API key and shows progress"""
563
- if not validate_api_key(api_key):
564
- return ("Please enter a valid Groq API Key (starts with 'gsk_').", "", "", "", "", "", "", "❌ Invalid API key")
565
-
566
- try:
567
- client = Groq(api_key=api_key)
568
-
569
- if image is None:
570
- return ("No image uploaded.", "", "", "", "", "", "", "⚠️ No image provided")
571
-
572
- progress(0, desc="Processing image...")
573
- base64_image = process_and_encode_image(image)
574
-
575
- progress(0.2, desc="Analyzing image...")
576
- analysis = analyze_image(base64_image)
577
-
578
- progress(0.4, desc="Generating recipe...")
579
- recipe_markdown = generate_recipe(analysis)
580
- recipe_parts = parse_recipe(recipe_markdown)
581
- ingredients = recipe_parts.get('ingredients', '')
582
-
583
- progress(0.6, desc="Calculating nutrition...")
584
- try:
585
- nutrition = get_nutritional_info(ingredients)
586
- except Exception as e:
587
- logger.error(f"Error getting nutritional info: {e}")
588
- nutrition = "Error retrieving nutritional information."
589
-
590
- progress(0.8, desc="Estimating costs...")
591
- try:
592
- cost = estimate_cost(ingredients)
593
- except Exception as e:
594
- logger.error(f"Error estimating cost: {e}")
595
- cost = "Error estimating cost."
596
-
597
- menu_description = recipe_parts.get('menu_description', '')
598
- instructions = recipe_parts.get('instructions', '')
599
-
600
- progress(1.0, desc="Done!")
601
- return (
602
- analysis,
603
- recipe_markdown,
604
- nutrition,
605
- cost,
606
- menu_description or "No menu description available.",
607
- ingredients,
608
- instructions,
609
- "✅ Recipe generated successfully!"
610
- )
611
- except Exception as e:
612
- error_message = f"An error occurred: {e}"
613
- logger.error(error_message)
614
- return (error_message, "", "", "", "", "", "", "❌ Error occurred")
615
-
616
- # Modified Gradio interface
617
- with gr.Blocks(theme=gr.themes.Ocean(), css=custom_css) as demo:
618
- # Store API key state
619
- api_key_state = gr.State("")
620
-
621
- with gr.Row():
622
- gr.HTML("""
623
- <a href="https://www.smartbrandstrategies.com?utm_source=foodfiles&utm_medium=app&utm_campaign=recipe_generator" target="_blank" style="text-decoration: none;">
624
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 100" style="width: 350px; height: 50px;">
625
- <circle cx="50" cy="50" r="45" fill="#2DD4BF" opacity="0.2"/>
626
- <circle cx="50" cy="50" r="35" fill="#2DD4BF" opacity="0.4"/>
627
- <path d="M30 30 L30 70 M50 30 L50 70 M70 30 L70 70"
628
- stroke="#2DD4BF"
629
- stroke-width="8"
630
- stroke-linecap="round"/>
631
- <text x="100" y="65"
632
- font-family="Arial"
633
- font-size="48"
634
- font-weight="bold"
635
- fill="#2DD4BF">
636
- FoodFiles
637
- </text>
638
- <text x="330" y="65"
639
- font-family="Arial"
640
- font-size="20"
641
- fill="#2DD4BF">
642
- by SmartBrandStrategies.com
643
- </text>
644
- </svg>
645
- </a>
646
- """)
647
-
648
- # API Key section with Groq badge in same row
649
- with gr.Row():
650
- with gr.Column(scale=3):
651
- api_key_input = gr.Textbox(
652
- label="Enter your Groq API Key",
653
- type="password",
654
- placeholder="gsk_...",
655
- value=""
656
- )
657
- with gr.Column(scale=1):
658
- gr.HTML("""<a href="https://groq.com" target="_blank" rel="noopener noreferrer">
659
- <img src="https://groq.com/wp-content/uploads/2024/03/PBG-mark1-color.svg"
660
- alt="Powered by Groq for fast inference."
661
- style="margin: 10px 0; max-width: 200px;"/>
662
- </a>""")
663
-
664
- with gr.Row():
665
- gr.Markdown("""To get your API key:
666
- 1. Visit [Groq Console](https://console.groq.com)
667
- 2. Sign up or log in
668
- 3. Go to API Keys section
669
- 4. Create a new API key (don't share with anyone)
670
- 5. Upload an image of a dish to get an analysis and likely recipe and more!""")
671
-
672
- # Add loading indicator
673
- with gr.Row():
674
- status_message = gr.Markdown("")
675
-
676
- with gr.Row():
677
- with gr.Column(scale=1):
678
- image_input = gr.Image(type="pil", label="Upload Dish Image")
679
- submit_button = gr.Button("Generate Recipe", variant="primary")
680
- with gr.Column(scale=2):
681
- with gr.Tabs():
682
- with gr.TabItem("Image Analysis"):
683
- analysis_output = gr.Markdown(label="Image Analysis")
684
- with gr.TabItem("Full Recipe"):
685
- recipe_output = gr.Markdown(label="Full Recipe")
686
- with gr.TabItem("Nutritional Information"):
687
- nutrition_output = gr.HTML(label="Nutritional Information")
688
- with gr.TabItem("Cost Estimation"):
689
- cost_output = gr.Markdown(label="Cost Estimation")
690
- with gr.TabItem("Menu Description"):
691
- menu_output = gr.Markdown(label="Menu Description")
692
- with gr.TabItem("Ingredients"):
693
- ingredients_output = gr.Markdown(label="Ingredients")
694
- with gr.TabItem("Instructions"):
695
- instructions_output = gr.Markdown(label="Instructions")
696
-
697
- # Function to save API key to state
698
- def save_api_key(api_key):
699
- if validate_api_key(api_key):
700
- return "✅ API key saved", api_key
701
- return "❌ Invalid API key format", ""
702
-
703
- # Wire up the components
704
- api_key_input.change(
705
- fn=save_api_key,
706
- inputs=[api_key_input],
707
- outputs=[status_message, api_key_state]
708
- )
709
-
710
- submit_button.click(
711
- fn=process_with_key,
712
- inputs=[
713
- image_input,
714
- api_key_input
715
- ],
716
- outputs=[
717
- analysis_output,
718
- recipe_output,
719
- nutrition_output,
720
- cost_output,
721
- menu_output,
722
- ingredients_output,
723
- instructions_output,
724
- status_message
725
- ]
726
- )
727
-
728
- if __name__ == "__main__":
729
- demo.launch(server_name="127.0.0.1", server_port=7860, share=True, debug=True)
 
1
+ import os
2
+ import base64
3
+ import logging
4
+ import time
5
+ import random
6
+ from typing import Optional, Tuple
7
+ import json
8
+
9
+ import gradio as gr
10
+ from groq import Groq
11
+ from PIL import Image
12
+ import re
13
+ import tempfile
14
+ from functools import partial
15
+ import time
16
+
17
+ # Configure logging
18
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Ensure GROQ_API_KEY is set
22
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
23
+ if not GROQ_API_KEY:
24
+ raise EnvironmentError("GROQ_API_KEY environment variable not set.")
25
+
26
+ # Initialize Groq client
27
+ client = Groq(api_key=GROQ_API_KEY)
28
+
29
+ # Constants
30
+ MAX_RETRIES = 5
31
+ IMAGE_RESIZE_FACTOR = 1
32
+ IMAGE_QUALITY = 85
33
+ MAX_TOKENS = {
34
+ 'analysis': 750,
35
+ 'recipe': 2000,
36
+ 'nutrition': 1000,
37
+ 'menu': 300,
38
+ 'cost': 1000
39
+ }
40
+
41
+ def process_and_encode_image(image: Image.Image) -> str:
42
+ """
43
+ Processes and encodes the image to base64 format suitable for API usage.
44
+ """
45
+ try:
46
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp_file:
47
+ temp_path = temp_file.name
48
+ image = image.convert('RGB')
49
+ image = image.resize(
50
+ (
51
+ int(image.width * IMAGE_RESIZE_FACTOR),
52
+ int(image.height * IMAGE_RESIZE_FACTOR)
53
+ )
54
+ )
55
+ image.save(temp_path, format="JPEG", optimize=True, quality=IMAGE_QUALITY)
56
+
57
+ with open(temp_path, "rb") as img_file:
58
+ base64_image = base64.b64encode(img_file.read()).decode('utf-8')
59
+ os.remove(temp_path)
60
+ logger.info("Image processed and encoded successfully.")
61
+ return base64_image
62
+ except Exception as e:
63
+ logger.error(f"Error processing image: {e}")
64
+ raise
65
+
66
+ def groq_api_call(model: str, messages: list, max_tokens: int, json_mode: bool = False) -> str:
67
+ """
68
+ Makes an API call to the Groq model with retry logic.
69
+ """
70
+ for attempt in range(MAX_RETRIES):
71
+ try:
72
+ response = client.chat.completions.create(
73
+ model=model,
74
+ messages=messages,
75
+ max_tokens=max_tokens,
76
+ response_format={"type": "json_object"} if json_mode else None
77
+ )
78
+ content = response.choices[0].message.content
79
+ logger.info(f"API call successful using model {model}.")
80
+ return content
81
+ except Exception as e:
82
+ logger.warning(f"API call failed on attempt {attempt + 1}: {e}")
83
+ if attempt < MAX_RETRIES - 1:
84
+ sleep_time = (2 ** attempt) + random.random()
85
+ logger.info(f"Retrying in {sleep_time:.2f} seconds...")
86
+ time.sleep(sleep_time)
87
+ else:
88
+ logger.error("Max retries reached. Failing the API call.")
89
+ raise
90
+
91
+ def analyze_image(base64_image: str) -> str:
92
+ """
93
+ Analyzes the image using the vision model and returns a detailed description.
94
+ """
95
+ prompt = "Our goal is to analyze images of food with as much detail as possible. Analyze the following image of a dish and provide a detailed description including ingredients, cooking techniques, presentation, textures, colors, and any unique elements. The analysis: "
96
+
97
+ messages = [
98
+ {
99
+ "role": "user",
100
+ "content": [
101
+ {"type": "text", "text": prompt},
102
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
103
+ ]
104
+ }
105
+ ]
106
+
107
+ analysis = groq_api_call(
108
+ model="llama-3.2-11b-vision-preview",
109
+ messages=messages,
110
+ max_tokens=MAX_TOKENS['analysis']
111
+ )
112
+ return analysis
113
+
114
+ def generate_recipe(analysis: str) -> str:
115
+ """
116
+ Generates a recipe based on the analysis.
117
+ """
118
+ prompt = f"""Based on the following analysis, generate a detailed recipe including:
119
+
120
+ 1. Recipe Name
121
+ 2. List of ingredients with approximate quantities
122
+ 3. Step-by-step instructions
123
+ 4. Variations or substitutions
124
+ 5. Serving suggestions
125
+ 6. Menu description suitable for a restaurant
126
+
127
+ **Format the recipe exactly as follows, using the same section headings and Markdown syntax:**
128
+
129
+ # **[Recipe Name]**
130
+
131
+ ## **Ingredients**
132
+ - [Ingredient 1]
133
+ - [Ingredient 2]
134
+ ...
135
+
136
+ ## **Instructions**
137
+ 1. [Step 1]
138
+ 2. [Step 2]
139
+ ...
140
+
141
+ ## **Variations**
142
+ - [Variation 1]
143
+ - [Variation 2]
144
+ ...
145
+
146
+ ## **Serving Suggestions**
147
+ - [Suggestion 1]
148
+ - [Suggestion 2]
149
+ ...
150
+
151
+ ## **Menu Description**
152
+ [Menu description here]
153
+
154
+ Analysis:
155
+ {analysis}
156
+ """
157
+
158
+ messages = [
159
+ {"role": "user", "content": prompt}
160
+ ]
161
+
162
+ recipe = groq_api_call(
163
+ model="mixtral-8x7b-32768",
164
+ messages=messages,
165
+ max_tokens=MAX_TOKENS['recipe']
166
+ )
167
+ return recipe
168
+
169
+ def parse_recipe(recipe_markdown: str) -> dict:
170
+ """
171
+ Parses the recipe Markdown into its components.
172
+ """
173
+ recipe_parts = {}
174
+
175
+ # Extract recipe name
176
+ recipe_name_match = re.search(r"# \*\*(.+?)\*\*", recipe_markdown)
177
+ recipe_parts['recipe_name'] = recipe_name_match.group(1).strip() if recipe_name_match else "Unknown Recipe"
178
+
179
+ # Sections to extract
180
+ sections = ['Ingredients', 'Instructions', 'Variations', 'Serving Suggestions', 'Menu Description']
181
+
182
+ for i, section in enumerate(sections):
183
+ pattern = rf"## \*\*{section}\*\*\n(.*?)(?=## \*\*{'|'.join(sections[i+1:])}\*\*|$)"
184
+ match = re.search(pattern, recipe_markdown, re.DOTALL)
185
+ recipe_parts[section.lower().replace(' ', '_')] = match.group(1).strip() if match else ""
186
+ return recipe_parts
187
+
188
+ def get_nutritional_info(ingredients: str) -> str:
189
+ """
190
+ Provides nutritional analysis for the given ingredients in a nutrition label format.
191
+ """
192
+ if not ingredients.strip():
193
+ return "Ingredients not provided."
194
+
195
+ prompt = f"""You are a nutrition expert API that provides nutritional analysis for given ingredients. Respond with a JSON object containing the nutritional information for the entire dish.
196
+
197
+ Analyze the following ingredients and provide nutritional information for the complete dish:
198
+
199
+ {ingredients}
200
+
201
+ The JSON response should have the following structure:
202
+ {{
203
+ "serving_size": "X g",
204
+ "calories": int,
205
+ "total_fat": "X g",
206
+ "saturated_fat": "X g",
207
+ "trans_fat": "X g",
208
+ "cholesterol": "X mg",
209
+ "sodium": "X mg",
210
+ "total_carbohydrate": "X g",
211
+ "dietary_fiber": "X g",
212
+ "total_sugars": "X g",
213
+ "protein": "X g",
214
+ "vitamin_d": "X mcg",
215
+ "calcium": "X mg",
216
+ "iron": "X mg",
217
+ "potassium": "X mg"
218
+ }}
219
+
220
+ Provide a single set of nutritional information for the entire dish, not individual ingredients. If you cannot provide exact values, use reasonable estimates based on similar dishes. Ensure all fields are present in the response, using "0" or "0 g" for any nutrients that are not applicable or present in negligible amounts.
221
+ """
222
+
223
+ messages = [
224
+ {"role": "system", "content": "You are a nutrition expert API that responds in JSON format."},
225
+ {"role": "user", "content": prompt}
226
+ ]
227
+
228
+ try:
229
+ nutrition_response = groq_api_call(
230
+ model="llama3-8b-8192",
231
+ messages=messages,
232
+ max_tokens=MAX_TOKENS['nutrition'],
233
+ json_mode=True
234
+ )
235
+
236
+ nutrition_data = json.loads(nutrition_response)
237
+ logger.info("Successfully parsed nutrition data JSON.")
238
+
239
+ # Ensure we have a single set of nutritional information
240
+ if isinstance(nutrition_data, dict) and 'serving_size' in nutrition_data:
241
+ nutrition_data = {'dish': nutrition_data}
242
+ elif not any('serving_size' in item for item in nutrition_data.values()):
243
+ raise ValueError("Invalid nutrition data format")
244
+
245
+ # Combine multiple ingredients if necessary
246
+ combined_nutrition = combine_nutrition_data(nutrition_data)
247
+
248
+ nutrition_label = generate_nutrition_label(combined_nutrition)
249
+ return nutrition_label
250
+ except Exception as e:
251
+ logger.error(f"Error in get_nutritional_info: {e}")
252
+ return f"Error parsing nutritional information: {e}"
253
+
254
+ def combine_nutrition_data(nutrition_data: dict) -> dict:
255
+ """
256
+ Combines nutritional information from multiple ingredients into a single set of data.
257
+ """
258
+ combined = {
259
+ "serving_size": "1 serving",
260
+ "calories": 0,
261
+ "total_fat": 0,
262
+ "saturated_fat": 0,
263
+ "trans_fat": 0,
264
+ "cholesterol": 0,
265
+ "sodium": 0,
266
+ "total_carbohydrate": 0,
267
+ "dietary_fiber": 0,
268
+ "total_sugars": 0,
269
+ "protein": 0,
270
+ "vitamin_d": 0,
271
+ "calcium": 0,
272
+ "iron": 0,
273
+ "potassium": 0
274
+ }
275
+
276
+ for item in nutrition_data.values():
277
+ for key, value in item.items():
278
+ if key == "serving_size":
279
+ continue
280
+ if isinstance(value, str):
281
+ # Remove 'g' or other units before conversion
282
+ value = float(re.sub(r'[^\d.]+', '', value))
283
+ combined[key] += value
284
+
285
+ # Convert back to strings with units
286
+ for key, value in combined.items():
287
+ if key == "calories":
288
+ combined[key] = int(value)
289
+ elif key != "serving_size":
290
+ unit = next((item[key].split()[-1] for item in nutrition_data.values() if key in item), "g")
291
+ combined[key] = f"{value:.1f} {unit}"
292
+
293
+ return combined
294
+
295
+ def generate_nutrition_label(nutrition_data: dict) -> str:
296
+ """
297
+ Generates an HTML nutrition label from the given nutritional data.
298
+ """
299
+ nutrition_label = f"""
300
+ <div class="nutrition-label">
301
+ <p class="nutrition-footnote">Please Note that these are estimations.</p
302
+ <h2 class="nutrition-title">Nutrition Facts</h2>
303
+ <p class="nutrition-serving">Serving Size {nutrition_data['serving_size']}</p>
304
+ <div class="nutrition-calories">
305
+ <span class="left">Calories</span>
306
+ <span class="right">{nutrition_data['calories']}</span>
307
+ </div>
308
+ <div class="nutrition-percent">% Daily Value*</div>
309
+ <div class="nutrition-item">
310
+ <span class="left"><strong>Total Fat</strong> {nutrition_data['total_fat']}</span>
311
+ <span class="right">{calculate_daily_value(nutrition_data['total_fat'], 78)}%</span>
312
+ </div>
313
+ <div class="nutrition-sub-item">
314
+ <span class="left">Saturated Fat {nutrition_data['saturated_fat']}</span>
315
+ <span class="right">{calculate_daily_value(nutrition_data['saturated_fat'], 20)}%</span>
316
+ </div>
317
+ <div class="nutrition-sub-item">
318
+ <span class="left"><em>Trans</em> Fat {nutrition_data['trans_fat']}</span>
319
+ </div>
320
+ <div class="nutrition-item">
321
+ <span class="left"><strong>Cholesterol</strong> {nutrition_data['cholesterol']}</span>
322
+ <span class="right">{calculate_daily_value(nutrition_data['cholesterol'], 300)}%</span>
323
+ </div>
324
+ <div class="nutrition-item">
325
+ <span class="left"><strong>Sodium</strong> {nutrition_data['sodium']}</span>
326
+ <span class="right">{calculate_daily_value(nutrition_data['sodium'], 2300)}%</span>
327
+ </div>
328
+ <div class="nutrition-item">
329
+ <span class="left"><strong>Total Carbohydrate</strong> {nutrition_data['total_carbohydrate']}</span>
330
+ <span class="right">{calculate_daily_value(nutrition_data['total_carbohydrate'], 275)}%</span>
331
+ </div>
332
+ <div class="nutrition-sub-item">
333
+ <span class="left">Dietary Fiber {nutrition_data['dietary_fiber']}</span>
334
+ <span class="right">{calculate_daily_value(nutrition_data['dietary_fiber'], 28)}%</span>
335
+ </div>
336
+ <div class="nutrition-sub-item">
337
+ <span class="left">Total Sugars {nutrition_data['total_sugars']}</span>
338
+ </div>
339
+ <div class="nutrition-sub-item">
340
+ <span class="left">Includes 0g Added Sugars</span>
341
+ <span class="right">0%</span>
342
+ </div>
343
+ <div class="nutrition-item">
344
+ <span class="left"><strong>Protein</strong> {nutrition_data['protein']}</span>
345
+ <span class="right">{calculate_daily_value(nutrition_data['protein'], 50)}%</span>
346
+ </div>
347
+ <div class="nutrition-item">
348
+ <span class="left">Vitamin D {nutrition_data['vitamin_d']}</span>
349
+ <span class="right">{calculate_daily_value(nutrition_data['vitamin_d'], 20)}%</span>
350
+ </div>
351
+ <div class="nutrition-item">
352
+ <span class="left">Calcium {nutrition_data['calcium']}</span>
353
+ <span class="right">{calculate_daily_value(nutrition_data['calcium'], 1300)}%</span>
354
+ </div>
355
+ <div class="nutrition-item">
356
+ <span class="left">Iron {nutrition_data['iron']}</span>
357
+ <span class="right">{calculate_daily_value(nutrition_data['iron'], 18)}%</span>
358
+ </div>
359
+ <div class="nutrition-item">
360
+ <span class="left">Potassium {nutrition_data['potassium']}</span>
361
+ <span class="right">{calculate_daily_value(nutrition_data['potassium'], 4700)}%</span>
362
+ </div>
363
+ <p class="nutrition-footnote">* The % Daily Value (DV) tells you how much a nutrient in a serving of food contributes to a daily diet. 2,000 calories a day is used for general nutrition advice.</p>
364
+ </div>
365
+ """
366
+ return nutrition_label
367
+
368
+ def calculate_daily_value(value_str: str, daily_value: int) -> int:
369
+ """
370
+ Calculate the percentage of daily value for a nutrient.
371
+ """
372
+ try:
373
+ # Extract numeric value using regex
374
+ numeric_value = re.search(r'\d+\.?\d*', value_str)
375
+ if numeric_value:
376
+ value = float(numeric_value.group())
377
+ return round((value / daily_value) * 100)
378
+ else:
379
+ return 0
380
+ except Exception as e:
381
+ logger.error(f"Error calculating daily value: {e}")
382
+ return 0
383
+
384
+ def estimate_cost(ingredients: str) -> str:
385
+ """
386
+ Estimates the cost of the ingredients.
387
+ """
388
+ if not ingredients.strip():
389
+ return "Ingredients not provided."
390
+
391
+ prompt = f"""You are a personal shopper and restaurant logistician. Estimate the total cost to produce the dish using the following ingredients. Provide a succint summary breakdown of costs for the recipe in question.
392
+
393
+ Ingredients:
394
+ {ingredients}
395
+
396
+ Assume average market prices.
397
+ """
398
+
399
+ messages = [
400
+ {"role": "user", "content": prompt}
401
+ ]
402
+
403
+ cost = groq_api_call(
404
+ model="llama-3.1-8b-instant",
405
+ messages=messages,
406
+ max_tokens=MAX_TOKENS['cost']
407
+ )
408
+ return cost
409
+
410
+ def create_menu_description(recipe: str) -> str:
411
+ """
412
+ Creates an enticing menu description based on the recipe.
413
+ """
414
+ prompt = f"""You are a restauranteur and influencer; Based on the following recipe, create an enticing menu description suitable for a restaurant.
415
+
416
+ Recipe:
417
+ {recipe}
418
+
419
+ The description should be 2-3 sentences.
420
+ """
421
+
422
+ messages = [
423
+ {"role": "user", "content": prompt}
424
+ ]
425
+
426
+ menu_description = groq_api_call(
427
+ model="llama-3.1-8b-instant",
428
+ messages=messages,
429
+ max_tokens=MAX_TOKENS['menu']
430
+ )
431
+ return menu_description
432
+
433
+ def process_image_and_generate_recipe(image: Optional[Image.Image]) -> Tuple[str, str, str, str, str, str, str]:
434
+ """
435
+ Main function that processes the image and generates all outputs.
436
+ """
437
+ if image is None:
438
+ return ("No image uploaded.", "", "", "", "", "", "")
439
+ try:
440
+ logger.info("Starting the recipe generation process.")
441
+ base64_image = process_and_encode_image(image)
442
+ analysis = analyze_image(base64_image)
443
+ recipe_markdown = generate_recipe(analysis)
444
+ logger.info(f"Generated Recipe:\n{recipe_markdown}")
445
+ recipe_parts = parse_recipe(recipe_markdown)
446
+ ingredients = recipe_parts.get('ingredients', '')
447
+ logger.info(f"Extracted Ingredients:\n{ingredients}")
448
+
449
+ try:
450
+ nutrition = get_nutritional_info(ingredients)
451
+ except Exception as e:
452
+ logger.error(f"Error getting nutritional info: {e}")
453
+ nutrition = "Error retrieving nutritional information."
454
+
455
+ try:
456
+ cost = estimate_cost(ingredients)
457
+ except Exception as e:
458
+ logger.error(f"Error estimating cost: {e}")
459
+ cost = "Error estimating cost."
460
+
461
+ menu_description = recipe_parts.get('menu_description', '')
462
+ instructions = recipe_parts.get('instructions', '')
463
+
464
+ return (
465
+ analysis,
466
+ recipe_markdown,
467
+ nutrition,
468
+ cost,
469
+ menu_description,
470
+ ingredients,
471
+ instructions
472
+ )
473
+ except Exception as e:
474
+ error_message = f"An error occurred: {e}"
475
+ logger.error(error_message)
476
+ return (error_message, "", "", "", "", "", "")
477
+
478
+ # Custom CSS for nutrition label only
479
+ custom_css = """
480
+ .nutrition-label {
481
+ border: 2px solid #333;
482
+ padding: 15px;
483
+ width: 100%;
484
+ max-width: 400px;
485
+ font-family: 'Arial', sans-serif;
486
+ background-color: white;
487
+ box-sizing: border-box;
488
+ color: black !important;
489
+ margin: 0 auto;
490
+ }
491
+ .nutrition-label * {
492
+ color: black !important;
493
+ }
494
+ .nutrition-title {
495
+ font-size: 24px;
496
+ font-weight: bold;
497
+ margin-bottom: 10px;
498
+ border-bottom: 10px solid #333;
499
+ padding-bottom: 10px;
500
+ }
501
+ .nutrition-serving {
502
+ font-size: 14px;
503
+ font-weight: bold;
504
+ margin-bottom: 15px;
505
+ border-bottom: 1px solid #333;
506
+ padding-bottom: 10px;
507
+ }
508
+ .nutrition-calories {
509
+ font-size: 32px;
510
+ font-weight: bold;
511
+ border-bottom: 10px solid #333;
512
+ padding: 10px 0;
513
+ margin-bottom: 10px;
514
+ }
515
+ .nutrition-percent {
516
+ font-size: 14px;
517
+ font-weight: bold;
518
+ text-align: right;
519
+ border-bottom: 1px solid #333;
520
+ padding-bottom: 5px;
521
+ margin-bottom: 10px;
522
+ }
523
+ .nutrition-item, .nutrition-sub-item {
524
+ font-size: 14px;
525
+ border-bottom: 1px solid #333;
526
+ padding: 5px 0;
527
+ display: flex;
528
+ justify-content: space-between;
529
+ flex-wrap: wrap;
530
+ }
531
+ .nutrition-item {
532
+ font-weight: bold;
533
+ }
534
+ .nutrition-sub-item {
535
+ padding-left: 20px;
536
+ }
537
+ .left {
538
+ flex: 1;
539
+ margin-right: 10px;
540
+ }
541
+ .right {
542
+ text-align: right;
543
+ }
544
+ .nutrition-footnote {
545
+ font-size: 10px;
546
+ margin-top: 15px;
547
+ line-height: 1.4;
548
+ }
549
+ """
550
+
551
+ # Add this before the Gradio interface definition
552
+ def validate_api_key(api_key: str) -> bool:
553
+ """Validates Groq API key format"""
554
+ if not api_key:
555
+ return False
556
+ return api_key.startswith("gsk_") and len(api_key) >= 40
557
+
558
+ # Modify the process function to handle state
559
+ def process_with_key(image: Optional[Image.Image],
560
+ api_key: str,
561
+ progress: gr.Progress = gr.Progress()) -> Tuple[str, str, str, str, str, str, str, str]:
562
+ """Processes image with API key and shows progress"""
563
+ if not validate_api_key(api_key):
564
+ return ("Please enter a valid Groq API Key (starts with 'gsk_').", "", "", "", "", "", "", "❌ Invalid API key")
565
+
566
+ try:
567
+ client = Groq(api_key=api_key)
568
+
569
+ if image is None:
570
+ return ("No image uploaded.", "", "", "", "", "", "", "⚠️ No image provided")
571
+
572
+ progress(0, desc="Processing image...")
573
+ base64_image = process_and_encode_image(image)
574
+
575
+ progress(0.2, desc="Analyzing image...")
576
+ analysis = analyze_image(base64_image)
577
+
578
+ progress(0.4, desc="Generating recipe...")
579
+ recipe_markdown = generate_recipe(analysis)
580
+ recipe_parts = parse_recipe(recipe_markdown)
581
+ ingredients = recipe_parts.get('ingredients', '')
582
+
583
+ progress(0.6, desc="Calculating nutrition...")
584
+ try:
585
+ nutrition = get_nutritional_info(ingredients)
586
+ except Exception as e:
587
+ logger.error(f"Error getting nutritional info: {e}")
588
+ nutrition = "Error retrieving nutritional information."
589
+
590
+ progress(0.8, desc="Estimating costs...")
591
+ try:
592
+ cost = estimate_cost(ingredients)
593
+ except Exception as e:
594
+ logger.error(f"Error estimating cost: {e}")
595
+ cost = "Error estimating cost."
596
+
597
+ menu_description = recipe_parts.get('menu_description', '')
598
+ instructions = recipe_parts.get('instructions', '')
599
+
600
+ progress(1.0, desc="Done!")
601
+ return (
602
+ analysis,
603
+ recipe_markdown,
604
+ nutrition,
605
+ cost,
606
+ menu_description or "No menu description available.",
607
+ ingredients,
608
+ instructions,
609
+ "✅ Recipe generated successfully!"
610
+ )
611
+ except Exception as e:
612
+ error_message = f"An error occurred: {e}"
613
+ logger.error(error_message)
614
+ return (error_message, "", "", "", "", "", "", "❌ Error occurred")
615
+
616
+ # Modified Gradio interface
617
+ with gr.Blocks(theme=gr.themes.Ocean(), css=custom_css) as demo:
618
+ # Store API key state
619
+ api_key_state = gr.State("")
620
+
621
+ with gr.Row():
622
+ gr.HTML("""
623
+ <a href="https://www.smartbrandstrategies.com?utm_source=foodfiles&utm_medium=app&utm_campaign=recipe_generator" target="_blank" style="text-decoration: none;">
624
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 100" style="width: 350px; height: 50px;">
625
+ <circle cx="50" cy="50" r="45" fill="#2DD4BF" opacity="0.2"/>
626
+ <circle cx="50" cy="50" r="35" fill="#2DD4BF" opacity="0.4"/>
627
+ <path d="M30 30 L30 70 M50 30 L50 70 M70 30 L70 70"
628
+ stroke="#2DD4BF"
629
+ stroke-width="8"
630
+ stroke-linecap="round"/>
631
+ <text x="100" y="65"
632
+ font-family="Arial"
633
+ font-size="48"
634
+ font-weight="bold"
635
+ fill="#2DD4BF">
636
+ FoodFiles
637
+ </text>
638
+ <text x="330" y="65"
639
+ font-family="Arial"
640
+ font-size="20"
641
+ fill="#2DD4BF">
642
+ by SmartBrandStrategies.com
643
+ </text>
644
+ </svg>
645
+ </a>
646
+ """)
647
+
648
+ # API Key section with Groq badge in same row
649
+ with gr.Row():
650
+ with gr.Column(scale=3):
651
+ api_key_input = gr.Textbox(
652
+ label="Enter your Groq API Key",
653
+ type="password",
654
+ placeholder="gsk_...",
655
+ value=""
656
+ )
657
+ with gr.Column(scale=1):
658
+ gr.HTML("""<a href="https://groq.com" target="_blank" rel="noopener noreferrer">
659
+ <img src="https://groq.com/wp-content/uploads/2024/03/PBG-mark1-color.svg"
660
+ alt="Powered by Groq for fast inference."
661
+ style="margin: 10px 0; max-width: 200px;"/>
662
+ </a>""")
663
+
664
+ with gr.Row():
665
+ gr.Markdown("""To get your API key:
666
+ 1. Visit [Groq Console](https://console.groq.com)
667
+ 2. Sign up or log in
668
+ 3. Go to API Keys section
669
+ 4. Create a new API key (don't share with anyone)
670
+ 5. Upload an image of a dish to get an analysis and likely recipe and more!""")
671
+
672
+ # Add loading indicator
673
+ with gr.Row():
674
+ status_message = gr.Markdown("")
675
+
676
+ with gr.Row():
677
+ with gr.Column(scale=1):
678
+ image_input = gr.Image(type="pil", label="Upload Dish Image")
679
+ submit_button = gr.Button("Generate Recipe", variant="primary")
680
+ with gr.Column(scale=2):
681
+ with gr.Tabs():
682
+ with gr.TabItem("Image Analysis"):
683
+ analysis_output = gr.Markdown(label="Image Analysis")
684
+ with gr.TabItem("Full Recipe"):
685
+ recipe_output = gr.Markdown(label="Full Recipe")
686
+ with gr.TabItem("Nutritional Information"):
687
+ nutrition_output = gr.HTML(label="Nutritional Information")
688
+ with gr.TabItem("Cost Estimation"):
689
+ cost_output = gr.Markdown(label="Cost Estimation")
690
+ with gr.TabItem("Menu Description"):
691
+ menu_output = gr.Markdown(label="Menu Description")
692
+ with gr.TabItem("Ingredients"):
693
+ ingredients_output = gr.Markdown(label="Ingredients")
694
+ with gr.TabItem("Instructions"):
695
+ instructions_output = gr.Markdown(label="Instructions")
696
+
697
+ # Function to save API key to state
698
+ def save_api_key(api_key):
699
+ if validate_api_key(api_key):
700
+ return "✅ API key saved", api_key
701
+ return "❌ Invalid API key format", ""
702
+
703
+ # Wire up the components
704
+ api_key_input.change(
705
+ fn=save_api_key,
706
+ inputs=[api_key_input],
707
+ outputs=[status_message, api_key_state]
708
+ )
709
+
710
+ submit_button.click(
711
+ fn=process_with_key,
712
+ inputs=[
713
+ image_input,
714
+ api_key_input
715
+ ],
716
+ outputs=[
717
+ analysis_output,
718
+ recipe_output,
719
+ nutrition_output,
720
+ cost_output,
721
+ menu_output,
722
+ ingredients_output,
723
+ instructions_output,
724
+ status_message
725
+ ]
726
+ )
727
+
728
+ if __name__ == "__main__":
729
+ demo.launch(server_name="127.0.0.1", server_port=7860, share=True, debug=True)