Open jamison-golson opened 4 months ago
The plan is to build a nice UI that allows users to explore the food they are eating. I am going to go with openfooddb. They are free and so far with my testing, the db seems to be up to date and reliable. Here is a list of keys I will be extracting from the API call: GET /api/v2/product/{barcode} List of keys: "product_name": null, "generic_name": null, "brands": null, "categories": null, "ingredients": null, "nutriments": null, "image_front_url": null, "image_nutrition_url": null, "ecoscore_grade": null, "ecoscore_score": null, "nutriscore_grade": null, "nutriscore_score": null, "states": null
This list is not complete
I ran into a weird problem when trying to render a chart using charts.js.
I originally wanted to generate the chart and the element that contains the chart after the user clicked the desired nutrition label item. I.E if a user selected 'protein' from the nutrition label, this code would fire:
const canvas = document.createElement('canvas');
canvas.id = 'nutrientChart';
detailsContainer.appendChild(canvas);
//...
currentChart = new Chart(canvas, {
type: 'doughnut',
data: {
labels: [`${nutrient.name} (${percentageOfDV.toFixed(1)}% of DV)`, 'Remaining'],
datasets: [{
data: [percentageOfDV, Math.max(0, 100 - percentageOfDV)],
backgroundColor: ['#FF6384', '#36A2EB']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: `${nutrient.name} as Percentage of Daily Value`
}
}
});
But every time, the div/canvas tags would be generated but the chart would not render. After a few sessions with gpt-4o and claude I got to this working code:
<div id="visualization-section">
<canvas id="myChart"></canvas>
</div>
const canvas = document.getElementById('myChart')
//....
const value = parseFloat(nutrient.value) || 0;
const percentageOfDV = (nutrient.value / dailyValues[nutrient.name]) * 100;
// Create the chart
currentChart = new Chart(canvas, {
type: 'doughnut',
data: {
labels: [`${nutrient.name} (${percentageOfDV.toFixed(1)}% of DV)`, 'Remaining'],
datasets: [{
data: [percentageOfDV, Math.max(0, 100 - percentageOfDV)],
backgroundColor: ['#FF6384', '#36A2EB']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: `${nutrient.name} as Percentage of Daily Value`
}
}
});
The new code, first places the canvas tag into the visualization div, then grabs it once the user clicks on the desired nutrition label item. The code finally renders the graph using the nutrition data. I will go through charts.js docs to see why the canvas element has to defined before creating the chart and update this later.
Commit: https://github.com/jamison-golson/muscle-man/commit/3af133232e929d70df3238c79804bf9484dac288
I decided to change my backend to flask so I can use python. I plan to add a lot more functionality to this very soon (i.e nutrition breakdown, workout classification, workout analysis, workout coach, etc...) and using python will make this task easier. This is my first time using flask so this will be pretty interesting.
Flask is very simple, which I like. switching everything over only took a few mins
I removed the server.js script as that was originally my 'backend' and in it's place, I added app.py. This is where the server is initialized and started. This file also houses all the routes created for various functions the backend is intended to handle. So far I have three routes: '/', '/fetch_product_data' and '/fetch_upc_from_image'
Route '/' grabs the html from the templates dir and renders it once the client goes to localhost:5000
@app.route("/")
def index():
return render_template("index.html")
Route '/fetch_upc_from_image' is called once the user uploads an image using the file upload field. It handles POST request and accepts images. The associated function then uses pyzbar, a python package built on top of zbar that reads one dimensional bar codes , to extract the UPC code from the image provided.
@app.route("/fetch_upc_from_image", methods=["POST"])
def fetch_upc_from_image():
if "image" not in request.files:
return jsonify({"error": "No image file in request"}), 400
file = request.files["image"]
if file.filename == "":
return jsonify({"error": "No selected file"}), 400
if file:
# filename = secure_filename(file.filename)
# filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# file.save(filepath)
image = Image.open(file)
barcodes = pyzbar.decode(image)
for barcode in barcodes:
upc = barcode.data.decode("utf-8")
# Here, you would process the image to extract the UPC
return jsonify(
{"upc": upc, "message": "File successfully uploaded and processed"}
)
Route '/fetch_product_data is called once a UPC code has either been successfully extracted from an image or been sent directly from the user.
// Text search functionality
const textSearch = document.createElement('input');
textSearch.type = 'text';
textSearch.id = 'text-search-input';
textSearch.placeholder = 'Enter UPC code';
document.getElementById('text-search').appendChild(textSearch);
const searchButton = document.createElement('button');
searchButton.textContent = 'Search';
searchButton.id = 'text-search-button';
document.getElementById('text-search').appendChild(searchButton);
async function fetchProductData(upc) {
try {
const response = await fetch('/fetch_product_data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ upc: upc }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error fetching product data:', error);
throw error;
}
}
It handles POST request and a string. The associated function then makes an API call to openfoodfacts and returns the requested data.
@app.route("/fetch_product_data", methods=["POST"])
def fetch_product_data():
upc = request.json["upc"]
try:
response = requests.get(f"https://world.openfoodfacts.org/api/v2/product/{upc}")
response.raise_for_status()
data = response.json()
processed_data = {
"code": data.get("code", upc),
"product_name": data.get("product", {}).get("product_name", "N/A"),
"generic_name": data.get("product", {}).get("generic_name", "N/A"),
"brands": data.get("product", {}).get("brands", "N/A"),
"categories": data.get("product", {}).get("categories", "N/A"),
"ingredients": data.get("product", {}).get("ingredients_text", "N/A"),
"nutriments": data.get("product", {}).get("nutriments", "N/A"),
"image_front_url": data.get("product", {}).get("image_front_url", "N/A"),
"image_nutrition_url": data.get("product", {}).get(
"image_nutrition_url", "N/A"
),
"ecoscore_grade": data.get("product", {}).get("ecoscore_grade", "N/A"),
"ecoscore_score": data.get("product", {}).get("ecoscore_score", "N/A"),
"nutriscore_grade": data.get("product", {}).get("nutriscore_grade", "N/A"),
"nutriscore_score": data.get("product", {}).get("nutriscore_score", "N/A"),
"states": data.get("product", {}).get("states", "N/A"),
}
return jsonify(processed_data)
except requests.RequestException as e:
return jsonify({"error": str(e)}), 400
Once I start adding LLMs/VM/TTS models, having the ability to simply define a route, give it a function and now that function is an api call on my server is great.
commit: 111681b805953a850bae3688889cda059e8298ae
I have decided to pivot the project. Originally I wanted to create a nutrition app that would help me keep up with what I am consuming. Instead, I want to go all out and develop an app that keeps track of everything related to nutrition, health and fitness. The app will be a fun and collaborative experience that allows you to keep track of all your health information but also compete with your friends/family based on what you eat and your activity level. The idea is to normalize all 'activities' (i.e boxing, basketball, weightlifting, walking, etc...) to a score based on the intensity level of the workout, the duration, the pace, etc.. Everything, that makes a workout, a workout. Then that score is also influenced by what you ate for the day. I updated the README on the dev branch to reflect these changes.
commit: 1157526
Since pivoting to a more robust and feature rich app, I decided to integrate react into my project. React makes handling data much easier through it's state management system and that is exactly what I need as my app grows and gets more complex.
So far I have designed five components: Dashboard, NutriScore, NutritionTracker, WebcamStart, and WorkoutLogger
Dashboard houses all the information that you would like to see quickly, I.E your calories consumed, workout time/duration, recent meals, etc.. Anything that you don't necessarily need to spend a lot of time looking at but is useful to see. So far the dashboard only displays simple things like calories consumed and last workout:
function Dashboard() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Nutrition Overview</h2>
<p>Calories consumed today: 1500 / 2000</p>
<div className="mt-4 h-4 bg-gray-200 rounded-full">
<div className="h-full bg-green-500 rounded-full" style={{width: '75%'}}></div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Workout Overview</h2>
<p>Last workout: Upper Body Strength (2 hours ago)</p>
<p>Next scheduled: Cardio (in 3 hours)</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Weekly Progress</h2>
<p>Weight: 70kg (-0.5kg)</p>
<p>Workouts completed: 4/5</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Goals</h2>
<ul className="list-disc list-inside">
<li>Reach 2000 calories daily</li>
<li>Complete 5 workouts this week</li>
<li>Increase protein intake</li>
</ul>
</div>
</div>
);
}
export default Dashboard;
I want to eventually make the dashboard fully customizable. The user will be able to make the layout look however they want and give it any theme they want. This will be later on down the road as I still need to get all the functionality done first.
The Nutriscore component gives a detailed breakdown of a food based on its UPC code. With some help from sonnet 3.5 I was able to recreate the nutriscore symbol that is adopted by various countries to quickly display the nutritional score based on a 5 letter grading scale. The cool thing about the nutriscore is, it's visually appealing, attention grabbing and it gives consumers a quick sense of how 'healthy' the food is before looking at the nutrition label. The nutriscore has been adopted in Belgium (2018), Switzerland (2019), Germany (2020), Luxembourg (2020) and the Netherlands (2021) by various brands. The openfoodfacts team has been keeping track of food's nutriscore worldwide since 2014 though and the information is freely available through their data dumps or api, which is great! The nutriscore algorithm is also freely available so when openfoodfacts does not provide a score, I should be able to calculate it based on the product's nutrients.
const NutriScore = ({ grade }) => {
const scores = ['a', 'b', 'c', 'd', 'e'];
const colors = ['#038141', '#85BB2F', '#FECB02', '#EE8100', '#E63E11'];
const normalizedGrade = grade ? grade.toLowerCase() : '';
return (
<div className="flex flex-col items-center bg-gray-100 p-4 rounded-lg">
<div className="text-2xl font-bold mb-2 text-gray-700">NUTRI-SCORE</div>
<div className="flex">
{scores.map((score, index) => (
<div
key={score}
className={`relative w-12 h-12 flex items-center justify-center text-white font-bold text-xl
${index === 0 ? 'rounded-l-full' : ''}
${index === scores.length - 1 ? 'rounded-r-full' : ''}`}
>
<div
className={`absolute inset-0
${index === 0 ? 'rounded-l-full' : ''}
${index === scores.length - 1 ? 'rounded-r-full' : ''}
${normalizedGrade === score ? 'ring-4 ring-blue-500 z-10' : ''}`}
style={{ backgroundColor: colors[index] }}
></div>
<span className="z-20 relative">{score.toUpperCase()}</span>
</div>
))}
</div>
</div>
);
};
The NutritionTracker component allows the user to either enter a upc code manually, upload an image and extract the upc code or manually enter the macros and other details for their food. The image upload functionality will be handled on the backend using pyzbar. It is fast and accurate.
The WorkoutLogger component is very basic and just logs a workout's name, duration and type (cardio, strength, flexibility). This is where the full workout tracker, analyzer and coach will live eventually though. I have been reading up on some reasearch into workout classification using ML and sensor data and from I have read, the results looks very promising. This will be my main focus now that I have a basic frontend going. The plan is to rely on a smartphone and/or a wearable smart device that will be the input into the workout classification model and the workout assistant. I will be writing about this more in the coming days.
The WebcamStart component is my attempt at decoding a barcode and extracting the upc code, directly in the browser. It does not work properly yet. I just implemented it to try my hand at running the decoding process in the browser but I don't know if I will keep it or just run the barcode through pyzbar
Most of these components are just skeletons of what will come.
commit: 1fedc0d687dca3c8af4ee151aadce0d772402043
I will be writing about problems/ideas/thoughts on the project in this thread