ロゴ ロゴ

【1週間プログラムジャム】イラスト無限生成アプリ

まえがき

世間一般ではもう夏休みは終わってますかね?われらが芝浦工業大学は少し長めなのですが、そろそろ終わります.楽しく過ごせましたか?

私はというと研究に使うアプリの開発、研究室のいろんな用事で結局ほぼ休みなんてありませんでした.どうもウグイスです.

実は電算ではコロナ禍以降開催されていなかった合宿が開催されました.その合宿のメインイベントで1週間プログラムジャムというものが開催されました.

お題発表が発表日の1週間前にされるので、そこからお題に沿って作成したプログラムを作ります.それを発表してみんながどんなのを作ったのかというのを見るイベントでした.

気が向いたらブログにしてといったので、ほかの人もブログにしてくれると思ってます.

まあまずは言い出しっぺの法則ということで私の作成プログラムを載せていこうかと思います.

今回の成果物は以下のGitHubから見れるようにしておきました.需要はあまりないかと思いますが,参考にしたい場合はどうぞ.

https://github.com/uguisu64/intl_npu_stable_loop

お題とアイデア出し

今回のお題は「ループ」でした.

つまりエンドレスやら終わらないとか、そういうものを連想しました.

実はいろいろネタは考えてました.今回は一つだけ作ったので、ほかのアイデアは供養としてここに乗せとこうかなと思います.

  1. イラスト無限生成アプリ(今回作ったやつ)
  2. LLMを使用した山手線ゲームアプリ
  3. LLMに無限に対話させるアプリ
  4. 遠藤を無限に集めるゲーム(endolessってことです)

こんな感じでアイデアだしをしたわけです.

作成の様子

最初はLLMを使ったものを作りたいなと思ってました.ですが問題がありました.ハードウェア問題です.

LLMを動かそうと思うと選択肢は二つあります.ChatGPTAPIを筆頭としたLLMのAPIを使う方法と、ローカルで動かす方法です.

APIは無料のものもありますが、たいていは使いすぎると従量課金されるタイプが多いので、ループというお題と相性が悪いのです.なので最初から却下でした.

次はローカルで動かす方法です.これが曲者だった.

まずローカルで動かすということはマシンを合宿先にもっていかなければなりません.(まあ家においてあるマシンをリモートで動かすという選択肢もありますが今回は却下)そうなると私の手持ちのマシンはonexfly(ram32gb), onexplayerx1(ram32gb), vivobookS 15(ram32gb)となります.このうちAI処理が得意なNPUが使えるのがcore ultra 7のonexplayerとSnapdragonXeliteのvivobookとなります.

こんなブログを読むような人は割と知っている人が多いと思いますが、LLMなどのAIのモデルというものは基本的にはモデルサイズと精度はトレードオフの関係にあります.つまりモデルはデカければデカいほどええ感じのモデルになります.

最近だと、8B(80億)パラメータのモデルがかなりええ感じのモデルになっています.Elyzaの作っているLLAMA3ベースのモデルとかかなりいい感じです.家のRadeon7900xtxで遊んで軽く感動しました.簡単なPythonプログラム程度なら吐いてくれるので、今のChatGPTには及ばないですが、ローカルでこれなら十二分だなと感じました.

ちなみに8BモデルはVRAMをおよそ16GBちょっと使います(←これ重要)


というわけでせっかくならこれを使いたいなと思いました.なので以前ブログで触れたIntelNPUを使う方法を使って実行させようとしました.

そうするとモデルのコンパイルはできるのですが、実行時にWindowsError -5xxxxxxxxみたいな感じのエラーが出てハングします.

で、いろいろ調べた感じだとどうにもメモリが足りていないみたいです.

ここで疑問に思った方もいるでしょう.onexplayerx1のメモリは32GBあるのに16GBちょっと使うだけのモデルがロードできないのはおかしいのではないかと.

ここは内臓GPUと同じような仕様になっていることが原因です.つまり内臓GPUと同じくメインメモリの半分までしかRAMを使えない仕様があるということです.

つまりCPUは32GBをフルで使えますが、NPUは半分の16GBしか使えないということになります.

今まではRAM問題に抵触しない程度のサイズのモデルしか試してなかったのでこの問題にはぶち当たらなかったわけで完全に失念してました.

というわけでElyzaのモデルが使えないということで、LLMを使ったアプリケーションは却下となりました.


というわけで以前いじったStableDiffusionで無限イラスト生成編をやることにしたわけです.

正直、基本的に画像生成のロジック部分は以前のブログから変わっていないので、主にGUIの追加部分が今回の成果となります.

というわけで、以下がソースコードとなります.

Pythonコード

from diffusers import StableDiffusionPipeline
import intel_npu_acceleration_library
import torch
import eel
import random
import base64
from io import BytesIO

gen_flg = False
gen_list = None

pipe = StableDiffusionPipeline.from_pretrained("stablediffusionapi/anything-v5").to("cpu")
pipe.reqires_safety_checker = False
pipe.safety_checker = None
text_encoder = pipe.text_encoder
text_encoder.eval()
unet = pipe.unet
unet.eval()
vae = pipe.vae
vae.eval()

text_encoder = intel_npu_acceleration_library.compile(text_encoder, dtype=torch.int8)
unet = intel_npu_acceleration_library.compile(unet, dtype=torch.int8)
vae = intel_npu_acceleration_library.compile(vae, dtype=torch.int8)

eel.init('web')

def gen_image():
    global gen_flg, gen_list
    while True:
        if gen_flg:
            prompt = ""
            for one_list in gen_list:
                prompt += str(random.choice(one_list)) + " ,"
            image = pipe(prompt,num_inference_steps=16).images[0]
            buffer = BytesIO()
            image.save(buffer, 'jpeg')
            base64_image = base64.b64encode(buffer.getvalue())
            eel.set_base64image("data:image/jpg;base64," + base64_image.decode("ascii"))
            eel.sleep(1)
        else:
            eel.sleep(1)


# JavaScriptからデータを受信するPython関数
@eel.expose
def receive_data(lists):
    global gen_flg, gen_list
    gen_list = lists
    gen_flg = True
    return "ok"

eel.spawn(gen_image)
# Eelサーバーを起動し、HTMLファイルを表示
eel.start('index.html')

HTMLファイル(index.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>loop_stable_diffusion</title>
</head>
<body>
    <img id="python_video" width="512" height="512">
    <h1>プロンプト入力</h1>
    <div id="listContainer">
        <div class="list-item">
            <input type="text" name="item1[]" placeholder="リスト1, アイテム1">
            <button onclick="addItem(this)">アイテムを追加</button>
        </div>
    </div>
    <button onclick="addList()">リストを追加</button>
    <button onclick="sendData()">送信</button>
    <button onclick="resetForm()">リセット</button>

    <script type="text/javascript" src="/eel.js"></script>
    <script type="text/javascript" src="main.js"></script>
</body>
</html>

JavaScript(main.js)

// リストを追加する関数
function addList() {
    const container = document.getElementById("listContainer");
    const newList = document.createElement("div");
    newList.classList.add("list-item");
    newList.innerHTML = `<input type="text" name="item[]" placeholder="新しいリスト, アイテム1">
                         <button onclick="addItem(this)">アイテムを追加</button>`;
    container.appendChild(newList);
}

// アイテムを追加する関数
function addItem(button) {
    const listItem = button.parentNode;
    const newItem = document.createElement("input");
    newItem.type = "text";
    newItem.name = "item[]";
    newItem.placeholder = "新しいアイテム";
    listItem.insertBefore(newItem, button);
}

// データをPythonに送信する関数
function sendData() {
    const lists = [];
    document.querySelectorAll(".list-item").forEach(list => {
        const items = [];
        list.querySelectorAll("input[type='text']").forEach(input => {
            items.push(input.value);
        });
        lists.push(items);
    });

    // データをPythonに送信
    eel.receive_data(lists)(function(response) {
        alert(response);
    });
}

// フォームをリセットする関数
function resetForm() {
    const container = document.getElementById("listContainer");

    // すべてのリストを削除
    container.innerHTML = '';

    // 初期状態として、1つのリストを追加
    const initialList = document.createElement("div");
    initialList.classList.add("list-item");
    initialList.innerHTML = `<input type="text" name="item1[]" placeholder="リスト1, アイテム1">
                             <button onclick="addItem(this)">アイテムを追加</button>`;
    container.appendChild(initialList);
}

eel.expose(set_base64image);
function set_base64image(base64image) {
    document.getElementById("python_video").src = base64image;
}

プロンプト固定で作り続けるのは芸がないので,リストを使って複数のプロンプトからランダムで作るようなプログラムにしてみました.

eelというWEBアプリを作る感じでローカルのGUIを作れるものを使いました.(まあ中身的にはサーバ立ててるだけみたいですけどね..)

ちょっとした工夫点としては,ファイル書き出ししてフォルダを汚したくないので,ByteIO使ってオンメモリで画像のやり取りをしてるくらいでしょうかね.まあそれくらいです.

ちなみに,アイデア出しとLLM実行で時間を使ってしまったので,めちゃくちゃChatGPTを使いまくって書きました.(主にHTMLとJS)

あとがき

今回は、以前作ったStableDiffusion画像生成byIntelNPUに適当にGUIを付けて、無限生成できるようにしたという内容でお送りしました.

eelを試してみましたが,割とPython側とWeb側のやり取りが楽なので,手軽にGUIを作るときには便利だなと思いました.ReactとかJSのライブラリを使えばええ感じのGUIになりそうな気がする.私はフロントはからきしなのでできませんが..

ちなみにStableDiffusionモデルのunet, vae, text_encoderをNPUで実行するように変換しているのですが、どうにもNPUが遊んでCPUがブン回っているように感じるので、今度原因調査をしてみたいと思います.もしかしてNPU向けに変換できてないか、StableDiffusionにはモデル内にCPUをぶん回す処理がほかにも入っているのかなのかな??有識者がいれば教えてください.

ここまでご精読ありがとうございました.次のブログでお会いしましょう.ウグイスでした~

コメント入力

関連サイト