食品画像から食材を推定しカロリーを計算

上級

これは自動化ワークフローで、16個のノードを含みます。主にCode, Gmail, Agent, TelegramTrigger, LmChatOpenRouterなどのノードを使用。 視覚 AI と Telegram を使用した外食画像分析によるカロリー推定

前提条件
  • Googleアカウント + Gmail API認証情報
  • Telegram Bot Token

カテゴリー

-
ワークフロープレビュー
ノード接続関係を可視化、ズームとパンをサポート
ワークフローをエクスポート
以下のJSON設定をn8nにインポートして、このワークフローを使用できます
{
  "id": "aZU2RbdXlp3eXCZh",
  "meta": {
    "instanceId": "15d6057a37b8367f33882dd60593ee5f6cc0c59310ff1dc66b626d726083b48d",
    "templateCredsSetupCompleted": true
  },
  "name": "Infer Ingredients from a Food Image and Estimate Calories",
  "tags": [],
  "nodes": [
    {
      "id": "agent1",
      "name": "AI Agent - 食材分析",
      "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": "Structured Output Parser",
      "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": "OpenRouter Chat Model",
      "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 for 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": "Send a 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": "Sticky 1: 概要",
      "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": "Sticky 2: 前提条件",
      "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": "Sticky 3: 入力形式",
      "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": "Sticky 4: モデル & プロンプト",
      "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": "Sticky 5: 出力スキーマ",
      "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": "Sticky 6: テスト手順",
      "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": "Sticky 7: エラー & 制限",
      "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": "Sticky 8: セキュリティ",
      "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": "Sticky 9: デリバリー",
      "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": "Sticky 10: 拡張性",
      "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
          }
        ]
      ]
    }
  }
}
よくある質問

このワークフローの使い方は?

上記のJSON設定コードをコピーし、n8nインスタンスで新しいワークフローを作成して「JSONからインポート」を選択、設定を貼り付けて認証情報を必要に応じて変更してください。

このワークフローはどんな場面に適していますか?

上級

有料ですか?

このワークフローは完全無料です。ただし、ワークフローで使用するサードパーティサービス(OpenAI APIなど)は別途料金が発生する場合があります。

ワークフロー情報
難易度
上級
ノード数16
カテゴリー-
ノードタイプ7
難易度説明

上級者向け、16ノード以上の複雑なワークフロー

外部リンク
n8n.ioで表示

このワークフローを共有

カテゴリー

カテゴリー: 34