Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -2,6 +2,8 @@ import streamlit as st
|
|
2 |
import json
|
3 |
import os
|
4 |
import uuid
|
|
|
|
|
5 |
from datetime import datetime
|
6 |
from io import BytesIO
|
7 |
from decimal import Decimal # Add this import for DynamoDB float handling
|
@@ -18,12 +20,80 @@ from dotenv import load_dotenv
|
|
18 |
# Load environment variables from .env file if it exists
|
19 |
load_dotenv()
|
20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
# Load AWS credentials using correct HF Secrets
|
22 |
AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY")
|
23 |
AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")
|
24 |
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
|
25 |
S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME", "food-image-crowdsourcing")
|
26 |
DYNAMODB_TABLE = os.getenv("DYNAMODB_TABLE", "image_metadata")
|
|
|
27 |
|
28 |
# Load Firebase credentials
|
29 |
FIREBASE_CONFIG = json.loads(os.getenv("FIREBASE_CONFIG", "{}"))
|
@@ -91,10 +161,11 @@ FOOD_SUGGESTIONS = [
|
|
91 |
"Peas", "Pecan Pie", "Peking Duck", "Pelmeni", "Pepperoni Pizza", "Pierogi", "Pineapple", "Pita Bread",
|
92 |
"Pizza", "Pljeskavica", "Pork Chops", "Pork Knuckle", "Portobello Mushrooms", "Potato pancakes", "Potato Salad",
|
93 |
"Poutine", "Poppy Seed Roll", "Pudding", "Pulled Pork", "Pumpkin", "Pumpkin Pie", "Radish", "Quesadillas", "Quiche", "Ramen", "Ratatouille",
|
94 |
-
"Ravioli", "Red Pepper", "Ribeye Steak", "Ribolita", "Rich Stew", "Risotto alla Milanese", "
|
|
|
95 |
"Sachertorte", "Saffron Rice", "Salad", "Salmon", "Sarma", "Sausage", "Sauerkraut", "Seafood Pasta",
|
96 |
"Seco de Chivo", "Shashlik", "Shashuka", "Shawarma", "Shepherd's Pie", "Shopska Salad", "Shrimp", "Shrimp Skewers",
|
97 |
-
"Soft Egg Noodles", "Sopes", "Soup Dumplings", "Sour Rye Soup", "Souvlaki", "Spaghetti Carbonara", "Spinach", "Sponge Cake",
|
98 |
"Spring Salad", "Spring Rolls", "Stuffed Cabbage", "Stuffed Grape Leaves", "Stuffed Mushrooms", "Stuffed Pepper", "Supreme Pizza", "Sushi",
|
99 |
"Swwet and Sour Pork", "Sweet Potato", "Swordfish Steak", "Szarlotka", "T-bone Steak", "Tacos", "Tamales", "Tandoori Chicken", "Teriyaki", "Tarator",
|
100 |
"Texas Style Brisket", "Tilapia", "Tiramisu", "Toast", "Tomato", "Tomato Soup", "Tostada", "Tteokbokki", "Tuna Steak",
|
@@ -208,6 +279,126 @@ def upload_to_s3(image, user_id, folder="", force_quality=None):
|
|
208 |
st.error(f"Failed to upload image: {e}")
|
209 |
return None
|
210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
211 |
def save_metadata(user_id, s3_path, food_name, portion_size, portion_unit, cooking_method, ingredients, tokens_awarded):
|
212 |
"""Save metadata to DynamoDB"""
|
213 |
if st.session_state.get("demo_mode", False):
|
@@ -276,6 +467,19 @@ if "custom_food_name" not in st.session_state:
|
|
276 |
if "form_key" not in st.session_state:
|
277 |
st.session_state["form_key"] = 0 # Add a form key to force re-rendering
|
278 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
279 |
def reset_form_fields():
|
280 |
"""Reset all form fields after adding an item by incrementing the form key"""
|
281 |
# Reset custom food name
|
@@ -305,289 +509,391 @@ def add_food_item(food_name, portion_size, portion_unit, cooking_method, ingredi
|
|
305 |
st.error("❌ Please fill in all required fields")
|
306 |
return False
|
307 |
|
308 |
-
#
|
309 |
-
|
310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
311 |
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
st.
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
st.session_state
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
if
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
st.
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
if not terms_accepted:
|
393 |
-
st.warning("⚠️ You must agree to the terms before proceeding.")
|
394 |
st.stop()
|
395 |
|
396 |
-
#
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
401 |
|
402 |
-
#
|
403 |
-
if
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
st.
|
424 |
-
|
425 |
-
|
426 |
-
|
|
|
|
|
427 |
|
428 |
-
|
429 |
-
if
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
with st.expander(f"🍽️ {item['food_name']} ({item['portion_size']} {item['portion_unit']})"):
|
437 |
-
st.write(f"**Cooking Method:** {item['cooking_method']}")
|
438 |
-
st.write(f"**Ingredients:** {', '.join(item['ingredients'])}")
|
439 |
-
if st.button(f"Remove Item #{i+1}", key=f"remove_{i}"):
|
440 |
-
st.session_state["food_items"].pop(i)
|
441 |
-
st.rerun()
|
442 |
-
|
443 |
-
# Food metadata form
|
444 |
-
st.subheader("🍲 Add Food Details")
|
445 |
-
|
446 |
-
# Use Streamlit form to capture Enter key and provide a better UX
|
447 |
-
# Use a dynamic key based on form_key to force re-rendering with default values
|
448 |
-
form_key = st.session_state.get("form_key", 0)
|
449 |
-
with st.form(key=f"food_item_form_{form_key}"):
|
450 |
-
food_selection = st.selectbox("Food Name", options=[""] + FOOD_SUGGESTIONS, index=0)
|
451 |
|
452 |
-
#
|
453 |
-
|
454 |
-
|
455 |
-
custom_food_name = st.text_input("Or enter a custom food name",
|
456 |
-
value=st.session_state["custom_food_name"])
|
457 |
|
458 |
-
#
|
459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
460 |
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
472 |
|
473 |
-
#
|
474 |
-
|
475 |
-
options=COOKING_METHODS,
|
476 |
-
index=0) # Always use default values
|
477 |
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
value=[],
|
482 |
-
suggestions=["Salt", "Pepper", "Olive Oil", "Butter", "Garlic", "Onion", "Tomato"],
|
483 |
-
maxtags=5
|
484 |
-
)
|
485 |
|
486 |
-
#
|
487 |
-
|
488 |
-
|
489 |
-
if
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
type="primary"):
|
510 |
-
if not st.session_state["food_items"]:
|
511 |
-
st.error("❌ Please add at least one food item before submitting")
|
512 |
-
else:
|
513 |
-
with st.spinner("Processing your submission..."):
|
514 |
-
all_saved = True
|
515 |
-
total_tokens = 0
|
516 |
-
|
517 |
-
# Determine image quality (simplified version)
|
518 |
-
image_quality = "high" if original_img.width >= 1000 and original_img.height >= 1000 else "standard"
|
519 |
-
|
520 |
-
# Get original image file size for comparison
|
521 |
-
original_size = get_image_size_kb(original_img)
|
522 |
-
|
523 |
-
# Ensure we have a properly processed image with the right settings
|
524 |
-
# Force resize and compression with settings that guarantee size reduction
|
525 |
-
processed_img = resize_image(original_img, max_size=512, quality=85)
|
526 |
-
processed_size = get_image_size_kb(processed_img)
|
527 |
-
|
528 |
-
# If the processed image isn't smaller enough, reduce quality further
|
529 |
-
if processed_size > original_size * 0.8: # Ensure at least 20% reduction
|
530 |
-
processed_img = resize_image(original_img, max_size=512, quality=70)
|
531 |
processed_size = get_image_size_kb(processed_img)
|
532 |
|
533 |
-
# If
|
534 |
-
if processed_size > original_size * 0.8:
|
535 |
-
processed_img = resize_image(original_img, max_size=
|
536 |
-
|
537 |
-
# Upload original to raw-uploads folder
|
538 |
-
raw_s3_path = upload_to_s3(original_img, st.session_state["user_id"],
|
539 |
-
folder="raw-uploads", force_quality=95)
|
540 |
-
|
541 |
-
# Upload only one processed image to processed-512x512 folder
|
542 |
-
processed_s3_path = upload_to_s3(processed_img, st.session_state["user_id"],
|
543 |
-
folder="processed-512x512", force_quality=85)
|
544 |
-
|
545 |
-
if raw_s3_path and processed_s3_path:
|
546 |
-
# Save each food item with the processed image path
|
547 |
-
for food_item in st.session_state["food_items"]:
|
548 |
-
# Check if metadata is complete
|
549 |
-
has_metadata = True # Already validated
|
550 |
-
|
551 |
-
# Check if the food is in a unique category (simplified)
|
552 |
-
is_unique_category = food_item["food_name"] not in ["Pizza", "Burger", "Pasta", "Salad"]
|
553 |
|
554 |
-
#
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
food_item["food_name"],
|
566 |
-
portion_size_decimal, # Use Decimal type
|
567 |
-
food_item["portion_unit"],
|
568 |
-
food_item["cooking_method"],
|
569 |
-
food_item["ingredients"],
|
570 |
-
tokens_awarded
|
571 |
-
)
|
572 |
-
|
573 |
-
if not success:
|
574 |
-
all_saved = False
|
575 |
-
break
|
576 |
|
577 |
-
if
|
578 |
-
|
579 |
-
st.session_state["
|
580 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
581 |
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
587 |
else:
|
588 |
-
st.error("Failed to
|
589 |
-
else:
|
590 |
-
st.error("Failed to upload images. Please try again.")
|
591 |
|
592 |
# Display earned tokens
|
593 |
st.sidebar.markdown("---")
|
|
|
2 |
import json
|
3 |
import os
|
4 |
import uuid
|
5 |
+
import re
|
6 |
+
import requests
|
7 |
from datetime import datetime
|
8 |
from io import BytesIO
|
9 |
from decimal import Decimal # Add this import for DynamoDB float handling
|
|
|
20 |
# Load environment variables from .env file if it exists
|
21 |
load_dotenv()
|
22 |
|
23 |
+
# Detect if running on mobile
|
24 |
+
def is_mobile():
|
25 |
+
# Try to detect mobile browsers based on User-Agent
|
26 |
+
try:
|
27 |
+
user_agent = st.get_current_user().user_agent
|
28 |
+
return any(device in user_agent.lower() for device in ["android", "iphone", "ipad", "mobile"])
|
29 |
+
except:
|
30 |
+
# If we can't detect, assume it might be mobile for better experience
|
31 |
+
return False
|
32 |
+
|
33 |
+
# Auto-expand sidebar on mobile
|
34 |
+
if is_mobile() and "sidebar_expanded" not in st.session_state:
|
35 |
+
st.session_state["sidebar_expanded"] = True
|
36 |
+
# Note: This doesn't directly control Streamlit's sidebar, but we'll use this flag
|
37 |
+
|
38 |
+
# Custom CSS for better mobile experience
|
39 |
+
st.markdown("""
|
40 |
+
<style>
|
41 |
+
/* Larger buttons for touch interfaces */
|
42 |
+
.stButton>button {
|
43 |
+
font-size: 18px !important;
|
44 |
+
padding: 12px 16px !important;
|
45 |
+
margin: 6px 0 !important;
|
46 |
+
}
|
47 |
+
|
48 |
+
/* Larger text inputs */
|
49 |
+
.stTextInput>div>div>input {
|
50 |
+
font-size: 16px !important;
|
51 |
+
padding: 10px !important;
|
52 |
+
}
|
53 |
+
|
54 |
+
/* Improve spacing for mobile */
|
55 |
+
.block-container {
|
56 |
+
padding-top: 2rem !important;
|
57 |
+
padding-bottom: 2rem !important;
|
58 |
+
}
|
59 |
+
|
60 |
+
/* Make form elements more touch-friendly */
|
61 |
+
.stSelectbox, .stNumberInput {
|
62 |
+
margin-bottom: 12px !important;
|
63 |
+
}
|
64 |
+
|
65 |
+
/* Visual cue for sidebar toggle on mobile */
|
66 |
+
@media (max-width: 768px) {
|
67 |
+
[data-testid="stSidebarNav"] {
|
68 |
+
position: relative;
|
69 |
+
}
|
70 |
+
[data-testid="stSidebarNav"]::after {
|
71 |
+
content: "👈 Menu";
|
72 |
+
position: absolute;
|
73 |
+
right: -60px;
|
74 |
+
top: 0;
|
75 |
+
background: #f0f2f6;
|
76 |
+
padding: 8px;
|
77 |
+
border-radius: 4px;
|
78 |
+
animation: pulse 2s infinite;
|
79 |
+
z-index: 1000;
|
80 |
+
}
|
81 |
+
@keyframes pulse {
|
82 |
+
0% { opacity: 1; }
|
83 |
+
50% { opacity: 0.6; }
|
84 |
+
100% { opacity: 1; }
|
85 |
+
}
|
86 |
+
}
|
87 |
+
</style>
|
88 |
+
""", unsafe_allow_html=True)
|
89 |
+
|
90 |
# Load AWS credentials using correct HF Secrets
|
91 |
AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY")
|
92 |
AWS_SECRET_KEY = os.getenv("AWS_SECRET_KEY")
|
93 |
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
|
94 |
S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME", "food-image-crowdsourcing")
|
95 |
DYNAMODB_TABLE = os.getenv("DYNAMODB_TABLE", "image_metadata")
|
96 |
+
HF_API_TOKEN = os.getenv("HF_API_TOKEN", "") # For Hugging Face Inference API
|
97 |
|
98 |
# Load Firebase credentials
|
99 |
FIREBASE_CONFIG = json.loads(os.getenv("FIREBASE_CONFIG", "{}"))
|
|
|
161 |
"Peas", "Pecan Pie", "Peking Duck", "Pelmeni", "Pepperoni Pizza", "Pierogi", "Pineapple", "Pita Bread",
|
162 |
"Pizza", "Pljeskavica", "Pork Chops", "Pork Knuckle", "Portobello Mushrooms", "Potato pancakes", "Potato Salad",
|
163 |
"Poutine", "Poppy Seed Roll", "Pudding", "Pulled Pork", "Pumpkin", "Pumpkin Pie", "Radish", "Quesadillas", "Quiche", "Ramen", "Ratatouille",
|
164 |
+
"Ravioli", "Red Pepper", "Ribeye Steak", "Ribolita", "Rich Stew", "Risotto alla Milanese", "Roll (Multi-grain)", "Roll (Multigrain)",
|
165 |
+
"Roll (Poppyseed)","Roll (Rye)", "Roll (Sesame)", "Roll (Sourdough)", "Roll (Wheat)", "Roll (White)", "Rugelach", "Rye Bread",
|
166 |
"Sachertorte", "Saffron Rice", "Salad", "Salmon", "Sarma", "Sausage", "Sauerkraut", "Seafood Pasta",
|
167 |
"Seco de Chivo", "Shashlik", "Shashuka", "Shawarma", "Shepherd's Pie", "Shopska Salad", "Shrimp", "Shrimp Skewers",
|
168 |
+
"Soft Egg Noodles", "Sopes", "Soup Dumplings", "Sour-Dough Bread", "Sour Rye Soup", "Souvlaki", "Spaghetti Carbonara", "Spinach", "Sponge Cake",
|
169 |
"Spring Salad", "Spring Rolls", "Stuffed Cabbage", "Stuffed Grape Leaves", "Stuffed Mushrooms", "Stuffed Pepper", "Supreme Pizza", "Sushi",
|
170 |
"Swwet and Sour Pork", "Sweet Potato", "Swordfish Steak", "Szarlotka", "T-bone Steak", "Tacos", "Tamales", "Tandoori Chicken", "Teriyaki", "Tarator",
|
171 |
"Texas Style Brisket", "Tilapia", "Tiramisu", "Toast", "Tomato", "Tomato Soup", "Tostada", "Tteokbokki", "Tuna Steak",
|
|
|
279 |
st.error(f"Failed to upload image: {e}")
|
280 |
return None
|
281 |
|
282 |
+
def transcribe_audio(audio_bytes):
|
283 |
+
"""Transcribe audio using Hugging Face's Whisper model via Inference API"""
|
284 |
+
try:
|
285 |
+
# Convert audio bytes to file-like object
|
286 |
+
audio_file = BytesIO(audio_bytes)
|
287 |
+
|
288 |
+
# Free Hugging Face Inference API endpoint for Whisper Tiny model
|
289 |
+
API_URL = "https://api-inference.huggingface.co/models/openai/whisper-tiny"
|
290 |
+
|
291 |
+
headers = {}
|
292 |
+
if HF_API_TOKEN:
|
293 |
+
headers["Authorization"] = f"Bearer {HF_API_TOKEN}"
|
294 |
+
|
295 |
+
# Make request to the free HF API
|
296 |
+
response = requests.post(
|
297 |
+
API_URL,
|
298 |
+
headers=headers,
|
299 |
+
data=audio_file
|
300 |
+
)
|
301 |
+
|
302 |
+
if response.status_code == 200:
|
303 |
+
result = response.json()
|
304 |
+
# Extract text from response
|
305 |
+
transcript = result.get("text", "")
|
306 |
+
return transcript
|
307 |
+
else:
|
308 |
+
# Fallback for rate limiting or errors
|
309 |
+
st.warning("Could not transcribe audio. Please try typing instead.")
|
310 |
+
return ""
|
311 |
+
|
312 |
+
except Exception as e:
|
313 |
+
st.error(f"Transcription error: {e}")
|
314 |
+
return ""
|
315 |
+
|
316 |
+
def parse_food_annotation(transcript, focus_fields=None):
|
317 |
+
"""
|
318 |
+
Parse the transcribed text to extract food details
|
319 |
+
Simple rule-based parsing for common patterns
|
320 |
+
Optional focus_fields parameter to prioritize specific fields
|
321 |
+
"""
|
322 |
+
# Default values
|
323 |
+
parsed_data = {
|
324 |
+
"food_name": "",
|
325 |
+
"portion_size": None,
|
326 |
+
"portion_unit": "",
|
327 |
+
"cooking_method": "Unknown",
|
328 |
+
"ingredients": []
|
329 |
+
}
|
330 |
+
|
331 |
+
# Try to extract food name
|
332 |
+
# Start with items from our suggestion list
|
333 |
+
for food in FOOD_SUGGESTIONS:
|
334 |
+
if food.lower() in transcript.lower():
|
335 |
+
parsed_data["food_name"] = food
|
336 |
+
break
|
337 |
+
|
338 |
+
# If no match, use the first few words as the food name
|
339 |
+
if not parsed_data["food_name"]:
|
340 |
+
words = transcript.split()
|
341 |
+
if words:
|
342 |
+
# Use first 3 words or less as food name
|
343 |
+
parsed_data["food_name"] = " ".join(words[:min(3, len(words))])
|
344 |
+
|
345 |
+
# Try to extract portion size and unit
|
346 |
+
# Look for patterns like "100 grams" or "2 slices"
|
347 |
+
size_match = re.search(r'(\d+(?:\.\d+)?)\s*(grams?|ounces?|cups?|pieces?|slices?)', transcript.lower())
|
348 |
+
if size_match:
|
349 |
+
try:
|
350 |
+
parsed_data["portion_size"] = float(size_match.group(1))
|
351 |
+
# Map to our standard units
|
352 |
+
unit_text = size_match.group(2).rstrip('s') # Remove plural 's'
|
353 |
+
if unit_text == "gram":
|
354 |
+
parsed_data["portion_unit"] = "grams"
|
355 |
+
elif unit_text == "ounce":
|
356 |
+
parsed_data["portion_unit"] = "ounce(s)"
|
357 |
+
elif unit_text == "cup":
|
358 |
+
parsed_data["portion_unit"] = "cup(s)"
|
359 |
+
elif unit_text == "slice":
|
360 |
+
parsed_data["portion_unit"] = "slice(s)"
|
361 |
+
elif unit_text == "piece":
|
362 |
+
parsed_data["portion_unit"] = "piece(s)"
|
363 |
+
except:
|
364 |
+
pass
|
365 |
+
|
366 |
+
# Try to extract cooking method
|
367 |
+
for method in COOKING_METHODS:
|
368 |
+
if method.lower() in transcript.lower():
|
369 |
+
parsed_data["cooking_method"] = method
|
370 |
+
break
|
371 |
+
|
372 |
+
# Simple ingredient extraction
|
373 |
+
common_ingredients = ["cheese", "tomato", "lettuce", "onion", "beef", "chicken", "salt", "pepper"]
|
374 |
+
found_ingredients = []
|
375 |
+
for ingredient in common_ingredients:
|
376 |
+
if ingredient.lower() in transcript.lower():
|
377 |
+
found_ingredients.append(ingredient.capitalize())
|
378 |
+
|
379 |
+
if found_ingredients:
|
380 |
+
parsed_data["ingredients"] = found_ingredients
|
381 |
+
|
382 |
+
# If focus_fields is provided, prioritize extracting those fields
|
383 |
+
if focus_fields:
|
384 |
+
# More targeted extraction methods for specific fields
|
385 |
+
if "food_name" in focus_fields:
|
386 |
+
# More aggressive food name extraction
|
387 |
+
# e.g., assume the entire transcript might be just the food name
|
388 |
+
if not parsed_data["food_name"]:
|
389 |
+
parsed_data["food_name"] = transcript.strip()
|
390 |
+
|
391 |
+
if "portion_size" in focus_fields or "portion_unit" in focus_fields:
|
392 |
+
# More aggressive portion extraction
|
393 |
+
# e.g., assume numbers are portion sizes even without units
|
394 |
+
if not parsed_data["portion_size"]:
|
395 |
+
number_match = re.search(r'(\d+(?:\.\d+)?)', transcript)
|
396 |
+
if number_match:
|
397 |
+
parsed_data["portion_size"] = float(number_match.group(1))
|
398 |
+
parsed_data["portion_unit"] = "piece(s)" # Default unit
|
399 |
+
|
400 |
+
return parsed_data
|
401 |
+
|
402 |
def save_metadata(user_id, s3_path, food_name, portion_size, portion_unit, cooking_method, ingredients, tokens_awarded):
|
403 |
"""Save metadata to DynamoDB"""
|
404 |
if st.session_state.get("demo_mode", False):
|
|
|
467 |
if "form_key" not in st.session_state:
|
468 |
st.session_state["form_key"] = 0 # Add a form key to force re-rendering
|
469 |
|
470 |
+
# Track partial annotation state for audio recording
|
471 |
+
if "partial_annotation" not in st.session_state:
|
472 |
+
st.session_state["partial_annotation"] = {
|
473 |
+
"food_name": "",
|
474 |
+
"portion_size": None,
|
475 |
+
"portion_unit": "",
|
476 |
+
"cooking_method": "",
|
477 |
+
"ingredients": []
|
478 |
+
}
|
479 |
+
|
480 |
+
if "missing_fields" not in st.session_state:
|
481 |
+
st.session_state["missing_fields"] = []
|
482 |
+
|
483 |
def reset_form_fields():
|
484 |
"""Reset all form fields after adding an item by incrementing the form key"""
|
485 |
# Reset custom food name
|
|
|
509 |
st.error("❌ Please fill in all required fields")
|
510 |
return False
|
511 |
|
512 |
+
# Main App UI
|
513 |
+
def main():
|
514 |
+
# Check if we should display the mobile welcome dialog
|
515 |
+
if is_mobile() and "mobile_welcome_shown" not in st.session_state:
|
516 |
+
st.session_state["mobile_welcome_shown"] = True
|
517 |
+
# Show welcome message for first-time mobile users
|
518 |
+
st.info("👋 Welcome to the Food Image Crowdsourcing App! Tap the menu icon (≡) in the top-right corner to login.")
|
519 |
+
|
520 |
+
# Improved authentication UI for mobile
|
521 |
+
if is_mobile():
|
522 |
+
# Show prominent login button if not logged in
|
523 |
+
if "user_id" not in st.session_state and not st.session_state.get("demo_mode", False):
|
524 |
+
st.title("🍽️ Food Image Crowdsourcing")
|
525 |
+
auth_container = st.container()
|
526 |
+
auth_container.warning("⚠️ Please login to continue")
|
527 |
+
|
528 |
+
# Big prominent login buttons
|
529 |
+
login_col1, login_col2 = st.columns(2)
|
530 |
+
with login_col1:
|
531 |
+
if st.button("📱 LOGIN", use_container_width=True, type="primary"):
|
532 |
+
st.session_state["sidebar_expanded"] = True
|
533 |
+
st.rerun()
|
534 |
+
with login_col2:
|
535 |
+
if st.button("✍️ SIGN UP", use_container_width=True):
|
536 |
+
st.session_state["sidebar_expanded"] = True
|
537 |
+
st.rerun()
|
538 |
+
|
539 |
+
st.markdown("### 🍕 Help us collect food images!")
|
540 |
+
st.markdown("Take pictures of your meals, label them, and earn tokens!")
|
541 |
+
|
542 |
+
# Add links to guidelines and terms
|
543 |
+
st.markdown("### 📚 Learn More")
|
544 |
+
with st.expander("📋 How It Works"):
|
545 |
+
try:
|
546 |
+
with open("PARTICIPATION_GUIDELINES.md", "r") as f:
|
547 |
+
guidelines = f.read()
|
548 |
+
st.markdown(guidelines, unsafe_allow_html=True)
|
549 |
+
except Exception as e:
|
550 |
+
st.error(f"Could not load guidelines: {e}")
|
551 |
+
|
552 |
+
with st.expander("🪙 Earn Tokens"):
|
553 |
+
try:
|
554 |
+
with open("TOKEN_REWARDS.md", "r") as f:
|
555 |
+
rewards = f.read()
|
556 |
+
st.markdown(rewards, unsafe_allow_html=True)
|
557 |
+
except Exception as e:
|
558 |
+
st.error(f"Could not load rewards information: {e}")
|
559 |
+
|
560 |
+
st.stop()
|
561 |
|
562 |
+
# Streamlit Layout - Authentication Section in Sidebar
|
563 |
+
st.sidebar.title("🔑 User Authentication")
|
564 |
+
auth_option = st.sidebar.radio("Select an option", ["Login", "Sign Up", "Logout"])
|
565 |
+
|
566 |
+
if auth_option == "Sign Up":
|
567 |
+
email = st.sidebar.text_input("Email")
|
568 |
+
password = st.sidebar.text_input("Password", type="password")
|
569 |
+
if st.sidebar.button("Sign Up"):
|
570 |
+
try:
|
571 |
+
if st.session_state.get("demo_mode", False):
|
572 |
+
st.sidebar.success("✅ Demo mode: User created successfully! Please log in.")
|
573 |
+
else:
|
574 |
+
user = auth.create_user(email=email, password=password)
|
575 |
+
st.sidebar.success("✅ User created successfully! Please log in.")
|
576 |
+
# Show continue button after signup
|
577 |
+
if st.sidebar.button("▶️ Continue to Login"):
|
578 |
+
st.rerun()
|
579 |
+
except Exception as e:
|
580 |
+
st.sidebar.error(f"Error: {e}")
|
581 |
+
|
582 |
+
if auth_option == "Login":
|
583 |
+
email = st.sidebar.text_input("Email")
|
584 |
+
password = st.sidebar.text_input("Password", type="password")
|
585 |
+
if st.sidebar.button("Login"):
|
586 |
+
try:
|
587 |
+
if st.session_state.get("demo_mode", False):
|
588 |
+
st.session_state["user_id"] = "demo_user_123"
|
589 |
+
st.session_state["tokens"] = 0 # Initialize token count
|
590 |
+
st.sidebar.success("✅ Demo mode: Logged in successfully!")
|
591 |
+
# Show continue button after login
|
592 |
+
if st.sidebar.button("▶️ Continue to App"):
|
593 |
+
st.rerun()
|
594 |
+
else:
|
595 |
+
user = auth.get_user_by_email(email)
|
596 |
+
st.session_state["user_id"] = user.uid
|
597 |
+
st.session_state["tokens"] = 0 # Initialize token count
|
598 |
+
st.sidebar.success("✅ Logged in successfully!")
|
599 |
+
# Show continue button after login
|
600 |
+
if st.sidebar.button("▶️ Continue to App"):
|
601 |
+
st.rerun()
|
602 |
+
except Exception as e:
|
603 |
+
st.sidebar.error(f"Login failed: {e}")
|
604 |
+
|
605 |
+
if auth_option == "Logout" and "user_id" in st.session_state:
|
606 |
+
del st.session_state["user_id"]
|
607 |
+
st.sidebar.success("✅ Logged out successfully!")
|
608 |
+
|
609 |
+
# Ensure user is logged in before uploading
|
610 |
+
if "user_id" not in st.session_state and not st.session_state.get("demo_mode", False):
|
611 |
+
st.warning("⚠️ Please log in to upload images.")
|
612 |
+
|
613 |
+
# Add links to guidelines and terms
|
614 |
+
st.markdown("### 📚 While You're Here")
|
615 |
+
st.markdown("Take a moment to read our guidelines and token system:")
|
616 |
+
|
617 |
+
# Use expanders instead of columns for better document display
|
618 |
+
with st.expander("📋 Participation Guidelines"):
|
619 |
+
try:
|
620 |
+
with open("PARTICIPATION_GUIDELINES.md", "r") as f:
|
621 |
+
guidelines = f.read()
|
622 |
+
st.markdown(guidelines, unsafe_allow_html=True)
|
623 |
+
except Exception as e:
|
624 |
+
st.error(f"Could not load guidelines: {e}")
|
625 |
+
|
626 |
+
with st.expander("🪙 Token Rewards System"):
|
627 |
+
try:
|
628 |
+
with open("TOKEN_REWARDS.md", "r") as f:
|
629 |
+
rewards = f.read()
|
630 |
+
st.markdown(rewards, unsafe_allow_html=True)
|
631 |
+
except Exception as e:
|
632 |
+
st.error(f"Could not load rewards information: {e}")
|
633 |
+
|
634 |
+
with st.expander("📜 Terms of Service"):
|
635 |
+
try:
|
636 |
+
with open("TERMS_OF_SERVICE.md", "r") as f:
|
637 |
+
terms = f.read()
|
638 |
+
st.markdown(terms, unsafe_allow_html=True)
|
639 |
+
except Exception as e:
|
640 |
+
st.error(f"Could not load terms: {e}")
|
641 |
+
|
|
|
|
|
642 |
st.stop()
|
643 |
|
644 |
+
# Streamlit Layout - Main App
|
645 |
+
st.title("🍽️ Food Image Review & Annotation")
|
646 |
+
|
647 |
+
# Compliance & Disclaimer Section
|
648 |
+
with st.expander("📜 Terms & Conditions", expanded=False):
|
649 |
+
st.markdown("### **Terms & Conditions**")
|
650 |
+
st.write(
|
651 |
+
"By uploading an image, you agree to transfer full copyright to the research team for AI training purposes."
|
652 |
+
" You are responsible for ensuring you own the image and it does not violate any copyright laws."
|
653 |
+
" We do not guarantee when tokens will be redeemable. Keep track of your user ID.")
|
654 |
+
terms_accepted = st.checkbox("I agree to the terms and conditions", key="terms_accepted")
|
655 |
+
if not terms_accepted:
|
656 |
+
st.warning("⚠️ You must agree to the terms before proceeding.")
|
657 |
+
st.stop()
|
658 |
|
659 |
+
# Mobile-friendly workflow indicator
|
660 |
+
if is_mobile():
|
661 |
+
# Show a progress indicator at the top
|
662 |
+
st.markdown("### 📱 Mobile Workflow")
|
663 |
+
workflow_steps = ["📷 Upload Image", "🔍 Review Image", "🏷️ Add Food Details", "📤 Submit"]
|
664 |
+
|
665 |
+
# Determine current step
|
666 |
+
current_step = 0
|
667 |
+
if "original_image" in st.session_state:
|
668 |
+
current_step = 1
|
669 |
+
if st.session_state["food_items"]:
|
670 |
+
current_step = 2
|
671 |
+
|
672 |
+
# Display steps with highlight on current
|
673 |
+
step_cols = st.columns(len(workflow_steps))
|
674 |
+
for i, (col, step) in enumerate(zip(step_cols, workflow_steps)):
|
675 |
+
if i == current_step:
|
676 |
+
col.markdown(f"**{step}** ✓")
|
677 |
+
else:
|
678 |
+
col.markdown(f"{step}")
|
679 |
+
|
680 |
+
st.markdown("---")
|
681 |
+
|
682 |
+
# Upload Image - Larger and more prominent on mobile
|
683 |
+
if is_mobile():
|
684 |
+
st.markdown("### 📷 Take or Upload a Food Photo")
|
685 |
+
st.info("Take a picture of your meal or upload an existing photo")
|
686 |
|
687 |
+
uploaded_file = st.file_uploader("Upload an image of your food", type=["jpg", "png", "jpeg"])
|
688 |
+
if uploaded_file:
|
689 |
+
original_img = Image.open(uploaded_file)
|
690 |
+
st.session_state["original_image"] = original_img
|
691 |
+
|
692 |
+
# If an image has been uploaded, process and display it
|
693 |
+
if "original_image" in st.session_state:
|
694 |
+
original_img = st.session_state["original_image"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
695 |
|
696 |
+
# Process the image - resize and compress with more visible difference
|
697 |
+
processed_img = resize_image(original_img, max_size=512, quality=85)
|
698 |
+
st.session_state["processed_image"] = processed_img
|
|
|
|
|
699 |
|
700 |
+
# Calculate file sizes
|
701 |
+
original_size = get_image_size_kb(original_img)
|
702 |
+
processed_size = get_image_size_kb(processed_img)
|
703 |
+
size_reduction = ((original_size - processed_size) / original_size) * 100 if original_size > 0 else 0
|
704 |
+
|
705 |
+
# On mobile, stack images vertically instead of side by side
|
706 |
+
if is_mobile():
|
707 |
+
st.markdown("### 🔍 Review Your Image")
|
708 |
+
|
709 |
+
# Original image
|
710 |
+
st.subheader("📷 Original Image")
|
711 |
+
st.markdown(f"<div style='border:2px solid red;padding:5px;'>", unsafe_allow_html=True)
|
712 |
+
st.image(original_img, caption=f"Original ({original_img.width}x{original_img.height} px, {original_size:.1f} KB)", use_container_width=True)
|
713 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
714 |
+
|
715 |
+
# Processed image
|
716 |
+
st.subheader("🖼️ Processed Image")
|
717 |
+
st.markdown(f"<div style='border:2px solid green;padding:5px;'>", unsafe_allow_html=True)
|
718 |
+
st.image(processed_img, caption=f"Processed ({processed_img.width}x{processed_img.height} px, {processed_size:.1f} KB)", use_container_width=True)
|
719 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
720 |
+
else:
|
721 |
+
# Desktop layout (side by side)
|
722 |
+
col1, col2 = st.columns(2)
|
723 |
+
with col1:
|
724 |
+
st.subheader("📷 Original Image")
|
725 |
+
st.markdown(f"<div style='border:2px solid red;padding:5px;'>", unsafe_allow_html=True)
|
726 |
+
st.image(original_img, caption=f"Original ({original_img.width}x{original_img.height} px, {original_size:.1f} KB)", use_container_width=True)
|
727 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
728 |
+
with col2:
|
729 |
+
st.subheader("🖼️ Processed Image")
|
730 |
+
st.markdown(f"<div style='border:2px solid green;padding:5px;'>", unsafe_allow_html=True)
|
731 |
+
st.image(processed_img, caption=f"Processed ({processed_img.width}x{processed_img.height} px, {processed_size:.1f} KB)", use_container_width=True)
|
732 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
733 |
|
734 |
+
# Show size reduction
|
735 |
+
if size_reduction > 5: # Only show if there's a meaningful reduction
|
736 |
+
st.success(f"✅ Image size reduced by {size_reduction:.1f}% for faster uploads and processing")
|
737 |
+
|
738 |
+
# Display existing food annotations if any
|
739 |
+
if st.session_state["food_items"]:
|
740 |
+
st.subheader("📋 Added Food Items")
|
741 |
+
for i, item in enumerate(st.session_state["food_items"]):
|
742 |
+
with st.expander(f"🍽️ {item['food_name']} ({item['portion_size']} {item['portion_unit']})"):
|
743 |
+
st.write(f"**Cooking Method:** {item['cooking_method']}")
|
744 |
+
st.write(f"**Ingredients:** {', '.join(item['ingredients'])}")
|
745 |
+
if st.button(f"Remove Item #{i+1}", key=f"remove_{i}"):
|
746 |
+
st.session_state["food_items"].pop(i)
|
747 |
+
st.rerun()
|
748 |
+
|
749 |
+
# Food metadata form
|
750 |
+
st.subheader("�� Add Food Details")
|
751 |
+
|
752 |
+
# Use Streamlit form to capture Enter key and provide a better UX
|
753 |
+
# Use a dynamic key based on form_key to force re-rendering with default values
|
754 |
+
form_key = st.session_state.get("form_key", 0)
|
755 |
+
with st.form(key=f"food_item_form_{form_key}"):
|
756 |
+
food_selection = st.selectbox("Food Name", options=[""] + FOOD_SUGGESTIONS, index=0)
|
757 |
+
|
758 |
+
# Only show custom food name if the dropdown is empty
|
759 |
+
custom_food_name = ""
|
760 |
+
if food_selection == "":
|
761 |
+
custom_food_name = st.text_input("Or enter a custom food name",
|
762 |
+
value=st.session_state["custom_food_name"])
|
763 |
+
|
764 |
+
# Determine the actual food name to use
|
765 |
+
food_name = food_selection if food_selection else custom_food_name
|
766 |
+
|
767 |
+
col1, col2 = st.columns(2)
|
768 |
+
with col1:
|
769 |
+
portion_size = st.number_input("Portion Size",
|
770 |
+
min_value=0.1,
|
771 |
+
step=0.1,
|
772 |
+
format="%.2f",
|
773 |
+
value=0.1) # Always use default values
|
774 |
+
with col2:
|
775 |
+
portion_unit = st.selectbox("Unit",
|
776 |
+
options=UNIT_OPTIONS,
|
777 |
+
index=0) # Always use default values
|
778 |
+
|
779 |
+
# Set Cooking Method with "Unknown" as the default (index 0)
|
780 |
+
cooking_method = st.selectbox("Cooking Method (optional)",
|
781 |
+
options=COOKING_METHODS,
|
782 |
+
index=0) # Always use default values
|
783 |
+
|
784 |
+
ingredients = st_tags.st_tags(
|
785 |
+
label="Main Ingredients (Add up to 5)",
|
786 |
+
text="Press enter to add",
|
787 |
+
value=[],
|
788 |
+
suggestions=["Salt", "Pepper", "Olive Oil", "Butter", "Garlic", "Onion", "Tomato"],
|
789 |
+
maxtags=5
|
790 |
+
)
|
791 |
+
|
792 |
+
# Submit button inside the form
|
793 |
+
submitted = st.form_submit_button(label="➕ Add This Food Item")
|
794 |
+
if submitted:
|
795 |
+
if add_food_item(food_name, portion_size, portion_unit, cooking_method, ingredients):
|
796 |
+
# Store the custom food name if needed for future use
|
797 |
+
if custom_food_name:
|
798 |
+
st.session_state["custom_food_name"] = custom_food_name
|
799 |
+
# Don't call reset_form_fields() here, it's already called in add_food_item
|
800 |
+
st.rerun()
|
801 |
|
802 |
+
# Make submit button more prominent
|
803 |
+
st.markdown("---")
|
|
|
|
|
804 |
|
805 |
+
# More prominent submit button with instructions
|
806 |
+
st.markdown("### 📤 Submit Your Food Annotations")
|
807 |
+
st.info("⚠️ After adding all your food items, click the button below to save your submission and earn tokens.")
|
|
|
|
|
|
|
|
|
808 |
|
809 |
+
# Create a larger, more visible submit button
|
810 |
+
submit_col1, submit_col2, submit_col3 = st.columns([1, 2, 1])
|
811 |
+
with submit_col2:
|
812 |
+
if st.button("📤 SUBMIT ALL FOOD ITEMS",
|
813 |
+
disabled=len(st.session_state["food_items"]) == 0,
|
814 |
+
use_container_width=True,
|
815 |
+
type="primary"):
|
816 |
+
if not st.session_state["food_items"]:
|
817 |
+
st.error("❌ Please add at least one food item before submitting")
|
818 |
+
else:
|
819 |
+
with st.spinner("Processing your submission..."):
|
820 |
+
all_saved = True
|
821 |
+
total_tokens = 0
|
822 |
+
|
823 |
+
# Determine image quality (simplified version)
|
824 |
+
image_quality = "high" if original_img.width >= 1000 and original_img.height >= 1000 else "standard"
|
825 |
+
|
826 |
+
# Get original image file size for comparison
|
827 |
+
original_size = get_image_size_kb(original_img)
|
828 |
+
|
829 |
+
# Ensure we have a properly processed image with the right settings
|
830 |
+
# Force resize and compression with settings that guarantee size reduction
|
831 |
+
processed_img = resize_image(original_img, max_size=512, quality=85)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
832 |
processed_size = get_image_size_kb(processed_img)
|
833 |
|
834 |
+
# If the processed image isn't smaller enough, reduce quality further
|
835 |
+
if processed_size > original_size * 0.8: # Ensure at least 20% reduction
|
836 |
+
processed_img = resize_image(original_img, max_size=512, quality=70)
|
837 |
+
processed_size = get_image_size_kb(processed_img)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
838 |
|
839 |
+
# If still not small enough, try more aggressive compression
|
840 |
+
if processed_size > original_size * 0.8:
|
841 |
+
processed_img = resize_image(original_img, max_size=480, quality=60)
|
842 |
+
|
843 |
+
# Upload original to raw-uploads folder
|
844 |
+
raw_s3_path = upload_to_s3(original_img, st.session_state["user_id"],
|
845 |
+
folder="raw-uploads", force_quality=95)
|
846 |
+
|
847 |
+
# Upload only one processed image to processed-512x512 folder
|
848 |
+
processed_s3_path = upload_to_s3(processed_img, st.session_state["user_id"],
|
849 |
+
folder="processed-512x512", force_quality=85)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
850 |
|
851 |
+
if raw_s3_path and processed_s3_path:
|
852 |
+
# Save each food item with the processed image path
|
853 |
+
for food_item in st.session_state["food_items"]:
|
854 |
+
# Check if metadata is complete
|
855 |
+
has_metadata = True # Already validated
|
856 |
+
|
857 |
+
# Check if the food is in a unique category (simplified)
|
858 |
+
is_unique_category = food_item["food_name"] not in ["Pizza", "Burger", "Pasta", "Salad"]
|
859 |
+
|
860 |
+
# Calculate tokens for this item
|
861 |
+
tokens_awarded = calculate_tokens(image_quality, has_metadata, is_unique_category)
|
862 |
+
total_tokens += tokens_awarded
|
863 |
+
|
864 |
+
# Convert float to Decimal for DynamoDB
|
865 |
+
portion_size_decimal = Decimal(str(food_item["portion_size"]))
|
866 |
+
|
867 |
+
# Save metadata to DynamoDB with processed image path
|
868 |
+
success = save_metadata(
|
869 |
+
st.session_state["user_id"],
|
870 |
+
processed_s3_path, # Use the processed image path
|
871 |
+
food_item["food_name"],
|
872 |
+
portion_size_decimal, # Use Decimal type
|
873 |
+
food_item["portion_unit"],
|
874 |
+
food_item["cooking_method"],
|
875 |
+
food_item["ingredients"],
|
876 |
+
tokens_awarded
|
877 |
+
)
|
878 |
+
|
879 |
+
if not success:
|
880 |
+
all_saved = False
|
881 |
+
break
|
882 |
|
883 |
+
if all_saved:
|
884 |
+
st.session_state["tokens"] += total_tokens
|
885 |
+
st.session_state["uploads_count"] += 1
|
886 |
+
st.success(f"✅ All food items uploaded successfully! You earned {total_tokens} tokens.")
|
887 |
+
|
888 |
+
# Clear the form and image for a new submission
|
889 |
+
st.session_state.pop("original_image", None)
|
890 |
+
st.session_state.pop("processed_image", None)
|
891 |
+
st.session_state["food_items"] = []
|
892 |
+
st.rerun()
|
893 |
+
else:
|
894 |
+
st.error("Failed to save some items. Please try again.")
|
895 |
else:
|
896 |
+
st.error("Failed to upload images. Please try again.")
|
|
|
|
|
897 |
|
898 |
# Display earned tokens
|
899 |
st.sidebar.markdown("---")
|