5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gemmaを使った日本語対応の食事プラン作成アプリ

Last updated at Posted at 2025-01-29

パーソナライズ栄養管理アプリ

個人の健康情報にもとづいて食事プランを提案するアプリです!

image.png

無題の動画 ‐ Clipchampで作成.gif

開発の軸としては、 日本語対応した Gemma のモデルを使うことを主に置いています。

栄養に関する計算方法はパッと使えるものを持ってきました。RAGなどの検索機能も使っていないです🤦‍♂️


機能の概要

このアプリは、4つの機能があります。

  1. ユーザー情報の登録
    年齢、性別、体重、身長、活動レベル、目標を入力して、個人の健康データを管理

  2. PFC バランス表示
    BMIや基礎代謝量から、必要なタンパク質、脂質、炭水化物の量を計算

    image.png

  3. 栄養計算
    入力した食材の栄養データの生成

image.png

  1. 食事プラン
    個人の目標と好みに応じたダイエットプランを自動生成

image.png


準備

プロジェクトの構成はこんな感じです。

app
│  app.py
│  helper.py
│  config.py
│  style.css
│  requirements.txt
└

必要なパッケージ

streamlit で実装するので以下をインストールします。

requirements.txt
streamlit
hydralit-components
streamlit_option_menu
import streamlit as st
import hydralit_components as hc
from streamlit_option_menu import option_menu
from config import *
from helper import *
import certifi
import requests

Gemma モデルについて

Hugging Face にモデルが公開されています。日本語版は jpn と書かれています。

投稿時点ではデプロイ方法には Inference API (Inference Providers内) がないので HF Inference Endpoints を利用してエンドポイントを取得しました。

image.png

Hugging Face と HF Inference Endpoints で API を利用する方法はこちらに書きました!

エンドポイントを取得できると以下のようになります。インスタンスは AWS を利用しました。

image.png

実装するときはシンプルに requests を使えばOKです🙆‍♂️


import requests

API_URL = "https://YOUR_ENDPOINT.aws.endpoints.huggingface.cloud"
headers = {
	"Accept" : "application/json",
	"Authorization": "Bearer hf_XXXXX",
	"Content-Type": "application/json" 
}

def query(payload):
	response = requests.post(API_URL, headers=headers, json=payload)
	return response.json()

output = query({
	"inputs": "Can you please let us know more details about your ",
	"parameters": {
		"max_new_tokens": 150
	}
})

実装方法

ユーザー情報の登録フォーム

サイドバーにフォームを配置し、年齢、性別、体重、身長、活動レベル、目標を入力・選択できるようにします。

ソースコード
# サイドバー
with st.sidebar:
    with st.form("form"):
        st.header("ユーザー情報")
        col1, col2 = st.columns(2)
        with col1:
            st.header("年齢")
            st.header("性別")
            st.header("体重 (kg)")
            st.header("身長 (cm)")
            st.header("活動レベル")
            st.header("目標")
        with col2:
            age = st.number_input("年齢を入力してください", min_value=5, value=person_info["年齢"], step=1, label_visibility="collapsed")
            gender = option = st.selectbox("性別を選んでください", gender_list, label_visibility="collapsed")
            weight = st.number_input("体重を入力してください", value=person_info["体重"], min_value=20, step=1, label_visibility="collapsed")
            height = st.number_input("身長を入力してください", value=person_info["身長"], min_value=100, step=1, label_visibility="collapsed")
            activity_level = st.selectbox('活動レベルを選択してください', activity_level, help=activity_details, label_visibility="collapsed")
            goal = st.selectbox('目標を選択してください', goal_list, label_visibility="collapsed")

        c = st.columns((1,4,1))
        with c[1]:
            submitted = st.form_submit_button("登録", type="primary")
            person_info["年齢"] = age
            person_info["性別"] = gender
            person_info["体重"] = weight
            person_info["身長"] = height
            person_info["活動レベル"] = activity_level
            person_info["目標"] = goal
            
activity_details = """
とても低い: 全く運動しない \n
やや低い: 週に1,2回程度の運動 \n
普通: 週に3-5回程度の運動 \n
やや高い: 週に6,7回程度の運動 \n
とても高い: ハードなエクササイズや肉体労働
"""

gender_list = ['男性', '女性', 'その他']

goal_list = ['筋肉増量', '体重減少', '維持']

activity_level = ["とても低い", "やや低い", "普通", "やや高い", "とても高い"]

特に工夫すべき点はありませんが、入力フォームは色々なアプリに使えるので気になる方はソースコードを見てください!


BMIとPFCバランス計算

登録されたデータを元にBMIや基礎代謝量(BMR)を計算します。ここでは、ハリス・ベネディクトの式 という計算式を使っています。

def calculate_bmi(person_info):
    bmi = person_info["体重"] / (person_info["身長"] / 100) ** 2
    if bmi < 18.5:
        bmi_class = "低体重"
    elif bmi < 25:
        bmi_class = "標準体重"
    elif bmi < 30:
        bmi_class = "やや肥満"
    else:
        bmi_class = "肥満"
    return bmi, bmi_class

def energy_calc(person_info):
    if person_info["性別"] == "男性":
        bmr = 88.362 + (13.397 * person_info["体重"]) + (4.799 * person_info["身長"] / 100) - (5.677 * person_info["年齢"])
    else:
        bmr = 447.593 + (9.247 * person_info["体重"]) + (3.100 * person_info["身長"] / 100) - (4.330 * person_info["年齢"])
    tdee = bmr * activity_level_multipliers[person_info["活動レベル"]]
    return bmr, tdee

def macro_perc(person_info, calories):
    if person_info["目標"].lower() == '体重減少':
        protein_percentage = 30
        fat_percentage = 25
    elif person_info["目標"].lower() == '維持':
        protein_percentage = 25
        fat_percentage = 30
    elif person_info["目標"].lower() == '筋肉増量':
        protein_percentage = 35
        fat_percentage = 20
    else:
        raise ValueError("無効な目標")

    carb_percentage = 100 - (protein_percentage + fat_percentage)

    protein = (protein_percentage / 100) * calories / 4
    fat = (fat_percentage / 100) * calories / 9
    carbs = (carb_percentage / 100) * calories / 4

    return {'タンパク質': protein, '脂質': fat, '炭水化物': carbs}

栄養計算

プロンプトは次の通りです。ポイントは上手くテーブルでデータを表示できるように誘導することです。

より本格的なものを目指す方は RAG などでデータベースを参照できると良いと思います

def diet(person_info):
  search = st.text_input("食べた物を入力してください!", placeholder="焼きそば")
  f = st.button("決定", type="primary")
  if f:
    with st.spinner(f"**{search}** について情報を探しています...."):
      prompt = f"""
      表形式で栄養データを生成してください。テキストではなくテーブルだけを返すようにします。
      対象は{search}についてです。
      テーブルは栄養データとして、以下の6つの特徴を必ず持ちます:
      1.量
      2.材料
      3.タンパク質
      4.脂質
      5.炭水化物
      6.カロリー
      """
      res = get_response(prompt)
      res = res[0]["generated_text"].split("\n\n ")[1]
      st.write(res)

placeholderspinner を使うと見やすくなります。

image.png


ダイエットプランの生成

こちらもプロンプトを紹介します。住んでいる国や食の好みは実験的に入れてみました。

def plan(person_info):
  with st.form("食事プラン"):
    st.header("あなたの情報")
    cols = st.columns(3)
    with cols[0]:
      loc = st.selectbox('あなたの住んでいる国はどこですか?', ["日本", "日本以外の国"])
    with cols[1]:
      remarks = st.text_input("食事の好み", placeholder="例:野菜が好き、肉が嫌い")
    s = st.form_submit_button("食事プランをつくる", type="primary")
  if s:
    with st.spinner('プランを生成中です....'):
      bmi, bmi_class = calculate_bmi(person_info)
      bmr, tdee = energy_calc(person_info)

      prompt = f"""
      表形式で食事プランを生成してください。テキストではなくテーブルだけを返すようにします。
      対象者の性別は{person_info["性別"]}{loc}に住む人で、BMRが {bmr} kcal、 BMIは{bmi}、1日の消費エネルギーは{tdee} kcalの人です。
      そして、食事プランのメニューに反映させるべき次の嗜好を持っています:{remarks}。
      目標は、{person_info["目標"]}です。
      テーブルは食事プランとして、以下の特徴を必ず持ちます:
      1.食事時間(朝食、昼食、夕食の3回)
      2.メニュー
      3.材料
      4.栄養素(g)
      5.カロリー(kcal)
      """
      res = get_response(prompt)
      res = res[0]["generated_text"].split("```")[1]
      st.write(res)

image.png


まとめ

食事プランの提案に関してはあまり実用的ではなかったですが、Gemma モデルとしては問題なく日本語でのやり取りができました👍

ソースコード全体

app.py

import streamlit as st
import hydralit_components as hc
from config import *
from helper import *
from streamlit_option_menu import option_menu
import warnings
import requests


headers = {
   "Accept" : "application/json",
   "Authorization": f"Bearer {HF_TOKEN}",
   "Content-Type": "application/json",
}

warnings.filterwarnings('ignore')

# ページセットアップ
st.set_page_config(
  page_title=PAGE_TITLE,
  layout='wide'
)

def query(payload):
	response = requests.post(API_URL, headers=headers, json=payload, verify=False)
	return response.json()

def get_response(prompt):
    res = query({
      "inputs": prompt,
      "parameters": {
        "max_new_tokens": 500
      }
    })
    print(res)
    return res

# CSS
local_css("style.css")

# サイドバー
with st.sidebar:
    with st.form("form"):
        st.header("ユーザー情報")
        col1, col2 = st.columns(2)
        with col1:
            st.header("年齢")
            st.header("性別")
            st.header("体重 (kg)")
            st.header("身長 (cm)")
            st.header("活動レベル")
            st.header("目標")
        with col2:
            age = st.number_input("年齢を入力してください", min_value=5, value=person_info["年齢"], step=1, label_visibility="collapsed")
            gender = option = st.selectbox("性別を選んでください", gender_list, label_visibility="collapsed")
            weight = st.number_input("体重を入力してください", value=person_info["体重"], min_value=20, step=1, label_visibility="collapsed")
            height = st.number_input("身長を入力してください", value=person_info["身長"], min_value=100, step=1, label_visibility="collapsed")
            activity_level = st.selectbox('活動レベルを選択してください', activity_level, help=activity_details, label_visibility="collapsed")
            goal = st.selectbox('目標を選択してください', goal_list, label_visibility="collapsed")

        c = st.columns((1,4,1))
        with c[1]:
            submitted = st.form_submit_button("登録", type="primary")
            person_info["年齢"] = age
            person_info["性別"] = gender
            person_info["体重"] = weight
            person_info["身長"] = height
            person_info["活動レベル"] = activity_level
            person_info["目標"] = goal

# メインコンテンツ
def home(person_info):
    bmi, bmi_class = calculate_bmi(person_info)
    bmr, tdee = energy_calc(person_info)
    macros_req = macro_perc(person_info, tdee)
    hc_theme = {'bgcolor': '#f9f9f9','title_color': 'orange','content_color': 'orange','icon_color': 'orange', 'icon': 'fa fa-question-circle'}

    cols = st.columns(3)
    with cols[0]:
      if bmi_class == "標準体重":
        hc.info_card(title='BMI', content=f'{round(bmi, 2)}' + " kcal", sentiment='good')
      else:
        hc.info_card(title='BMI', content=f'{round(bmi, 2)}', sentiment='bad')
    with cols[1]:
      hc.info_card(title='基礎代謝', content=f'{round(bmr, 2)} kcal',theme_override=hc_theme)

    st.header("PFCバランス")
    cols = st.columns(3)
    theme_neutral = {'bgcolor': '#EFF8F7','title_color': 'green','content_color': 'green','icon_color': 'green', 'icon': 'fa fa-check-circle'}
    with cols[0]:
      hc.info_card(title='タンパク質', content=f'{round(macros_req["タンパク質"], 2)} g', theme_override=theme_neutral)
    with cols[1]:
      hc.info_card(title='脂質', content=f'{round(macros_req["脂質"], 2)} g',theme_override=theme_neutral)
    with cols[2]:
      hc.info_card(title='炭水化物', content=f'{round(macros_req["炭水化物"], 2)} g',theme_override=theme_neutral)

def diet(person_info):
  search = st.text_input("食べた物を入力してください!", placeholder="焼きそば")
  f = st.button("決定", type="primary")
  if f:
    with st.spinner(f"**{search}** について情報を探しています...."):
      prompt = f"""
      表形式で栄養データを生成してください。テキストではなくテーブルだけを返すようにします。
      対象は{search}についてです。
      テーブルは栄養データとして、以下の6つの特徴を必ず持ちます:
      1.量
      2.材料
      3.タンパク質
      4.脂質
      5.炭水化物
      6.カロリー
      """
      res = get_response(prompt)
      res = res[0]["generated_text"].split("\n\n ")[1]
      st.write(res)

def plan(person_info):
  with st.form("食事プラン"):
    st.header("あなたの情報")
    cols = st.columns(3)
    with cols[0]:
      loc = st.selectbox('あなたの住んでいる国はどこですか?', ["日本", "日本以外の国"])
    with cols[1]:
      remarks = st.text_input("食事の好み", placeholder="例:野菜が好き、肉が嫌い")
    s = st.form_submit_button("食事プランをつくる", type="primary")
  if s:
    with st.spinner('プランを生成中です....'):
      bmi, bmi_class = calculate_bmi(person_info)
      bmr, tdee = energy_calc(person_info)

      prompt = f"""
      表形式で食事プランを生成してください。テキストではなくテーブルだけを返すようにします。
      対象者の性別は{person_info["性別"]}{loc}に住む人で、BMRが {bmr} kcal、 BMIは{bmi}、1日の消費エネルギーは{tdee} kcalの人です。
      そして、食事プランのメニューに反映させるべき次の嗜好を持っています:{remarks}。
      目標は、{person_info["目標"]}です。
      テーブルは食事プランとして、以下の特徴を必ず持ちます:
      1.食事時間(朝食、昼食、夕食の3回)
      2.メニュー
      3.材料
      4.栄養素(g)
      5.カロリー(kcal)
      """
      res = get_response(prompt)
      res = res[0]["generated_text"].split("```")[1]
      st.write(res)


selected = option_menu(
    menu_title=None,
    options=["ホーム", "栄養計算", "食事プラン"],
    icons=["house", "calculator", "envelope"],
    menu_icon="cast",
    default_index=0,
    orientation="horizontal",
)

# 画面遷移
if selected == "ホーム":
  home(person_info)
elif selected == "栄養計算":
  diet(person_info)
elif selected == "食事プラン":
  plan(person_info)
else:
  st.error("あなたの情報を登録してください")

helper.py
from config import *

# BMI判定
def calculate_bmi(person_info):
    bmi = person_info["体重"] / (person_info["身長"] / 100) ** 2

    if bmi < 18.5:
        bmi_class = "低体重"
    elif bmi < 25:
        bmi_class = "標準体重"
    elif bmi < 30:
        bmi_class = "やや肥満"
    else:
        bmi_class = "肥満"
    return bmi, bmi_class

# カロリー計算
def energy_calc(person_info):
    if person_info["性別"] == "男性":
        bmr = 88.362 + (13.397 * person_info["体重"]) + (4.799 * person_info["身長"] / 100) - (5.677 * person_info["年齢"])
    else:
        bmr = 447.593 + (9.247 * person_info["体重"]) + (3.100 * person_info["身長"] / 100) - (4.330 * person_info["年齢"])
    tdee = bmr * activity_level_multipliers[person_info["活動レベル"]]
    return bmr, tdee

# PFC計算
def macro_perc(person_info, calories):
    if person_info["目標"].lower() == '体重減少':
        protein_percentage = 30
        fat_percentage = 25
    elif person_info["目標"].lower() == '維持':
        protein_percentage = 25
        fat_percentage = 30
    elif person_info["目標"].lower() == '筋肉増量':
        protein_percentage = 35
        fat_percentage = 20
    else:
        raise ValueError("無効な目標")

    carb_percentage = 100 - (protein_percentage + fat_percentage)

    protein = (protein_percentage / 100) * calories / 4
    fat = (fat_percentage / 100) * calories / 9
    carbs = (carb_percentage / 100) * calories / 4

    return {'タンパク質': protein, '脂質': fat, '炭水化物': carbs}

# CSS読み込み
def local_css(file_name):
    with open(file_name) as f:
        st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
config.py
PAGE_TITLE = "ダイエット支援LLMアプリ"

activity_level_multipliers = {
    "とても低い": 1.2,
    "やや低い": 1.375,
    "普通": 1.55,
    "やや高い": 1.725,
    "とても高い": 1.9,
}

activity_details = """
とても低い: 全く運動しない \n
やや低い: 週に1,2回程度の運動 \n
普通: 週に3-5回程度の運動 \n
やや高い: 週に6,7回程度の運動 \n
とても高い: ハードなエクササイズや肉体労働
"""

gender_list = ['男性', '女性', 'その他']

goal_list = ['筋肉増量', '体重減少', '維持']

activity_level = ["とても低い", "やや低い", "普通", "やや高い", "とても高い"]

# Define the macronutrient percentages
macronutrient_percentages = {
    "炭水化物": (45, 65),
    "タンパク質": (10, 35),
    "脂質": (20, 35),
}

# Set the person's information
person_info = {
    "年齢": 25,
    "性別": "Male",
    "身長": 165,
    "体重": 70,
    "活動レベル": "普通",
    "目標": "体重減少"
}

API_URL = "https://YOUR_ENDPOINT.endpoints.huggingface.cloud"
HF_TOKEN = "hf_XXXX"


Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?