Déduire les ingrédients et estimer les calories à partir d'images de nourriture

Avancé

Ceci est uncontenant 16 nœuds.Utilise principalement des nœuds comme Code, Gmail, Agent, TelegramTrigger, LmChatOpenRouter. Estimation des calories avec analyse d'images alimentaires par IA visuelle et Telegram

Prérequis
  • Compte Google et informations d'identification Gmail API
  • Token Bot Telegram

Catégorie

-
Aperçu du workflow
Visualisation des connexions entre les nœuds, avec support du zoom et du déplacement
Exporter le workflow
Copiez la configuration JSON suivante dans n8n pour importer et utiliser ce workflow
{
  "id": "aZU2RbdXlp3eXCZh",
  "meta": {
    "instanceId": "15d6057a37b8367f33882dd60593ee5f6cc0c59310ff1dc66b626d726083b48d",
    "templateCredsSetupCompleted": true
  },
  "name": "Infer Ingredients from a Food Image and Estimate Calories",
  "tags": [],
  "nodes": [
    {
      "id": "agent1",
      "name": "Agent IA - Analyse des ingrédients",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        752,
        400
      ],
      "parameters": {
        "text": "=あなたは料理と栄養の専門家です。提供された料理の画像を分析して、以下の情報を日本語で提供してください:\n\n1. 料理名\n2. 推定される主な食材とその分量(グラム)\n3. 各食材のカロリー\n4. 合計推定カロリー\n5. 栄養バランスの簡単な評価\n\n画像URL: {{ $json.message.photo[0].file_id }}",
        "options": {
          "systemMessage": "以下の指示に従って、ユーザーが入力した料理の情報を分析し、JSON形式で出力してください。\n\n1.  **役割:** 料理レシピアナライザー\n2.  **入力:** ユーザーからの料理名や食材に関するテキスト\n3.  **処理:**\n    *   料理名を特定する。\n    *   食材リストを抽出し、各食材の「名前(name)」「分量(amount)」「カロリー(calories)」を特定する。分量とカロリーは一般的な値を推定してもよい。\n    *   全食材のカロリーを合計し、「合計カロリー(totalCalories)」を計算する。\n    *   計算結果に基づき、「栄養バランスの評価(nutritionEvaluation)」を作成する。\n4.  **出力:** 以下のJSONスキーマに厳密に従ったJSONオブジェクトのみを出力する。\n    *   `dishName`: string\n    *   `ingredients`: array of objects\n        *   `name`: string\n        *   `amount`: number (単位: g)\n        *   `calories`: number (単位: kcal)\n    *   `totalCalories`: number (単位: kcal)\n    *   `nutritionEvaluation`: string\n5.  **注意:**\n    *   出力にJSON以外のテキスト(挨拶、説明など)を含めないでください。\n    *   JSONのキー名は指定通りにしてください。"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 2
    },
    {
      "id": "parser1",
      "name": "Analyseur de sortie structurée",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        912,
        608
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"dishName\": {\n      \"type\": \"string\",\n      \"description\": \"料理名\"\n    },\n    \"ingredients\": {\n      \"type\": \"array\",\n      \"description\": \"食材リスト\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": {\n            \"type\": \"string\",\n            \"description\": \"食材名\"\n          },\n          \"amount\": {\n            \"type\": \"number\",\n            \"description\": \"分量(グラム)\"\n          },\n          \"calories\": {\n            \"type\": \"number\",\n            \"description\": \"カロリー(kcal)\"\n          }\n        }\n      }\n    },\n    \"totalCalories\": {\n      \"type\": \"number\",\n      \"description\": \"合計カロリー(kcal)\"\n    },\n    \"nutritionEvaluation\": {\n      \"type\": \"string\",\n      \"description\": \"栄養バランスの評価\"\n    }\n  },\n  \"required\": [\"dishName\", \"ingredients\", \"totalCalories\", \"nutritionEvaluation\"]\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "52bb2509-5870-4f4c-96ea-c607548b15b1",
      "name": "Telegram Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        480,
        400
      ],
      "webhookId": "f6a51604-9b18-4756-a50e-1e5eb1f8da6f",
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {
          "download": true
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "Ikb8BOl71y5C8wqu",
          "name": "Telegram account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a8ee11fd-47bc-4524-89a5-a19380041613",
      "name": "Modèle de chat OpenRouter",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        608,
        608
      ],
      "parameters": {
        "model": "openai/gpt-5-mini",
        "options": {
          "temperature": 0.3
        }
      },
      "credentials": {
        "openRouterApi": {
          "id": "fMR5QJezr3tD108w",
          "name": "簡易デモ"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7397c3e2-c057-4f6b-9afa-010b0a610789",
      "name": "Format pour Gmail",
      "type": "n8n-nodes-base.code",
      "position": [
        1152,
        400
      ],
      "parameters": {
        "jsCode": "// AI Agentから出力されたJSON形式の料理分析結果を取得します。\nconst analysisResult = $input.first().json.output;\n\n// 結果が存在しない、または期待した形式でない場合はエラーを防ぐための処理\nif (!analysisResult || !analysisResult.dishName) {\n  return [{\n    json: {\n      subject: \"料理分析エラー\",\n      htmlBody: \"<p>AIによる分析結果を取得できませんでした。</p>\"\n    }\n  }];\n}\n\n// 1. メールの件名を作成\nconst subject = `【料理分析レポート】 ${analysisResult.dishName}`;\n\n// 2. HTMLメールの本文を生成\nlet htmlBody = `\n<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <style>\n    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f9f9f9; margin: 0; padding: 0; }\n    .container { max-width: 600px; margin: 20px auto; padding: 25px; background-color: #ffffff; border: 1px solid #e0e0e0; border-radius: 12px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); }\n    h1 { font-size: 26px; color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; margin-top: 0; }\n    h2 { font-size: 20px; color: #e74c3c; margin-bottom: 15px; }\n    h3 { font-size: 18px; color: #34495e; margin-top: 30px; border-bottom: 1px solid #eeeeee; padding-bottom: 8px;}\n    p { margin-top: 0; color: #555; }\n    table { width: 100%; border-collapse: collapse; margin-top: 15px; }\n    th, td { text-align: left; padding: 12px; border-bottom: 1px solid #dddddd; }\n    th { background-color: #f2f2f2; color: #333; }\n    tr:last-child td { border-bottom: none; }\n    .total-calories { text-align: center; background-color: #fff3cd; padding: 15px; border-radius: 8px; margin-top: 20px; }\n    .evaluation { background-color: #eafaf1; padding: 15px; border-left: 5px solid #2ecc71; border-radius: 5px; margin-top: 20px;}\n    .footer { text-align: center; margin-top: 25px; font-size: 12px; color: #999; }\n  </style>\n</head>\n<body>\n  <div class=\"container\">\n    <h1>${analysisResult.dishName}</h1>\n    \n    <div class=\"total-calories\">\n      <h2>合計推定カロリー: ${analysisResult.totalCalories} kcal</h2>\n    </div>\n\n    <h3>詳細な食材リスト</h3>\n    <table>\n      <thead>\n        <tr>\n          <th>食材名</th>\n          <th>分量 (g)</th>\n          <th>カロリー (kcal)</th>\n        </tr>\n      </thead>\n      <tbody>\n`;\n\n// 各食材の情報をテーブルの行として追加\nif (analysisResult.ingredients && analysisResult.ingredients.length > 0) {\n  analysisResult.ingredients.forEach(ingredient => {\n    htmlBody += `\n        <tr>\n          <td>${ingredient.name}</td>\n          <td>${ingredient.amount}</td>\n          <td>${ingredient.calories}</td>\n        </tr>\n    `;\n  });\n}\n\nhtmlBody += `\n      </tbody>\n    </table>\n\n    <h3>栄養バランスの評価</h3>\n    <div class=\"evaluation\">\n      <p>${analysisResult.nutritionEvaluation}</p>\n    </div>\n\n    <div class=\"footer\">\n      <p>このレポートはAIによって自動生成されました。</p>\n    </div>\n  </div>\n</body>\n</html>\n`;\n\n// 3. 後続のGmailノードで使えるようにデータを返す\nreturn [{\n  json: {\n    subject: subject,\n    htmlBody: htmlBody\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ae0d956b-599d-4bba-b14b-67245e3287fb",
      "name": "Envoyer un message1",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1408,
        400
      ],
      "webhookId": "b0128308-fba0-4291-996d-e38ac9edaed4",
      "parameters": {
        "sendTo": "t.minamig20@gmail.com",
        "message": "={{ $json.htmlBody }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "={{ $json.subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "qEtK64Ero6pOyPe0",
          "name": "Gmail account 16"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "9be8d7fb-d689-41bb-b638-755bddd7a618",
      "name": "Note 1 : Vue d'ensemble",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        80
      ],
      "parameters": {
        "color": 5,
        "width": 520,
        "height": 260,
        "content": "## 🗒️ Sticky 1: Overview\n**What this template does**\n- Accepts a **food image** and runs **AI-based ingredient inference** → **calorie estimation** → **concise summary**\n- Input can be an **image URL** or **Base64 image** (via Webhook or a messaging app)\n\n**Why it’s useful**\n- Quickly generates a calorie rough estimate and a short nutrition comment for logging, sharing, or feedback loops\n\n**No hardcoded secrets**\n- All API keys must be set via **Credentials/Environment Variables** (never hardcode keys in nodes)"
      },
      "typeVersion": 1
    },
    {
      "id": "0bd3d0eb-a030-4d79-a390-17e41d705f7e",
      "name": "Note 2 : Prérequis",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        -16
      ],
      "parameters": {
        "color": 4,
        "width": 520,
        "height": 280,
        "content": "## 🗒️ Sticky 2: Prerequisites & Credentials\n**Connections**\n- LLM (OpenAI or compatible) with **vision** capability\n- One input channel (Webhook **or** Telegram/LINE, etc.)\n\n**Environment Variables (examples)**\n- `LLM_MODEL` (e.g., `gpt-4o` or any vision-capable model)\n- `LLM_TEMPERATURE` (e.g., `0.3`)\n- `WEBHOOK_SECRET` (optional, if you validate requests)\n\n**Scopes / Permissions**\n- If image URLs are external, ensure they are publicly accessible or use signed/temporary URLs"
      },
      "typeVersion": 1
    },
    {
      "id": "ff6d1495-3223-4305-a354-57a22adf71f3",
      "name": "Note 3 : Format d'entrée",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        560
      ],
      "parameters": {
        "color": 2,
        "width": 520,
        "height": 300,
        "content": "## 🗒️ Sticky 3: Input Format (Sample)\n**Expected JSON**\n```json\n{\n  \"imageUrl\": \"https://example.com/dish.jpg\"\n  // or\n  // \"imageBase64\": \"data:image/jpeg;base64,....\"\n}\n```\n**Notes**\n- Template assumes **one image** per request (extend with arrays for multiple images)\n- If you receive platform-specific payloads (e.g., Telegram `file_id`, LINE message objects), **normalize** them first to `imageUrl` or `imageBase64` before calling the LLM"
      },
      "typeVersion": 1
    },
    {
      "id": "1bd2ee7d-a5f9-42f4-9974-4698fb05b553",
      "name": "Note 4 : Modèle & Prompt",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        864
      ],
      "parameters": {
        "color": 3,
        "width": 520,
        "height": 260,
        "content": "## 🗒️ Sticky 4: Model & Prompt Policy\n**Model**\n- Use a **vision-capable** LLM (e.g., `gpt-4o`)\n- Keep `temperature` low (0.2–0.3) for stable outputs\n\n**Prompt Rules**\n- The model must return **strict JSON only**, with the fields below (no extra text)\n- Keep names concise (e.g., \"grilled chicken\", \"white rice\", \"mixed salad\")\n- Provide amounts in grams when possible (estimates are fine)"
      },
      "typeVersion": 1
    },
    {
      "id": "84f0f90c-e819-4022-a76e-b63353ca9968",
      "name": "Note 5 : Schéma de sortie",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1184,
        -16
      ],
      "parameters": {
        "width": 520,
        "height": 280,
        "content": "## 🗒️ Sticky 5: Output Schema (Fixed)\n**JSON structure**\n```json\n{\n  \"dishName\": \"string\",\n  \"ingredients\": [\n    { \"name\": \"string\", \"amount\": 0, \"calories\": 0 }\n  ],\n  \"totalCalories\": 0,\n  \"nutritionEvaluation\": \"string\"\n}\n```\n**Validation**\n- If fields are empty/missing, route to an **error branch** and return a helpful message for the user"
      },
      "typeVersion": 1
    },
    {
      "id": "fe6e558b-4f6a-4c0a-9b06-e3939b07be3f",
      "name": "Note 6 : Étapes de test",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1200,
        816
      ],
      "parameters": {
        "color": 5,
        "width": 520,
        "height": 220,
        "content": "## 🗒️ Sticky 6: Test Steps (Under 1 Minute)\n1) Turn on the **Webhook** node and copy the **Test URL**\n2) Send a sample payload with a valid `imageUrl`\n3) Confirm the run returns the **strict JSON** structure\n4) If using Telegram/Slack replies, authenticate and send a test message to verify delivery"
      },
      "typeVersion": 1
    },
    {
      "id": "24bbd216-9c8b-4a4f-b446-dd82585117fd",
      "name": "Note 7 : Erreurs & Limites",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1696,
        0
      ],
      "parameters": {
        "color": 4,
        "width": 520,
        "height": 260,
        "content": "## 🗒️ Sticky 7: Error Handling & Limits\n**Retries**\n- For LLM calls, implement **exponential backoff** on 429/5xx\n- If the image fetch fails, **retry**, then go to **error branch**\n\n**Limits**\n- Watch for model **context/vision limits**; downscale/thumbnail images if needed\n- When rate-limited, use **Delay** + **Retry** to recover gracefully"
      },
      "typeVersion": 1
    },
    {
      "id": "55619913-3b70-43d8-ab38-f5b8eb4a6375",
      "name": "Note 8 : Sécurité",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1776,
        784
      ],
      "parameters": {
        "color": 3,
        "width": 520,
        "height": 220,
        "content": "## 🗒️ Sticky 8: Security & PII\n- Store API keys/tokens in **Credentials/Env Vars** (never in node fields)\n- If image URLs are private, use **signed/temporary URLs**\n- **Do not log** personal data; mask/remove PII from execution logs\n- Use **HTTPS only** for all external requests"
      },
      "typeVersion": 1
    },
    {
      "id": "95833663-f776-40c5-9225-30f07e580acb",
      "name": "Note 9 : Livraison",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        0
      ],
      "parameters": {
        "color": 2,
        "width": 520,
        "height": 220,
        "content": "## 🗒️ Sticky 9: Delivery Options (No Gmail)\n- Reply via **Telegram/Slack** or return via **HTTP Response**\n- Optionally log results to **Google Sheets** or **Notion** for history/analytics\n- If email is required, use **SMTP** or a non-Gmail provider (per your stack)"
      },
      "typeVersion": 1
    },
    {
      "id": "d1f31505-89ed-4483-bb52-d1f98c02db36",
      "name": "Note 10 : Extensibilité",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1840,
        400
      ],
      "parameters": {
        "width": 520,
        "height": 240,
        "content": "## 🗒️ Sticky 10: Extensibility (Review Bonus)\n- **Multilingual output** (ja/en) switch\n- Normalize serving units (piece/slice) → **gram conversion**\n- Categorize components (main dish / side / sauce)\n- Add **evidence-based** nutrition comments (e.g., high fat/high carb) with short rationale"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "timezone": "Asia/Tokyo",
    "errorWorkflow": "",
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "versionId": "8766f935-4d36-4d19-9f54-9179e0a59262",
  "connections": {
    "7397c3e2-c057-4f6b-9afa-010b0a610789": {
      "main": [
        [
          {
            "node": "ae0d956b-599d-4bba-b14b-67245e3287fb",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "52bb2509-5870-4f4c-96ea-c607548b15b1": {
      "main": [
        [
          {
            "node": "agent1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "a8ee11fd-47bc-4524-89a5-a19380041613": {
      "ai_languageModel": [
        [
          {
            "node": "agent1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "agent1": {
      "main": [
        [
          {
            "node": "7397c3e2-c057-4f6b-9afa-010b0a610789",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "parser1": {
      "ai_outputParser": [
        [
          {
            "node": "agent1",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  }
}
Foire aux questions

Comment utiliser ce workflow ?

Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.

Dans quelles scénarios ce workflow est-il adapté ?

Avancé

Est-ce payant ?

Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.

Informations sur le workflow
Niveau de difficulté
Avancé
Nombre de nœuds16
Catégorie-
Types de nœuds7
Description de la difficulté

Adapté aux utilisateurs avancés, avec des workflows complexes contenant 16+ nœuds

Liens externes
Voir sur n8n.io

Partager ce workflow

Catégories

Catégories: 34