🐶

ChatGPT+DALL・Eで画像付きスライドを自動作成する

2023/05/01に公開

はじめに

この記事は、前回作成したものをベースに、各スライドに画像を挿入するためのプロンプトを追加+スライドの内容をベースにDALL・Eで画像を生成する機能を追加したものです。

必要なもの

  • OpenAIのAPIキー
    3ヶ月で失効する$18の無料枠があります。(以降は従量課金製)
    ChatGPTとDALL・EのAPIリクエストに使用します
    APIの料金については以下参照

https://openai.com/pricing

  • Pythonのライブラリ
    • openai
    • unstructured
    • selenium

実装

テキストデータの読み込み

今回はWikipediaの犬のページから情報を抽出します。テキストデータのロード部分を以下のように変更しています。

- loader = PyPDFLoader("./docs/doj_cloud_act_white_paper_2019_04_10.pdf")
- pages = loader.load_and_split()
- state_of_the_union = "".join([x.page_content for x in pages])
+ urls = ["https://ja.wikipedia.org/wiki/イヌ",]
+ loader = SeleniumURLLoader(urls=urls)
+ data = loader.load()[0].page_content

- texts = text_splitter.create_documents([state_of_the_union])
+ texts = text_splitter.create_documents([data])

スライドに画像を埋め込む

生成するスライドに画像を挿入するために、プロンプトに記述したFew-Shotの部分に![bg right 40% 40%]({ file name }.png)を追加しています。


---
 <!-- スライド n -->
 # { タイトル }

 - { 本文 }
 - { 本文 }
 - { 本文 } 

+ ![bg right 40% 40%]({ file name }.png)

"""
'''

画像生成のためのプロンプト作成

スライドの内容から#で始まるタイトル部分を正規表現で抽出しています。
このタイトル部分をベースにChatGPTにリクエストを投げ、画像生成のためのプロンプトを作成させます。

def create_prompt(text:str) -> str:
    title_pattern = r"#.*?\n"
    title = re.findall(title_pattern, text)[0]
 
    theme = f"タイトル:{title}"

    status = '''
    あなたにはテーマが与えられるので、画像生成AIであるDall・eを使用して文章を説明するための画像を生成するためのプロンプトを作成してください。
    プロンプトを生成する際は以下の条件を遵守してください。

    # 条件
    - プロンプトは英語で作成してください
    - 与えられたテーマについてできるだけ詳細に情景を記述してください
    - 一文で記述してください
    - 詳細に説明する場合にはコンマ区切りで文末に接続してください
    - プロンプトの分のみを出力してください

    '''
    prefix = [
            {"role": "system", "content": status},
    ]


    comversation = [{"role": "user", "content": theme}]
    messages = prefix + comversation
    response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages,
    temperature = 0,
    )

    prompt = response["choices"][0]["message"]["content"]
    return prompt

DALL・Eによる画像生成

ChatGPTのリクエストと同様に、DALL・Eによる画像生成でもOpenAIのライブラリを使用してリクエストを送信します。
生成した画像は指定のパスに保存するようにしています。ここでは生成するスライドと同じディレクトリに生成することを想定しています。

def create_image(prompt:str, image_path:str) -> None:
    NUMBER_OF_IMAGES = 1

    response = openai.Image.create(
        prompt=prompt,
        n=NUMBER_OF_IMAGES,
        size="512x512",
        response_format="b64_json",
    )

    img_data = base64.b64decode(response["data"][0]["b64_json"])
    with open(image_path, "wb") as f:
        f.write(img_data)

スライド生成

各スライド作成後に画像が含まれているかを正規表現で確認し、含まれている場合のみ先述のプロンプト作成および画像生成を行います。

pattern = r"!\[\s*.*\s*\]\(\s*.*\s*\)"

if re.search(pattern, answer):
    new_text = re.sub(pattern, "", answer)
    prompt = create_prompt(new_text)

    file_markdowns = re.findall(pattern, answer)
    for file_markdown in file_markdowns:
        filename = file_markdown[file_markdown.find("(")+1:-1]
        create_image(prompt, filename)
全体のコードはこちら
main.py
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain.document_loaders import SeleniumURLLoader
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain
import time
import os
import openai
import re

from create_prompt import create_prompt
from create_image import create_image

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 400,
    chunk_overlap  = 20,
    length_function = len,
)

urls = ["https://ja.wikipedia.org/wiki/イヌ",]

loader = SeleniumURLLoader(urls=urls)
data = loader.load()[0].page_content

texts = text_splitter.create_documents([data])

os.environ["OPENAI_API_KEY"]="your api key"

embeddings = OpenAIEmbeddings()
filename = "faiss_index_dog"
if os.path.isdir(filename):
    vectorstore = FAISS.load_local("faiss_index_dog", embeddings)
else:
    vectorstore = FAISS.from_documents(texts, embedding=embeddings)
vectorstore.save_local("faiss_index_dog")


llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
retrieval_chain_agent = ConversationalRetrievalChain.from_llm(llm, vectorstore.as_retriever(), return_source_documents=True, verbose = True)

topic = "犬の種類"

query = f'''
{topic}について、重要なテーマを2つまでカンマ区切りで挙げてください。カンマ区切りのテーマのリストのみを出力してください。
'''
chat_history = []

result = retrieval_chain_agent({"question": query, "chat_history": chat_history})
chat_history.append(query)
chat_history.append(result)

results = []
topics = result["answer"].split(",")

if len(topics) < 2:
    raise Exception
for t in topics:
    query = f'{t}について要点を記述してください。'
    time.sleep(10)
    result = retrieval_chain_agent({"question": query, "chat_history": []})
    results.append(result["answer"])

status = '''
あなたは優秀なエバンジェリストです。与えられた文章について簡潔に説明するためのスライドを作成します。
スライドを作成する際は次のようなMarpのデザインテンプレートを使用したマークダウン形式で表現されます。
箇条書きの文はできるだけ短くまとめてください。
必要に応じて画像も挿入してください。
"""

---
<!-- スライド n -->
# { タイトル }

- { 本文 }
- { 本文 }
- { 本文 } 

![bg right 40% 40%]({ file name }.png)


"""
'''
prefix = [
        {"role": "system", "content": status},
]


slides = []
for r in results:
    data = f"""
次に与えられる文章の内容についてMarpによるマークダウン形式でスライドを日本語で作成してください。
必要に応じてスライドは2枚以上に分割してください:
{r}

    """
    comversation = [{"role": "user", "content": data}]
    messages = prefix + comversation
    response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages,
    temperature = 0,
    )
    time.sleep(10)
    answer = response["choices"][0]["message"]["content"]

    pattern = r"!\[\s*.*\s*\]\(\s*.*\s*\)"

    if re.search(pattern, answer):
        new_text = re.sub(pattern, "", answer)
        prompt = create_prompt(new_text)
        file_markdowns = re.findall(pattern, answer)
        for file_markdown in file_markdowns:
            filename = file_markdown[file_markdown.find("(")+1:-1]
            create_image(prompt, filename)

    slides.append(answer)

output_str = """---
marp: true
_theme: gaia
paginate: true
backgroundColor: #f5f5f5

"""
for i in slides:
    output_str += i + "\n"


with open("slide.md", "w") as file:
    file.write(output_str)

create_prompt.py
import openai
import re

def create_prompt(text:str) -> str:
    title_pattern = r"#.*?\n"
    title = re.findall(title_pattern, text)[0]
 
    theme = f"タイトル:{title}"

    status = '''
    あなたにはテーマが与えられるので、画像生成AIであるDall・eを使用して文章を説明するための画像を生成するためのプロンプトを作成してください。
    プロンプトを生成する際は以下の条件を遵守してください。

    # 条件
    - プロンプトは英語で作成してください
    - 与えられたテーマについてできるだけ詳細に情景を記述してください
    - 一文で記述してください
    - 詳細に説明する場合にはコンマ区切りで文末に接続してください
    - プロンプトの分のみを出力してください

    '''
    prefix = [
            {"role": "system", "content": status},
    ]


    comversation = [{"role": "user", "content": theme}]
    messages = prefix + comversation
    response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages,
    temperature = 0,
    )

    prompt = response["choices"][0]["message"]["content"]
    return prompt
create_image.py
import openai
import base64

def create_image(prompt:str, image_path:str):
    NUMBER_OF_IMAGES = 1

    response = openai.Image.create(
        prompt=prompt,
        n=NUMBER_OF_IMAGES,
        size="512x512",
        response_format="b64_json",
    )

    img_data = base64.b64decode(response["data"][0]["b64_json"])
    with open(image_path, "wb") as f:
        f.write(img_data)

出力結果

今回の出力結果のサンプルです。各スライドに画像が挿入されています。

おわりに

スライドの内容に加えて、スライドのコンテンツに関連する画像を生成しスライドに埋め込むところまで自動でできました。スライドに画像を挿入する頻度や画像自体の精度については向上の余地があると思いますが、完全自動生成で作成したものとしては及第点ではないかと思います。

Discussion