shukdevdatta123 commited on
Commit
6a8c21a
Β·
verified Β·
1 Parent(s): 229a60d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +152 -337
app.py CHANGED
@@ -1,13 +1,10 @@
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
 
12
  def encode_image_to_base64(image_path):
13
  """Convert image to base64 encoding"""
@@ -20,15 +17,12 @@ def save_uploaded_image(image):
20
  return None
21
 
22
  with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
23
- if isinstance(image, dict) and "path" in image: # Gradio returns image as dict
24
- # Copy the uploaded image to our temporary file
25
  with open(image["path"], "rb") as img_file:
26
  temp_file.write(img_file.read())
27
  elif isinstance(image, Image.Image):
28
- # If it's a PIL Image, save it
29
  image.save(temp_file.name, format="JPEG")
30
  else:
31
- # Try to handle other formats
32
  try:
33
  Image.open(image).save(temp_file.name, format="JPEG")
34
  except Exception:
@@ -44,13 +38,10 @@ def analyze_single_image(client, img_path):
44
 
45
  user_prompt = "Please identify all the food ingredients visible in this image. List each ingredient on a new line."
46
 
47
- # First, convert the image to base64
48
  try:
49
  with open(img_path, "rb") as image_file:
50
- # Read the binary data and encode as base64
51
  base64_image = base64.b64encode(image_file.read()).decode('utf-8')
52
 
53
- # Create message with the image properly formatted
54
  content = [
55
  {"type": "text", "text": user_prompt},
56
  {
@@ -64,14 +55,8 @@ def analyze_single_image(client, img_path):
64
  response = client.chat.completions.create(
65
  model="meta-llama/Llama-Vision-Free",
66
  messages=[
67
- {
68
- "role": "system",
69
- "content": system_prompt
70
- },
71
- {
72
- "role": "user",
73
- "content": content
74
- }
75
  ],
76
  max_tokens=500,
77
  temperature=0.2
@@ -82,22 +67,18 @@ def analyze_single_image(client, img_path):
82
  return f"Error analyzing image: {str(e)}"
83
 
84
  def get_recipe_suggestions(api_key, images, num_recipes=3, dietary_restrictions="None", cuisine_preference="Any"):
85
- """
86
- Get recipe suggestions based on the uploaded images of ingredients
87
- """
88
  if not api_key:
89
  return "Please provide your Together API key."
90
 
91
  if not images or len(images) == 0 or all(img is None for img in images):
92
  return "Please upload at least one image of ingredients."
93
 
94
- # Filter out None values
95
  valid_images = [img for img in images if img is not None]
96
 
97
  if len(valid_images) == 0:
98
  return "No valid images were uploaded. Please try again."
99
 
100
- # Save all uploaded images
101
  image_paths = []
102
  for img in valid_images:
103
  img_path = save_uploaded_image(img)
@@ -108,20 +89,16 @@ def get_recipe_suggestions(api_key, images, num_recipes=3, dietary_restrictions=
108
  return "Failed to process the uploaded images."
109
 
110
  try:
111
- # Initialize Together client with the provided API key
112
  client = Together(api_key=api_key)
113
 
114
- # First, analyze each image separately to identify ingredients
115
  all_ingredients = []
116
  for img_path in image_paths:
117
  ingredients_text = analyze_single_image(client, img_path)
118
  all_ingredients.append(ingredients_text)
119
 
120
- # Combine all ingredients into one list
121
  combined_ingredients = "\n\n".join([f"Image {i+1} ingredients:\n{ingredients}"
122
  for i, ingredients in enumerate(all_ingredients)])
123
 
124
- # Now generate recipes based on all identified ingredients
125
  system_prompt = """You are a culinary expert AI assistant that specializes in creating recipes based on available ingredients.
126
  You will be provided with lists of ingredients identified from multiple images. Your task is to suggest creative,
127
  detailed recipes that use as many of the identified ingredients as possible.
@@ -146,31 +123,22 @@ def get_recipe_suggestions(api_key, images, num_recipes=3, dietary_restrictions=
146
 
147
  Please be creative with your recipe suggestions and try to use ingredients from multiple images if possible."""
148
 
149
- # Generate recipe suggestions based on all identified ingredients
150
  response = client.chat.completions.create(
151
  model="meta-llama/Llama-Vision-Free",
152
  messages=[
153
- {
154
- "role": "system",
155
- "content": system_prompt
156
- },
157
- {
158
- "role": "user",
159
- "content": user_prompt
160
- }
161
  ],
162
- max_tokens=2048,
163
- temperature=0.7
164
  )
165
 
166
- # Clean up the temporary files
167
  for img_path in image_paths:
168
  try:
169
  os.unlink(img_path)
170
  except:
171
  pass
172
 
173
- # Add information about the ingredients identified
174
  result = "## πŸ“‹ Ingredients Identified\n\n"
175
  result += combined_ingredients
176
  result += "\n\n---\n\n"
@@ -180,7 +148,6 @@ def get_recipe_suggestions(api_key, images, num_recipes=3, dietary_restrictions=
180
  return result
181
 
182
  except Exception as e:
183
- # Clean up the temporary files in case of error
184
  for img_path in image_paths:
185
  try:
186
  os.unlink(img_path)
@@ -188,99 +155,86 @@ def get_recipe_suggestions(api_key, images, num_recipes=3, dietary_restrictions=
188
  pass
189
  return f"Error: {str(e)}"
190
 
191
- # Enhanced Custom CSS for a more appealing interface
192
  custom_css = """
193
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
194
 
195
  :root {
196
- --primary-color: #FF6B6B;
197
- --primary-dark: #e55858;
198
- --secondary-color: #4ECDC4;
199
- --accent-color: #FFD166;
200
- --background-color: #f8f9fa;
201
- --text-color: #212529;
202
- --card-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
203
- --hover-shadow: 0 12px 28px rgba(0, 0, 0, 0.15);
204
- --border-radius: 12px;
205
- --font-family: 'Poppins', sans-serif;
 
206
  }
207
 
208
  body {
209
  font-family: var(--font-family);
210
- background-color: var(--background-color);
211
  color: var(--text-color);
212
  margin: 0;
213
  padding: 0;
 
214
  }
215
 
216
  .container {
217
- max-width: 1200px;
218
  margin: 0 auto;
219
- padding: 20px;
220
  }
221
 
222
  .app-header {
223
  text-align: center;
224
- margin-bottom: 40px;
225
- padding: 50px 20px;
226
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
227
  border-radius: var(--border-radius);
228
- color: white;
229
- box-shadow: var(--card-shadow);
230
- position: relative;
231
- overflow: hidden;
232
- }
233
-
234
- .app-header::before {
235
- content: '';
236
- position: absolute;
237
- top: 0;
238
- left: 0;
239
- right: 0;
240
- bottom: 0;
241
- background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path fill="rgba(255,255,255,0.05)" d="M30,20 L70,20 L50,50 Z"></path></svg>') repeat;
242
- opacity: 0.3;
243
  }
244
 
245
  .app-title {
246
- font-size: 3.5em;
247
- margin-bottom: 15px;
248
  font-weight: 700;
249
- text-shadow: 0 2px 4px rgba(0,0,0,0.1);
250
- position: relative;
 
 
251
  }
252
 
253
  .app-subtitle {
254
- font-size: 1.3em;
255
- opacity: 0.9;
256
- max-width: 700px;
257
  margin: 0 auto;
258
  line-height: 1.6;
259
  font-weight: 300;
260
- position: relative;
261
  }
262
 
263
  .input-section, .output-section {
264
- background-color: white;
265
  border-radius: var(--border-radius);
266
- padding: 30px;
267
- box-shadow: var(--card-shadow);
268
- margin-bottom: 30px;
269
- transition: all 0.3s ease;
270
- border: 1px solid rgba(0,0,0,0.05);
271
  }
272
 
273
  .input-section:hover, .output-section:hover {
274
- box-shadow: var(--hover-shadow);
275
  transform: translateY(-5px);
 
276
  }
277
 
278
  .section-header {
279
  color: var(--primary-color);
280
- margin-top: 0;
281
- font-size: 1.7em;
282
- border-bottom: 2px solid var(--secondary-color);
283
- padding-bottom: 15px;
284
  margin-bottom: 25px;
285
  font-weight: 600;
286
  display: flex;
@@ -288,104 +242,70 @@ body {
288
  }
289
 
290
  .section-header i {
291
- margin-right: 10px;
292
- font-size: 1.2em;
293
  }
294
 
295
  .image-upload-container {
296
- border: 3px dashed var(--secondary-color);
297
  border-radius: var(--border-radius);
298
- padding: 30px;
299
  text-align: center;
300
- margin-bottom: 25px;
 
301
  transition: all 0.3s ease;
302
- background-color: rgba(78, 205, 196, 0.05);
303
- position: relative;
304
  }
305
 
306
  .image-upload-container:hover {
307
  border-color: var(--primary-color);
308
- background-color: rgba(255, 107, 107, 0.05);
309
- transform: translateY(-3px);
310
  }
311
 
312
  .image-upload-icon {
313
- font-size: 3em;
314
  color: var(--secondary-color);
315
  margin-bottom: 15px;
316
  }
317
 
318
  .image-upload-text {
319
- color: #666;
320
  font-weight: 500;
 
321
  }
322
 
323
  button.primary-button {
324
- background: linear-gradient(135deg, var(--primary-color) 0%, #FF8E8E 100%);
325
  color: white;
326
  border: none;
327
- padding: 15px 30px;
328
  border-radius: 50px;
329
  font-size: 1.2em;
330
  cursor: pointer;
331
  transition: all 0.3s ease;
332
- box-shadow: 0 4px 10px rgba(255, 107, 107, 0.3);
333
  font-weight: 600;
334
- display: block;
335
  width: 100%;
336
  margin-top: 30px;
337
- position: relative;
338
- overflow: hidden;
339
  }
340
 
341
  button.primary-button:hover {
342
- transform: translateY(-3px);
343
- box-shadow: 0 8px 15px rgba(255, 107, 107, 0.4);
344
- background: linear-gradient(135deg, #FF8E8E 0%, var(--primary-dark) 100%);
345
  }
346
 
347
  button.primary-button:active {
348
- transform: translateY(1px);
349
- box-shadow: 0 2px 5px rgba(255, 107, 107, 0.4);
350
- }
351
-
352
- button.primary-button::after {
353
- content: '';
354
- position: absolute;
355
- top: 50%;
356
- left: 50%;
357
- width: 5px;
358
- height: 5px;
359
- background: rgba(255, 255, 255, 0.5);
360
- opacity: 0;
361
- border-radius: 100%;
362
- transform: scale(1, 1) translate(-50%, -50%);
363
- transform-origin: 50% 50%;
364
- }
365
-
366
- button.primary-button:focus:not(:active)::after {
367
- animation: ripple 1s ease-out;
368
- }
369
-
370
- @keyframes ripple {
371
- 0% {
372
- transform: scale(0, 0);
373
- opacity: 0.5;
374
- }
375
- 100% {
376
- transform: scale(100, 100);
377
- opacity: 0;
378
- }
379
  }
380
 
381
  .gradio-slider.svelte-17l1npl {
382
- margin-bottom: 25px;
383
  }
384
 
385
  .recipe-card {
386
- border-left: 5px solid var(--accent-color);
387
  padding: 20px;
388
- background-color: #f9f9f9;
389
  margin-bottom: 20px;
390
  border-radius: 0 var(--border-radius) var(--border-radius) 0;
391
  transition: all 0.3s ease;
@@ -393,29 +313,29 @@ button.primary-button:focus:not(:active)::after {
393
 
394
  .recipe-card:hover {
395
  transform: translateX(5px);
396
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
397
  }
398
 
399
  .recipe-title {
400
  color: var(--primary-color);
401
- font-size: 1.5em;
402
- margin-bottom: 10px;
403
  font-weight: 600;
404
  }
405
 
406
  .footer {
407
  text-align: center;
408
- margin-top: 60px;
409
- padding: 30px 0;
410
- color: #6c757d;
411
  font-size: 1em;
412
- background-color: #f1f3f5;
413
  border-radius: var(--border-radius);
414
- box-shadow: inset 0 2px 10px rgba(0,0,0,0.05);
415
  }
416
 
417
  .footer-content {
418
- max-width: 700px;
419
  margin: 0 auto;
420
  }
421
 
@@ -430,37 +350,30 @@ button.primary-button:focus:not(:active)::after {
430
 
431
  .footer-links a {
432
  color: var(--secondary-color);
433
- margin: 0 10px;
434
  text-decoration: none;
435
  transition: color 0.3s ease;
436
  }
437
 
438
  .footer-links a:hover {
439
  color: var(--primary-color);
440
- text-decoration: underline;
441
- }
442
-
443
- .icon {
444
- color: var(--primary-color);
445
- margin-right: 10px;
446
- font-size: 1.2em;
447
  }
448
 
449
  .input-group {
450
- margin-bottom: 25px;
451
  }
452
 
453
  .input-group label {
454
  display: block;
455
- margin-bottom: 10px;
456
- font-weight: 600;
457
  color: var(--text-color);
458
  font-size: 1.1em;
459
  }
460
 
461
  .gallery-container {
462
  display: grid;
463
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
464
  gap: 20px;
465
  margin-top: 20px;
466
  }
@@ -468,25 +381,22 @@ button.primary-button:focus:not(:active)::after {
468
  .gallery-item {
469
  border-radius: var(--border-radius);
470
  overflow: hidden;
471
- box-shadow: var(--card-shadow);
472
  transition: transform 0.3s ease;
473
  aspect-ratio: 1 / 1;
474
- object-fit: cover;
475
  }
476
 
477
  .gallery-item:hover {
478
  transform: scale(1.05);
479
- box-shadow: var(--hover-shadow);
480
  }
481
 
482
- /* Loading Spinner */
483
  .loading-container {
484
  position: fixed;
485
  top: 0;
486
  left: 0;
487
  width: 100%;
488
  height: 100%;
489
- background-color: rgba(0, 0, 0, 0.7);
490
  display: flex;
491
  justify-content: center;
492
  align-items: center;
@@ -502,170 +412,97 @@ button.primary-button:focus:not(:active)::after {
502
  }
503
 
504
  .loading-spinner {
505
- width: 80px;
506
- height: 80px;
 
 
507
  border-radius: 50%;
508
- position: relative;
509
- animation: spin 1.2s linear infinite;
510
- }
511
-
512
- .loading-spinner::before,
513
- .loading-spinner::after {
514
- content: '';
515
- position: absolute;
516
- border-radius: 50%;
517
- }
518
-
519
- .loading-spinner::before {
520
- width: 100%;
521
- height: 100%;
522
- background: linear-gradient(to right, var(--primary-color) 0%, transparent 100%);
523
- animation: spin 2s linear infinite;
524
- }
525
-
526
- .loading-spinner::after {
527
- width: 75%;
528
- height: 75%;
529
- background-color: rgba(0, 0, 0, 0.7);
530
- top: 12.5%;
531
- left: 12.5%;
532
  }
533
 
534
  .loading-text {
535
  position: absolute;
536
- bottom: -40px;
537
- color: white;
538
  font-size: 1.2em;
539
  font-weight: 500;
540
  }
541
 
542
  @keyframes spin {
543
- 0% {
544
- transform: rotate(0deg);
545
- }
546
- 100% {
547
- transform: rotate(360deg);
548
- }
549
  }
550
 
551
  .recipe-output {
552
- max-height: 800px;
553
  overflow-y: auto;
554
- padding-right: 15px;
555
- line-height: 1.6;
556
  }
557
 
558
  .recipe-output::-webkit-scrollbar {
559
- width: 8px;
560
  }
561
 
562
  .recipe-output::-webkit-scrollbar-track {
563
- background: #f1f1f1;
564
  border-radius: 10px;
565
  }
566
 
567
  .recipe-output::-webkit-scrollbar-thumb {
568
- background: var(--secondary-color);
569
- border-radius: 10px;
570
- }
571
-
572
- .recipe-output::-webkit-scrollbar-thumb:hover {
573
  background: var(--primary-color);
 
574
  }
575
 
576
  .recipe-output h2 {
577
  color: var(--primary-color);
578
  border-bottom: 2px solid var(--secondary-color);
579
- padding-bottom: 10px;
580
- font-size: 1.8em;
581
- margin-top: 30px;
582
  }
583
 
584
  .recipe-output h3 {
585
  color: var(--secondary-color);
586
- font-size: 1.4em;
587
- margin-top: 25px;
588
  }
589
 
590
- /* Form inputs styling */
591
  input[type="password"], input[type="text"] {
592
- border: 2px solid #e9ecef;
 
593
  border-radius: var(--border-radius);
594
- padding: 15px;
595
  font-size: 1em;
596
  width: 100%;
 
597
  transition: all 0.3s ease;
598
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
599
  }
600
 
601
  input[type="password"]:focus, input[type="text"]:focus {
602
- border-color: var(--secondary-color);
603
  outline: none;
604
- box-shadow: 0 0 0 3px rgba(78, 205, 196, 0.2);
605
  }
606
 
607
- /* Custom dropdown styling */
608
  select {
609
  appearance: none;
610
- background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23FF6B6B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E") no-repeat right 15px center;
611
- border: 2px solid #e9ecef;
612
  border-radius: var(--border-radius);
613
- padding: 15px 45px 15px 15px;
614
  font-size: 1em;
615
  width: 100%;
 
616
  transition: all 0.3s ease;
617
- box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
618
  }
619
 
620
  select:focus {
621
- border-color: var(--secondary-color);
622
  outline: none;
623
- box-shadow: 0 0 0 3px rgba(78, 205, 196, 0.2);
624
- }
625
-
626
- /* Tooltip styling */
627
- .tooltip {
628
- position: relative;
629
- display: inline-block;
630
- cursor: pointer;
631
  }
632
 
633
- .tooltip .tooltiptext {
634
- visibility: hidden;
635
- width: 250px;
636
- background-color: #333;
637
- color: #fff;
638
- text-align: center;
639
- border-radius: 6px;
640
- padding: 10px;
641
- position: absolute;
642
- z-index: 1;
643
- bottom: 125%;
644
- left: 50%;
645
- margin-left: -125px;
646
- opacity: 0;
647
- transition: opacity 0.3s;
648
- font-size: 0.9em;
649
- box-shadow: 0 5px 15px rgba(0,0,0,0.2);
650
- }
651
-
652
- .tooltip .tooltiptext::after {
653
- content: "";
654
- position: absolute;
655
- top: 100%;
656
- left: 50%;
657
- margin-left: -5px;
658
- border-width: 5px;
659
- border-style: solid;
660
- border-color: #333 transparent transparent transparent;
661
- }
662
-
663
- .tooltip:hover .tooltiptext {
664
- visibility: visible;
665
- opacity: 1;
666
- }
667
-
668
- /* Media queries for responsive design */
669
  @media (max-width: 768px) {
670
  .app-title {
671
  font-size: 2.5em;
@@ -676,15 +513,15 @@ select:focus {
676
  }
677
 
678
  .input-section, .output-section {
679
- padding: 20px;
680
  }
681
 
682
  .section-header {
683
- font-size: 1.5em;
684
  }
685
 
686
  button.primary-button {
687
- padding: 12px 25px;
688
  font-size: 1.1em;
689
  }
690
  }
@@ -699,15 +536,14 @@ select:focus {
699
  }
700
 
701
  .input-section, .output-section {
702
- padding: 15px;
703
  }
704
 
705
  .section-header {
706
- font-size: 1.3em;
707
  }
708
  }
709
 
710
- /* Remove Gradio branding */
711
  .gradio-container {
712
  max-width: 100% !important;
713
  }
@@ -717,21 +553,18 @@ footer.footer-links {
717
  }
718
  """
719
 
720
- # Custom HTML header with icons and improved design
721
  html_header = """
722
  <div class="app-header">
723
  <div class="app-title">🍲 Visual Recipe Assistant</div>
724
- <div class="app-subtitle">Upload images of ingredients you have on hand and get personalized recipe suggestions powered by AI</div>
725
  </div>
726
-
727
  <div id="loading-overlay" class="loading-container">
728
  <div class="loading-spinner">
729
- <div class="loading-text">Generating your recipes...</div>
730
  </div>
731
  </div>
732
-
733
  <script>
734
- // JavaScript to handle loading state
735
  function showLoading() {
736
  document.getElementById('loading-overlay').classList.add('visible');
737
  }
@@ -739,16 +572,38 @@ html_header = """
739
  function hideLoading() {
740
  document.getElementById('loading-overlay').classList.remove('visible');
741
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
  </script>
743
  """
744
 
745
- # Custom HTML footer with improved design
746
  html_footer = """
747
  <div class="footer">
748
  <div class="footer-content">
749
  <p><span class="footer-brand">🍲 Visual Recipe Assistant</span></p>
750
  <p>Powered by Meta's Llama-Vision-Free Model & Together AI</p>
751
- <p>Upload multiple ingredient images for more creative recipe combinations</p>
752
  <div class="footer-links">
753
  <a href="#" target="_blank">How It Works</a>
754
  <a href="#" target="_blank">Privacy Policy</a>
@@ -756,43 +611,9 @@ html_footer = """
756
  </div>
757
  </div>
758
  </div>
759
-
760
- <script>
761
- // Add event listener to the submit button
762
- document.addEventListener('DOMContentLoaded', function() {
763
- const submitBtn = document.querySelector('button.primary-button');
764
- if (submitBtn) {
765
- submitBtn.addEventListener('click', function() {
766
- showLoading();
767
-
768
- // Hide loading after the response is received (this is approximate)
769
- setTimeout(function() {
770
- const output = document.querySelector('.recipe-output');
771
- if (output && output.textContent.trim().length > 0) {
772
- hideLoading();
773
- } else {
774
- // Check every second until content appears
775
- const checkInterval = setInterval(function() {
776
- if (output && output.textContent.trim().length > 0) {
777
- hideLoading();
778
- clearInterval(checkInterval);
779
- }
780
- }, 1000);
781
-
782
- // Fallback: hide after 30 seconds regardless
783
- setTimeout(function() {
784
- hideLoading();
785
- clearInterval(checkInterval);
786
- }, 30000);
787
- }
788
- }, 3000);
789
- });
790
- }
791
- });
792
- </script>
793
  """
794
 
795
- # Create the Gradio interface with improved design
796
  with gr.Blocks(css=custom_css) as app:
797
  gr.HTML(html_header)
798
 
@@ -802,14 +623,13 @@ with gr.Blocks(css=custom_css) as app:
802
  gr.HTML('<h3 class="section-header"><i class="icon">πŸ”‘</i> API Configuration</h3>')
803
  api_key_input = gr.Textbox(
804
  label="Together API Key",
805
- placeholder="Enter your Together API key here...",
806
  type="password",
807
  elem_classes="input-group",
808
- info="Your API key will remain private and is used only for this session."
809
  )
810
 
811
  gr.HTML('<h3 class="section-header"><i class="icon">πŸ“·</i> Upload Ingredients</h3>')
812
- # Use File component to handle multiple image uploads
813
  file_upload = gr.File(
814
  label="Upload images of ingredients",
815
  file_types=["image"],
@@ -863,7 +683,6 @@ with gr.Blocks(css=custom_css) as app:
863
 
864
  gr.HTML(html_footer)
865
 
866
- # Handle file uploads to display in gallery
867
  def update_gallery(files):
868
  if not files:
869
  return gr.Gallery.update(visible=False)
@@ -871,22 +690,18 @@ with gr.Blocks(css=custom_css) as app:
871
 
872
  file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input)
873
 
874
- # Handle recipe generation
875
  def process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference):
876
  if not files:
877
  return "Please upload at least one image of ingredients."
878
 
879
- # Get actual image files from the uploaded files
880
  images = [file.name for file in files]
881
  return get_recipe_suggestions(api_key, images, num_recipes, dietary_restrictions, cuisine_preference)
882
 
883
- # Set up the submission action
884
  submit_button.click(
885
  fn=process_recipe_request,
886
  inputs=[api_key_input, file_upload, num_recipes, dietary_restrictions, cuisine_preference],
887
  outputs=output
888
  )
889
 
890
- # Launch the app
891
  if __name__ == "__main__":
892
  app.launch()
 
1
  import gradio as gr
2
  import base64
 
 
 
 
3
  import os
 
4
  import tempfile
5
  import uuid
6
+ from together import Together
7
+ from PIL import Image
8
 
9
  def encode_image_to_base64(image_path):
10
  """Convert image to base64 encoding"""
 
17
  return None
18
 
19
  with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
20
+ if isinstance(image, dict) and "path" in image:
 
21
  with open(image["path"], "rb") as img_file:
22
  temp_file.write(img_file.read())
23
  elif isinstance(image, Image.Image):
 
24
  image.save(temp_file.name, format="JPEG")
25
  else:
 
26
  try:
27
  Image.open(image).save(temp_file.name, format="JPEG")
28
  except Exception:
 
38
 
39
  user_prompt = "Please identify all the food ingredients visible in this image. List each ingredient on a new line."
40
 
 
41
  try:
42
  with open(img_path, "rb") as image_file:
 
43
  base64_image = base64.b64encode(image_file.read()).decode('utf-8')
44
 
 
45
  content = [
46
  {"type": "text", "text": user_prompt},
47
  {
 
55
  response = client.chat.completions.create(
56
  model="meta-llama/Llama-Vision-Free",
57
  messages=[
58
+ {"role": "system", "content": system_prompt},
59
+ {"role": "user", "content": content}
 
 
 
 
 
 
60
  ],
61
  max_tokens=500,
62
  temperature=0.2
 
67
  return f"Error analyzing image: {str(e)}"
68
 
69
  def get_recipe_suggestions(api_key, images, num_recipes=3, dietary_restrictions="None", cuisine_preference="Any"):
70
+ """Get recipe suggestions based on uploaded images of ingredients"""
 
 
71
  if not api_key:
72
  return "Please provide your Together API key."
73
 
74
  if not images or len(images) == 0 or all(img is None for img in images):
75
  return "Please upload at least one image of ingredients."
76
 
 
77
  valid_images = [img for img in images if img is not None]
78
 
79
  if len(valid_images) == 0:
80
  return "No valid images were uploaded. Please try again."
81
 
 
82
  image_paths = []
83
  for img in valid_images:
84
  img_path = save_uploaded_image(img)
 
89
  return "Failed to process the uploaded images."
90
 
91
  try:
 
92
  client = Together(api_key=api_key)
93
 
 
94
  all_ingredients = []
95
  for img_path in image_paths:
96
  ingredients_text = analyze_single_image(client, img_path)
97
  all_ingredients.append(ingredients_text)
98
 
 
99
  combined_ingredients = "\n\n".join([f"Image {i+1} ingredients:\n{ingredients}"
100
  for i, ingredients in enumerate(all_ingredients)])
101
 
 
102
  system_prompt = """You are a culinary expert AI assistant that specializes in creating recipes based on available ingredients.
103
  You will be provided with lists of ingredients identified from multiple images. Your task is to suggest creative,
104
  detailed recipes that use as many of the identified ingredients as possible.
 
123
 
124
  Please be creative with your recipe suggestions and try to use ingredients from multiple images if possible."""
125
 
 
126
  response = client.chat.completions.create(
127
  model="meta-llama/Llama-Vision-Free",
128
  messages=[
129
+ {"role": "system", "content": system_prompt},
130
+ {"role": "user", "content": user_prompt}
 
 
 
 
 
 
131
  ],
132
+ max_tokens=3000,
133
+ temperature=0.6
134
  )
135
 
 
136
  for img_path in image_paths:
137
  try:
138
  os.unlink(img_path)
139
  except:
140
  pass
141
 
 
142
  result = "## πŸ“‹ Ingredients Identified\n\n"
143
  result += combined_ingredients
144
  result += "\n\n---\n\n"
 
148
  return result
149
 
150
  except Exception as e:
 
151
  for img_path in image_paths:
152
  try:
153
  os.unlink(img_path)
 
155
  pass
156
  return f"Error: {str(e)}"
157
 
158
+ # Enhanced Custom CSS
159
  custom_css = """
160
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
161
 
162
  :root {
163
+ --primary-color: #6366f1;
164
+ --primary-dark: #4f46e5;
165
+ --secondary-color: #10b981;
166
+ --accent-color: #f59e0b;
167
+ --background-color: #1f2937;
168
+ --card-background: rgba(31, 41, 55, 0.8);
169
+ --text-color: #f3f4f6;
170
+ --border-radius: 16px;
171
+ --glass-background: rgba(255, 255, 255, 0.1);
172
+ --glass-border: rgba(255, 255, 255, 0.2);
173
+ --font-family: 'Inter', sans-serif;
174
  }
175
 
176
  body {
177
  font-family: var(--font-family);
178
+ background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
179
  color: var(--text-color);
180
  margin: 0;
181
  padding: 0;
182
+ min-height: 100vh;
183
  }
184
 
185
  .container {
186
+ max-width: 1400px;
187
  margin: 0 auto;
188
+ padding: 40px 20px;
189
  }
190
 
191
  .app-header {
192
  text-align: center;
193
+ margin-bottom: 60px;
194
+ padding: 60px 20px;
195
+ background: var(--glass-background);
196
  border-radius: var(--border-radius);
197
+ backdrop-filter: blur(10px);
198
+ border: 1px solid var(--glass-border);
199
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
200
  }
201
 
202
  .app-title {
203
+ font-size: 3.2em;
 
204
  font-weight: 700;
205
+ margin-bottom: 15px;
206
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
207
+ -webkit-background-clip: text;
208
+ -webkit-text-fill-color: transparent;
209
  }
210
 
211
  .app-subtitle {
212
+ font-size: 1.2em;
213
+ opacity: 0.85;
214
+ max-width: 800px;
215
  margin: 0 auto;
216
  line-height: 1.6;
217
  font-weight: 300;
 
218
  }
219
 
220
  .input-section, .output-section {
221
+ background: var(--card-background);
222
  border-radius: var(--border-radius);
223
+ padding: 40px;
224
+ margin-bottom: 40px;
225
+ backdrop-filter: blur(10px);
226
+ border: 1px solid var(--glass-border);
227
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
228
  }
229
 
230
  .input-section:hover, .output-section:hover {
 
231
  transform: translateY(-5px);
232
+ box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
233
  }
234
 
235
  .section-header {
236
  color: var(--primary-color);
237
+ font-size: 1.8em;
 
 
 
238
  margin-bottom: 25px;
239
  font-weight: 600;
240
  display: flex;
 
242
  }
243
 
244
  .section-header i {
245
+ margin-right: 12px;
246
+ font-size: 1.3em;
247
  }
248
 
249
  .image-upload-container {
250
+ border: 2px dashed var(--secondary-color);
251
  border-radius: var(--border-radius);
252
+ padding: 40px;
253
  text-align: center;
254
+ margin-bottom: 30px;
255
+ background: var(--glass-background);
256
  transition: all 0.3s ease;
 
 
257
  }
258
 
259
  .image-upload-container:hover {
260
  border-color: var(--primary-color);
261
+ background: rgba(99, 102, 241, 0.05);
 
262
  }
263
 
264
  .image-upload-icon {
265
+ font-size: 3.5em;
266
  color: var(--secondary-color);
267
  margin-bottom: 15px;
268
  }
269
 
270
  .image-upload-text {
271
+ color: var(--text-color);
272
  font-weight: 500;
273
+ opacity: 0.9;
274
  }
275
 
276
  button.primary-button {
277
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
278
  color: white;
279
  border: none;
280
+ padding: 16px 32px;
281
  border-radius: 50px;
282
  font-size: 1.2em;
283
  cursor: pointer;
284
  transition: all 0.3s ease;
285
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
286
  font-weight: 600;
 
287
  width: 100%;
288
  margin-top: 30px;
 
 
289
  }
290
 
291
  button.primary-button:hover {
292
+ transform: translateY(-2px);
293
+ box-shadow: 0 8px 20px rgba(99, 102, 241, 0.6);
 
294
  }
295
 
296
  button.primary-button:active {
297
+ transform: translateY(0);
298
+ box-shadow: 0 2px 10px rgba(99, 102, 241, 0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  }
300
 
301
  .gradio-slider.svelte-17l1npl {
302
+ margin-bottom: 30px;
303
  }
304
 
305
  .recipe-card {
306
+ border-left: 4px solid var(--accent-color);
307
  padding: 20px;
308
+ background: var(--glass-background);
309
  margin-bottom: 20px;
310
  border-radius: 0 var(--border-radius) var(--border-radius) 0;
311
  transition: all 0.3s ease;
 
313
 
314
  .recipe-card:hover {
315
  transform: translateX(5px);
316
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
317
  }
318
 
319
  .recipe-title {
320
  color: var(--primary-color);
321
+ font-size: 1.6em;
322
+ margin-bottom: 15px;
323
  font-weight: 600;
324
  }
325
 
326
  .footer {
327
  text-align: center;
328
+ margin-top: 80px;
329
+ padding: 40px 0;
330
+ color: var(--text-color);
331
  font-size: 1em;
332
+ background: var(--card-background);
333
  border-radius: var(--border-radius);
334
+ border: 1px solid var(--glass-border);
335
  }
336
 
337
  .footer-content {
338
+ max-width: 800px;
339
  margin: 0 auto;
340
  }
341
 
 
350
 
351
  .footer-links a {
352
  color: var(--secondary-color);
353
+ margin: 0 15px;
354
  text-decoration: none;
355
  transition: color 0.3s ease;
356
  }
357
 
358
  .footer-links a:hover {
359
  color: var(--primary-color);
 
 
 
 
 
 
 
360
  }
361
 
362
  .input-group {
363
+ margin-bottom: 30px;
364
  }
365
 
366
  .input-group label {
367
  display: block;
368
+ margin-bottom: 12px;
369
+ font-weight: 500;
370
  color: var(--text-color);
371
  font-size: 1.1em;
372
  }
373
 
374
  .gallery-container {
375
  display: grid;
376
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
377
  gap: 20px;
378
  margin-top: 20px;
379
  }
 
381
  .gallery-item {
382
  border-radius: var(--border-radius);
383
  overflow: hidden;
384
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
385
  transition: transform 0.3s ease;
386
  aspect-ratio: 1 / 1;
 
387
  }
388
 
389
  .gallery-item:hover {
390
  transform: scale(1.05);
 
391
  }
392
 
 
393
  .loading-container {
394
  position: fixed;
395
  top: 0;
396
  left: 0;
397
  width: 100%;
398
  height: 100%;
399
+ background: rgba(17, 24, 39, 0.9);
400
  display: flex;
401
  justify-content: center;
402
  align-items: center;
 
412
  }
413
 
414
  .loading-spinner {
415
+ width: 60px;
416
+ height: 60px;
417
+ border: 6px solid var(--glass-background);
418
+ border-top: 6px solid var(--primary-color);
419
  border-radius: 50%;
420
+ animation: spin 1s linear infinite;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  }
422
 
423
  .loading-text {
424
  position: absolute;
425
+ margin-top: 80px;
426
+ color: var(--text-color);
427
  font-size: 1.2em;
428
  font-weight: 500;
429
  }
430
 
431
  @keyframes spin {
432
+ 0% { transform: rotate(0deg); }
433
+ 100% { transform: rotate(360deg); }
 
 
 
 
434
  }
435
 
436
  .recipe-output {
437
+ max-height: 900px;
438
  overflow-y: auto;
439
+ padding-right: 20px;
440
+ line-height: 1.7;
441
  }
442
 
443
  .recipe-output::-webkit-scrollbar {
444
+ width: 10px;
445
  }
446
 
447
  .recipe-output::-webkit-scrollbar-track {
448
+ background: var(--glass-background);
449
  border-radius: 10px;
450
  }
451
 
452
  .recipe-output::-webkit-scrollbar-thumb {
 
 
 
 
 
453
  background: var(--primary-color);
454
+ border-radius: 10px;
455
  }
456
 
457
  .recipe-output h2 {
458
  color: var(--primary-color);
459
  border-bottom: 2px solid var(--secondary-color);
460
+ padding-bottom: 12px;
461
+ font-size: 2em;
462
+ margin-top: 40px;
463
  }
464
 
465
  .recipe-output h3 {
466
  color: var(--secondary-color);
467
+ font-size: 1.5em;
468
+ margin-top: 30px;
469
  }
470
 
 
471
  input[type="password"], input[type="text"] {
472
+ border: 1px solid var(--glass-border);
473
+ background: var(--glass-background);
474
  border-radius: var(--border-radius);
475
+ padding: 14px;
476
  font-size: 1em;
477
  width: 100%;
478
+ color: var(--text-color);
479
  transition: all 0.3s ease;
 
480
  }
481
 
482
  input[type="password"]:focus, input[type="text"]:focus {
483
+ border-color: var(--primary-color);
484
  outline: none;
485
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
486
  }
487
 
 
488
  select {
489
  appearance: none;
490
+ background: var(--glass-background) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E") no-repeat right 15px center;
491
+ border: 1px solid var(--glass-border);
492
  border-radius: var(--border-radius);
493
+ padding: 14px 45px 14px 14px;
494
  font-size: 1em;
495
  width: 100%;
496
+ color: var(--text-color);
497
  transition: all 0.3s ease;
 
498
  }
499
 
500
  select:focus {
501
+ border-color: var(--primary-color);
502
  outline: none;
503
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
 
 
 
 
 
 
 
504
  }
505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  @media (max-width: 768px) {
507
  .app-title {
508
  font-size: 2.5em;
 
513
  }
514
 
515
  .input-section, .output-section {
516
+ padding: 25px;
517
  }
518
 
519
  .section-header {
520
+ font-size: 1.6em;
521
  }
522
 
523
  button.primary-button {
524
+ padding: 14px 25px;
525
  font-size: 1.1em;
526
  }
527
  }
 
536
  }
537
 
538
  .input-section, .output-section {
539
+ padding: 20px;
540
  }
541
 
542
  .section-header {
543
+ font-size: 1.4em;
544
  }
545
  }
546
 
 
547
  .gradio-container {
548
  max-width: 100% !important;
549
  }
 
553
  }
554
  """
555
 
556
+ # Custom HTML header
557
  html_header = """
558
  <div class="app-header">
559
  <div class="app-title">🍲 Visual Recipe Assistant</div>
560
+ <div class="app-subtitle">Transform your ingredients into gourmet dishes with AI-powered recipe suggestions</div>
561
  </div>
 
562
  <div id="loading-overlay" class="loading-container">
563
  <div class="loading-spinner">
564
+ <div class="loading-text">Crafting your recipes...</div>
565
  </div>
566
  </div>
 
567
  <script>
 
568
  function showLoading() {
569
  document.getElementById('loading-overlay').classList.add('visible');
570
  }
 
572
  function hideLoading() {
573
  document.getElementById('loading-overlay').classList.remove('visible');
574
  }
575
+
576
+ document.addEventListener('DOMContentLoaded', function() {
577
+ const submitBtn = document.querySelector('button.primary-button');
578
+ const output = document.querySelector('.recipe-output');
579
+
580
+ if (submitBtn && output) {
581
+ submitBtn.addEventListener('click', function() {
582
+ showLoading();
583
+
584
+ const observer = new MutationObserver(function(mutations) {
585
+ if (output.textContent.trim().length > 0) {
586
+ hideLoading();
587
+ observer.disconnect();
588
+ }
589
+ });
590
+
591
+ observer.observe(output, { childList: true, characterData: true, subtree: true });
592
+
593
+ setTimeout(hideLoading, 30000);
594
+ });
595
+ }
596
+ });
597
  </script>
598
  """
599
 
600
+ # Custom HTML footer
601
  html_footer = """
602
  <div class="footer">
603
  <div class="footer-content">
604
  <p><span class="footer-brand">🍲 Visual Recipe Assistant</span></p>
605
  <p>Powered by Meta's Llama-Vision-Free Model & Together AI</p>
606
+ <p>Create culinary masterpieces from your pantry</p>
607
  <div class="footer-links">
608
  <a href="#" target="_blank">How It Works</a>
609
  <a href="#" target="_blank">Privacy Policy</a>
 
611
  </div>
612
  </div>
613
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  """
615
 
616
+ # Create the Gradio interface
617
  with gr.Blocks(css=custom_css) as app:
618
  gr.HTML(html_header)
619
 
 
623
  gr.HTML('<h3 class="section-header"><i class="icon">πŸ”‘</i> API Configuration</h3>')
624
  api_key_input = gr.Textbox(
625
  label="Together API Key",
626
+ placeholder="Enter your Together API key...",
627
  type="password",
628
  elem_classes="input-group",
629
+ info="Your API key is used only for this session and remains private."
630
  )
631
 
632
  gr.HTML('<h3 class="section-header"><i class="icon">πŸ“·</i> Upload Ingredients</h3>')
 
633
  file_upload = gr.File(
634
  label="Upload images of ingredients",
635
  file_types=["image"],
 
683
 
684
  gr.HTML(html_footer)
685
 
 
686
  def update_gallery(files):
687
  if not files:
688
  return gr.Gallery.update(visible=False)
 
690
 
691
  file_upload.change(fn=update_gallery, inputs=file_upload, outputs=image_input)
692
 
 
693
  def process_recipe_request(api_key, files, num_recipes, dietary_restrictions, cuisine_preference):
694
  if not files:
695
  return "Please upload at least one image of ingredients."
696
 
 
697
  images = [file.name for file in files]
698
  return get_recipe_suggestions(api_key, images, num_recipes, dietary_restrictions, cuisine_preference)
699
 
 
700
  submit_button.click(
701
  fn=process_recipe_request,
702
  inputs=[api_key_input, file_upload, num_recipes, dietary_restrictions, cuisine_preference],
703
  outputs=output
704
  )
705
 
 
706
  if __name__ == "__main__":
707
  app.launch()