NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

生成系AIと共に作るネットコムアドベントカレンダー

本記事は  【Advent Calendar 2023】  25日目の記事です。
🎁 24日目② ▶▶Merry Christmas!! 🎄

こんにちは!志水です。最近はインフルや胃腸炎流行ってますね。私も漏れなく胃腸炎貰いましたが、発症直前にカレー食べてて、胃腸炎で苦しんでる時にカレーの香りで更に苦しみ続け、おかげさまでカレーが食べれなくなりました。

はじめに

さて、本記事はネットコムのアドベントカレンダーの最終日なので、アドベントカレンダー作ろうと思います(?)

というのも、今回のアドベントカレンダーは下記で一覧が見れる状態なのですが、全部の記事が一目で分かるページが欲しいと思って作ろうと思いました。

tech.nri-net.com

まず、作ったのは下記になります。

https://d2vmu48pnp4egz.cloudfront.net/

基本的にはブログの内容を解釈して1日目から25日目までを画像化して並べました。なんとなく傾向が掴めたり、圧倒的違和感のある記事がどれかが分かるかなと思います。

大体椅子に座ってる人が多いのですが、山に登ったりサッカーしたりしてる記事はやっぱり目を引きますね。

今回のプロンプト

今回利用した生成系AIのプロンプトの内容は下記になります

  • Claude
    • モデルID: anthropic.claude-v2
    • 規約: Anthropic Claude v2(anthropic.claude-v2): ANTHROPIC BEDROCK AI SERVICES AGREEMENT(last modified: October 3, 2023)
  • Stable Diffusion
    • モデルID: stability.stable-diffusion-xl
    • 規約: Stability AI Stable Diffusion XL(stability.stable-diffusion-xl-v0): STABILITY AMAZON BEDROCK END USER LICENSE AGREEMENT(Last Updated May 30, 2023)
  • 実行時期
    • 2023/12/22

構成と流れ

今回作ってみた構成図は下記になります。

全体の流れは以下の通りです

  1. ブログのURLをAmazon DynamoDBから取得
  2. URLへコンテンツ取得する
  3. 取得したコンテンツからプロンプトを生成
  4. プロンプトを使って画像を生成
  5. 生成した画像をアップ
  6. 画像からページを生成
  7. ページをS3へデプロイ

ここで、特に説明が必要なのはステップ③と④です。これらのステップでは、二つの異なるAIモデルを使用しています。

  • ステップ③:プロンプトの生成
    • このステップでは、Claudeというモデルを使用しています。
    • 具体的には、ブログの内容をClaudeに入力し、その内容に基づいて高品質な画像を生成できるプロンプトを作成します。
    • つまり、Claudeはブログの内容を解析し、それを基にして、StableDiffusionが理解しやすい形でのプロンプトを生成する役割を担っています。
  • ステップ④:画像の生成
    • ここでは、Claudeによって作成されたプロンプトを利用して、StableDiffusionというモデルで画像を生成します。
    • このプロセスは比較的シンプルで、ClaudeからのプロンプトをそのままStableDiffusionに入力し、画像を生成するという流れです。

では、各ステップについてコードを元に説明していきます。

① ブログのURLをDynamoDBから取得

まず、DynamoDBテーブルをCDKから作成します。キーは日付にしたいのでNUMBERにしています。

table = dynamodb.Table(
    self, "MyTable",
    partition_key=dynamodb.Attribute(
        name="id",
        type=dynamodb.AttributeType.NUMBER
    )
)

作成したテーブルにブログのURLを手入力しました。

そこから、AWS LambdaをCDKで作成して

lambda_runtime = lambda_.Runtime.PYTHON_3_12
lambda_function = lambda_.Function(
    self, "MyLambda",
    runtime=lambda_runtime,
    handler="lambda_handler.handler",
    code=lambda_.Code.from_asset("lambda/create-image"),
    role=lambda_role,
    timeout=Duration.seconds(900),
    layers=[lambda_layer],
)

# LambdaにDynamoDBへの読み取り権限を付与
table.grant_read_data(lambda_function)

# Lambdaに必要な環境変数を設定
lambda_function.add_environment("TABLE_NAME", table.table_name)

Lambda内でDynamoDBから日ごとのURLを取得します。

for i in range(25):
    index = i + 1
    response = dynamodb.get_item(
        TableName=table_name,
        Key={'id': {'N': str(index)}}
    )
    url = response['Item'].get('url').get('S')

② URLへコンテンツ取得する

先程取得したURLをもとにコンテンツ取得を行っています。また、何度も取得するのも嫌なので取得したらDynamoDBに入れてます。そのLambdaのコードは下記になります。

def get_netcom_blog_contents(url: str) -> str:
    # URLからコンテンツを取得
    try:
        content_response = requests.get(url)
        content_response.raise_for_status()
    except requests.RequestException as e:
        return f"Error fetching URL content: {e}"

    # lxmlを使用してHTMLをパースし、XPathで特定のセクションを抽出
    tree = html.fromstring(content_response.content.decode('utf-8'))
    specific_content = tree.xpath('/html/body/div[2]/div/div[3]/div/div/div/div/article/div')
    
    
    # 抽出されたセクションからHTMLタグを削除
    for element in specific_content:
        etree.strip_tags(element, '*')
    extracted_text = ''.join([etree.tostring(e, encoding='unicode', method='text') for e in specific_content])
    
    # 不要な改行とスペースを削除
    cleaned_text = ' '.join(extracted_text.split())

    return cleaned_text

# URLからブログの内容を取得
blog_contents=get_netcom_blog_contents(url)
print("blog contents")
# print(blog_contents)
print(response['Item'].get('contents').get('S'))
print()

# DynamoDBに内容を保存
update_response = dynamodb.update_item(
    TableName=table_name,
    Key={'id': {'N': str(index)}},
    UpdateExpression="SET contents = :val",
    ExpressionAttributeValues={
        ':val': {'S': blog_contents}
    }
)

③ 取得したコンテンツからプロンプトを生成

取得したコンテンツを元に、StableDiffusion用のプロンプトをClaudeに作成してもらいます。コードは下記です。

def call_claude(prompt):
    prompt_config = {
        "prompt": claude_prompt_format(prompt),
        "max_tokens_to_sample": 4096,
        "temperature": 0.7,
        "top_k": 250,
        "top_p": 0.5,
        "stop_sequences": [],
    }

    body = json.dumps(prompt_config)

    modelId = "anthropic.claude-v2"
    accept = "application/json"
    contentType = "application/json"

    response = bedrock_runtime.invoke_model(
        body=body, modelId=modelId, accept=accept, contentType=contentType
    )
    response_body = json.loads(response.get("body").read())

    results = response_body.get("completion")
    return results

# ブログの内容から画像用のプロンプトの生成
base_claude_prompt = """
Stable Diffusionに入力するプロンプトを作成してください。

・要件 
下記ブログ記事の内容を象徴する画像を生成したい。
高品質な画像を作成したい
出力内容は必要最低限にしたい
出力内容はプロンプトのみにしたい
出力内容にプロンプトの説明や導入などは記載しないでほしい
プロンプトは英語で欲しい

・ブログ記事の内容
"""

# Claudeの実行
claude_prompt=base_claude_prompt+blog_contents
stability_prompt = call_claude(claude_prompt)

StableDiffusionに必要なプロンプトなので、出力は英語にしてもらっています。また、Claudeはプロンプトの説明や導入をしたがりますが、やらないようにも依頼しています。

④ プロンプトを使って画像を生成

先程作成したプロンプトをStableDiffusionへ入力して画像を作成します。その時のLambdaのコードは下記です。

# Bedrock api call to stable diffusion
def generate_image(text):
    """
    Purpose:
        Uses Bedrock API to generate an Image
    Args/Requests:
         text: Prompt
    Return:
        image: base64 string of image
    """
    body = {
        "text_prompts": [{"text": text}],
        "cfg_scale": 10,
        "seed": 0,
        "steps": 50,
        "style_preset": "comic-book",
    }

    body = json.dumps(body)

    modelId = "stability.stable-diffusion-xl"
    accept = "application/json"
    contentType = "application/json"

    response = bedrock_runtime.invoke_model(
        body=body, modelId=modelId, accept=accept, contentType=contentType
    )
    response_body = json.loads(response.get("body").read())

    encoded_png_data = response_body.get("artifacts")[0].get("base64")
    decoded_png_data = base64.b64decode(encoded_png_data)
    return decoded_png_data

# 画像を生成
image = generate_image(stability_prompt)

パラメータのstyle_presetはどのような感じの画像にするかを選べることができます。今回はアドベントカレンダーとしての画像なので、comic-bookが合いそうと思い選びました。

⑤ 生成した画像をアップ

まずは、CDKで画像をアップするためのS3バケットを下記コードで作成します。

# 画像用のバケットの作成
asset_bucket = s3.Bucket(
    self,
    id=asset_bucket_name,
    bucket_name=asset_bucket_name,
)

# Lambdaに必要な環境変数を設定
lambda_function.add_environment("BUCKET_NAME", asset_bucket.bucket_name)

その後、前ステップで生成した画像をLambdaからS3へアップしています。

bucket_name = os.environ['BUCKET_NAME']
s3.put_object(
    Bucket=bucket_name,
    Key=f"img/netcom/{index}.png",
    Body=image,
    ContentType='image/png'
)

⑥ 画像からページを生成

アップした画像をもとにページを生成します。今回はNext.jsを使ってHTMLを作成します。といっても大したことはせず、アップした画像をCloud9に落とし、それを元にカレンダーを生成しました。

落とした画像をNext.jsのpublicディレクトリ配下に配置して、page.tsx を下記に修正しました。

import Image from 'next/image'

export default function Home() {
  // 1日から25日までの配列を生成
  const days = Array.from({ length: 25 }, (_, i) => i + 1);
  // CloudFrontのベースURL
  const cloudFrontBaseUrl = "https://d2vmu48pnp4egz.cloudfront.net/img/netcom/";

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24 bg-gradient-to-r from-gray-300 to-gray-200 dark:from-gray-800 dark:to-gray-700">
      <h1 className="text-3xl font-bold mb-8">ネットコムアドベントカレンダー</h1>
      <div className="grid grid-cols-5 gap-4">
        {days.map(day => (
          <div key={day} className="border rounded-lg p-4 flex flex-col items-center justify-center">
            <div className="text-2xl font-semibold">{day}</div>
            <Image
              src={`/img/${day}.png`}
              alt={`Day ${day}`}
              width={100}
              height={100}
            />
          </div>
        ))}
      </div>
    </main>
  );
}

これを $ npm run build して、出力されたoutディレクトリのものをcdkディレクトリの ./assets/cloudfront へ移動させておきます。

⑦ ページをS3へデプロイ

作成したページを公開するために、CDKでAmazon CloudFrontとAmazon Simple Storage Service(S3)を作ります。

bucket = s3.Bucket(
    self,
    id=bucket_name,
    bucket_name=bucket_name,
    public_read_access=True,
)

# CloudFrontディストリビューションの作成
distribution = cloudfront.CloudFrontWebDistribution(self, "MyDistribution",
    origin_configs=[
        cloudfront.SourceConfiguration(
            s3_origin_source=cloudfront.S3OriginConfig(s3_bucket_source=bucket),
            behaviors=[cloudfront.Behavior(
                is_default_behavior=True,
                forwarded_values=cloudfront.CfnDistribution.ForwardedValuesProperty(
                    query_string=True,
                    cookies=cloudfront.CfnDistribution.CookiesProperty(forward='all')
                )
            )]
        )
    ])

前ステップでビルドしたコンテンツをCDKからS3へデプロイするために下記コードをCDKに追加して cdk deploy します。

# S3バケットへのファイルアップロード
deployment = s3_deployment.BucketDeployment(self, "DeployWebsite",
    sources=[s3_deployment.Source.asset("./assets/cloudfront")],  # ローカルのディレクトリパス
    destination_bucket=bucket,
    distribution=distribution,
    distribution_paths=["/*"])

すると、最初にお見せしたようなページがCloudFront経由で見れるようになりました。

まとめ

初めてアドベントカレンダーの最終日を担当したので、せっかくならとアドベントカレンダーをまとめるような記事にしてみました。
もしアドベントカレンダーをまとめたくなれば是非参考にしてください。
まだまだプロンプトやら構成やら改善の余地はあるのですが、大好きなAmazon BedrockやAWS Cloud Development Kit(CDK)をいじり倒せて楽しかったです。

執筆者志水友輔

2023 Japan AWS Ambassador / 2021,2023 Japan AWS Top Engineer / 2021-2023 APN ALL AWS Certifications Engineers
大阪でAWSを中心としたクラウドの導入、設計、構築を専門に行っています。Generative AIとCDKとつけ麺が大好物

X:https://twitter.com/shimi023

Amazon著者ページ:Amazon.co.jp: 志水友輔:作品一覧、著者略歴