Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -12,9 +12,10 @@ import openai
|
|
12 |
# Initialize YOLOv8 for multi-label food detection
|
13 |
model = YOLO("yolov8n.pt") # Nano model for speed, fine-tune on food data later
|
14 |
|
15 |
-
# Agent Functions (registered with AutoGen)
|
16 |
def recognize_foods(image):
|
17 |
start = time.time()
|
|
|
18 |
# Check if image is valid (not None or empty)
|
19 |
if image is None or image.size == 0:
|
20 |
print("Warning: Invalid or empty image detected.")
|
@@ -23,6 +24,8 @@ def recognize_foods(image):
|
|
23 |
# Convert to RGB and resize to 640x640
|
24 |
try:
|
25 |
pil_image = Image.fromarray(image).convert('RGB').resize((640, 640))
|
|
|
|
|
26 |
except Exception as e:
|
27 |
print(f"Error processing image: {str(e)}")
|
28 |
return [] # Return empty list on preprocessing failure
|
@@ -34,10 +37,11 @@ def recognize_foods(image):
|
|
34 |
for result in results:
|
35 |
for cls in result.boxes.cls:
|
36 |
label = model.names[int(cls)]
|
37 |
-
if "food" in label.lower() or label in ["broccoli", "carrot", "green bean", "chicken", "turkey", "pasta", "rice", "
|
38 |
conf = result.boxes.conf[result.boxes.cls == cls].item()
|
39 |
foods.append((label, conf))
|
40 |
detected = True
|
|
|
41 |
if not detected:
|
42 |
print("Warning: No food items detected in the image. Check YOLOv8 model or image quality.")
|
43 |
print(f"Recognition took {time.time() - start:.2f}s: Found foods {foods}")
|
@@ -45,6 +49,7 @@ def recognize_foods(image):
|
|
45 |
|
46 |
def estimate_sizes(image, foods):
|
47 |
start = time.time()
|
|
|
48 |
if not foods:
|
49 |
print("Warning: No foods to estimate sizes for.")
|
50 |
return {}
|
@@ -52,6 +57,7 @@ def estimate_sizes(image, foods):
|
|
52 |
# Resize to match YOLO output for consistency
|
53 |
try:
|
54 |
img_cv = cv2.cvtColor(image, cv2.COLOR_RGB2BGR).resize((640, 640))
|
|
|
55 |
except Exception as e:
|
56 |
print(f"Error resizing image for size estimation: {str(e)}")
|
57 |
return {}
|
@@ -70,6 +76,7 @@ def estimate_sizes(image, foods):
|
|
70 |
# Simple heuristic: scale box area to grams (tune this based on data)
|
71 |
grams = min(500, int((box_area / (640 * 640)) * 500)) # Cap at 500g
|
72 |
sizes[label] = grams
|
|
|
73 |
|
74 |
# Fallback: even split if no boxes found
|
75 |
if not sizes:
|
@@ -77,12 +84,14 @@ def estimate_sizes(image, foods):
|
|
77 |
area = total_area / len(foods) # Even split for now
|
78 |
grams = min(500, int(area / (640 * 640) * 100)) # 100g per ~640k pixels, capped at 500g
|
79 |
sizes[food] = grams
|
|
|
80 |
|
81 |
print(f"Size estimation took {time.time() - start:.2f}s: Estimated sizes {sizes}")
|
82 |
return sizes
|
83 |
|
84 |
def fetch_nutrition(foods_with_sizes, nutritionix_key):
|
85 |
start = time.time()
|
|
|
86 |
if not nutritionix_key:
|
87 |
print("Warning: No Nutritionix API key provided.")
|
88 |
return "Please provide a Nutritionix API key for nutrition data."
|
@@ -98,12 +107,13 @@ def fetch_nutrition(foods_with_sizes, nutritionix_key):
|
|
98 |
}
|
99 |
# Build query from foods and sizes
|
100 |
query = "\n".join([f"{size}g {food}" for food, size in foods_with_sizes.items()])
|
|
|
101 |
body = {"query": query}
|
102 |
|
103 |
try:
|
104 |
response = requests.post(url, headers=headers, json=body, timeout=10)
|
105 |
if response.status_code != 200:
|
106 |
-
print(f"Nutritionix API error: {response.text}")
|
107 |
return f"Nutritionix API error: {response.text}"
|
108 |
|
109 |
data = response.json().get("foods", [])
|
@@ -127,6 +137,7 @@ def fetch_nutrition(foods_with_sizes, nutritionix_key):
|
|
127 |
|
128 |
def get_nutrition_advice(nutrition_data, openai_key):
|
129 |
start = time.time()
|
|
|
130 |
if not openai_key:
|
131 |
print("Warning: No OpenAI API key provided—skipping advice.")
|
132 |
return "No OpenAI key provided—skipping advice."
|
@@ -140,6 +151,7 @@ def get_nutrition_advice(nutrition_data, openai_key):
|
|
140 |
[f"- {food}: {data['calories']} cal, {data['protein']}g protein, {data['fat']}g fat, {data['carbs']}g carbs"
|
141 |
for food, data in nutrition_data.items()]
|
142 |
)
|
|
|
143 |
response = openai.Completion.create(
|
144 |
model="text-davinci-003",
|
145 |
prompt=prompt,
|
@@ -157,31 +169,31 @@ def get_nutrition_advice(nutrition_data, openai_key):
|
|
157 |
# AutoGen Agent Definitions
|
158 |
food_recognizer = AssistantAgent(
|
159 |
name="FoodRecognizer",
|
160 |
-
system_message="Identify all food items in the image and return a list of (label, probability) pairs. Parse the message for the image data and call recognize_foods with it.",
|
161 |
function_map={"recognize_foods": recognize_foods}
|
162 |
)
|
163 |
|
164 |
size_estimator = AssistantAgent(
|
165 |
name="SizeEstimator",
|
166 |
-
system_message="Estimate portion sizes in grams for each recognized food based on the image. Parse the previous message for the list of foods and call estimate_sizes with the image and foods.",
|
167 |
function_map={"estimate_sizes": estimate_sizes}
|
168 |
)
|
169 |
|
170 |
nutrition_fetcher = AssistantAgent(
|
171 |
name="NutritionFetcher",
|
172 |
-
system_message="Fetch nutritional data from the Nutritionix API using the user's key. Parse the previous message for the foods and sizes dictionary and the initial message for the Nutritionix key, then call fetch_nutrition.",
|
173 |
function_map={"fetch_nutrition": fetch_nutrition}
|
174 |
)
|
175 |
|
176 |
advice_agent = AssistantAgent(
|
177 |
name="NutritionAdvisor",
|
178 |
-
system_message="Provide basic nutrition advice based on the food data using the user's OpenAI key. Parse the previous message for the nutrition data and the initial message for the OpenAI key, then call get_nutrition_advice.",
|
179 |
function_map={"get_nutrition_advice": get_nutrition_advice}
|
180 |
)
|
181 |
|
182 |
orchestrator = AssistantAgent(
|
183 |
name="Orchestrator",
|
184 |
-
system_message="Coordinate the workflow, format the output, and return the final result as text. Start by asking FoodRecognizer to process the image, then SizeEstimator, then NutritionFetcher, then NutritionAdvisor (if OpenAI key provided), and finally format the results into 'Food Analysis:\\n- food1 (size1g, prob1% confidence): calories1 cal, protein1g protein, fat1g fat, carbs1g carbs\\n...' for each food, followed by '\\nNutrition Advice:\\n' and the advice if available.",
|
185 |
function_map={}
|
186 |
)
|
187 |
|
@@ -207,11 +219,14 @@ manager = GroupChatManager(groupchat=group_chat)
|
|
207 |
# Orchestrator Logic (via AutoGen chat)
|
208 |
def orchestrate_workflow(image, nutritionix_key, openai_key=None):
|
209 |
start = time.time()
|
|
|
|
|
210 |
|
211 |
# Initiate chat with Orchestrator, passing image and keys as message
|
212 |
message = f"Process this image: {image} with Nutritionix key: {nutritionix_key}"
|
213 |
if openai_key:
|
214 |
message += f" and OpenAI key: {openai_key}"
|
|
|
215 |
response = manager.initiate_chat(
|
216 |
orchestrator,
|
217 |
message=message,
|
@@ -223,11 +238,14 @@ def orchestrate_workflow(image, nutritionix_key, openai_key=None):
|
|
223 |
# Get the last message from chat history
|
224 |
last_message = response.chat_history[-1]
|
225 |
result = last_message.get("content", "No output from agents.")
|
|
|
226 |
else:
|
227 |
result = "No output from agents."
|
|
|
228 |
|
229 |
if isinstance(result, dict):
|
230 |
result = result.get("text", "No text output from agents.")
|
|
|
231 |
|
232 |
# Split result into nutrition and advice if OpenAI key was provided
|
233 |
if openai_key and isinstance(result, str) and "\nNutrition Advice:\n" in result:
|
@@ -238,7 +256,7 @@ def orchestrate_workflow(image, nutritionix_key, openai_key=None):
|
|
238 |
nutrition = result if result != "No output from agents." else "No nutrition data."
|
239 |
advice = "No advice available (OpenAI key required)."
|
240 |
|
241 |
-
print(f"Total time: {time.time() - start:.2f}s")
|
242 |
return nutrition, advice
|
243 |
|
244 |
# Gradio Interface
|
|
|
12 |
# Initialize YOLOv8 for multi-label food detection
|
13 |
model = YOLO("yolov8n.pt") # Nano model for speed, fine-tune on food data later
|
14 |
|
15 |
+
# Agent Functions (registered with AutoGen, enhanced debugging)
|
16 |
def recognize_foods(image):
|
17 |
start = time.time()
|
18 |
+
print(f"Recognize_foods called with image shape: {image.shape if image is not None else 'None'}")
|
19 |
# Check if image is valid (not None or empty)
|
20 |
if image is None or image.size == 0:
|
21 |
print("Warning: Invalid or empty image detected.")
|
|
|
24 |
# Convert to RGB and resize to 640x640
|
25 |
try:
|
26 |
pil_image = Image.fromarray(image).convert('RGB').resize((640, 640))
|
27 |
+
img_np = np.array(pil_image)
|
28 |
+
print(f"Image converted to RGB, shape: {img_np.shape}, min RGB: {img_np.min()}, max RGB: {img_np.max()}")
|
29 |
except Exception as e:
|
30 |
print(f"Error processing image: {str(e)}")
|
31 |
return [] # Return empty list on preprocessing failure
|
|
|
37 |
for result in results:
|
38 |
for cls in result.boxes.cls:
|
39 |
label = model.names[int(cls)]
|
40 |
+
if "food" in label.lower() or label in ["waffle fry", "lettuce", "cucumber", "tomato", "broccoli", "carrot", "green bean", "chicken", "turkey", "pasta", "rice", "potato", "bread", "curry"]: # Expanded list
|
41 |
conf = result.boxes.conf[result.boxes.cls == cls].item()
|
42 |
foods.append((label, conf))
|
43 |
detected = True
|
44 |
+
print(f"Detected: {label} with confidence {conf:.2f}, box: {result.boxes.xyxy[result.boxes.cls == cls]}")
|
45 |
if not detected:
|
46 |
print("Warning: No food items detected in the image. Check YOLOv8 model or image quality.")
|
47 |
print(f"Recognition took {time.time() - start:.2f}s: Found foods {foods}")
|
|
|
49 |
|
50 |
def estimate_sizes(image, foods):
|
51 |
start = time.time()
|
52 |
+
print(f"Estimate_sizes called with foods: {foods}")
|
53 |
if not foods:
|
54 |
print("Warning: No foods to estimate sizes for.")
|
55 |
return {}
|
|
|
57 |
# Resize to match YOLO output for consistency
|
58 |
try:
|
59 |
img_cv = cv2.cvtColor(image, cv2.COLOR_RGB2BGR).resize((640, 640))
|
60 |
+
print(f"Image resized to shape: {img_cv.shape}")
|
61 |
except Exception as e:
|
62 |
print(f"Error resizing image for size estimation: {str(e)}")
|
63 |
return {}
|
|
|
76 |
# Simple heuristic: scale box area to grams (tune this based on data)
|
77 |
grams = min(500, int((box_area / (640 * 640)) * 500)) # Cap at 500g
|
78 |
sizes[label] = grams
|
79 |
+
print(f"Estimated size for {label}: {grams}g (via bounding box)")
|
80 |
|
81 |
# Fallback: even split if no boxes found
|
82 |
if not sizes:
|
|
|
84 |
area = total_area / len(foods) # Even split for now
|
85 |
grams = min(500, int(area / (640 * 640) * 100)) # 100g per ~640k pixels, capped at 500g
|
86 |
sizes[food] = grams
|
87 |
+
print(f"Estimated size for {food}: {grams}g (via fallback)")
|
88 |
|
89 |
print(f"Size estimation took {time.time() - start:.2f}s: Estimated sizes {sizes}")
|
90 |
return sizes
|
91 |
|
92 |
def fetch_nutrition(foods_with_sizes, nutritionix_key):
|
93 |
start = time.time()
|
94 |
+
print(f"Fetch_nutrition called with foods_with_sizes: {foods_with_sizes}, key: {nutritionix_key[:5]}... (partial)")
|
95 |
if not nutritionix_key:
|
96 |
print("Warning: No Nutritionix API key provided.")
|
97 |
return "Please provide a Nutritionix API key for nutrition data."
|
|
|
107 |
}
|
108 |
# Build query from foods and sizes
|
109 |
query = "\n".join([f"{size}g {food}" for food, size in foods_with_sizes.items()])
|
110 |
+
print(f"Nutritionix query: {query}")
|
111 |
body = {"query": query}
|
112 |
|
113 |
try:
|
114 |
response = requests.post(url, headers=headers, json=body, timeout=10)
|
115 |
if response.status_code != 200:
|
116 |
+
print(f"Nutritionix API error (status {response.status_code}): {response.text}")
|
117 |
return f"Nutritionix API error: {response.text}"
|
118 |
|
119 |
data = response.json().get("foods", [])
|
|
|
137 |
|
138 |
def get_nutrition_advice(nutrition_data, openai_key):
|
139 |
start = time.time()
|
140 |
+
print(f"Get_nutrition_advice called with nutrition_data: {nutrition_data}, key: {openai_key[:5]}... (partial)")
|
141 |
if not openai_key:
|
142 |
print("Warning: No OpenAI API key provided—skipping advice.")
|
143 |
return "No OpenAI key provided—skipping advice."
|
|
|
151 |
[f"- {food}: {data['calories']} cal, {data['protein']}g protein, {data['fat']}g fat, {data['carbs']}g carbs"
|
152 |
for food, data in nutrition_data.items()]
|
153 |
)
|
154 |
+
print(f"OpenAI prompt: {prompt}")
|
155 |
response = openai.Completion.create(
|
156 |
model="text-davinci-003",
|
157 |
prompt=prompt,
|
|
|
169 |
# AutoGen Agent Definitions
|
170 |
food_recognizer = AssistantAgent(
|
171 |
name="FoodRecognizer",
|
172 |
+
system_message="Identify all food items in the image and return a list of (label, probability) pairs. Parse the message for the image data (e.g., 'Process this image: <numpy_array>') and call recognize_foods with it.",
|
173 |
function_map={"recognize_foods": recognize_foods}
|
174 |
)
|
175 |
|
176 |
size_estimator = AssistantAgent(
|
177 |
name="SizeEstimator",
|
178 |
+
system_message="Estimate portion sizes in grams for each recognized food based on the image. Parse the previous message for the list of foods (e.g., '[(\"food1\", 0.85), ...]') and call estimate_sizes with the image and foods from the message history.",
|
179 |
function_map={"estimate_sizes": estimate_sizes}
|
180 |
)
|
181 |
|
182 |
nutrition_fetcher = AssistantAgent(
|
183 |
name="NutritionFetcher",
|
184 |
+
system_message="Fetch nutritional data from the Nutritionix API using the user's key. Parse the previous message for the foods and sizes dictionary (e.g., {'food1': 150, ...}) and the initial message for the Nutritionix key (e.g., 'with Nutritionix key: <key>'), then call fetch_nutrition.",
|
185 |
function_map={"fetch_nutrition": fetch_nutrition}
|
186 |
)
|
187 |
|
188 |
advice_agent = AssistantAgent(
|
189 |
name="NutritionAdvisor",
|
190 |
+
system_message="Provide basic nutrition advice based on the food data using the user's OpenAI key. Parse the previous message for the nutrition data (e.g., {'food1': {'calories': 200, ...}}) and the initial message for the OpenAI key (e.g., 'with OpenAI key: <key>'), then call get_nutrition_advice.",
|
191 |
function_map={"get_nutrition_advice": get_nutrition_advice}
|
192 |
)
|
193 |
|
194 |
orchestrator = AssistantAgent(
|
195 |
name="Orchestrator",
|
196 |
+
system_message="Coordinate the workflow, format the output, and return the final result as text. Parse the initial message for the image, Nutritionix key, and OpenAI key. Start by asking FoodRecognizer to process the image, then SizeEstimator, then NutritionFetcher, then NutritionAdvisor (if OpenAI key provided), and finally format the results into 'Food Analysis:\\n- food1 (size1g, prob1% confidence): calories1 cal, protein1g protein, fat1g fat, carbs1g carbs\\n...' for each food, followed by '\\nNutrition Advice:\\n' and the advice if available.",
|
197 |
function_map={}
|
198 |
)
|
199 |
|
|
|
219 |
# Orchestrator Logic (via AutoGen chat)
|
220 |
def orchestrate_workflow(image, nutritionix_key, openai_key=None):
|
221 |
start = time.time()
|
222 |
+
print(f"Orchestrate_workflow called with image shape: {image.shape if image is not None else 'None'}, "
|
223 |
+
f"Nutritionix key: {nutritionix_key[:5]}..., OpenAI key: {openai_key[:5]}... (partial)")
|
224 |
|
225 |
# Initiate chat with Orchestrator, passing image and keys as message
|
226 |
message = f"Process this image: {image} with Nutritionix key: {nutritionix_key}"
|
227 |
if openai_key:
|
228 |
message += f" and OpenAI key: {openai_key}"
|
229 |
+
print(f"Starting chat with message: {message[:100]}...") # Truncate for readability
|
230 |
response = manager.initiate_chat(
|
231 |
orchestrator,
|
232 |
message=message,
|
|
|
238 |
# Get the last message from chat history
|
239 |
last_message = response.chat_history[-1]
|
240 |
result = last_message.get("content", "No output from agents.")
|
241 |
+
print(f"Chat history last message: {result}")
|
242 |
else:
|
243 |
result = "No output from agents."
|
244 |
+
print("Warning: No chat history in response.")
|
245 |
|
246 |
if isinstance(result, dict):
|
247 |
result = result.get("text", "No text output from agents.")
|
248 |
+
print(f"Result is dict, extracted text: {result}")
|
249 |
|
250 |
# Split result into nutrition and advice if OpenAI key was provided
|
251 |
if openai_key and isinstance(result, str) and "\nNutrition Advice:\n" in result:
|
|
|
256 |
nutrition = result if result != "No output from agents." else "No nutrition data."
|
257 |
advice = "No advice available (OpenAI key required)."
|
258 |
|
259 |
+
print(f"Total time: {time.time() - start:.2f}s, Nutrition: {nutrition[:50]}..., Advice: {advice[:50]}...")
|
260 |
return nutrition, advice
|
261 |
|
262 |
# Gradio Interface
|