1
0

ChatGPTでDalle(画像生成)用のプロンプトを作るアプリ②(Amplify等技術面紹介)

Last updated at Posted at 2023-12-12

はじめに

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

Demo

使った技術

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

Amplify

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

Figma to Code

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

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

自動生成されたReactコードの一例
HeroView.jsx
/***************************************************************************
 * 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

コード
createSaveImageModal.tsx

"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で作ったコンポーネントを複数表示する機能を使いました。

コード
showImageCollection.tsx
"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を取得するようにしました。

コード
createSaveImage.ts
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)を使いました。

コード
chat.ts
"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を指定して、画像を取得しました。

コード
createDalleImageAsBase64.ts
"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に変換しました。

コード
createSaveImage.ts
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を使うとスピーディーにアプリが作れるので、プロトタイピングだけでなくハッカソンにもお勧めです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0