shukdevdatta123 commited on
Commit
6ff0a34
·
verified ·
1 Parent(s): 5e321e2

Create v3.txt

Browse files
Files changed (1) hide show
  1. v3.txt +887 -0
v3.txt ADDED
@@ -0,0 +1,887 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import base64
3
+ import requests
4
+ import io
5
+ from PIL import Image
6
+ import json
7
+ import os
8
+ from together import Together
9
+ import tempfile
10
+ import uuid
11
+ import time
12
+
13
+ def encode_image_to_base64(image_path):
14
+ """Convert image to base64 encoding"""
15
+ with open(image_path, "rb") as image_file:
16
+ return base64.b64encode(image_file.read()).decode('utf-8')
17
+
18
+ def analyze_single_image(client, img_path):
19
+ """Analyze a single image to identify ingredients"""
20
+ system_prompt = """You are a culinary expert AI assistant that specializes in identifying ingredients in images.
21
+ Your task is to analyze the provided image and list all the food ingredients you can identify.
22
+ Be specific and detailed about what you see. Only list ingredients, don't suggest recipes yet."""
23
+
24
+ user_prompt = "Please identify all the food ingredients visible in this image. List each ingredient on a new line."
25
+
26
+ try:
27
+ with open(img_path, "rb") as image_file:
28
+ base64_image = base64.b64encode(image_file.read()).decode('utf-8')
29
+
30
+ content = [
31
+ {"type": "text", "text": user_prompt},
32
+ {
33
+ "type": "image_url",
34
+ "image_url": {
35
+ "url": f"data:image/jpeg;base64,{base64_image}"
36
+ }
37
+ }
38
+ ]
39
+
40
+ response = client.chat.completions.create(
41
+ model="meta-llama/Llama-Vision-Free",
42
+ messages=[
43
+ {"role": "system", "content": system_prompt},
44
+ {"role": "user", "content": content}
45
+ ],
46
+ max_tokens=500,
47
+ temperature=0.2
48
+ )
49
+
50
+ return response.choices[0].message.content
51
+ except Exception as e:
52
+ return f"Error analyzing image: {str(e)}"
53
+
54
+ def format_ingredients_html(all_ingredients):
55
+ """Format the identified ingredients in HTML"""
56
+ html_content = """
57
+ <div class="ingredients-identified">
58
+ <h2>📋 Ingredients Identified</h2>
59
+ """
60
+
61
+ for i, ingredients in enumerate(all_ingredients):
62
+ html_content += f"""
63
+ <div class="ingredient-group">
64
+ <h3>Image {i+1} Ingredients</h3>
65
+ <ul class="ingredient-list">
66
+ """
67
+
68
+ # Split ingredients by new line and create list items
69
+ ingredient_lines = ingredients.strip().split('\n')
70
+ for line in ingredient_lines:
71
+ if line.strip():
72
+ html_content += f"<li>{line.strip()}</li>\n"
73
+
74
+ html_content += """
75
+ </ul>
76
+ </div>
77
+ """
78
+
79
+ html_content += "</div>"
80
+ return html_content
81
+
82
+ def format_recipe_to_html(recipe_text):
83
+ """Convert the recipe text to formatted HTML"""
84
+ # Initialize the HTML content with the recipe suggestions header
85
+ html_content = """
86
+ <div class="recipe-suggestions">
87
+ <h2>🍽️ Recipe Suggestions</h2>
88
+ """
89
+
90
+ # Split the text by recipe (assume recipes are separated by a recipe name heading)
91
+ recipe_sections = []
92
+ current_recipe = ""
93
+ lines = recipe_text.split('\n')
94
+
95
+ # Process lines to identify recipe sections
96
+ for line in lines:
97
+ if line.strip().startswith(("Recipe ", "# ", "## ", "### ")):
98
+ # If we've collected some content for a recipe, save it
99
+ if current_recipe:
100
+ recipe_sections.append(current_recipe)
101
+ current_recipe = ""
102
+
103
+ # Add line to current recipe
104
+ current_recipe += line + "\n"
105
+
106
+ # Add the last recipe if exists
107
+ if current_recipe:
108
+ recipe_sections.append(current_recipe)
109
+
110
+ # Process each recipe section into HTML
111
+ for recipe in recipe_sections:
112
+ if not recipe.strip():
113
+ continue
114
+
115
+ html_content += '<div class="recipe-card">'
116
+
117
+ lines = recipe.split('\n')
118
+ in_ingredients = False
119
+ in_instructions = False
120
+
121
+ for line in lines:
122
+ # Handle recipe title
123
+ if any(x in line.lower() for x in ["recipe ", "# recipe"]) or line.strip().startswith(("# ", "## ")):
124
+ title = line.replace("#", "").replace("Recipe:", "").replace("Recipe", "").strip()
125
+ html_content += f'<h3 class="recipe-title">{title}</h3>\n'
126
+ continue
127
+
128
+ # Handle description
129
+ if "description" in line.lower() and ":" in line:
130
+ description = line.split(":", 1)[1].strip()
131
+ html_content += f'<p class="recipe-description">{description}</p>\n'
132
+ continue
133
+
134
+ # Start ingredients section
135
+ if "ingredients" in line.lower() and not in_ingredients:
136
+ in_ingredients = True
137
+ in_instructions = False
138
+ html_content += '<div class="recipe-ingredients">\n'
139
+ html_content += '<h4>Ingredients</h4>\n<ul>\n'
140
+ continue
141
+
142
+ # Start instructions section
143
+ if any(x in line.lower() for x in ["instructions", "directions", "steps", "preparation"]) and not in_instructions:
144
+ if in_ingredients:
145
+ html_content += '</ul>\n</div>\n'
146
+ in_ingredients = False
147
+
148
+ in_instructions = True
149
+ html_content += '<div class="recipe-instructions">\n'
150
+ html_content += '<h4>Instructions</h4>\n<ol>\n'
151
+ continue
152
+
153
+ # Handle cooking time
154
+ if "cooking time" in line.lower() or "prep time" in line.lower() or "time" in line.lower():
155
+ if in_ingredients:
156
+ html_content += '</ul>\n</div>\n'
157
+ in_ingredients = False
158
+ if in_instructions:
159
+ html_content += '</ol>\n</div>\n'
160
+ in_instructions = False
161
+
162
+ time_info = line.strip()
163
+ html_content += f'<p class="recipe-time"><strong>⏱️ {time_info}</strong></p>\n'
164
+ continue
165
+
166
+ # Handle difficulty level
167
+ if "difficulty" in line.lower():
168
+ difficulty = line.strip()
169
+ html_content += f'<p class="recipe-difficulty"><strong>🔍 {difficulty}</strong></p>\n'
170
+ continue
171
+
172
+ # Handle nutritional info
173
+ if "nutritional" in line.lower():
174
+ if in_ingredients:
175
+ html_content += '</ul>\n</div>\n'
176
+ in_ingredients = False
177
+ if in_instructions:
178
+ html_content += '</ol>\n</div>\n'
179
+ in_instructions = False
180
+
181
+ html_content += '<div class="recipe-nutrition">\n'
182
+ html_content += f'<h4>{line.strip()}</h4>\n<ul>\n'
183
+ continue
184
+
185
+ # Process ingredient line
186
+ if in_ingredients and line.strip() and not line.lower().startswith(("ingredients", "instructions")):
187
+ item = line.strip()
188
+ if item.startswith("- "):
189
+ item = item[2:]
190
+ elif item.startswith("* "):
191
+ item = item[2:]
192
+
193
+ if item:
194
+ html_content += f'<li>{item}</li>\n'
195
+ continue
196
+
197
+ # Process instruction line
198
+ if in_instructions and line.strip() and not line.lower().startswith(("ingredients", "instructions")):
199
+ step = line.strip()
200
+ if step.startswith("- "):
201
+ step = step[2:]
202
+ elif step.startswith("* "):
203
+ step = step[2:]
204
+ elif step.startswith(". "):
205
+ step = step[2:]
206
+ elif step[0].isdigit() and step[1] in [".", ")"]:
207
+ step = step[2:].strip()
208
+
209
+ if step:
210
+ html_content += f'<li>{step}</li>\n'
211
+ continue
212
+
213
+ # Process nutrition line
214
+ if "nutritional" in line.lower() and line.strip() and not line.startswith(("ingredients", "instructions")):
215
+ item = line.strip()
216
+ if item.startswith("- "):
217
+ item = item[2:]
218
+ elif item.startswith("* "):
219
+ item = item[2:]
220
+
221
+ if item and not item.lower().startswith("nutritional"):
222
+ html_content += f'<li>{item}</li>\n'
223
+ continue
224
+
225
+ # Close any open sections
226
+ if in_ingredients:
227
+ html_content += '</ul>\n</div>\n'
228
+ if in_instructions:
229
+ html_content += '</ol>\n</div>\n'
230
+
231
+ html_content += '</div>\n' # Close recipe card
232
+
233
+ html_content += '</div>\n' # Close recipe suggestions div
234
+
235
+ return html_content
236
+
237
+ def get_recipe_suggestions(api_key, image_paths, num_recipes=3, dietary_restrictions="None", cuisine_preference="Any"):
238
+ """Get recipe suggestions based on the uploaded images of ingredients"""
239
+ if not api_key:
240
+ return "Please provide your Together API key."
241
+
242
+ if not image_paths or len(image_paths) == 0:
243
+ return "Please upload at least one image of ingredients."
244
+
245
+ try:
246
+ client = Together(api_key=api_key)
247
+
248
+ all_ingredients = []
249
+ for img_path in image_paths:
250
+ ingredients_text = analyze_single_image(client, img_path)
251
+ all_ingredients.append(ingredients_text)
252
+
253
+ combined_ingredients = "\n\n".join([f"Image {i+1} ingredients:\n{ingredients}"
254
+ for i, ingredients in enumerate(all_ingredients)])
255
+
256
+ system_prompt = """You are a culinary expert AI assistant that specializes in creating recipes based on available ingredients.
257
+ You will be provided with lists of ingredients identified from multiple images. Your task is to suggest creative,
258
+ detailed recipes that use as many of the identified ingredients as possible.
259
+
260
+ For each recipe suggestion, include:
261
+ 1. Recipe name
262
+ 2. Brief description of the dish
263
+ 3. Complete ingredients list (including estimated quantities and any additional staple ingredients that might be needed)
264
+ 4. Step-by-step cooking instructions
265
+ 5. Approximate cooking time
266
+ 6. Difficulty level (Easy, Medium, Advanced)
267
+ 7. Nutritional highlights
268
+
269
+ Format each recipe clearly with headings for each section. Make sure to separate recipes clearly.
270
+ Consider any dietary restrictions and cuisine preferences mentioned by the user."""
271
+
272
+ user_prompt = f"""Based on the following ingredients identified from multiple images, suggest {num_recipes} creative and delicious recipes.
273
+
274
+ {combined_ingredients}
275
+
276
+ Dietary restrictions to consider: {dietary_restrictions}
277
+ Cuisine preference: {cuisine_preference}
278
+
279
+ Please be creative with your recipe suggestions and try to use ingredients from multiple images if possible."""
280
+
281
+ response = client.chat.completions.create(
282
+ model="meta-llama/Llama-Vision-Free",
283
+ messages=[
284
+ {"role": "system", "content": system_prompt},
285
+ {"role": "user", "content": user_prompt}
286
+ ],
287
+ max_tokens=2048,
288
+ temperature=0.7
289
+ )
290
+
291
+ recipe_text = response.choices[0].message.content
292
+
293
+ # Format ingredients and recipes as HTML
294
+ ingredients_html = format_ingredients_html(all_ingredients)
295
+ recipes_html = format_recipe_to_html(recipe_text)
296
+
297
+ # Combine HTML content
298
+ html_content = ingredients_html + "<hr>" + recipes_html
299
+
300
+ # Create a downloadable HTML file with styling
301
+ full_html = create_downloadable_html(ingredients_html, recipes_html, dietary_restrictions, cuisine_preference)
302
+
303
+ # Generate a unique filename for the downloadable HTML
304
+ timestamp = int(time.time())
305
+ file_name = f"recipes_{timestamp}.html"
306
+ file_path = os.path.join(tempfile.gettempdir(), file_name)
307
+
308
+ with open(file_path, "w", encoding="utf-8") as f:
309
+ f.write(full_html)
310
+
311
+ return [html_content, file_path]
312
+ except Exception as e:
313
+ return [f"Error: {str(e)}", None]
314
+
315
+ def create_downloadable_html(ingredients_html, recipes_html, dietary_restrictions, cuisine_preference):
316
+ """Create a complete HTML document with styling for download"""
317
+ html = f"""<!DOCTYPE html>
318
+ <html lang="en">
319
+ <head>
320
+ <meta charset="UTF-8">
321
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
322
+ <title>Your Personalized Recipes</title>
323
+ <style>
324
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
325
+ :root {{
326
+ --primary-color: #FF6F61;
327
+ --secondary-color: #4BB543;
328
+ --accent-color: #F0A500;
329
+ --background-color: #F4F4F9;
330
+ --text-color: #333333;
331
+ --card-background: #FFFFFF;
332
+ --border-radius: 12px;
333
+ --box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 20px;
334
+ }}
335
+ body {{
336
+ font-family: 'Poppins', sans-serif;
337
+ background-color: var(--background-color);
338
+ color: var(--text-color);
339
+ margin: 0;
340
+ padding: 0;
341
+ line-height: 1.6;
342
+ }}
343
+ .container {{
344
+ max-width: 1000px;
345
+ margin: 0 auto;
346
+ padding: 20px;
347
+ }}
348
+ header {{
349
+ background-color: var(--primary-color);
350
+ color: white;
351
+ padding: 40px 20px;
352
+ text-align: center;
353
+ border-radius: 0 0 20px 20px;
354
+ margin-bottom: 30px;
355
+ }}
356
+ h1 {{
357
+ font-size: 2.5em;
358
+ margin-bottom: 10px;
359
+ }}
360
+ .recipe-info {{
361
+ display: flex;
362
+ justify-content: center;
363
+ gap: 20px;
364
+ margin-bottom: 20px;
365
+ flex-wrap: wrap;
366
+ }}
367
+ .info-badge {{
368
+ background-color: rgba(255, 255, 255, 0.2);
369
+ padding: 8px 16px;
370
+ border-radius: 20px;
371
+ font-size: 0.9em;
372
+ }}
373
+ .ingredients-identified, .recipe-suggestions {{
374
+ background-color: var(--card-background);
375
+ border-radius: var(--border-radius);
376
+ padding: 25px;
377
+ margin-bottom: 30px;
378
+ box-shadow: var(--box-shadow);
379
+ }}
380
+ h2 {{
381
+ color: var(--primary-color);
382
+ border-bottom: 2px solid var(--primary-color);
383
+ padding-bottom: 10px;
384
+ margin-top: 0;
385
+ }}
386
+ .ingredient-group {{
387
+ margin-bottom: 20px;
388
+ }}
389
+ h3 {{
390
+ color: var(--accent-color);
391
+ margin-bottom: 10px;
392
+ }}
393
+ .ingredient-list {{
394
+ list-style-type: disc;
395
+ padding-left: 20px;
396
+ }}
397
+ .recipe-card {{
398
+ background-color: #f9f9f9;
399
+ border-radius: var(--border-radius);
400
+ padding: 20px;
401
+ margin-bottom: 30px;
402
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
403
+ }}
404
+ .recipe-title {{
405
+ color: var(--secondary-color);
406
+ font-size: 1.8em;
407
+ margin-top: 0;
408
+ margin-bottom: 15px;
409
+ }}
410
+ .recipe-description {{
411
+ font-style: italic;
412
+ margin-bottom: 20px;
413
+ color: #666;
414
+ }}
415
+ .recipe-ingredients, .recipe-instructions, .recipe-nutrition {{
416
+ margin-bottom: 20px;
417
+ }}
418
+ .recipe-ingredients h4, .recipe-instructions h4, .recipe-nutrition h4 {{
419
+ color: var(--primary-color);
420
+ margin-bottom: 10px;
421
+ }}
422
+ .recipe-ingredients ul {{
423
+ list-style-type: disc;
424
+ }}
425
+ .recipe-instructions ol {{
426
+ padding-left: 20px;
427
+ }}
428
+ .recipe-instructions li {{
429
+ margin-bottom: 10px;
430
+ }}
431
+ .recipe-time, .recipe-difficulty {{
432
+ color: #666;
433
+ margin: 10px 0;
434
+ }}
435
+ hr {{
436
+ border: 0;
437
+ height: 1px;
438
+ background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));
439
+ margin: 30px 0;
440
+ }}
441
+ footer {{
442
+ text-align: center;
443
+ margin-top: 50px;
444
+ padding: 20px;
445
+ color: #666;
446
+ font-size: 0.9em;
447
+ }}
448
+ </style>
449
+ </head>
450
+ <body>
451
+ <header>
452
+ <h1>🍲 Your Personalized Recipes</h1>
453
+ <div class="recipe-info">
454
+ <span class="info-badge">Dietary: {dietary_restrictions}</span>
455
+ <span class="info-badge">Cuisine: {cuisine_preference}</span>
456
+ <span class="info-badge">Generated: {time.strftime("%Y-%m-%d")}</span>
457
+ </div>
458
+ </header>
459
+
460
+ <div class="container">
461
+ {ingredients_html}
462
+
463
+ <hr>
464
+
465
+ {recipes_html}
466
+
467
+ <footer>
468
+ <p>Generated by Visual Recipe Assistant</p>
469
+ <p>Powered by Meta's Llama-Vision-Free Model & Together AI</p>
470
+ </footer>
471
+ </div>
472
+ </body>
473
+ </html>
474
+ """
475
+ return html
476
+
477
+ def update_gallery(files):
478
+ """Update the gallery with uploaded image paths"""
479
+ if not files or len(files) == 0:
480
+ return gr.update(visible=False)
481
+ return gr.update(value=files, visible=True)
482
+
483
+ def process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference):
484
+ """Process the recipe request with uploaded files"""
485
+ if not files:
486
+ return ["Please upload at least one image of ingredients.", None]
487
+ return get_recipe_suggestions(api_key, files, num_recipes, dietary_restrictions, cuisine_preference)
488
+
489
+ custom_css = """
490
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
491
+ :root {
492
+ --primary-color: #FF6F61; /* Warm coral */
493
+ --secondary-color: #4BB543; /* Fresh green */
494
+ --accent-color: #F0A500; /* Golden yellow */
495
+ --background-color: #F4F4F9; /* Light grayish background */
496
+ --text-color: #333333; /* Dark gray text */
497
+ --card-background: #FFFFFF; /* White for cards */
498
+ --border-radius: 12px;
499
+ --font-family: 'Poppins', sans-serif;
500
+ --box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 20px;
501
+ --hover-shadow: rgba(0, 0, 0, 0.15) 0px 8px 30px;
502
+ }
503
+ body {
504
+ font-family: var(--font-family);
505
+ background-color: var(--background-color);
506
+ color: var(--text-color);
507
+ margin: 0;
508
+ padding: 0;
509
+ }
510
+ .container {
511
+ max-width: 1200px;
512
+ margin: 0 auto;
513
+ padding: 20px;
514
+ }
515
+ .app-header {
516
+ background-color: var(--primary-color);
517
+ color: white;
518
+ padding: 60px 20px;
519
+ text-align: center;
520
+ border-radius: 0 0 30px 30px;
521
+ box-shadow: var(--box-shadow);
522
+ }
523
+ .app-title {
524
+ font-size: 2.8em;
525
+ font-weight: 700;
526
+ margin-bottom: 10px;
527
+ }
528
+ .app-subtitle {
529
+ font-size: 1.3em;
530
+ font-weight: 300;
531
+ max-width: 800px;
532
+ margin: 0 auto;
533
+ }
534
+ .input-section, .output-section {
535
+ background-color: var(--card-background);
536
+ border-radius: var(--border-radius);
537
+ padding: 30px;
538
+ box-shadow: var(--box-shadow);
539
+ margin-bottom: 30px;
540
+ }
541
+ .section-header {
542
+ font-size: 1.6em;
543
+ font-weight: 600;
544
+ color: var(--text-color);
545
+ margin-bottom: 20px;
546
+ border-bottom: 2px solid var(--primary-color);
547
+ padding-bottom: 10px;
548
+ }
549
+ .section-header2 {
550
+ font-size: 1.6em;
551
+ font-weight: 600;
552
+ color: var(--text-color);
553
+ border-bottom: 2px solid var(--primary-color);
554
+ padding-bottom: 10px;
555
+ }
556
+ .image-upload-container {
557
+ border: 2px dashed var(--secondary-color);
558
+ padding: 40px;
559
+ text-align: center;
560
+ background-color: rgba(75, 181, 67, 0.1);
561
+ transition: all 0.3s ease;
562
+ }
563
+ .image-upload-container:hover {
564
+ border-color: var(--primary-color);
565
+ background-color: rgba(255, 111, 97, 0.1);
566
+ }
567
+ button.primary-button {
568
+ background-color: var(--primary-color);
569
+ color: white;
570
+ border: none;
571
+ padding: 16px 32px;
572
+ border-radius: 6px;
573
+ font-size: 1.1em;
574
+ cursor: pointer;
575
+ transition: all 0.3s ease;
576
+ width: 100%;
577
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
578
+ }
579
+ button.primary-button:hover {
580
+ background-color: #E15F52;
581
+ box-shadow: var(--hover-shadow);
582
+ }
583
+ button.download-button {
584
+ background-color: var(--secondary-color);
585
+ color: white;
586
+ border: none;
587
+ padding: 12px 24px;
588
+ border-radius: 6px;
589
+ font-size: 1em;
590
+ cursor: pointer;
591
+ transition: all 0.3s ease;
592
+ margin-top: 15px;
593
+ display: flex;
594
+ align-items: center;
595
+ justify-content: center;
596
+ gap: 8px;
597
+ margin-left: auto;
598
+ margin-right: auto;
599
+ }
600
+ button.download-button:hover {
601
+ background-color: #3da037;
602
+ box-shadow: var(--hover-shadow);
603
+ }
604
+ .gallery-container {
605
+ display: grid;
606
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
607
+ gap: 20px;
608
+ margin-top: 30px;
609
+ }
610
+ .gallery-item {
611
+ border-radius: var(--border-radius);
612
+ overflow: hidden;
613
+ box-shadow: var(--box-shadow);
614
+ transition: transform 0.3s ease;
615
+ aspect-ratio: 1 / 1;
616
+ object-fit: cover;
617
+ }
618
+ .gallery-item:hover {
619
+ transform: scale(1.05);
620
+ box-shadow: var(--hover-shadow);
621
+ }
622
+ .recipe-output {
623
+ font-size: 1.2em;
624
+ line-height: 1.7;
625
+ color: var(--text-color);
626
+ max-height: 600px;
627
+ overflow-y: auto;
628
+ padding-right: 15px;
629
+ }
630
+ .ingredients-identified, .recipe-suggestions {
631
+ background-color: #f9f9f9;
632
+ border-radius: var(--border-radius);
633
+ padding: 20px;
634
+ margin-bottom: 20px;
635
+ }
636
+ .recipe-card {
637
+ background-color: white;
638
+ border-radius: var(--border-radius);
639
+ padding: 20px;
640
+ margin-bottom: 20px;
641
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
642
+ }
643
+ .recipe-title {
644
+ color: var(--secondary-color);
645
+ font-size: 1.8em;
646
+ margin-top: 0;
647
+ margin-bottom: 15px;
648
+ }
649
+ .recipe-description {
650
+ font-style: italic;
651
+ margin-bottom: 20px;
652
+ color: #666;
653
+ }
654
+ .recipe-ingredients, .recipe-instructions, .recipe-nutrition {
655
+ margin-bottom: 20px;
656
+ }
657
+ .recipe-ingredients h4, .recipe-instructions h4, .recipe-nutrition h4 {
658
+ color: var(--primary-color);
659
+ margin-bottom: 10px;
660
+ }
661
+ .loading-container {
662
+ display: flex;
663
+ flex-direction: column;
664
+ justify-content: center;
665
+ align-items: center;
666
+ position: fixed;
667
+ top: 0;
668
+ left: 0;
669
+ width: 100%;
670
+ height: 100%;
671
+ background-color: rgba(0, 0, 0, 0.5);
672
+ z-index: 1000;
673
+ opacity: 0;
674
+ visibility: hidden;
675
+ transition: opacity 0.3s ease, visibility 0.3s ease;
676
+ }
677
+ .loading-container.visible {
678
+ opacity: 1;
679
+ visibility: visible;
680
+ }
681
+ .loading-spinner {
682
+ border: 8px solid #f3f3f3;
683
+ border-top: 8px solid var(--primary-color);
684
+ border-radius: 50%;
685
+ width: 60px;
686
+ height: 60px;
687
+ animation: spin 1s linear infinite;
688
+ }
689
+ @keyframes spin {
690
+ 0% { transform: rotate(0deg); }
691
+ 100% { transform: rotate(360deg); }
692
+ }
693
+ .loading-text {
694
+ color: white;
695
+ font-size: 1.3em;
696
+ margin-top: 20px;
697
+ }
698
+ .footer {
699
+ background-color: var(--card-background);
700
+ padding: 40px 20px;
701
+ text-align: center;
702
+ color: var(--text-color);
703
+ font-size: 1.1em;
704
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
705
+ }
706
+ .footer-content {
707
+ max-width: 800px;
708
+ margin: 0 auto;
709
+ }
710
+ .footer-brand {
711
+ font-weight: 700;
712
+ color: var(--primary-color);
713
+ }
714
+ .footer-links a {
715
+ color: var(--secondary-color);
716
+ text-decoration: none;
717
+ margin: 0 15px;
718
+ transition: color 0.3s ease;
719
+ }
720
+ .footer-links a:hover {
721
+ color: var(--primary-color);
722
+ }
723
+ """
724
+
725
+ html_header = """
726
+ <div class="app-header">
727
+ <div class="app-title">🍲 Visual Recipe Assistant</div>
728
+ <div class="app-subtitle">Upload images of ingredients you have on hand and get personalized recipe suggestions powered by AI</div>
729
+ </div>
730
+ <div id="loading-overlay" class="loading-container">
731
+ <div class="loading-spinner"></div>
732
+ <div class="loading-text">Generating your recipes...</div>
733
+ </div>
734
+ <script>
735
+ function showLoading() {
736
+ document.getElementById('loading-overlay').classList.add('visible');
737
+ }
738
+
739
+ function hideLoading() {
740
+ document.getElementById('loading-overlay').classList.remove('visible');
741
+ }
742
+ </script>
743
+ """
744
+
745
+ html_footer = """
746
+ <div class="footer">
747
+ <div class="footer-content">
748
+ <p><span class="footer-brand">🍲 Visual Recipe Assistant</span></p>
749
+ <p>Powered by Meta's Llama-Vision-Free Model & Together AI</p>
750
+ <p>Upload multiple ingredient images for more creative recipe combinations</p>
751
+ <div class="footer-links">
752
+ <a href="#" target="_blank">How It Works</a>
753
+ <a href="#" target="_blank">Privacy Policy</a>
754
+ <a href="#" target="_blank">Contact Us</a>
755
+ </div>
756
+ </div>
757
+ </div>
758
+ <script>
759
+ document.addEventListener('DOMContentLoaded', function() {
760
+ const submitBtn = document.querySelector('button.primary-button');
761
+ if (submitBtn) {
762
+ submitBtn.addEventListener('click', function() {
763
+ showLoading();
764
+ // Check every second for output content
765
+ const checkInterval = setInterval(function() {
766
+ const output = document.querySelector('.recipe-output');
767
+ if (output && output.innerHTML.trim().length > 0) {
768
+ hideLoading();
769
+ clearInterval(checkInterval);
770
+ clearTimeout(forceHideTimeout);
771
+
772
+ // Show download button if it exists
773
+ const downloadBtn = document.getElementById('download-button');
774
+ if (downloadBtn) {
775
+ downloadBtn.style.display = 'flex';
776
+ }
777
+ }
778
+ }, 1000);
779
+
780
+ // Force hide after 120 seconds
781
+ const forceHideTimeout = setTimeout(function() {
782
+ hideLoading();
783
+ clearInterval(checkInterval);
784
+ }, 120000);
785
+ });
786
+ }
787
+ });
788
+ </script>
789
+ """
790
+
791
+ with gr.Blocks(css=custom_css) as app:
792
+ gr.HTML(html_header)
793
+
794
+ # Store the generated html file path for download
795
+ html_file_path = gr.State(None)
796
+
797
+ with gr.Row():
798
+ with gr.Column(scale=1):
799
+ with gr.Group(elem_classes="input-section"):
800
+ gr.HTML('<h3 class="section-header">API Configuration</h3>')
801
+ api_key_input = gr.Textbox(
802
+ label="Together API Key",
803
+ placeholder="Enter your Together API key here...",
804
+ type="password",
805
+ elem_classes="input-group"
806
+ )
807
+
808
+ gr.HTML('<h3 class="section-header">Upload Ingredients</h3>')
809
+ file_upload = gr.File(
810
+ label="Upload images of ingredients",
811
+ file_types=["image"],
812
+ file_count="multiple",
813
+ elem_classes="image-upload-container"
814
+ )
815
+
816
+ image_input = gr.Gallery(
817
+ label="Uploaded Ingredients",
818
+ elem_id="ingredient-gallery",
819
+ columns=3,
820
+ rows=2,
821
+ height="auto",
822
+ visible=False
823
+ )
824
+
825
+ gr.HTML('<h3 class="section-header2">Recipe Preferences</h3>')
826
+ with gr.Row():
827
+ num_recipes = gr.Slider(
828
+ minimum=1,
829
+ maximum=5,
830
+ value=3,
831
+ step=1,
832
+ label="Number of Recipe Suggestions",
833
+ elem_classes="input-group"
834
+ )
835
+
836
+ with gr.Row():
837
+ with gr.Column():
838
+ dietary_restrictions = gr.Dropdown(
839
+ choices=["None", "Vegetarian", "Vegan", "Gluten-Free", "Dairy-Free", "Low-Carb", "Keto", "Paleo"],
840
+ value="None",
841
+ label="Dietary Restrictions",
842
+ elem_classes="input-group"
843
+ )
844
+
845
+ with gr.Column():
846
+ cuisine_preference = gr.Dropdown(
847
+ choices=["Any", "Italian", "Asian", "Mexican", "Mediterranean", "Indian", "American", "French", "Middle Eastern"],
848
+ value="Any",
849
+ label="Cuisine Preference",
850
+ elem_classes="input-group"
851
+ )
852
+
853
+ submit_button = gr.Button("Get Recipe Suggestions", elem_classes="primary-button")
854
+
855
+ with gr.Column(scale=1):
856
+ with gr.Group(elem_classes="output-section"):
857
+ gr.HTML('<h3 class="section-header">Your Personalized Recipes</h3>')
858
+ output = gr.HTML(elem_classes="recipe-output")
859
+ download_button = gr.Button("📥 Download Recipes as HTML", elem_classes="download-button", visible=False, elem_id="download-button")
860
+
861
+ gr.HTML(html_footer)
862
+
863
+ # Update functions
864
+ def process_and_update_file_path(api_key, files, num_recipes, dietary_restrictions, cuisine_preference):
865
+ """Process recipe request and update file path state"""
866
+ result = process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference)
867
+ html_content = result[0]
868
+ file_path = result[1]
869
+ return html_content, file_path, gr.update(visible=file_path is not None)
870
+
871
+ file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input)
872
+
873
+ submit_button.click(
874
+ fn=process_and_update_file_path,
875
+ inputs=[api_key_input, file_upload, num_recipes, dietary_restrictions, cuisine_preference],
876
+ outputs=[output, html_file_path, download_button]
877
+ )
878
+
879
+ # Handle download button click
880
+ download_button.click(
881
+ fn=lambda x: x,
882
+ inputs=html_file_path,
883
+ outputs=gr.File(label="Download Recipe HTML")
884
+ )
885
+
886
+ if __name__ == "__main__":
887
+ app.launch()