![[Amazon Bedrock Flows] 冷蔵庫の食材と近所のスーパーのチラシから、レシピを提案するやつ作ってみました](https://devio2024-media.developers.io/image/upload/f_auto,q_auto,w_3840/v1774679350/user-gen-eyecatch/mpnpavv9vv3tioeveryh.png)
[Amazon Bedrock Flows] 冷蔵庫の食材と近所のスーパーのチラシから、レシピを提案するやつ作ってみました
1 はじめに
製造ビジネステクノロジー部の平内(SIN)です。
「今日の夕飯、何にしよう...」冷蔵庫を開けて中身を眺めながら、悩んだ経験をお持ちの方は、いらっしゃらないでしょうか。冷蔵庫には色々な食材があるけれど、何を作ればいいか思いつかない。一方で、ポストに入っていたスーパーのチラシには特売品がずらり。この特売品をうまく活用できれば、お得に美味しい料理が作れるはず...。
そんな課題を解決するため、Amazon Bedrock Flowsを使って「FridgeFlyer」というシステムを作ってみました。
冷蔵庫とチラシの画像から、冷蔵庫にある食材を活かしつつ、チラシの特売品を少し買い足せば作れるレシピを提案してくれます。
今回は、このシステム(FridgeFlyer)の実装について説明させてください。
最初に、出力されたデータを閲覧しているようすです。
一番上の、「冷蔵庫の中身」と「スーパーのチラシ」が入力で、それを読み取った情報や、レシピなどは、全て生成されたものです。
2 システム概要
FridgeFlyerは、2つの画像を入力として受け取り、複数の出力を生成するシステムです。
入力
- 冷蔵庫の写真: 冷蔵庫を開けて撮影した写真
- チラシ画像: 近所のスーパーのチラシ(特売品などが掲載されたもの)
出力
- レシピ: 料理2品とデザート1品の計3品
- 料理画像: 各レシピのイメージ画像(AIが生成)
- 買い物リスト: チラシから買い足す商品と合計金額
- HTML: 上記を見やすくまとめたWebページ
使用している技術スタック
- インフラ: AWS CDK(TypeScript)
- ワークフロー: Amazon Bedrock Flows
- LLM: Claude Opus 4.6(画像分析・レシピ生成)
- 画像生成: Amazon Nova Canvas
- コンピューティング: AWS Lambda(Python 3.12)
- ストレージ: Amazon S3
3 Bedrock Flowsの構成
FridgeFlyerのBedrock Flowは以下のような構成になっています。
① 入力画像は、S3バケットに置かれています
② それぞれの画像は、Lambda上で処理され、Claude Opus 4.6でその内容を読み取ります
③ 読み取られた内容をプロンプトに入れて、Claude Opus 4.6 でレシピ提案します
④ レシピ提案された料理とデザートの画像を Nova Canvasで生成し、S3に保存します
⑤ 全ての出力が揃うのを待機します
⑥ Flowsの出力はレシピ提案のみとなります


Bedrock Flowの入力制約への対応
Bedrock Flowsの現時点での制約として、FlowInputNodeに直接画像データを渡すことができません。プロンプトノードはテキストベースの入出力が前提となっており、バイナリデータの受け渡しはサポートされていません。
そこで、以下のような対応を行いました。
- 事前にS3バケットに画像をアップロードしておく
- Flow起動時には「start」という文字列のみを渡す
- Lambda関数内でS3から画像を取得し、Bedrockで処理する
以下は、CDKでのFlowノード定義の一部です。
// チラシ処理ノード - 統合Lambda(ノード名から処理対象を判定)
{
name: 'FlyerProcessorNode',
type: 'LambdaFunction',
configuration: {
lambdaFunction: {
lambdaArn: imageProcessorLambda.functionArn,
},
},
inputs: [
{
name: 'codeHookInput',
type: 'String',
expression: '$.data',
},
],
// ... 省略
},
// 冷蔵庫処理ノード - 同じLambdaを使用
{
name: 'FridgeProcessorNode',
type: 'LambdaFunction',
configuration: {
lambdaFunction: {
lambdaArn: imageProcessorLambda.functionArn, // 同一のLambda
},
},
// ... 省略
},
FlyerProcessorNodeとFridgeProcessorNodeが同一のLambda関数を呼び出しています。Lambda関数内でノード名を判定し、処理対象のファイルを決定しています。
4 画像処理Lambda
画像処理Lambda(fridge-flyer-image-processor)は、冷蔵庫の写真とチラシ画像を分析するためのLambdaです。こちらのLambda関数も、ノード名で分岐し2つの役割を担っています。
ノード名による処理分岐
Bedrock FlowからLambdaが呼び出される際、イベントにノード名が含まれています。これを利用して処理対象を判定します。
def get_source_key_from_node_name(event: dict[str, Any]) -> str:
"""
ノード名から処理対象のS3キーを判定する
"""
node_name = event.get("node", {}).get("name", "")
if "Fridge" in node_name:
return "fridge.jpg"
elif "Flyer" in node_name:
return "flyer.jpg"
else:
# フォールバック: 環境変数から取得
return os.environ.get("SOURCE_KEY", "fridge.jpg")
FridgeProcessorNodeから呼ばれたときはfridge.jpgを、FlyerProcessorNodeから呼ばれたときはflyer.jpgを処理します。
Claude Opus 4.6による画像分析
Claude Opus 4.6のconverse APIを使用して画像の内容を分析しますが、このとき、Claude Opusの処理には時間がかかるため、boto3のタイムアウト設定を延長しています。
# boto3のタイムアウト設定(Claude Opusは処理に時間がかかるため延長)
BEDROCK_CONFIG = Config(
read_timeout=600, # 10分
connect_timeout=60,
retries={'max_attempts': 2}
)
LLMの処理時間を考慮しないと、Lambda内でのAPI呼び出しがタイムアウトしてしまうため、この設定が必要です。
分析結果の例


5 画像生成LambdaとMergeNode
画像生成Lambda
fridge-flyer-image-generatorは、レシピから料理のイメージ画像を生成するLambdaです。Amazon Nova Canvasを使用して、各レシピに対応する画像を生成しています。
こちらも画像処理Lambdaと同様に、ノード名から生成対象のレシピを判定します。
def get_recipe_index_from_node_name(event: dict[str, Any]) -> int:
"""
ノード名からrecipe_indexを判定する
"""
node_name = event.get("node", {}).get("name", "")
if "Dish1" in node_name:
return 1
elif "Dish2" in node_name:
return 2
elif "Dessert" in node_name:
return 3
else:
return int(os.environ.get("RECIPE_INDEX", "1"))
MergeNodeの必要性
Bedrock Flowの出力ノードは1つの入力しか受け取れません。しかし、FridgeFlyerでは3つの画像生成が並列で実行されます。Flowの完了時に全ての画像生成が終わっていることを保証するために、MergeNodeを配置しています。
def handler(event: dict[str, Any], context: Any) -> str:
"""
複数の入力を受け取りレシピテキストを返す
このLambdaは、3つの画像生成Lambdaとレシピ生成の完了を待機するために使用される。
"""
# Bedrock Flowからの入力形式に対応
if isinstance(event, dict) and "node" in event:
node_inputs = event.get("node", {}).get("inputs", [])
inputs_dict: dict[str, str] = {}
for input_item in node_inputs:
name = input_item.get("name", "")
value = input_item.get("value", "")
inputs_dict[name] = value
recipe_text = inputs_dict.get("recipe_text", "")
# image1_path, image2_path, image3_pathも受け取るが、
# 待機のみが目的なので値は使用しない
return str(recipe_text)
MergeNodeは、4つの入力(レシピテキスト + 画像パス3つ)が全て揃った時点で起動され、レシピテキストをそのまま出力します。LLMを使わずLambdaで実装することで、コストと速度を最適化しています。
6 画像サイズ制限への対応
Bedrockの5MB制限
Bedrockには画像サイズの制限があり、5MBを超える画像は処理できません。しかも、画像データはBase64エンコードされて送信されるため、約1.37倍にサイズが増加します。
つまり、元の画像が4MBあると、Base64エンコード後は約5.5MBになり、制限を超えてしまいます。
チラシ画像(flyer.jpg)がBedrockの5MB制限を超えています:
image exceeds 5 MB maximum: 6591084 bytes > 5242880 bytes
- S3から読み込んだサイズ: 4.9MB
- Bedrockに送信時のサイズ: 6.59MB(Base64エンコードで増加)
段階的圧縮処理
この問題に対応するため、画像アップロード前に圧縮処理を行っています。目標サイズは3.5MBです(3.5MB × 1.37 ≈ 4.8MB < 5MB)。
def compress_image_if_needed(image_path: Path, max_size_mb: float = 3.5) -> bytes:
"""
画像が指定サイズを超える場合は圧縮する
"""
max_size_bytes = int(max_size_mb * 1024 * 1024)
original_size = image_path.stat().st_size
if original_size <= max_size_bytes:
return image_path.read_bytes() # 圧縮不要
# 段階的に圧縮を試行
quality = 85
max_dimension = 2000
while quality >= 30:
if max(img.size) > max_dimension:
ratio = max_dimension / max(img.size)
new_size = (int(img.size[0] * ratio), int(img.size[1] * ratio))
resized_img = img.resize(new_size, Image.Resampling.LANCZOS)
buffer = io.BytesIO()
resized_img.save(buffer, format="JPEG", quality=quality, optimize=True)
compressed_data = buffer.getvalue()
if len(compressed_data) <= max_size_bytes:
return compressed_data
quality -= 10 # 85 → 75 → 65 → ...
max_dimension -= 200 # 2000 → 1800 → 1600 → ...
圧縮は以下のステップで段階的に行われます。
| 試行 | quality | max_dimension | 説明 |
|---|---|---|---|
| 1回目 | 85 | 2000px | 高品質で試行 |
| 2回目 | 75 | 1800px | 少し圧縮 |
| 3回目 | 65 | 1600px | 中程度 |
| 4回目 | 55 | 1400px | やや低品質 |
| 5回目 | 45 | 1200px | 低品質 |
| 6回目 | 35 | 1000px | 最低品質 |
3.5MB以下になった時点でループを抜けて返します。
7 実行方法と結果
実行手順
- CDKでデプロイ
cd cdk
npm install
cdk deploy
- FlowのIDを確認
aws cloudformation describe-stacks \
--stack-name FridgeFlyerStack \
--query "Stacks[0].Outputs" \
--output table
-
画像を準備:
recipe/ディレクトリにfridge.jpgとflyer.jpgを配置 -
実行
cd recipe
python3 generate_recipe.py
実行ログの例
============================================================
Fridge Flyer - レシピ生成スクリプト
============================================================
画像をS3にアップロード中...
- flyer.jpg -> s3://fridge-flyer-XXXX/flyer.jpg
- fridge.jpg -> s3://fridge-flyer-XXXX/fridge.jpg
Bedrock Flowを実行中...
(画像分析とレシピ生成に数分かかります。お待ちください...)
- 出力を受信しました
- Flow完了
- レスポンス取得完了(1906文字)
HTMLを生成中...
- HTMLを保存: output/index.html
============================================================
完了!
============================================================
出力結果
実行が完了すると、ブラウザでHTMLが自動的に開きます。



8 まとめ
今回は、Amazon Bedrock Flowsを使って「冷蔵庫の写真とチラシからレシピを提案するシステム」を構築しました。
Amazon Bedrock Flowsでは、現状、画像を入出力として扱うことが出来ないため、どうしてもS3やLambdaに頼った実装となってしまいますが、全体の処理の組み立てや、調整には、非常に有用に利用できると思いました。
今回、一番感動したのは、Opus 4.6の画像理解能力でした。しかし、このような大規模モデルは処理に時間がかかるため、Lambda自体のタイムアウトだけでなく、boto3のread_timeoutにも注意する必要があることを学びました。
ソースコード全体は以下のGitHubリポジトリで公開しています。
9 おまけ
登場する冷蔵庫は、「ブログを書く」という目的で、以前に会社で買ってもらったものです。この冷蔵庫が登場するブログが、これまで、いくつかあるのですが、LLMの進化を感じながら並べてみました。
2021.01 Deep Learning 物体検出
[レジなし無人販売冷蔵庫] 遂に完成しました\(^o^)/
2023.05 Segment Anything Model + 分類モデル
[ChatGPT] 冷蔵庫内の写真から「おすすめレシピ」を受け取ってみました 〜食品は、Segment Anything と 転移学習した分類モデルで検出してます〜
2024.11 LLM GPT-4o
[GPT-4o] 冷蔵庫内の写真から「おすすめレシピ」を受け取ってみました。
今回 2026.04 LLM Opus 4.6 + Amazon Nova Canvas
[Amazon Bedrock Flows] 冷蔵庫の食材と近所のスーパーのチラシから、レシピを提案するやつ作ってみました






