Bedrockで絵本が作れるか試してみた
Bedrockで絵本が作れるか試してみた
3月にJAWS-UG&SORACOMのコラボ開催のイベントで登壇したのですが、思いのほか受けが良かったので記事にもしてみました。
きっかけは子供が新しい絵本しか読まなくなったので、生成AIでリーズブルかつ大量に絵本が作れないかと思い、PoC的な感覚でやって見ました。
Bedrockとは
つまるところ以下のスライド[1]でよく整理されているかと思いますが、詳しくはBlackbeltなどをご参照ください。
BedrockのFlowを使う
今回はBedrockのFlowという機能を使ってパイプラインを作って絵本を作成してみました。
本当はAIエージェントを組み合わせてやってみたかったのですが、Flowはリリースされたばかりの機能でまだサポートしていなかったようなのでシンプルなLLMを使うところを試してみました。
大まかなアプローチ
- ざっくりとしたお題を生成AIに渡して、絵本をストーリーを作成してもらう
- 各ページの挿絵を生成するためのプロンプトを作成してもらう
- 各ページの画像を生成、ストーリーのファイルをS3にアップロード
STEP1
- まず初めに全体のストーリーを作成してもらうために主軸となるキーワードを与えます。
- 例えば、「バナナが仲間を引き連れて鬼を退治する」とかを与えて、ストーリーの肉付けをしてもらうイメージです。
- 幼児向けの絵本だと平均的500~1000文字らしいので、そのくらいを目安にストーリーを作ってもらうように依頼をします。
ストーリーを依頼するプロンプトの例
役割
あなたは世界で一流の絵本のストーリー構成を見出す絵本作家です。
依頼事項
・絵本のストーリーを1000文字程度で考えてください。
・絵本は5ページ構成のため、各ページについて200文字程度のストーリーとしてください。
・各ページのストーリーには、ストーリー全体の要約を先頭に付け加えるようにしてください。また、要約は200文字の中には含みません。
STEP2
- 今回はPoCなので5ページぐらいの短めの絵本を作ります。
- 全体のストーリーを1000文字ぐらいとし、1ページ200文字ぐらいにうまく分割してもらい、分割したページごとに挿絵を用意するために、ページごとのストーリーから挿絵用の画像を生成するプロンプトを別途用意してもらいます。
- 分割されたストーリーだけを画像生成AIに渡しても全体のストーリーとしての統一感がぶれたりするため、全体のストーリーを概要を分割されたストーリーにくっつけて画像を作ってもらいます。
例)
全体のストーリー概要+1ページ目の分割されたストーリー+画像生成特有のプロンプト
全体のストーリー概要+2ページ目の分割されたストーリー+画像生成特有のプロンプト
…
全体のストーリー概要+5ページ目の分割されたストーリー+画像生成特有のプロンプト
画像生成用のプロンプトを依頼したプロンプトの例
ストーリーから画像を生成をするためのプロンプトを英語で作成してください。
プロンプトを作成する際は画像をうまく生成するためのTipsを考慮してください。
ストーリー
XXX
画像をうまく生成するためのTips
-
具体的な説明をする
抽象的な単語よりも、詳細な情報を含める方が良い画像になります。
例: 「緑豊かな丘に囲まれた静かな湖、青い空に白い雲が浮かぶ」 -
構図・視点を指定する
カメラアングルやフレーミングを指定すると、意図した構図になります。
例: 「鳥瞰図(トップダウンビュー)」 、「近景(クローズアップ)」 -
スタイル・アートの種類を明確に
どんなテイストの画像がほしいか指定するとより理想に近づきます。
例: 「ジブリ風のアニメーションスタイル」 、「油絵風、印象派のようなタッチ」 -
ライティング(光源)を指定
光の当たり方によって印象が変わるため、環境光や時間帯を指定すると良い。
例: 「黄金色の夕焼けが広がる」 、「暗闇の中、月明かりだけが照らす」 -
カラーパレットを指定
色の雰囲気を指定すると、全体の統一感が生まれる。
例: 「パステルカラーの夢のような風景」 、「モノクロ、白黒映画のような雰囲気」 -
主題(メインオブジェクト)を明確に
何を中心に描くかをはっきり指定する。
例: 「黒猫が月の光の下で座っている」 、「サムライが竹林の中で刀を構えている」 -
環境や背景を指定
背景が重要な場合は詳細に説明。
例: 「海辺の町の賑やかな市場」 、「雪が積もる静かな森の奥」 -
雰囲気や感情を指定
画像にどんな感情や雰囲気を持たせるかを指定すると、より表現が豊かになる。
例: 「ノスタルジックで暖かい雰囲気」 、「恐怖を感じるダークで不気味な風景」 -
ネガティブプロンプト(除外したい要素)を活用
生成してほしくない要素を指定すると、意図しないものを排除できる。
例: 「リアルな人間の顔を含めない」 、「ぼやけた背景は避ける」 -
キーワードの優先度をつける
重要な単語を先に書くと、AIがそれを優先しやすくなる。
例: 「美しい夕焼けの海岸、波が優しく打ち寄せる、温かい色合い」 、「近未来的な街、ネオンが輝く、サイバーパンク風」
STEP3
- STEP2で出来上がった画像生成用のプロンプトをJSON形式で出力してもらい、STEP3の入力にします。
- Flowから画像生成AIを呼び出すことはできないため、Lambdaを介して画像生成AIを呼び出します。
- Lambdaでは画像生成AIの呼び出しと、生成された画像をS3にアップロードする処理をしています。
- 画像生成AIにプロンプトを渡す際にはモデルによるのかもしれませんが日本語だと上手く画像を生成できないようですので、
プロンプトを英語化してもらっています。
↓生成AIに適当に作ってもらったコード
Lambda関数
import json
import boto3
import uuid
from botocore.config import Config
default_region = "us-east-1"
bedrock_runtime = boto3.client("bedrock-runtime", region_name=default_region)
my_config = Config(region_name=default_region, signature_version="s3v4")
s3 = boto3.client("s3", config=my_config)
bucket_name = "バケット名"
def generate_image(prompt):
"""Amazon Bedrockで画像を生成"""
response = bedrock_runtime.invoke_model(
body=json.dumps({"text_prompts": [{"text": prompt}]}),
contentType="application/json",
accept="image/png",
modelId="stability.stable-diffusion-xl-v1"
)
return response['body']
def upload_to_s3(uuid, image_body, page_number, file_extension="png"):
"""S3 にファイルをアップロード"""
random_uuid = uuid
s3_key = f"page_{page_number}_{random_uuid}.{file_extension}"
s3.upload_fileobj(image_body, bucket_name, s3_key, ExtraArgs={"ContentType": f"image/{file_extension}"})
return s3_key
def lambda_handler(event, context):
"""Lambda 関数のエントリーポイント"""
random_uuid = uuid.uuid4().hex
text = event['node']['inputs'][0]['value']
clean_json_string = text.strip("```json\n").strip("```")
parsed_data = json.loads(clean_json_string)
if "pages" not in parsed_data:
return "Invalid input: 'pages' field is required."
# ストーリーファイルのアップロード
json_s3_key = f"story_{random_uuid}.json"
s3.put_object(Bucket=bucket_name, Key=json_s3_key, Body=json.dumps(parsed_data), ContentType="application/json")
# 画像生成とアップロード
for page_data in parsed_data["pages"]:
prompt = page_data.get("prompt", "")
page_number = page_data.get("page", 0)
if not prompt:
continue
image_body = generate_image(prompt)
s3_key = upload_to_s3(random_uuid , image_body, page_number)
return "result from my func invoke bedrock image."
出来上がった絵本
「バナナが仲間を引き連れて鬼を退治する」というお題を与えて、絵本を作ってもらいました。
1ページ目
むかしむかし、森の中に一本のバナナが住んでいました。ある日、森に恐ろしい鬼が現れ、みんなの大切な宝物を奪い始めました。「このままじゃ、みんなが悲しくなってしまう」とバナナは考えました。勇気を出して、「ぼくが鬼を退治しに行くよ!」と宣言すると、森の仲間たちは心配そうな顔をしました。でも、バナナは決心していました。「ひとりじゃ無理かもしれないけど、仲間がいれば大丈夫」そう言って、バナナは旅の準備を始めました。
2ページ目
バナナが最初に出会ったのは、木から落ちそうになっていた赤いリンゴでした。バナナはリンゴを助け、鬼退治の計画を話しました。「僕も行くよ!ぼくは丸くて転がるのが得意だから、きっと役に立つよ」とリンゴは言いました。次に二人が出会ったのは、細い蔓にぶら下がっていたブドウでした。「私たちは小さいけど、たくさん集まれば大きな力になれるわ」とブドウも仲間に加わりました。最後に出会ったのは、真っ赤なイチゴでした。「わたしは小さくても甘い香りで鬼の気を引くことができるわ」とイチゴも言いました。
3ページ目
バナナとリンゴとブドウとイチゴは、鬼が住むという暗い洞窟へと向かいました。途中、激しい雨に降られました。そのとき、バナナは自分の皮で仲間たちを雨からガードしました。険しい崖にさしかかると、リンゴが転がって道を作り、小さなブドウたちは力を合わせて橋を作りました。洞窟の入り口は暗くて怖かったけれど、イチゴが甘い香りを漂わせると、不思議と勇気が湧いてきました。「一人じゃ無理でも、みんなならきっとできる!」とバナナは仲間たちに言いました。
4ページ目
洞窟の奥で、彼らはついに鬼を見つけました。大きくて青い顔の鬼は、盗んだ宝物を抱えていました。「返しなさい!みんなの大切な宝物を!」とバナナが叫びました。鬼は怒って「うるさい!ここから出ていけ!」と大声で吠えました。そのとき、イチゴが甘い香りを漂わせると、鬼の表情が少し和らぎました。リンゴが鬼の周りを転がって気を引き、ブドウたちは宝物を取り戻そうとしました。バナナは鬼に近づき、勇気を出して尋ねました。「どうして人の宝物を奪うの?」
5ページ目
「誰も私と遊んでくれないから...」と鬼は悲しそうに答えました。バナナは仲間たちを見て、みんなでうなずきあいました。「宝物を返してくれたら、私たちが友達になるよ」とバナナは提案しました。鬼は信じられない顔をしましたが、みんなの優しい笑顔に心を打たれました。こうして鬼は宝物をすべて返し、バナナたち果物と鬼は本当の友達になりました。森に戻ると、みんなは大喜び。それからというもの、森では毎週「果物と鬼のパーティー」が開かれ、バナナたちの勇気と友情の物語は、森中に語り継がれたのでした。
感想
細目でみれば、全体的には統一感のある挿絵になったのではないかと思います…
あとストーリーと挿絵も半目で見ればなんとなく一致していそうです。
ただ、主人公のバナナが2ページ目以降でなくなったり、仲間のリンゴやブドウが統一できてなかったりと、ここら辺は工夫が必要そうかと思いました。もしかしたらキャラクターを定めた絵本は難しいのかもしれません。
いろいろ細かい突っ込みはありそうですが、個人的に70点ぐらいの出来ではないかなーと思っています。
補足
今回使った生成AIのモデルは以下です。
ストーリー作成:Claude 3.7 Sonnet(推論)
画像生成:Stability AI SDXL 1.0
最後に
なんとなく生成AIでもある程度絵本が作れることがわかりました。(できる可能性は感じました)
プロンプトで改善できる余地がありそうなので改善しつつ、また生成AIのモデルもいくつか試してみたいと思います。(AWS製のNova Canvasとか)
最近では4oでも画像生成ができるようになったとかでこちらでも試してみたいなと思いました。
もう少し目途が付いたら今度はアプリ化をしてみたいと思います!
最後まで読んでいただきありがとうございました。
もしよければコメントやいいねをいただけると励みになります。よろしくお願いいたします。
Discussion