← Projects

MacroTracker

I built a food tracker where I log a meal by typing or saying one sentence, and a worker on my Mac turns it into entries with Apple's on-device model.

Year
2026
Built with
Next.js, Supabase, Python, on-device AI

Overview

MacroTracker is a web app I built for myself to track what I eat. I wanted logging a meal to take a few seconds, so I made it possible to type or say what I ate in plain words and let the app work out the rest.

The web app uses Next.js and Supabase. It stores my own foods with their values per 100 g, logs each item in grams or in servings, and shows the day's totals against my targets. It also has meals made of several foods, a barcode scanner, a food search that pulls from Open Food Facts and USDA, and a CSV export.

The newer part is Quick Log. I type a sentence like 2 eggs and 80 g rice in the box on the Today page, or I say it to Siri through a "Log Food" Shortcut on my iPhone. A small worker on my Mac reads the sentence with Apple's on-device Foundation Models, finds each food, works out the grams and writes the log entries. A second Shortcut, "Scan Label", reads a nutrition label with the phone's camera and adds the product as a new food.

How it works

The phone and the Today box both add a row to an inbox table in Supabase. On the Mac, a launchd job runs food_worker.py every 30 seconds. It claims up to five waiting rows and handles each one in four steps.

  • Parse. A short shell script, foodparse, sends the sentence to Apple's model with a JSON schema. The model returns a list of items, each with the words I used, a food name in English, a quantity, a unit and a meal. It also knows the Québec meal names, so souper becomes dinner.
  • Find the food. The worker scores every food in my own table against the item's words. If nothing scores high enough, it searches USDA and saves the best match as a new food, with USDA's portion weights as its servings.
  • Work out the grams. Grams are used as written. Otherwise the worker looks for a matching serving, such as "1 large" for an egg. If there is none, it uses a default weight and marks the entry as estimated.
  • Write the logs. The row ends as done, needs_review or error, with a one line summary that shows on the Today page.

A label message starts with label:. The phone has already read the text, so the model only puts the printed numbers into fields, and the worker converts them to values per 100 g.

Design decisions

  • The model never gives calories or macros. It only reads words, and every number comes from my foods table or from USDA. A wrong guess then shows up as the wrong food, which I can see and fix, and not as a made up calorie count.
  • No service role key. The phone and the worker each use a long random token, and the database keeps only its SHA-256 hash. All their access goes through a few SECURITY DEFINER SQL functions that act only on the data of the token's owner. I create and revoke tokens in the app's settings.
  • A queue between the phone and the Mac. The phone only adds a row, so logging still works while the Mac is asleep, and the worker catches up when it wakes. A row stuck in processing for 10 minutes is tried again, and after 3 attempts it is marked as an error with a retry button.
  • Text recognition on the phone. I first read labels with Tesseract in the browser, then built a Python OCR service with OpenCV and Tesseract. Moving that step into the Shortcut left nothing extra to host, and the Mac only has to sort the text into fields.

Problems I ran into

Most of the problems came from picking the right food from my words. None of them caused an error. Each one showed up as a wrong entry.

  • My foods have a caption, such as "1 egg = 50 g assumed" or the USDA entry they came from. At first I scored the name and the caption together, so the extra words lowered the score of an exact name match. The worker now scores the name on its own.
  • A USDA search for egg also returns "Bagels, egg". USDA names the main food first, so the worker now gives a bonus when the first word of an entry is one of mine. Common staples like egg and rice also get a preferred search phrase.
  • "Slices of toast" matched pita bread. The worker now prefers a food that has a serving for the unit I asked for.
  • The serving search for "slice" also matched "sliced", and "1 large" lost to "1 cup (4.86 large eggs)". A portion word now has to match a whole word, and a word right after the amount wins.

Brand names were another case. When my words say more than the generic food name, as with "big mac", the worker searches my exact words first.

Results

  • Logging a meal is now one sentence, typed on the Today page or said to Siri, without opening the app on the phone.
  • On the 9 label photos in my test set, Scan Label got every serving size and every calorie, protein, carb and fat value exactly right.
  • A one off script imported my old food list as 37 foods with their servings. Where the old list had a piece with no weight, the script assumed a typical weight and wrote it in the food's caption.
  • When a food is not found or a portion is a guess, the entry is marked for review instead of being logged silently. A --dry mode shows what a sentence would log without writing anything.
  • While a message is waiting, the Today page refreshes itself every 6 seconds, so I can watch it turn into entries.

What I'd do next

  • The worker depends on my Mac and on Apple Intelligence being turned on. If the Mac stays off, the queue waits. Parsing on the phone would remove that, but the Shortcut would then also have to do the food search.
  • Matching relies on word scores and a short list of French to English synonyms. A food I log under a new name still goes to USDA the first time. Learning names from the corrections I make on the Today page would fix that.
  • The matcher has no automated tests yet. The cases from the problems above would make a good first set.

← Projets

MacroTracker

J'ai construit un suivi alimentaire où j'entre un repas en écrivant ou en disant une seule phrase, et un programme sur mon Mac la transforme en entrées avec le modèle d'Apple qui tourne sur l'appareil.

Année
2026
Outils
Next.js, Supabase, Python, IA sur l'appareil

Aperçu

MacroTracker est une application web que j'ai construite pour moi, pour suivre ce que je mange. Je voulais qu'entrer un repas prenne quelques secondes, alors j'ai rendu possible d'écrire ou de dire ce que j'ai mangé en mots simples et de laisser l'application faire le reste.

L'application web utilise Next.js et Supabase. Elle garde mes propres aliments avec leurs valeurs pour 100 g, enregistre chaque aliment en grammes ou en portions et affiche les totaux de la journée par rapport à mes objectifs. Elle a aussi des repas composés de plusieurs aliments, un lecteur de codes-barres, une recherche d'aliments qui puise dans Open Food Facts et USDA, et un export CSV.

La partie plus récente s'appelle Quick Log. J'écris une phrase comme 2 eggs and 80 g rice dans la boîte de la page Today, ou je la dis à Siri grâce à un raccourci « Log Food » sur mon iPhone. Un petit programme sur mon Mac lit la phrase avec les Foundation Models d'Apple, qui tournent sur l'appareil, trouve chaque aliment, calcule les grammes et écrit les entrées. Un second raccourci, « Scan Label », lit une étiquette nutritionnelle avec la caméra du téléphone et ajoute le produit comme nouvel aliment.

Fonctionnement

Le téléphone et la boîte de la page Today ajoutent tous deux une ligne à une table inbox dans Supabase. Sur le Mac, une tâche launchd lance food_worker.py toutes les 30 secondes. Il prend jusqu'à cinq lignes en attente et traite chacune en quatre étapes.

  • L'analyse. Un court script shell, foodparse, envoie la phrase au modèle d'Apple avec un schéma JSON. Le modèle renvoie une liste d'aliments, chacun avec les mots que j'ai utilisés, un nom d'aliment en anglais, une quantité, une unité et un repas. Il connaît aussi les noms de repas du Québec, donc souper devient le repas du soir.
  • La recherche de l'aliment. Le programme compare chaque aliment de ma table aux mots de l'entrée et lui donne un score. Si aucun n'atteint un score suffisant, il cherche dans USDA et enregistre le meilleur résultat comme nouvel aliment, avec les poids de portion de USDA comme portions.
  • Le calcul des grammes. Les grammes sont repris tels quels. Sinon, le programme cherche une portion qui correspond, comme « 1 large » pour un œuf. S'il n'y en a pas, il prend un poids par défaut et marque l'entrée comme estimée.
  • L'écriture. La ligne se termine en done, needs_review ou error, avec un résumé d'une ligne affiché sur la page Today.

Un message d'étiquette commence par label:. Le téléphone a déjà lu le texte, donc le modèle ne fait que placer les chiffres imprimés dans des champs, et le programme les convertit en valeurs pour 100 g.

Choix de conception

  • Le modèle ne donne jamais de calories ni de macros. Il ne fait que lire des mots, et chaque chiffre vient de ma table d'aliments ou de USDA. Une erreur se voit alors comme un mauvais aliment, que je peux repérer et corriger, et non comme un nombre de calories inventé.
  • Aucune clé de service. Le téléphone et le programme utilisent chacun un long jeton aléatoire, et la base de données n'en garde que l'empreinte SHA-256. Tous leurs accès passent par quelques fonctions SQL SECURITY DEFINER qui n'agissent que sur les données du propriétaire du jeton. Je crée et révoque les jetons dans les réglages de l'application.
  • Une file d'attente entre le téléphone et le Mac. Le téléphone ne fait qu'ajouter une ligne, donc l'enregistrement fonctionne même quand le Mac est en veille, et le programme rattrape son retard au réveil. Une ligne bloquée en traitement depuis 10 minutes est relancée, et après 3 tentatives elle passe en erreur avec un bouton pour réessayer.
  • La lecture du texte sur le téléphone. J'ai d'abord lu les étiquettes avec Tesseract dans le navigateur, puis j'ai construit un service OCR en Python avec OpenCV et Tesseract. Déplacer cette étape dans le raccourci ne laisse rien de plus à héberger, et le Mac n'a qu'à classer le texte dans des champs.

Problèmes rencontrés

La plupart des problèmes venaient du choix du bon aliment à partir de mes mots. Aucun ne produisait d'erreur. Chacun se voyait comme une mauvaise entrée.

  • Mes aliments ont une légende, comme « 1 egg = 50 g assumed » ou l'entrée USDA d'où ils viennent. Au début, je comptais le nom et la légende ensemble, donc les mots en trop baissaient le score d'un nom pourtant identique. Le programme compte maintenant le nom seul.
  • Une recherche USDA pour egg renvoie aussi « Bagels, egg ». USDA nomme l'aliment principal en premier, donc le programme donne maintenant un bonus quand le premier mot d'une entrée fait partie des miens. Les aliments de base comme l'œuf et le riz ont aussi une phrase de recherche préférée.
  • « Slices of toast » trouvait du pain pita. Le programme préfère maintenant un aliment qui a une portion pour l'unité demandée.
  • La recherche de portion pour « slice » trouvait aussi « sliced », et « 1 large » perdait contre « 1 cup (4.86 large eggs) ». Un mot de portion doit maintenant correspondre à un mot entier, et le mot placé juste après la quantité l'emporte.

Les marques étaient un autre cas. Quand mes mots disent plus que le nom générique de l'aliment, comme « big mac », le programme cherche d'abord mes mots exacts.

Résultats

  • Entrer un repas tient maintenant en une phrase, écrite sur la page Today ou dite à Siri, sans ouvrir l'application sur le téléphone.
  • Sur les 9 photos d'étiquettes de mon jeu de test, Scan Label a trouvé exactement chaque taille de portion et chaque valeur de calories, de protéines, de glucides et de lipides.
  • Un script ponctuel a importé mon ancienne liste d'aliments, soit 37 aliments avec leurs portions. Quand l'ancienne liste donnait une pièce sans poids, le script a supposé un poids typique et l'a noté dans la légende de l'aliment.
  • Quand un aliment est introuvable ou qu'une portion est devinée, l'entrée est marquée à vérifier au lieu d'être enregistrée sans rien dire. Un mode --dry montre ce qu'une phrase enregistrerait sans rien écrire.
  • Pendant qu'un message attend, la page Today se rafraîchit toutes les 6 secondes, donc je peux le voir se transformer en entrées.

Pour la suite

  • Le programme dépend de mon Mac et d'Apple Intelligence activé. Si le Mac reste éteint, la file attend. Faire l'analyse sur le téléphone réglerait cela, mais le raccourci devrait alors aussi faire la recherche d'aliments.
  • La correspondance repose sur des scores de mots et une courte liste de synonymes du français vers l'anglais. Un aliment que j'entre sous un nouveau nom part encore vers USDA la première fois. Apprendre des noms à partir des corrections que je fais sur la page Today réglerait cela.
  • Le moteur de correspondance n'a pas encore de tests automatisés. Les cas des problèmes ci-dessus feraient une bonne première série.