ChatGPTでDalle用プロンプト作成アプリ②

ChatGPTでDalle用プロンプト作成アプリ②

はじめに

本記事は#AWSAmplifyJP Advent Calendar 2023の13日目の記事です。 クソアプリ Advent Calendar 2023に参加するために、ChatGPTでDalle(画像生成)用のプロンプトを作りました。 尚、アプリの機能は下記記事で紹介しています。

https://qiita.com/tanosugi/private/927add61c89565cd5448

Demo

削除済み

使った技術

この記事では使った技術を紹介したいと思います。

Amplify

Amplifyの機能を多く使ったので、コード等とともに紹介します。

Figma to Code

まず、FigmaでLPや、Webアプリのコンポーネントを作り、Figma to Code機能でReactのコードを得ました。アプリの種類にもよりますが、自分でゼロからコード書く場合の数分の一から半分のコーディングで済みます。AIにコードを書いてもらうvercelのv0等が最近登場しましたが、個人的にはプロンプトで指示するよりGUIで書いたものがコードになる方が好きです。

スクリーンショット 2023-12-10 172216.png

自動生成されたReactコードの一例

/*************************************************************************** * The contents of this file were generated with Amplify Studio. * * Please refrain from making any modifications to this file. * * Any changes to this file will be overwritten when running amplify pull. * **************************************************************************/ /* eslint-disable */ import * as React from "react"; import { getOverrideProps } from "./utils"; import { Button, Flex, Image, Text } from "@aws-amplify/ui-react"; export default function HeroView(props) { const { overrides, ...rest } = props; return ( <Flex gap="0" direction="row" width="1490px" height="unset" justifyContent="center" alignItems="center" position="relative" padding="0px 0px 0px 0px" {...getOverrideProps(overrides, "HeroView")} {...rest} > <Flex gap="10px" direction="column" width="720px" height="392px" justifyContent="space-between" alignItems="center" shrink="0" position="relative" padding="0px 0px 0px 0px" {...getOverrideProps(overrides, "Frame 3")} > <Flex gap="24px" direction="column" width="316px" height="unset" justifyContent="center" alignItems="center" grow="1" shrink="1" basis="0" position="relative" padding="0px 0px 0px 0px" {...getOverrideProps(overrides, "HeroMessage")} > <Flex gap="16px" direction="column" width="unset" height="unset" justifyContent="center" alignItems="center" shrink="0" alignSelf="stretch" position="relative" padding="0px 0px 0px 0px" {...getOverrideProps(overrides, "Message")} > <Text fontFamily="Inter" fontSize="16px" fontWeight="700" color="rgba(64,170,191,1)" lineHeight="24px" textAlign="center" display="block" direction="column" justifyContent="unset" width="unset" height="unset" gap="unset" alignItems="unset" shrink="0" alignSelf="stretch" position="relative" padding="0px 0px 0px 0px" whiteSpace="pre-wrap" children="Easy Image" {...getOverrideProps(overrides, "Eyebrow")} ></Text> <Text fontFamily="Inter" fontSize="24px" fontWeight="600" color="rgba(13,26,38,1)" lineHeight="30px" textAlign="center" display="block" direction="column" justifyContent="unset" width="unset" height="unset" gap="unset" alignItems="unset" shrink="0" alignSelf="stretch" position="relative" padding="0px 0px 0px 0px" whiteSpace="pre-wrap" children="画像を簡単に生成" {...getOverrideProps(overrides, "Heading")} ></Text> <Text fontFamily="Inter" fontSize="16px" fontWeight="400" color="rgba(48,64,80,1)" lineHeight="24px" textAlign="center" display="block" direction="column" justifyContent="unset" letterSpacing="0.01px" width="unset" height="unset" gap="unset" alignItems="unset" shrink="0" alignSelf="stretch" position="relative" padding="0px 0px 0px 0px" whiteSpace="pre-wrap" children="生成したい画像について簡単に伝えるだけで、自動で画像生成用のプロンプトを作り、実行できます。" {...getOverrideProps(overrides, "Body")} ></Text> </Flex> <Button width="unset" height="unset" shrink="0" size="large" isDisabled={false} variation="primary" children="試してみる" {...getOverrideProps(overrides, "Button")} ></Button> </Flex> </Flex> <Image width="720px" height="392px" display="block" gap="unset" alignItems="unset" justifyContent="unset" shrink="0" position="relative" padding="0px 0px 0px 0px" objectFit="cover" {...getOverrideProps(overrides, "1700652527621-cat 1")} ></Image> </Flex> ); }

Amplify Form Builder, react-modal

入力フォームはForm Builderで作りました。react-modalで、一つのフォームが入力終わって送信されたら次のモーダルが開くようにしました。

スクリーンショット 2023-11-23 174823.png

コード

"use client"; import Modal from "react-modal"; import { CreatedImage } from "@/models"; import { createFromDalleAndSaveToS3, getSignedS3UrlFromKey, } from "@/utils/createSaveImage"; import { Button, Flex, Loader } from "@aws-amplify/ui-react"; import { DataStore } from "aws-amplify/datastore"; import React, { ChangeEvent, useState, } from "react"; import PromptCreateForm from "@/ui-components/PromptCreateForm"; import PromptEditForm from "@/ui-components/PromptEditForm"; import createPrompt from "@/utils/chat"; import ImageCreateSuccessView from "@/ui-components/ImageCreateSuccessView"; export default function CreateSaveImageModal({ openApiKey, }: { openApiKey: string; }) { const [modalToOpen, setModalToOpen] = useState(""); const [generationTarget, setGenerationTarget] = useState(""); const [adjective, setAdjective] = useState(""); const [languageOfPrompt, setLanguageOfPrompt] = useState(""); const [numberOfWordsPrompt, setNumberOfWordsPrompt] = useState(""); const [prompt, setPrompt] = useState(""); const [imageUrl, setImageUrl] = useState(""); const [loadingDalle, setLoadingDalle] = useState(false); const createImage = async (prompt: string) => { setLoadingDalle(true); const key = await createFromDalleAndSaveToS3(openApiKey, prompt); const url = await getSignedS3UrlFromKey(key); setImageUrl(url); DataStore.save(new CreatedImage({ title: prompt, s3Url: key })); setLoadingDalle(false); }; const onSubmitPromptCreateForm = async () => { setModalToOpen("CreatingPrompt"); const resp = await createPrompt({ openApiKey: openApiKey, generationTarget: generationTarget, adjective: adjective, languageOfPrompt: languageOfPrompt, numberOfWordsPrompt: numberOfWordsPrompt, }); setPrompt(resp); setModalToOpen("PromptEditForm"); }; const onSubmitPromptEditForm = async () => { setModalToOpen("CreatingImage"); await createImage(prompt); setModalToOpen("ImageCreateSuccessView"); }; return ( <> {loadingDalle ? ( "Creating Image..." ) : ( <Button variation="primary" onClick={() => setModalToOpen("PromptCreateForm")} > プロンプトを作って画像を生成する </Button> )} <Modal isOpen={modalToOpen == "PromptCreateForm"}> <PromptCreateForm onSubmit={onSubmitPromptCreateForm} overrides={{ generationTarget: { value: generationTarget, onChange: (event: ChangeEvent<HTMLInputElement>) => setGenerationTarget(event.target.value), }, adjective: { value: adjective, onChange: (event: ChangeEvent<HTMLInputElement>) => setAdjective(event.target.value), }, languageOfPrompt: { value: languageOfPrompt, onChange: (event: any) => { setLanguageOfPrompt(event.target.value); // console.log("languageOfPrompt:", languageOfPrompt); }, }, numberOfWordsPrompt: { value: numberOfWordsPrompt, onChange: (event: any) => { setNumberOfWordsPrompt(event.target.value); console.log("numberOfWordsPrompt:", numberOfWordsPrompt); }, }, }} /> </Modal> <Modal isOpen={modalToOpen == "CreatingPrompt"}> <Flex height="100%" direction="column" alignItems="center" justifyContent={"center"} > {"プロンプトを生成しています。。。"} <Loader variation="linear" /> </Flex> </Modal> <Modal isOpen={modalToOpen == "PromptEditForm"}> <PromptEditForm onSubmit={onSubmitPromptEditForm} overrides={{ Field0: { defaultValue: prompt, value: prompt, onChange: (event: any) => { let { value } = event.target; setPrompt(value); }, }, }} /> </Modal> <Modal isOpen={modalToOpen == "CreatingImage"}> <Flex height="100%" direction="column" alignItems="center" justifyContent={"center"} > {"画像を生成しています。。。"} <Loader variation="linear" /> </Flex> </Modal> <Modal isOpen={modalToOpen == "ImageCreateSuccessView"}> {imageUrl != "" && ( <ImageCreateSuccessView overrides={{ "1700652527621-cat 1": { src: imageUrl }, Button: { onClick: () => { setModalToOpen(""); setImageUrl(""); }, }, prompt: { children: prompt }, }} /> )} </Modal> </> ); }

Datastore, Figma to CodeのCollection

生成したプロンプトや画像の一覧はDatastoreに保存し、Figma to Codeで作ったコンポーネントを使って表示するようにしました。Collectionという、Figmaで作ったコンポーネントを複数表示する機能を使いました。

コード

"use client"; import { CreatedImage } from "@/models"; import ImageCardViewCollection from "@/ui-components/ImageCardViewCollection"; import { getSignedS3UrlFromKey } from "@/utils/createSaveImage"; import { DataStore, Predicates, SortDirection } from "aws-amplify/datastore"; import { useEffect, useState } from "react"; export default function App() { const [url, setUrl] = useState(""); const [createdImages, setCreatedImages] = useState<CreatedImage[]>([]); const fetchCreatedImage = async () => { const resp = await DataStore.query(CreatedImage, Predicates.ALL, { sort: (s) => s.createdAt(SortDirection.DESCENDING), }); setCreatedImages([]); const imgs: CreatedImage[] = []; resp.forEach(async (item) => { const urlResp = await getSignedS3UrlFromKey(item.s3Url); await setUrl(urlResp); const img: CreatedImage = new CreatedImage({ title: item.title, s3Url: urlResp, }); imgs.push(img); }); setCreatedImages(imgs); }; useEffect(() => { fetchCreatedImage(); const subscription = DataStore.observe(CreatedImage).subscribe(fetchCreatedImage); return () => { subscription.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> <ImageCardViewCollection items={createdImages} overrideItems={({ item, index }) => ({ overrides: { ImageCardView: { width: "100%" } }, })} /> </> ); }

S3

生成した画像はAmplify経由でS3に格納し、一覧表示するときはSignedURLを取得するようにしました。 コード

export async function uploadBlobToS3(blob: Blob) { const key = `${Date.now()}-cat.png`; try { const result = await uploadData({ key: key, data: blob, options: { accessLevel: "guest", contentType: "image/png" }, }).result; console.log("Succeeded: ", result); } catch (error) { console.log("Error : ", error); } return key; } export async function getSignedS3UrlFromKey(key: string) { const getUrlResult = await getUrl({ key: key, options: { validateObjectExistence: false, expiresIn: 20 }, }); return getUrlResult.url.toString(); }

認証(パスワード、Googleログイン)

Amplifyでは認証はfirebase等と同じ感覚でかんたんに導入できます。GoogleログインはGCPとの間でAPI Key等を相互に参照するので、GUIで少し設定が必要です。

コード

return ( <Authenticator> {children} </Authenticator> );

Nextjs 14

Amplify HostingでNextjs 14を使う場合は、Amazon Linux 2023をビルドイメージに使うだけです。Nextjs 14がリリースされてしばらくしてからサポートされました。 https://github.com/aws-amplify/amplify-hosting/issues/3773

Langchain, ChatGPT API

ここからはAmplifyとは別の今回使った技術です。最初にChatGPTにプロンプトを送って、ChatGPTに画像生成用のプロンプトを作ってもらいますが、ユーザーが入力した単語や数字等をプロンプトに埋め込むのにLangchainのLCEL (LangChain Expression Language)を使いました。 コード

"use server"; import { PromptTemplate } from "langchain/prompts"; import { ChatOpenAI } from "langchain/chat_models/openai"; export default async function createPrompt({ openApiKey, generationTarget, adjective, languageOfPrompt, numberOfWordsPrompt, }: { openApiKey: string; generationTarget: string; adjective: string; languageOfPrompt: string; numberOfWordsPrompt: string; }) { const model = new ChatOpenAI({ openAIApiKey: openApiKey }); console.log("languageOfPrompt:", languageOfPrompt); const promptTemplate = PromptTemplate.fromTemplate( `下記条件でDalleに出力させるためのプロンプトを考えてください。箇条書きではなく文章で 生成したいもの: {generationTarget} どんな画像にしたいか: {adjective} 制約1:プロンプトのみを返してください。プロンプトの説明は不要です。 制約2:プロンプトは一つだけ作成してください。 制約3:プロンプトは出力が美しくなるように{numberOfWordsPrompt}単語前後で詳細に作成してください。 プロンプトの言語:{languageOfPrompt} ` ); const chain = promptTemplate.pipe(model); console.log(chain); const result = await chain.invoke({ generationTarget: generationTarget, adjective: adjective, languageOfPrompt: languageOfPrompt, numberOfWordsPrompt: numberOfWordsPrompt && numberOfWordsPrompt != "" ? numberOfWordsPrompt : "20", }); console.log(result); return String(result.content); }

Dalle API

Ddalle APIはresponse_formatにb64_jsonを指定して、画像を取得しました。

コード

"use server"; import OpenAI from "openai"; export async function createDalleImageAsBase64({ openApiKey, prompt, }: { openApiKey: string; prompt: string; }) { const openai = new OpenAI({ apiKey: openApiKey, }); const image = await openai.images.generate({ model: "dall-e-2", prompt: prompt, response_format: "b64_json", size: "256x256", }); const b64_json: string = image.data[0].b64_json ? image.data[0].b64_json : ""; return b64_json; }

b64-to-blob

取得した画像をS3に格納する前に、blobに変換しました。 コード

export async function createFromDalleAndSaveToS3( openApiKey: string, prompt: string ) { const b64_json = await createDalleImageAsBase64({ openApiKey: openApiKey, prompt: prompt, }); const blob = b64toblob(b64_json); const key = await uploadBlobToS3(blob); return key; }

まとめ

AmpliifyやChatGPTのAPIを使うとスピーディーにアプリが作れるので、プロトタイピングだけでなくハッカソンにもお勧めです。

CC BY-NC 4.0 お問い合わせ Privacy Policy2025 © tanosugi.RSS
ChatGPTでDalle用プロンプト作成アプリ② | Falcon Apps Tech Blog