簡単な画像生成AIアプリをdiffusersとFastAPIとvuetifyで作ったりして

この記事では**diffusers**（Stable Diffusion）と**FastAPI**（Python）と**vuetify**（TypeScript／vue.js）を使って画像が生成できるためのウェブアプリを作成する例を説明します。


# 前置き

私は以前からvue.jsをフロントエンドとしてウェブサイトを作っていました。主に使ったライブラリーは[vuetify](https://vuetifyjs.com)で、2020年にQiitaで記事を書いたこともあります。

https://qiita.com/phyblas/items/f3f40df76f85d61d5286

バックエンドの方は色々試したことがあります。Pythonでは以前responderというフレームワークを使っていたのですが、今年に入ってからFastAPIを使い始めました。記事も書きました。

https://qiita.com/phyblas/items/c3ff92b6dd353f887f1d

だからこれからウェブサイトを作るならFastAPI＋vue.jsの組み合わせが一番だと思っています。

そして最近生成AIに興味があってStable Diffusionを始めました。diffusersというモジュールを使ったら簡単にStable Diffusionモデルを扱って画像を生成できます。

diffusersとStable Diffusionの基本に関してはこの記事に纏めて入門を書きました。

https://qiita.com/phyblas/items/00f750b8277f66fb9b13

ということでvue.js（vuetify）+FastAPI+diffusersで生成AIのウェブアプリを作ってみることにしました。

これは本格的なウェブアプリではなく、ただ簡単な機能だけ搭載して、初心者の勉強の参考にする例として書いたものです。

vue.jsは今までJavaScriptで書いていましたが、最近TypeScriptという言語を知って、vue.jsではJavaScriptの代わりにTypeScriptを使うもできるとわかったから今回ちょっとTypeScriptに挑戦してみることにしました。とはいっても殆どJavaScriptと変わらないから新しく勉強することは少しだけです。


# 作るもの

作る説明に入る前にとりあえず今回どんなウェブアプリを作るか見ておきましょう。

まずブラウザで作成完了でデプロイしウェブアプリにアクセスしたらこんな画面になります。
![stadifapp01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/3cc528da-42be-f2fc-c5ab-e0093ce42d5c.png)

左にラジオボタンで、入力した注文だけ画像を生成するか、元画像を使って新しい画像を生成するか、という選択肢があります。まずは言葉だけ生成してみましょう。

例えば試しに「水遊びしている和服の水色三つ編み少女」と入力して、画像サイズは既定値のままにして「生成開始」ボタンを押したら生成が始まって、この画面に入って暫く待つことになります。
![stadifapp02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/65cb76ad-8647-c627-d1f2-e2d630179a75.png)

そして生成が終わったら結果は下に出てきます。ちゃんと書いた通りの画像が出てきますね。
![stadifapp03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/29a636a2-6245-a202-94cf-f93fc518c6b3.jpeg)

左の「保存」ボタンを押したら名前を付けて画像を保存することができます。
![stadifapp04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/1d8f36c4-56a6-f343-9d58-92b0a804de99.jpeg)

次は「元画像から生成する」を選択してみます。そうしたら右の方が変わって、画像ファイルの選択が現れます。「変化の強さ」はどれくらい元画像から変わるかを示すパラメータであり0（そのまま）から1（完全に変わる）まで設定できます。
![stadifapp05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/750443c2-68be-e879-c4b4-40c0ecb1622d.png)

もしファイル選択をしていないまま生成を開始しようとしたら忠告が出てきます。
![stadifapp06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/ff540d18-6677-a79b-48a4-d42db7dc2308.png)

試しに先程生成された画像を選んで、「ゴッホ風」と入力します。
![stadifapp07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/92aa2922-67ff-7018-28e6-880d52869583.png)

そうしたら微妙だけどゴッホが描いたような絵（多分ね）に変換された画像ができます。
![stadifapp08.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/d9963345-1980-caff-85e2-bea0d0b9f50c.jpeg)

はい、終わりです。これくらいしかない小さく地味なウェブアプリですね。

このような簡単なウェブアプリを今回のテュートリアルにします。


# 使うライブラリーとフレームワーク

ウェブアプリ開発は普段一つの言語か一つのフレームワークで作るものではないのですね。色んな組み合わせで作れますが、今回使うのは主この3つに分けられます。

<table>
<tr><td rowspan="2" style="border: 1.5px solid #8866AA; text-align: center;"><strong>役目</strong></td><td style="border: 1.5px solid #8866AA; text-align: center;" colspan="2"><strong>ライブラリー</strong></td><td rowspan="2" style="border: 1.5px solid #8866AA; text-align: center;"><strong>言語／フレームワーク</strong></td></tr>
<tr><td style="border: 1.5px solid #8866AA; text-align: center;"><strong>メイン</strong></td><td style="border: 1.5px solid #8866AA; text-align: center;"><strong>その他</strong></td></tr>
<tr><td style="border: 1.5px solid #8866AA;">画像生成AI</td><td style="border: 1.5px solid #8866AA;"><a href="https://github.com/huggingface/diffusers">diffusers</a></td><td style="border: 1.5px solid #8866AA;"><a href="https://pytorch.org/">pytorch</a><br><a href="https://github.com/huggingface/transformers">transformers</a><br><a href="https://github.com/terryyin/translate-python">translate</a><br><a href="https://pypi.org/project/pillow/">PIL</a></td><td rowspan="3" style="border: 1.5px solid #8866AA;">Python</td></tr>
<tr></tr>
<tr><td style="border: 1.5px solid #8866AA;">バックエンド</td><td style="border: 1.5px solid #8866AA;"><a href="https://fastapi.tiangolo.com/ja/">fastapi</a></td><td style="border: 1.5px solid #8866AA;"><a href="https://www.uvicorn.org/">uvicorn</a></td></tr>
<tr></tr>
<tr><td style="border: 1.5px solid #8866AA;">フロントエンド</td><td style="border: 1.5px solid #8866AA;"><a href="https://vuetifyjs.com">vuetify</a></td><td style="border: 1.5px solid #8866AA;"><a href="https://github.com/axios/axios">axios</a></td><td style="border: 1.5px solid #8866AA;">TypeScript／<a href="https://cn.vuejs.org/">vue.js</a></td></tr>
</table>


# 環境設定

## インストール

まずは必要なPythonライブラリーのインストールです。バックエンド関連のライブラリーはfastapiがメインだが、その他にuvicornとmultipartも必要なので、全部一緒にインストールします。どれもpipで簡単にインストールできます。
```
pip install fastapi uvicorn python-multipart
```

生成AIのライブラリーはpytorchが基本ですが、これはGPUを使うかどうか違うので、まずはこのリンクを参考にしてpytorchをインストールします。
https://pytorch.org/get-started/locally/

そしてdiffusersとその他の関連のライブラリーは簡単にpipでインストールできます。
```
pip install diffusers transformers translate
```

vue.jsの方はyarnをインストールしていれば十分です。


## 今回使っている環境

バージョンが違うと書き方や作動は大きく違うことがあります。特にvue.js関連は違いが大きすぎて古い知識はすぐ使えなくなって面倒です。私が2020年に書いた[vuetifyの記事](https://qiita.com/phyblas/items/f3f40df76f85d61d5286)も今回の書き方とは大分違って、今になってほぼ使い物になっていません。

だから今回使う言語や関連のライブラリーのバージョンをここに書いておきます。殆ど今2024年7月時点での最新のものです。

<table>
<tr><td><strong>python</strong></td><td>3.12.2</td></tr>
<tr><td>diffusers</td><td>0.29.2</td></tr>
<tr><td>pytorch</td><td>2.3.1</td></tr>
<tr><td>transformers</td><td>4.41.2</td></tr>
<tr><td>translate</td><td>3.6.1</td></tr>
<tr><td>PIL</td><td>10.3.0</td></tr>
<tr><td>fastapi</td><td>0.111.0</td></tr>
<tr><td>uvicorn</td><td>0.30.1</td></tr>
<tr><td>multipart</td><td>0.0.9</td></tr>
<tr><td><strong>vue</strong></td><td>3.4.21</td></tr>
<tr><td>vuetify</td><td>3.5.8</td></tr>
<tr><td>axios</td><td>1.7.2</td></tr>
<tr><td>vite</td><td>5.1.5</td></tr>
<tr><td>yarn</td><td>1.22.22</td></tr>
<tr><td>typescript</td><td>5.4.2</td></tr>
</table>


## 使うパソコン

画像生成AIはGPUがあった方が一番ですが、もしなければCPUのままで遅いだけで作れないわけではありません。ただウェブアプリに作るとしたら遅すぎるときついかもですね。メモリーも十分大きくないと無理です。

今回私はメモリー24GBのMacBook Air M3を使っています。macbookにはmpsというGPUがあります。CUDAほど速くないが、512×512サイズの画像を生成する程度は十分だと思います。

ただ本格的にウェブアプリを作る時はCUDAのGPUを搭載したコンピュータでないと話にならないかもしれません。


# 全体の構成

作成するプロジェクトは2つのフォルダに分けられます。ここでは`stadifweb`と`stadifvue`という名前を付けます。`stadifweb`はウェブアプリのメインフォルダで、`stadifvue`はプリコンパイルのファイルを作るためのvue.jsプロジェクトフォルダ。

```
├──stadifweb
│   ├── web.py
│   ├── seisei.py
│   ├── cuteyukimixAdorable_kemiaomiao.safetensors
│   ├── index.html
│   ├── favicon.ico
│   ├── assets
│   └── img
├──stadifvue
│   ├── src
│   │   ├── App.vue
│   │   └── (その他）
│   └── (その他）
```

各ファイルを纏めたらこのようになります。

<table>
<tr><td style="text-align: center; border: 1.5px solid #814B4B;"><strong>名前</strong></td><td style="text-align: center; border: 1.5px solid #814B4B;"><strong>種類</strong></td><td style="text-align: center; border: 1.5px solid #814B4B;"><strong>説明</strong></td></tr>
<tr><td style="border: 1.5px solid #814B4B;">stadifweb</td><td style="border: 1.5px solid #814B4B;">フォルダ</td><td style="border: 1.5px solid #814B4B;">ウェブアプリのメインフォルダ</td></tr>
<tr><td style="border: 1.5px solid #814B4B;">├ web.py</td><td style="border: 1.5px solid #814B4B;">Python</td><td style="border: 1.5px solid #814B4B;">FastAPIで書いたウェブアプリの核心</td></tr>
<tr><td style="border: 1.5px solid #814B4B;">├ seisei.py</td><td style="border: 1.5px solid #814B4B;">Python</td><td style="border: 1.5px solid #814B4B;">diffusersで書いた生成AI。web.pyから呼び出される</td></tr>
<tr><td style="border: 1.5px solid #814B4B;">├ <a href="https://civitai.com/models/28169/cuteyukimixadorable-style">cuteyukimixAdorable_kemiaomiao.safetensors</a></td><td style="border: 1.5px solid #814B4B;">バイナリ</td><td style="border: 1.5px solid #814B4B;">生成AIのStable Diffusionのチェックポイントモデル</td></tr>
<tr><td style="border: 1.5px solid #814B4B;">├ index.html<br>├ favicon.ico<br>├ assets</td><td style="border: 1.5px solid #814B4B;">プリコンパイル</td><td style="border: 1.5px solid #814B4B;">vue.jsによってプリコンパイルされたもの。直接手を出す必要がない</td></tr>
<tr><td style="border: 1.5px solid #814B4B;">├ img</td><td style="border: 1.5px solid #814B4B;">フォルダ</td><td style="border: 1.5px solid #814B4B;">生成された画像を保存するフォルダ。最初は空っぽ</td></tr>
<tr><td style="border: 1.5px solid #814B4B;">stadifvue</td><td style="border: 1.5px solid #814B4B;">フォルダ</td><td style="border: 1.5px solid #814B4B;">vue.jsプロジェクトのフォルダ</td></tr>
<tr><td style="border: 1.5px solid #814B4B;">　├ App.vue</td><td style="border: 1.5px solid #814B4B;">vue.js</td><td style="border: 1.5px solid #814B4B;">ウェブ画面を作るhtmlとTypeScript</td></tr>
</table>

ここで書き換えるのはただ3つだけ。
- web.py
- seisei.py
- App.vue


`cuteyukimixAdorable_kemiaomiao.safetensors`は「CuteYukiMix」というStable Diffusionのチェックポイントモデルのファイルです。Stable Diffusionで画像を生成するといっても使うモデルによって違う絵ができます。可愛いアニメ風の絵を作るためのモデルが色々ありますが、今回はこのモデルを選びました。このリンクからダウンロードできます。

https://civitai.com/models/28169/cuteyukimixadorable-style

ファイルサイズは2GBもあるので、今回プロジェクトで一番大きいファイルです。


# 画像生成AI（diffusers／Stable Diffusion）

Stable Diffusionで画像を生成する部分のPythonコードです。書き方などここでは詳しく説明しませんが、[私が以前書いた入門](https://qiita.com/phyblas/items/00f750b8277f66fb9b13)を参考に。
```python:seisei.py
import torch
from diffusers import StableDiffusionPipeline,StableDiffusionImg2ImgPipeline,DPMSolverMultistepScheduler
from translate import Translator

honyaku = Translator('en','ja').translate # プロンプトを日本語から英語に翻訳するための関数
model_file = 'cuteyukimixAdorable_kemiaomiao.safetensors'

dtype = torch.float16
if(torch.cuda.is_available()):
    device = 'cuda'
elif(torch.backends.mps.is_available()):
    device = 'mps'
else:
    device = 'cpu'
    dtype = torch.float32

# 言葉だけで画像を生成する（txt2img）関数
def seisei_txt2img(prompt,hirosa,takasa):
    pipe = StableDiffusionPipeline.from_single_file(model_file,torch_dtype=dtype).to(device)
    pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
    return pipe(
        honyaku(prompt),
        width=hirosa,
        height=takasa,
        num_inference_steps=20,
        clip_skip=2
    ).images[0]

# 元画像から画像を生成する（img2img）関数
def seisei_img2img(prompt,motogazou,tsuyosa):
    pipe = StableDiffusionImg2ImgPipeline.from_single_file(model_file,torch_dtype=dtype).to(device)
    pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
    return pipe(
        honyaku(prompt),
        motogazou,
        strength=tsuyosa,
        num_inference_steps=30,
        clip_skip=2
    ).images[0]
```

`device`のところはCUDAのGPUを搭載している場合はCUDAを、macの場合はMPSを、それ以外はGPUを使わずただCPUで実行することになります。


# バックエンド（FastAPI）

ウェブアプリを起動して管理するコードです。ここでフロントエンドからの注文を受け取って、`seisei.py`で書いた画像生成関数が呼び出されます。

書き方についてここでは詳しく説明しませんが[私が以前書いたFastAPIの記事](https://qiita.com/phyblas/items/c3ff92b6dd353f887f1d)など他の基本の記事が沢山あるので参考に。
```python:web.py
import os,io,datetime
from PIL import Image
from fastapi import FastAPI,Request,HTTPException
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from seisei import seisei_txt2img,seisei_img2img

# 画像を保存するためのフォルダ
if(not os.path.exists('img')):
    os.makedirs('img')

app = FastAPI()
# CORSを設定して開発中のvueから接続できるようにする
app.add_middleware(
    CORSMiddleware,
    allow_origins=['http://localhost:3000'],
    allow_credentials=True,
    allow_methods=['*'],
    allow_headers=['*'],
)

# 生成の注文を受け取って処理する
@app.post('/seisei_suru')
async def seisei_suru(request: Request):
    try:
        form = await request.form() # フォームデータを受け取る
        prompt = form['prompt']
        # 言葉だけによる生成（txt2img）の場合
        if(form['seisei_mode']=='txt2img'):
            hirosa = int(form['hirosa'])
            takasa = int(form['takasa'])
            gazou = seisei_txt2img(prompt,hirosa,takasa)
            gazou_file = 't2i_'
        # 元画像からの生成（img2img）の場合
        else:
            motogazou = Image.open(io.BytesIO(await form['motogazou'].read())).convert('RGB')
            tsuyosa = float(form['tsuyosa'])
            gazou = seisei_img2img(prompt,motogazou,tsuyosa)
            gazou_file = 'i2i_'
        
        # 画像ファイルの名前に日付と時間を付ける
        gazou_file += datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S')+'.jpg'
        gazou.save(os.path.join('img',gazou_file)) # 生成された画像を保存する
        return {'gazou_file': gazou_file}
    except Exception as err: # エラーが出てきたらその内容を返す
        raise HTTPException(status_code=500,detail='エラー：'+str(err))

# 静的ファイルの置く場所を直接このプロジェクトフォルダにする
app.mount(path="/",app=StaticFiles(directory='.',html=True))
```

準備が終わったらこのプロジェクトフォルダに入ってこのコマンドで実行します。
```
uvicorn web:app --reload
```
```:結果
INFO:     Will watch for changes in these directories: ['/Users/phyblas/stadifweb']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [93860] using WatchFiles
INFO:     Started server process [93862]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
```

これで`http://127.0.0.1:8000`からアクセスできるようになります。でもまだフロントエンドの方を使っていないから今のままでは入っても何もないはずです。


# フロントエンド（vuetify）

## プロジェクト作成

コマンドラインでこれを打ってvuetifyを中心とするvue.jsプロジェクトを作成します。
```
yarn create vuetify
```
```:結果
yarn create v1.22.22
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-vuetify@2.2.5" with binaries:
      - create-vuetify
[##########] 10/10
Vuetify.js - Material Component Framework for Vue

✔ Project name: … stadifvue
✔ Which preset would you like to install? › Default (Vuetify)
✔ Use TypeScript? … No / Yes
✔ Would you like to install dependencies with yarn, npm, pnpm, or bun? › yarn
✔ Install Dependencies? … No / Yes

◌ Generating scaffold...
◌ Installing dependencies with yarn...

yarn install v1.22.22
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 20.14s.

stadifvue has been generated at /Users/phyblas/Library/CloudStorage/Dropbox/web/stadifvue

Discord community: https://community.vuetifyjs.com
Github: https://github.com/vuetifyjs/vuetify
Support Vuetify: https://github.com/sponsors/johnleider
✨  Done in 136.04s.
```

プロジェクト名を入れた後選択は4つ出てきました。ここで選んだのは：
✔ Default (Vuetify)
✔ Yes
✔ yarn
✔ Yes

作成した後フォルダの中に入って次に行きます。
```
cd stadifvue
```


## ライブラリー追加

プロジェクトを作成したら次はvuetify以外使いたいライブラリーのインストールです。今回複雑なことはしていないので使うのはaxiosだけです。

```
yarn add axios
```
```:結果
yarn add v1.22.22
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
success Saved 9 new dependencies.
info Direct dependencies
└─ axios@1.7.2
info All dependencies
├─ asynckit@0.4.0
├─ axios@1.7.2
├─ combined-stream@1.0.8
├─ delayed-stream@1.0.0
├─ follow-redirects@1.15.6
├─ form-data@4.0.0
├─ mime-db@1.52.0
├─ mime-types@2.1.35
└─ proxy-from-env@1.1.0
✨  Done in 1.09s.
```

作成されたプロジェクトフォルダにはいっぱいファイルやフォルダが入っているが、ここではそのままにしておいてよくて、書き換えるのは`src`サブフォルダの中にある`App.vue`というファイルだけです。


## App.vue

`App.vue`はウェブアプリを織りなすメインファイルです。

大きいアプリならコンポネントごとファイルに分けられて`App.vue`の中からコンポネントを呼び出すことになりますが、今回は小さくて簡単なアプリなので全部このまま直接`App.vue`の中で書くことにします。

一応こうしたら`src`の中の`components`フォルダはもう要らないので消してもいいです。

```vue:App.vue
<template>
  <v-app>
    <v-main>
      <v-container>
        <v-sheet class="px-3 py-2">
          <v-row class="text-h4 my-2">
            <v-col class="mx-auto" style="text-align: center; color: aquamarine;">
              生成AIで画像を作りしましょう！
            </v-col>
          </v-row>
          <v-row>
            <v-col cols="1" class="d-flex"
              style="align-items: center; text-align: right; min-width: 120px;">
              どんな画像が<br />欲しい？
            </v-col>
            <v-col>
              <v-textarea v-model="prompt" rows="2" bg-color="#eef" hide-details />
            </v-col>
          </v-row>
          <v-row>
            <v-col cols="3" class="my-1 py-1"
              style="align-items: center; max-height: 90px; min-width: 220px;">
              <v-radio-group v-model="seisei_mode">
                <v-radio label="言葉だけで生成する" value="txt2img" />
                <v-radio label="元画像から生成する" value="img2img" />
              </v-radio-group>
            </v-col>
            <v-col v-if="seisei_mode == 'txt2img'" class="d-flex my-1 py-1"
              style="align-items: center; max-height: 90px; min-width: 400px;">
              <div class="mx-2">画像サイズ</div>
              <v-text-field v-model="hirosa" type="number"
                variant="outlined" label="広さ" max-width="120"
                bg-color="#133" suffix="px" hide-details />
              <v-text-field v-model="takasa" type="number"
                variant="outlined" label="高さ" max-width="120"
                bg-color="#133" suffix="px" hide-details />
            </v-col>
            <v-col v-else class="d-flex" style="min-width: 400px;">

              <div>
                <v-label style="width: 120px;">
                  変化の強さ {{ tsuyosa }}
                </v-label>
                <v-slider v-model="tsuyosa"
                  min="0" max="1" step="0.01" style="width: 120px;" />
              </div>

              <v-file-input label="元画像にするファイルを選択"
                v-model="motogazou" accept=".jpg, .jpeg, .png, .gif, .webp"
                hide-details />
            </v-col>
          </v-row>
          <v-row>
            <v-col>
              <v-btn @click="seisei_suru" class="text-h6"
                color="primary" width="300" elevation="3">
                生成開始
              </v-btn>
              <v-dialog v-model="seisei_shiteru" persistent style="text-align: center;">
                <v-sheet class="py-4 px-4" style="max-width: 600; align-items: center;">
                  <div class="text-h6">
                    今生成している。少々お待ちください<br />... ... ...<br />... ... ...
                  </div>
                </v-sheet>
              </v-dialog>
            </v-col>
          </v-row>
          <v-row v-if="gazou_url" style="background-color: #444;">
            <v-col style="max-width: 100px;">
              <v-btn class="text-h6" color="secondary" elevation="3">
                保存
                <v-dialog v-model="hozon_shitai" activator="parent" max-width="400">
                  <v-sheet class="py-4 px-4">
                    <v-text-field v-model="hozon_file"
                      label="保存するファイルの名前" max-width="300" hide-details />
                    <v-btn @click="gazou_hozon()" class="text-h6 my-2" color="secondary" elevation="3">
                      保存する
                    </v-btn>
                  </v-sheet>
                </v-dialog>
              </v-btn>
            </v-col>
            <v-col>
              <img :src="gazou_url" />
            </v-col>
          </v-row>
        </v-sheet>
      </v-container>
    </v-main>
  </v-app>
</template>

<script lang="ts">
import axios from "axios";
// ルートのURL。ここではFastAPIのデフォルトのローカルだが、本番のデプロイでは実際に使うURLにする
const root_url = "http://127.0.0.1:8000";

export default {
  data() {
    return {
      prompt: "", // 注文
      seisei_mode: "txt2img", // txt2imgかimg2imgか
      hirosa: 512, // 生成する画像の広さ
      takasa: 512, // 生成する画像の高さ
      tsuyosa: 0.5, // img2imgの変化の強さ
      motogazou: null as any, // 元画像のファイル
      hozon_file: "", // 保存するファイルの名前
      hozon_shitai: false, // 保存のダイアログの表示
      seisei_shiteru: false, // 生成している途中に出てくるダイアログの表示
      gazou_url: "", // 生成された画像のURL
    };
  },
  methods: {
    // 「生成開始」が押された時の処理
    seisei_suru() {
      // img2imgを選んだのに元画像のファイルがまだ選択されていない場合
      if (this.seisei_mode == "img2img" && !this.motogazou) {
        alert("まず元画像のファイルを選択してください");
        return;
      }

      // 生成するための準備
      this.seisei_shiteru = true; // 生成しているという状態にしてダイアログを表示される
      let form = new FormData(); // フォームデータを作成
      form.append("prompt", this.prompt);
      form.append("seisei_mode", this.seisei_mode);
      if (this.seisei_mode == "txt2img") {
        // 広さと高さは8で割り切れる必要がある
        this.hirosa -= this.hirosa % 8;
        this.takasa -= this.takasa % 8;
        // 大きさの限界を制限する
        if (this.hirosa < 8) this.hirosa = 8;
        else if (this.hirosa > 1024) this.hirosa = 1024;
        if (this.takasa < 8) this.takasa = 8;
        else if (this.takasa > 1024) this.takasa = 1024;
        form.append("hirosa", String(this.hirosa));
        form.append("takasa", String(this.takasa));
      }
      else { // img2imgの場合
        form.append("tsuyosa", String(this.tsuyosa));
        form.append("motogazou", this.motogazou);
      }

      // バックエンドの方に接続して生成の情報を送る
      axios.post(root_url + "/seisei_suru", form)
        .then((res) => {
          // 間違いがなければ暫く待った後生成された画像のURLが返されてここに表示させる
          this.gazou_url = root_url + '/img/' + res.data.gazou_file;
          this.hozon_file = res.data.gazou_file;
        })
        .catch((err) => {
          // 何か間違いがあればエラーの内容を表示する
          alert(err.response.data.detail);
        })
        .finally(() => {
          // 生成完了後（又はエラーが出たら）生成しているという状態が終わって、ダイアログを閉じる
          this.seisei_shiteru = false;
        });
    },
    // 「保存する」ボタンが押された時の処理
    gazou_hozon() {
      axios.get(this.gazou_url, { responseType: 'blob' })
        .then(response => {
          const blob = new Blob([response.data]);
          const link = document.createElement('a');
          link.href = URL.createObjectURL(blob);
          link.download = this.hozon_file;
          link.click();
          URL.revokeObjectURL(link.href);
          this.hozon_shitai = false;
        })
        .catch((err) => {
          alert(err)
        });
    }
  },
  watch: {
    // 広さと高さが整数にしかなれないようにする
    hirosa(b, a) {
      let intb = parseInt(b.toString());
      if (intb != b) this.hirosa = a;
    },
    takasa(b, a) {
      let intb = parseInt(b.toString());
      if (intb != b) this.takasa = a;
    },
  },
};
</script>

```


## 開発中の実行

ファイルが整ったら次は実行して正常に動くか試します。
```
yarn run dev
```
```:結果
yarn run v1.22.22
$ vite

  VITE v5.3.3  ready in 399 ms

  ➜  Local:   http://localhost:3000/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
```

そしてブラウザで`http://localhost:3000/`に入ったらウェブアプリの画面が表示されて使えるはずです。


## プリコンパイル

ちゃんと思った通り動くようになっていると確認できたら開発が終わって次は本番のためコンパイルします。
```
yarn run build
```
```:結果
yarn run v1.22.22
$ vue-tsc --noEmit && vite build
vite v5.3.3 building for production...
✓ 246 modules transformed.
dist/index.html                                             1.20 kB │ gzip:  0.57 kB
dist/assets/materialdesignicons-webfont-CveiMJFx.woff2    343.66 kB
dist/assets/materialdesignicons-webfont-BbcIfeS3.woff     493.82 kB
dist/assets/materialdesignicons-webfont-Df9Q6YdI.ttf    1,089.80 kB
dist/assets/materialdesignicons-webfont-YdEl25M1.eot    1,090.02 kB
dist/assets/index-KtCkQxIU.css                            588.67 kB │ gzip: 85.99 kB
dist/assets/index-BGeibSey.js                             275.77 kB │ gzip: 95.81 kB
✓ built in 725ms
✨  Done in 2.42s.
```

そうしたら`index.html`と`assets`フォルダと`favicon.ico`が`dist`というフォルダに現れて、これら全部ウェブアプリプロジェクトのメインフォルダのstadifwebに置きます。

![stadifapp09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/823fc23b-3b37-d034-cf3f-d801b5574fed.png)

これで整いました。

![stadifapp10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/6e17dbaf-2da0-0611-a6fc-46fd103d7caf.png)

その後ブラウザで`http://127.0.0.1:8000`にアクセスして正常に使えるようになったら完成です。

はい、これでお終い！



# 参考

- [FastAPIで作成したデータをVue.jsで表示させる](https://qiita.com/inoshun/items/75260e3543e9fb96f614)
- [Vue.jsとFastAPI間をaxiosで通信させたら404エラーが出た](https://qiita.com/ueno-8-ueno/items/9dd4a02769e8245f66a4)
- [FastAPIでつくるVue SPAアプリの雛形](https://qiita.com/sand/items/4b3e222dbb9315e2b0c0)
- [FastAPI と Vue.js で作るJWT認証](https://qiita.com/sand/items/dcf2694e990b1462d674)
- [FastAPIを使ってファイルアップロード機能を作成する](https://qiita.com/t-iguchi/items/2680dfbe16e960dbcfdf)
- [FastAPI でデータをダウンロードさせる](https://qiita.com/hoto17296/items/5624231b8d44b8f1275e)
- [FastAPIでアップロードしたファイルをそのままS3にアップロードする方法](https://qiita.com/NaoyaOshiro/items/34b69116d25279c6a7eb)
- [【Python】REST API でファイルをアップロードする (FastAPI)](https://qiita.com/XPT60/items/1ae2bb99e81bd8c8bc98)
- [FastAPIを使用した基本的なファイルアップロード機能の実装方法](https://qiita.com/Tadataka_Takahashi/items/b2b36f0a98002c4c8e34)