Skip to content

Commit e319f90

Browse files
authored
Merge pull request #28 from jeremyLabrado/fix/migrate-to-nova-canvas
Modernize preferences UI, add new parameters and improve image generation prompt
2 parents c5ff1d3 + 2f8b126 commit e319f90

File tree

13 files changed

+1265
-344
lines changed

13 files changed

+1265
-344
lines changed

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ We developed this exhibit to create an interactive serverless application using
2020
## Features overview
2121

2222
- **Personalized product information**: Curious about what is in a product and if it is good for you?
23-
Just scan the barcode with the app for an explained list of ingredients/alergens and a personalized summary based on your preferences.
23+
Just scan the barcode with the app for an explained list of ingredients/allergens and a personalized summary based on your preferences, health goals, and dietary restrictions. The app provides direct allergen detection and quantitative nutritional analysis using data from Open Food Facts.
2424

2525
- **Personalized recipe generator**: Capture a photo of the ingredients in your fridge, and the app will generate recipes based on your preferences using those ingredients.
2626

@@ -61,6 +61,8 @@ The architecture of the application can be split in 4 blocks:
6161
#### Product Management:
6262

6363
- **Implementation**: Using AWS Lambda for server-side logic and a database from [Open Food Facts](https://fr.openfoodfacts.org/) accessed through APIs.
64+
- **Data Integration**: The app retrieves allergen tags and nutritional data (calories, sugars, fats, proteins, etc.) from Open Food Facts API for accurate, data-driven recommendations.
65+
- **Safety Features**: Direct allergen detection from API data ensures reliable allergen warnings without relying solely on ingredient text parsing.
6466

6567
#### Product Summary and Generative Recipe:
6668

@@ -223,6 +225,31 @@ The output format is a Markdown file to faciliate the display of the recipe on t
223225

224226
- **Solution**: The same prompt is utilized, but the LLM is instructed to generate the output in a specific language, catering to the user's language preference (English/French).
225227

228+
**Direct Allergen Detection and Nutritional Analysis**
229+
230+
- **Challenge**: Ensuring accurate allergen warnings and providing quantitative nutritional recommendations based on user health goals.
231+
232+
- **Solution**: Integrated Open Food Facts API to retrieve `allergens_tags` and `nutriments` fields. The app filters key nutritional data (calories, sugars, fats, proteins, salt, fiber) and stores them in DynamoDB. Product summaries now include:
233+
- Direct allergen detection with prominent warnings
234+
- Specific nutritional values (e.g., "539 kcal/100g", "56.3g sugars")
235+
- Health goal-specific recommendations (weight loss, muscle gain, etc.)
236+
- Dietary preference compatibility (keto, low carb, low sodium)
237+
238+
A custom `DecimalEncoder` handles DynamoDB Decimal type serialization to JSON, ensuring proper data formatting in API responses.
239+
240+
**Dietary Labels and Religious Requirements**
241+
242+
- **Challenge**: Accurately identifying vegan, vegetarian, halal, and kosher products without relying solely on ingredient text parsing.
243+
244+
- **Solution**: Integrated `labels_tags` and `categories` fields from Open Food Facts API. The app now provides:
245+
- Direct vegan/vegetarian detection from product labels
246+
- Halal and kosher certification identification
247+
- Category-based product context for better recommendations
248+
- Religious requirement matching with clear certification status
249+
- Fallback to ingredient analysis when labels are unavailable
250+
251+
This reduces LLM hallucination and provides more confident dietary and religious compatibility assessments.
252+
226253

227254

228255
## Key Technical Features

lambda/barcode_image/index.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def generate_product_summary_prompt(
2424
):
2525
return f"""Human:
2626
You are a nutrition expert. I will give you a nutritional list of a product, as sold per 100 g / 100 ml.
27-
What, in your opinion, is the most unhealthy component? You must imagine the quantity of the most unhealthy component in terms of the quotient so that I realize how bad it is.
28-
Respond in the form of a prompt in English, which will be used to generate an image in English. Respond only with the prompt.
27+
What, in your opinion, is the most notable nutritional characteristic? Create a visual representation using actual food items or ingredients.
28+
Respond in the form of a prompt in English for image generation. The prompt should describe a clean, professional food photography scene WITHOUT any text, labels, or words visible in the image.
2929
-----------------------------------
3030
Example 1:
3131
Nutritional list:
@@ -41,7 +41,7 @@ def generate_product_summary_prompt(
4141
"salt_100g": "0,107 g"
4242
4343
Response:
44-
A jar of chocolate hazelnut spread next to 14 cubes of sugar labeled "diabetes danger".
44+
A jar of chocolate hazelnut spread surrounded by sugar cubes and hazelnuts on a white background, professional food photography, no text or labels visible.
4545
4646
-------------------------------------
4747
Example 2:
@@ -59,7 +59,7 @@ def generate_product_summary_prompt(
5959
"proteins_100g": "12,5",
6060
"salt_100g": "1,02"
6161
62-
Response: "an hamburger and an evil teaspoon full of salt"
62+
Response: A hamburger with a small pile of salt crystals beside it, professional food photography, clean white background, no text visible.
6363
-------------------------------------
6464
Example 3
6565
Liste nutritionnelle:
@@ -75,7 +75,7 @@ def generate_product_summary_prompt(
7575
"fiber_100g": "3.5",
7676
"proteins_100g": "6.1",
7777
"salt_100g": "1.2"
78-
Response: "barbecue potato chips and a salt shaker"
78+
Response: Barbecue potato chips in a bowl with salt crystals scattered around, professional food photography, no text or labels.
7979
8080
-------------------------------------
8181
@@ -128,7 +128,8 @@ def get_image(prompt):
128128
body=json.dumps({
129129
"taskType": "TEXT_IMAGE",
130130
"textToImageParams": {
131-
"text": f"{prompt}, professional food photography, studio lighting, clean composition, high resolution, editorial quality, commercial product photography style"
131+
"text": f"{prompt}, professional food photography, studio lighting, clean composition, high resolution, editorial quality, commercial product photography style",
132+
"negativeText": "text, words, letters, labels, writing, typography, captions, watermarks, logos, signs, numbers, alphabet"
132133
},
133134
"imageGenerationConfig": {
134135
"numberOfImages": 1,

lambda/barcode_ingredients/index.py

Lines changed: 107 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import time
22
import boto3
33
import json
4+
from decimal import Decimal
45
from botocore.exceptions import ClientError
56
import urllib.parse
67
import requests
@@ -16,6 +17,12 @@
1617
bedrock = boto3.client("bedrock-runtime")
1718
dynamodb = boto3.resource('dynamodb')
1819

20+
class DecimalEncoder(json.JSONEncoder):
21+
def default(self, obj):
22+
if isinstance(obj, Decimal):
23+
return float(obj)
24+
return super(DecimalEncoder, self).default(obj)
25+
1926

2027
PRODUCT_TABLE_NAME = os.environ['PRODUCT_TABLE_NAME']
2128
OPEN_FOOD_FACTS_TABLE_NAME = os.environ['OPEN_FOOD_FACTS_TABLE_NAME']
@@ -101,7 +108,7 @@ def make_api_request(product_code):
101108
api_url = os.environ.get('API_URL')
102109
url = f'{api_url}/api/v2/product/{product_code}'
103110
headers = {'Accept': 'application/json'}
104-
fixed_params = {'fields': 'ingredients_text,additives_tags,product_name'}
111+
fixed_params = {'fields': 'ingredients_text,additives_tags,product_name,allergens_tags,nutriments,labels_tags,categories'}
105112
full_url = f'{url}?{urllib.parse.urlencode(fixed_params)}'
106113
logger.debug("Calling the API to get the product informations")
107114

@@ -158,6 +165,41 @@ def call_claude_haiku(prompt_text):
158165
results = response_body.get("content")[0].get("text")
159166
return results
160167

168+
def filter_nutriments(nutriments):
169+
"""
170+
Filters nutriments to only include key nutritional fields.
171+
Converts numeric values to Decimal for DynamoDB compatibility.
172+
173+
Args:
174+
nutriments (dict): Full nutriments dictionary from API
175+
176+
Returns:
177+
dict: Filtered nutriments with only key fields (excludes None values)
178+
"""
179+
if not nutriments:
180+
return {}
181+
182+
key_fields = [
183+
'energy-kcal_100g',
184+
'carbohydrates_100g',
185+
'sugars_100g',
186+
'fat_100g',
187+
'saturated-fat_100g',
188+
'salt_100g',
189+
'sodium_100g',
190+
'proteins_100g',
191+
'fiber_100g'
192+
]
193+
194+
# Filter out None values and convert to Decimal for DynamoDB
195+
filtered = {}
196+
for k in key_fields:
197+
if k in nutriments and nutriments[k] is not None:
198+
# Convert to Decimal for DynamoDB compatibility
199+
filtered[k] = Decimal(str(nutriments[k]))
200+
201+
return filtered
202+
161203
def clean_text_in_brackets(text):
162204
"""
163205
Removes text enclosed within parentheses, square brackets, or curly braces from the given text.
@@ -257,8 +299,8 @@ def get_product_from_db(product_code, language):
257299
product_code (str): The code of the product to retrieve information for.
258300
259301
Returns:
260-
tuple: A tuple containing product name, ingredients, and additives if the product is found in the database;
261-
otherwise, returns (None, None, None).
302+
tuple: A tuple containing product name, ingredients, additives, allergens, nutriments, labels, and categories if the product is found in the database;
303+
otherwise, returns (None, None, None, None, None, None, None).
262304
"""
263305

264306
table = dynamodb.Table(PRODUCT_TABLE_NAME)
@@ -276,19 +318,23 @@ def get_product_from_db(product_code, language):
276318
product_name = item.get('product_name')
277319
ingredients = item.get('ingredients')
278320
additives = item.get('additives')
321+
allergens = item.get('allergens_tags', [])
322+
nutriments = item.get('nutriments', {})
323+
labels = item.get('labels_tags', [])
324+
categories = item.get('categories', '')
279325

280326
# Check if either ingredients or additives don't exist, then return None
281327
if ingredients is None or additives is None:
282-
return None, None, None
283-
return product_name, ingredients, additives
328+
return None, None, None, None, None, None, None
329+
return product_name, ingredients, additives, allergens, nutriments, labels, categories
284330
else:
285-
return None, None, None
331+
return None, None, None, None, None, None, None
286332
except Exception as e:
287333
logger.error("Error while getting the Product from database", e)
288-
return None, None, None
334+
return None, None, None, None, None, None, None
289335

290336
@tracer.capture_method
291-
def write_product_to_db(product_code, language, product_name, ingredients, additives):
337+
def write_product_to_db(product_code, language, product_name, ingredients, additives, allergens, nutriments, labels, categories):
292338
"""
293339
Writes product information product table.
294340
@@ -297,6 +343,10 @@ def write_product_to_db(product_code, language, product_name, ingredients, addit
297343
product_name (str): The name of the product.
298344
ingredients (list): The list of ingredients of the product.
299345
additives (list): The list of additives of the product.
346+
allergens (list): The list of allergens tags.
347+
nutriments (dict): The filtered nutriments data.
348+
labels (list): The list of labels tags.
349+
categories (str): The product categories.
300350
301351
Returns:
302352
None
@@ -316,6 +366,22 @@ def write_product_to_db(product_code, language, product_name, ingredients, addit
316366

317367
if ingredients is not None:
318368
item['ingredients'] = ingredients
369+
370+
# Only add allergens if list is not empty
371+
if allergens and len(allergens) > 0:
372+
item['allergens_tags'] = allergens
373+
374+
# Only add nutriments if dict is not empty
375+
if nutriments and len(nutriments) > 0:
376+
item['nutriments'] = nutriments
377+
378+
# Only add labels if list is not empty
379+
if labels and len(labels) > 0:
380+
item['labels_tags'] = labels
381+
382+
# Only add categories if not empty
383+
if categories:
384+
item['categories'] = categories
319385

320386
# Write item to DynamoDB table
321387
response = table.put_item(Item=item)
@@ -370,8 +436,8 @@ def fetch_new_product(product_code, language):
370436
product_code (str): The code of the product to fetch.
371437
372438
Returns:
373-
tuple: A tuple containing dictionaries of ingredients and additives, along with the product name,
374-
if the product information is successfully fetched from the API; otherwise, returns (None, None, None, None).
439+
tuple: A tuple containing dictionaries of ingredients, additives, allergens, nutriments, labels, categories, and product name,
440+
if the product information is successfully fetched from the API; otherwise, returns (None, None, None, None, None, None, None).
375441
"""
376442

377443
response_data = get_product_from_open_food_facts_db(product_code)
@@ -382,6 +448,11 @@ def fetch_new_product(product_code, language):
382448
if response_data is not None:
383449

384450
additives=[]
451+
allergens=[]
452+
nutriments={}
453+
labels=[]
454+
categories=''
455+
385456
if 'product' not in response_data or 'ingredients_text' not in response_data['product']:
386457
raise ValueError("Missing ingredients in Open Food Facts API. Unable to generate a personalized summary for this product.")
387458

@@ -397,11 +468,27 @@ def fetch_new_product(product_code, language):
397468
response_additives = additives
398469
if additives:
399470
response_additives = parse_additives_description(additives, language)
471+
472+
# Extract allergens
473+
if 'product' in response_data and 'allergens_tags' in response_data['product']:
474+
allergens = response_data['product']['allergens_tags']
475+
476+
# Extract and filter nutriments
477+
if 'product' in response_data and 'nutriments' in response_data['product']:
478+
nutriments = filter_nutriments(response_data['product']['nutriments'])
479+
480+
# Extract labels
481+
if 'product' in response_data and 'labels_tags' in response_data['product']:
482+
labels = response_data['product']['labels_tags']
483+
484+
# Extract categories
485+
if 'product' in response_data and 'categories' in response_data['product']:
486+
categories = response_data['product']['categories']
400487

401-
return response_ingredients, response_additives, product_name
488+
return response_ingredients, response_additives, product_name, allergens, nutriments, labels, categories
402489

403490
else:
404-
return None, None, None
491+
return None, None, None, None, None, None, None
405492

406493
@logger.inject_lambda_context(log_event=True)
407494
@tracer.capture_lambda_handler
@@ -412,18 +499,18 @@ def handler(event, context):
412499
product_code = fields[1]
413500
language = fields[2]
414501
logger.debug("ProductCode="+product_code)
415-
product_name, response_ingredients, response_additives = get_product_from_db(product_code, language)
502+
product_name, response_ingredients, response_additives, allergens, nutriments, labels, categories = get_product_from_db(product_code, language)
416503

417504
if product_name is not None:
418505
logger.debug("Product found in the database")
419506
else:
420507
logger.debug("Product not found in the database")
421508

422-
response_ingredients, response_additives, product_name = fetch_new_product(product_code, language)
509+
response_ingredients, response_additives, product_name, allergens, nutriments, labels, categories = fetch_new_product(product_code, language)
423510

424511

425512
if response_ingredients is not None:
426-
write_product_to_db(product_code, language, product_name, response_ingredients, response_additives)
513+
write_product_to_db(product_code, language, product_name, response_ingredients, response_additives, allergens, nutriments, labels, categories)
427514

428515
if(response_ingredients is None):
429516
response_ingredients = {"Ingredients Generation Error": "Description Generation Unavailable"}
@@ -433,14 +520,18 @@ def handler(event, context):
433520
"ingredients_description": response_ingredients,
434521
"additives_description": response_additives,
435522
"product_name": product_name,
523+
"allergens_tags": allergens,
524+
"nutriments": nutriments,
525+
"labels_tags": labels,
526+
"categories": categories,
436527
}
437528

438529
logger.debug("Response", extra=response)
439530

440531
# Return JSON response
441532
return {
442533
"statusCode": 200,
443-
"body": json.dumps(response),
534+
"body": json.dumps(response, cls=DecimalEncoder),
444535
"headers": {
445536
"Access-Control-Allow-Headers": "*",
446537
"Access-Control-Allow-Origin": "*",

0 commit comments

Comments
 (0)