tdurzynski commited on
Commit
1731e5c
·
verified ·
1 Parent(s): 472a82c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +574 -268
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", "Rugelach", "Rye Bread",
 
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
- # Streamlit Layout - Authentication Section
309
- st.sidebar.title("🔑 User Authentication")
310
- auth_option = st.sidebar.radio("Select an option", ["Login", "Sign Up", "Logout"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
- if auth_option == "Sign Up":
313
- email = st.sidebar.text_input("Email")
314
- password = st.sidebar.text_input("Password", type="password")
315
- if st.sidebar.button("Sign Up"):
316
- try:
317
- if st.session_state.get("demo_mode", False):
318
- st.sidebar.success(" Demo mode: User created successfully! Please log in.")
319
- else:
320
- user = auth.create_user(email=email, password=password)
321
- st.sidebar.success(" User created successfully! Please log in.")
322
- except Exception as e:
323
- st.sidebar.error(f"Error: {e}")
324
-
325
- if auth_option == "Login":
326
- email = st.sidebar.text_input("Email")
327
- password = st.sidebar.text_input("Password", type="password")
328
- if st.sidebar.button("Login"):
329
- try:
330
- if st.session_state.get("demo_mode", False):
331
- st.session_state["user_id"] = "demo_user_123"
332
- st.session_state["tokens"] = 0 # Initialize token count
333
- st.sidebar.success("✅ Demo mode: Logged in successfully!")
334
- else:
335
- user = auth.get_user_by_email(email)
336
- st.session_state["user_id"] = user.uid
337
- st.session_state["tokens"] = 0 # Initialize token count
338
- st.sidebar.success(" Logged in successfully!")
339
- except Exception as e:
340
- st.sidebar.error(f"Login failed: {e}")
341
-
342
- if auth_option == "Logout" and "user_id" in st.session_state:
343
- del st.session_state["user_id"]
344
- st.sidebar.success("✅ Logged out successfully!")
345
-
346
- # Ensure user is logged in before uploading
347
- if "user_id" not in st.session_state and not st.session_state.get("demo_mode", False):
348
- st.warning("⚠️ Please log in to upload images.")
349
-
350
- # Add links to guidelines and terms
351
- st.markdown("### 📚 While You're Here")
352
- st.markdown("Take a moment to read our guidelines and token system:")
353
-
354
- # Use expanders instead of columns for better document display
355
- with st.expander("📋 Participation Guidelines"):
356
- try:
357
- with open("PARTICIPATION_GUIDELINES.md", "r") as f:
358
- guidelines = f.read()
359
- st.markdown(guidelines, unsafe_allow_html=True)
360
- except Exception as e:
361
- st.error(f"Could not load guidelines: {e}")
362
-
363
- with st.expander("🪙 Token Rewards System"):
364
- try:
365
- with open("TOKEN_REWARDS.md", "r") as f:
366
- rewards = f.read()
367
- st.markdown(rewards, unsafe_allow_html=True)
368
- except Exception as e:
369
- st.error(f"Could not load rewards information: {e}")
370
-
371
- with st.expander("📜 Terms of Service"):
372
- try:
373
- with open("TERMS_OF_SERVICE.md", "r") as f:
374
- terms = f.read()
375
- st.markdown(terms, unsafe_allow_html=True)
376
- except Exception as e:
377
- st.error(f"Could not load terms: {e}")
378
-
379
- st.stop()
380
-
381
- # Streamlit Layout - Main App
382
- st.title("🍽️ Food Image Review & Annotation")
383
-
384
- # Compliance & Disclaimer Section
385
- with st.expander("📜 Terms & Conditions", expanded=False):
386
- st.markdown("### **Terms & Conditions**")
387
- st.write(
388
- "By uploading an image, you agree to transfer full copyright to the research team for AI training purposes."
389
- " You are responsible for ensuring you own the image and it does not violate any copyright laws."
390
- " We do not guarantee when tokens will be redeemable. Keep track of your user ID.")
391
- terms_accepted = st.checkbox("I agree to the terms and conditions", key="terms_accepted")
392
- if not terms_accepted:
393
- st.warning("⚠️ You must agree to the terms before proceeding.")
394
  st.stop()
395
 
396
- # Upload Image
397
- uploaded_file = st.file_uploader("Upload an image of your food", type=["jpg", "png", "jpeg"])
398
- if uploaded_file:
399
- original_img = Image.open(uploaded_file)
400
- st.session_state["original_image"] = original_img
 
 
 
 
 
 
 
 
 
401
 
402
- # If an image has been uploaded, process and display it
403
- if "original_image" in st.session_state:
404
- original_img = st.session_state["original_image"]
405
-
406
- # Process the image - resize and compress with more visible difference
407
- processed_img = resize_image(original_img, max_size=512, quality=85)
408
- st.session_state["processed_image"] = processed_img
409
-
410
- # Calculate file sizes
411
- original_size = get_image_size_kb(original_img)
412
- processed_size = get_image_size_kb(processed_img)
413
- size_reduction = ((original_size - processed_size) / original_size) * 100 if original_size > 0 else 0
414
-
415
- # Display images side by side with border to highlight differences
416
- col1, col2 = st.columns(2)
417
- with col1:
418
- st.subheader("📷 Original Image")
419
- st.markdown(f"<div style='border:2px solid red;padding:5px;'>", unsafe_allow_html=True)
420
- st.image(original_img, caption=f"Original ({original_img.width}x{original_img.height} px, {original_size:.1f} KB)", use_container_width=True)
421
- st.markdown("</div>", unsafe_allow_html=True)
422
- with col2:
423
- st.subheader("🖼️ Processed Image")
424
- st.markdown(f"<div style='border:2px solid green;padding:5px;'>", unsafe_allow_html=True)
425
- st.image(processed_img, caption=f"Processed ({processed_img.width}x{processed_img.height} px, {processed_size:.1f} KB)", use_container_width=True)
426
- st.markdown("</div>", unsafe_allow_html=True)
 
 
427
 
428
- # Show size reduction
429
- if size_reduction > 5: # Only show if there's a meaningful reduction
430
- st.success(f"✅ Image size reduced by {size_reduction:.1f}% for faster uploads and processing")
431
-
432
- # Display existing food annotations if any
433
- if st.session_state["food_items"]:
434
- st.subheader("📋 Added Food Items")
435
- for i, item in enumerate(st.session_state["food_items"]):
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
- # Only show custom food name if the dropdown is empty
453
- custom_food_name = ""
454
- if food_selection == "":
455
- custom_food_name = st.text_input("Or enter a custom food name",
456
- value=st.session_state["custom_food_name"])
457
 
458
- # Determine the actual food name to use
459
- food_name = food_selection if food_selection else custom_food_name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
- col1, col2 = st.columns(2)
462
- with col1:
463
- portion_size = st.number_input("Portion Size",
464
- min_value=0.1,
465
- step=0.1,
466
- format="%.2f",
467
- value=0.1) # Always use default values
468
- with col2:
469
- portion_unit = st.selectbox("Unit",
470
- options=UNIT_OPTIONS,
471
- index=0) # Always use default values
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
 
473
- # Set Cooking Method with "Unknown" as the default (index 0)
474
- cooking_method = st.selectbox("Cooking Method (optional)",
475
- options=COOKING_METHODS,
476
- index=0) # Always use default values
477
 
478
- ingredients = st_tags.st_tags(
479
- label="Main Ingredients (Add up to 5)",
480
- text="Press enter to add",
481
- value=[],
482
- suggestions=["Salt", "Pepper", "Olive Oil", "Butter", "Garlic", "Onion", "Tomato"],
483
- maxtags=5
484
- )
485
 
486
- # Submit button inside the form
487
- submitted = st.form_submit_button(label="➕ Add This Food Item")
488
- if submitted:
489
- if add_food_item(food_name, portion_size, portion_unit, cooking_method, ingredients):
490
- # Store the custom food name if needed for future use
491
- if custom_food_name:
492
- st.session_state["custom_food_name"] = custom_food_name
493
- # Don't call reset_form_fields() here, it's already called in add_food_item
494
- st.rerun()
495
-
496
- # Make submit button more prominent
497
- st.markdown("---")
498
-
499
- # More prominent submit button with instructions
500
- st.markdown("### 📤 Submit Your Food Annotations")
501
- st.info("⚠️ After adding all your food items, click the button below to save your submission and earn tokens.")
502
-
503
- # Create a larger, more visible submit button
504
- submit_col1, submit_col2, submit_col3 = st.columns([1, 2, 1])
505
- with submit_col2:
506
- if st.button("📤 SUBMIT ALL FOOD ITEMS",
507
- disabled=len(st.session_state["food_items"]) == 0,
508
- use_container_width=True,
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 still not small enough, try more aggressive compression
534
- if processed_size > original_size * 0.8:
535
- processed_img = resize_image(original_img, max_size=480, quality=60)
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
- # Calculate tokens for this item
555
- tokens_awarded = calculate_tokens(image_quality, has_metadata, is_unique_category)
556
- total_tokens += tokens_awarded
557
-
558
- # Convert float to Decimal for DynamoDB
559
- portion_size_decimal = Decimal(str(food_item["portion_size"]))
560
-
561
- # Save metadata to DynamoDB with processed image path
562
- success = save_metadata(
563
- st.session_state["user_id"],
564
- processed_s3_path, # Use the processed image path
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 all_saved:
578
- st.session_state["tokens"] += total_tokens
579
- st.session_state["uploads_count"] += 1
580
- st.success(f"✅ All food items uploaded successfully! You earned {total_tokens} tokens.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
- # Clear the form and image for a new submission
583
- st.session_state.pop("original_image", None)
584
- st.session_state.pop("processed_image", None)
585
- st.session_state["food_items"] = []
586
- st.rerun()
 
 
 
 
 
 
 
587
  else:
588
- st.error("Failed to save some items. Please try again.")
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("---")