Vol. 01
2026.06.11
2026.05.28 ZENN // TECH

キャラクターを「育てる」ローカルStable Diffusionツール Umaru をリリースした話

ORIGIN: ZENN.DEV READ ORIGINAL

はじめに ― Tsurezure に続く第2弾、Umaru とは


例によってGPTに作って貰った訴求画像。BOOTHのサムネが正方形なので最適化した。

「StoryForge」という小説執筆ワークスペースを作った話を以前書いたとおもう(現在は Tsurezure という名前にリブランド、6/1にv2.0.0としてアプデ)。
あれを作っているときから「キャラクターの絵も手元で完結させたい」という欲求がずっとあって、今回それを形にしたのが Umaru(うまる) です。
そもそも、大真面目に個人開発のソレを同人活動だと思ってやってるのが大きいかも。

Umaru は、PC の中でキャラクターを描き、育てていくための創作ワークスペースという立ち位置。
みんなおなじみの画像生成系 Stable Diffusion を使って、イラスト生成からキャラクターの記憶づくり、さらには専用 LoRA の学習まで、一つのアプリで分かりやすく通貫で完結できるようにした。
ゴリゴリの日本語対応。英語に認知負荷掛けたくないクリエイターの方にもおすすめ…
そう、おすすめだよ。

Windows 11 向けのデスクトップアプリで、BOOTH にて有償配布中。アカウント登録もログインも不要。メモリバカ食いのChromeを開かなくても良い。そう、Tauriだからな!


なぜ「キャラクター育成ワークスペース」なのか ― ただの SD UI じゃないぞという話

巷に SD 系の GUI ツールは溢れかえってると思う。なんで今さらと思われると思う。特にエンジニア目線だと。

既存ツールの殆どは英語だし、何より色々調べないと行けない。
構築負荷も、非エンジニア目線だと割と高い。
誰も彼もpythonが扱えるという訳でもない。

そして、なにより絵を出力するのに特化していたり、LoRAに特化していたり、拡張性に特化していたりと、様々な特色がある。
どれ使ったらいいんだ…?というのが最初の悩みだろうと思う。(多分)

日本語でわかりやすいUIだったり、サクサク何枚も出したり、プロンプトを比較したり、ギャラリーがあったり、再現性を高めたりしてもいいじゃんと思って作った次第である。

「昨日出したあのポーズをもう一回試したい」「この子の服を変えたらどうなるか」「そもそも顔の方向性が複数あって迷っている」――こういう 反復と選択のサイクル を低負荷で。
一発生成ではなく、候補を広げて選ぶ体験そのものをプロダクトの核に置いた結果、アプリ内の機能の命名からしてちょっと変だったりする。

生成は「描いてもらう」、比較生成は「お祈り」、気に入った絵の保存は「この子らしさ」、LoRA 学習は「覚えはじめる」。技術用語をある程度排除して、初心者でもなんとなく「ああ、それをやるのか」とわかる言葉を選んだつもり。

プロダクト駆動時に作った、Philosophy.md にも書いた。

AI によって人間の絵作りやキャラクター設計を置き換えるのではなく、
キャラクターを考え、候補を作り、比べ、選び、記憶として残し、
自分のキャラクターとして育てられる状態を作ることを目的とする。

「AI に任せる」より「AIはあくまで補助輪としてつかう」という思想の元、プロダクトを構築したのだった。
元から自分の絵柄LoRAがある、という人にもおすすめ。
特にキャラクター選択画面のGUIは拘った、ウチの子がパーティ編成画面みたく並ぶのは必見。


主な機能紹介 ― お祈り・この子らしさ・LoRA 学習まで

ざっと機能を紹介。

キャラクター登録

名前・読み・コンセプト・性格・世界観メモを登録できます。「Prompt 雛形」として「この子を描くときのデフォルトタグ」「絶対避けたい描写」を設定できる。
キャラクターを選択したあと、生成画面へ雛形をワンクリックで復元できる。

つくる(画像生成)

ポジティブ・ネガティブプロンプト、モデル・LoRA 選択、サイズ・ステップ数・CFG・サンプラーを設定して生成。NF4 量子化(bitsandbytes)と TAESDXL 軽量 VAE にも対応しているので、VRAM が少なめの環境でも動くようにした。
少なくとも筆者環境 RTX 3070 で困ってない。
初心者でも筆者と似たような環境の人なら不自由無く画像生成できる様になっている。
一応生ログが出る様もしてる。オイラはこっちが好き。

お祈り

A〜D の 4 組 × 3、5、10 枚 を組単位で生成して見比べる機能。
敢えて言おう、最低12連ガチャみたいなもん。

「組毎に CFG を変えて比べたい」「組毎に追加タグを変えたい」など、変数を組ごとに変えて一気に候補出し。
その後、各組から代表を選んで順位をつけて決勝画像を決めます。
つまり、検証し審美眼を鍛えるための機能です。
良いと思った画像はそのままLoRA素材に持っていける。

「いい一枚を作る」より「いい候補を広げて選ぶ」を意識した機能で、個人的にはこれが一番 Umaru らしいと思っている。


この子らしさ(記憶素材)

「この絵はこのキャラっぽい」と思ったものをキャラクターの記憶画像として保存できる。あとから大量の画像フォルダから探さなくても良いように。
後述のアルバムから削除してもここに保存した画像は残る。
15 枚以上貯めると、LoRA 学習へ進める。

LoRA 学習(覚えはじめる)

kohya-ss/sd-scripts を内部で使って、GUI から LoRA 学習を実行できる。学習データは「この子らしさ」に保存した画像から自動で作成。
この画面で15枚以上を更に審美し、学習を開始できる。
完了後はキャラクターに紐づいて、以降の生成で使えるようになる。
ただしサポート地獄になるのが見えたので学習は NVIDIA GPU を必須要項とした。
それと、当然だがアプリ内にkohya-ss/sd-scriptsはBundleしていない。でも初心者でも詰まない。
後述するが、ここにも一工夫ある。

タグ辞書・補完

好きな CSV を取り込むとリアルタイムタグ補完が使えるようにした。
メモ帳やらなんやらに呪文を貼っておく文化は終わるのだ…(大言壮語)
英語・エイリアスから検索でき、_ とスペースは同一視。Danbooru 互換のものが使えるようにした。
日本語翻訳がある辞書を登録すれば、日本語を打ち込むと英字タグ候補がプロンプトに出てくる。
英字で二文字打ち込んでも、ちゃんとタグ候補がリアルタイムで出てくる。
これが超便利。

プロンプト整頓 / ボリューム

重複タグや矛盾した記述、CLIP の 77 トークン上限を超えそうな長さを検出して教えてくれる。(ただ自動では消さない。気づかせるだけ)。
LPWにも対応済み。


技術スタック ― Tauri v2 + React/TS + FastAPI sidecar 構成

全体構造

Tauri v2 (Rust)
  ├─ フロントエンド: React 19 + TypeScript + Vite (→ esbuild fallback)
  ├─ 状態管理: Zustand + TanStack React Query
  └─ サイドカー: FastAPI (Python 3.11)
       ├─ 画像生成: diffusers + PyTorch (CPU / CUDA)
       ├─ NF4 量子化: bitsandbytes
       ├─ LoRA: PEFT
       ├─ データ: SQLite (aiosqlite)
       └─ 学習: kohya-ss/sd-scripts (初回同意後に自動取得)

なぜ Tauri か

メモリ消費を小さく保ちたかった、という理由が一番大きい。SD 系アプリは Python バックエンドだけで VRAM + RAM をガッツリ食うので、デスクトップシェル側はできるだけ軽くしたい。
Electron だと Chromium を丸ごと抱えてしまうので Tauri を選んだ。

Python はバンドル同梱

モデルが自動でインストールされる系のツールはどれもセットアップが大変という偏見がある。
Umaru は Python 3.11 の embed 版を NSIS インストーラーに同梱してシュッと展開する。ユーザーは Python を別途インストールしなくていいしpythonのVer違いに悩まされることもない。
初回起動時に %APPDATA%\Umaru\python\ へ展開して、そこに pip で依存を入れちゃう。
諸々クリーンでやってるので、信用ベースではあるが。
セキュリティ意識の高い人は怖いって思うよね、わかる。
もちろんその辺りは同意してから買ってもらうつもりだが、はたから見るとマジでやべーアプリだな…
あとSD用のライブラリも勝手にはインストールしない。
きちんと初回セットアップとして、ユーザーにポチっとしてもらう設計。
透明性は高くありたい。

FastAPI サイドカー

Python バックエンドは Tauri の子プロセスとして起動。ポートはランダム(portpicker)で毎回変わるようにした。フロントエンドとバックエンドの間は普通の HTTP / WebSocket 。生成進捗や LoRA 学習のログのリアルタイム配信には WebSocket を使っている。


作っていて詰まったところ ― 実装上のハマりポイント

これが一番生々しい話…?

1. Node.js 24 で Vite のプロダクションビルドがクラッシュする

一番最初に詰まったのがこれ。開発環境では vite dev が動くのに vite build がすっ飛んで落ちる。調べてみると Node.js 24 と Vite の組み合わせで production build 時にクラッシュするバグ が存在していた。

これを回避するために、プロダクションビルドだけ esbuild 直呼び に切り替えた。build.mjs というスクリプトをゼロから書いた。
いや敢えて誤解を恐れずに言おう、書いてもらったと。

// build.mjs (抜粋)
// CLAUDE.md の回避策:
// Node.js 24 で Vite production build がクラッシュするため esbuild で直接バンドルする。

import * as esbuild from 'esbuild';

const result = await esbuild.build({
  entryPoints: ['src/main.tsx'],
  bundle: true,
  outdir: assetsDir,
  format: 'esm',
  target: ['es2020', 'chrome105'],
  jsx: 'automatic',
  minify: true,
  metafile: true,
  define: { 'process.env.NODE_ENV': '"production"' },
  loader: {
    '.tsx': 'tsx',
    '.ts': 'ts',
    '.css': 'css',
    '.png': 'file',
    '.jpg': 'file',
    '.svg': 'file',
  },
  entryNames: '[name]-[hash]',
  assetNames: '[name]-[hash]',
});

vite.config.ts は dev 用に残しつつ、tauri build は esbuild 経由で走らせるという二刀流になった。Vite の資産(HMR とかプラグイン)は開発時だけ享受して、本番は素の esbuild で固める割り切り。正直かなり泥臭い解決策のようだが、動いたので OK。


2. 認証トークンの渡し方に悩んだ ― stdin 経由が正解だった

Tauri 側が起動した FastAPI に「このプロセスが正規の Umaru からの通信だ」と証明させるために認証トークンが必要です。最初は CLI 引数で渡そうとしたんですが、プロセスリストに丸見えになるのでセキュリティ的にアウトでした。環境変数も似たような問題があります。

最終的な解決策は stdin 経由でトークンを一行だけ流し込む方法です。

// src-tauri/src/lib.rs (抜粋)

let token = Alphanumeric.sample_string(&mut rand::rng(), 48);

let mut child = command
    .stdin(Stdio::piped())  // stdin を繋ぐ
    .spawn()?;

// stdin に 48 文字のトークンを一行だけ書いて即閉じる
let Some(stdin) = child.stdin.as_mut() else {
    let _ = child.kill();
    return Err("起動トークンを渡せません".to_string());
};
stdin.write_all(format!("{token}\n").as_bytes())?;

Python 側では起動直後に stdin から一行読んでトークンを受け取り、以降は X-Umaru-Token ヘッダーでの照合に secrets.compare_digest() を使った(タイミング攻撃対策)。

ps aux や Task Manager でプロセス引数を覗いてもトークンは見えない。stdin は読んだ瞬間に消える。地味だけどこれはちゃんとやっておきたかった。


3. 親プロセスが死んだとき Python が孤児になる問題

Tauri がクラッシュしたり強制終了されたとき、子プロセスの FastAPI がそのまま残り続けてしまうことがある。VRAM を握ったまま次回起動できなくなる、という最悪の状況が考えられる。

これを防ぐために ハートビート機構 を実装した。
波紋かよ。

Rust 側は専用スレッドを立てて 10 秒おきに FastAPI の /api/heartbeat をたたく。

// src-tauri/src/lib.rs (抜粋)
let heartbeat_handle = std::thread::spawn(move || {
    loop {
        // shutdown フラグを 250ms 刻みでチェックしながら 10 秒待つ
        for _ in 0..40 {
            if heartbeat_shutdown.load(Ordering::SeqCst) { break; }
            thread::sleep(Duration::from_millis(250));
        }
        // HTTP で /api/heartbeat を POST
        let addr = SocketAddr::from(([127, 0, 0, 1], heartbeat_port));
        let _ = TcpStream::connect_timeout(&addr, Duration::from_millis(1000))
            .and_then(|mut stream| {
                let request = format!(
                    "POST /api/heartbeat HTTP/1.1\r\nHost: ...\r\nX-Umaru-Token: {}\r\n\r\n",
                    heartbeat_config.token
                );
                stream.write_all(request.as_bytes())
            });
    }
});

Python 側は watch_parent_process() で監視しています。

# backend/security.py (抜粋)
async def watch_parent_process(shutdown_callback) -> None:
    while True:
        await asyncio.sleep(5)
        # 親プロセスが存在するか確認
        if runtime_security.parent_pid:
            if not _is_process_alive(runtime_security.parent_pid):
                await shutdown_callback("親プロセスが終了しました")
                return
        # ハートビートが 30 秒以上途切れたら自刃
        if time.monotonic() - runtime_security.last_heartbeat > 30:
            await shutdown_callback("ハートビートが途切れました(30秒以上応答なし)")
            return

親が死ぬかハートビートが 30 秒以上届かなくなると os._exit(0) で FastAPI ごと落ちます。ちょっとものものしいですが、GPU メモリを綺麗に解放するためには必要な処理。
アプリも軽いし、初回起動時スプラッシュ表示でなるべくユーザーに負荷を掛けない様にしてるので、再起動が容易というのもあって、なんかあったらアプリ落とせば良いと思って作った。


4. パイプラインキャッシュのキーが複雑になりすぎた

SD モデルの読み込みは遅い(数十秒以上かかる)ので、同じ設定なら前回読み込んだパイプラインを再利用したい。ただし「同じ設定」の判断がめちゃくちゃ難しい。

現在のキャッシュキーは以下の要素で構成されている。

# backend/services.py (抜粋)
def _pipeline_cache_key(
    model_path: str,
    lora_path: str | None,
    pipeline_kind: str,   # "sdxl" / "sd1" / "illustrious" etc.
    device: str,
    load_in_4bit: bool,   # NF4 量子化の ON/OFF
    scheduler: str,
    lora_weight: float,
    use_tiny_vae: bool,
) -> tuple:
    return (
        model_path,
        lora_path,
        pipeline_kind,
        device,
        load_in_4bit,
        scheduler,
        round(lora_weight, 4) if lora_path else 0.0,
        use_tiny_vae,
    )

NF4 量子化の ON/OFF はモデルの重みそのものを変えてしまうので当然キャッシュを分けないといけない。さらにスケジューラー(DPM とか Euler とか)は変えるたびにパイプラインを再構築する必要があるものとそうでないものがあって、「どこまでキャッシュを活かせるか」の判断が試行錯誤だった。

最終的には「スケジューラーが変わったらキャッシュミス」というシンプルな判断。ちょっと再読み込みが多いかもしれないけど、バグるよりはいい。


5. LoRA 学習のセットアップが「ちゃんとやる」と大変

LoRA 学習に使う kohya-ss/sd-scripts は、ライセンス上(Apache-2.0)バンドルできないので、初回利用時にユーザーの同意を取ってからダウンロード・インストールします。

このセットアップフローを ステートマシン で管理しています。

not_installed
  → consent_required   (同意画面表示)
  → downloading        (ZIP ダウンロード中)
  → extracting         (展開中)
  → creating_venv      (仮想環境作成中)
  → installing_dependencies  (pip install 中)
  → verifying          (動作確認中)
  → ready              (完了)
  → error              (どこかで失敗)

ここで詰まったのが セキュリティチェック の実装です。

配布前に sd-scripts の特定コミットの SHA256 を training_runtime_manifest.json に固定して、ダウンロード後に照合します。

# backend/training_runtime.py (抜粋)
def _manifest_ready_for_release(manifest: dict) -> tuple[bool, str]:
    sd = manifest.get("sd_scripts") or {}
    ref = sd.get("ref")
    commit = sd.get("commit")
    sha = sd.get("sha256")
    if not ref and not commit:
        return False, "sd-scripts の ref / commit が manifest に固定されていません"
    if not sha:
        return False, "sd-scripts の sha256 が manifest に固定されていません"
    return True, "manifest 固定済み"

さらに ZIP 展開時には Zip Slip 攻撃../../../ なパスで意図しない場所にファイルを展開されるやつ)の対策も入れた。「個人制作のツールにそこまで?」と思うかもしれんが、不特定多数に配布するなら一応やっておきたかった。
というか個人開発で同人だからこそ、そこは誠意の見せ所ではないかと思った。

6. 誠意の見せ所といえば

そうライセンス関係だ。
個人開発で、AI駆動でしょう?どうせちゃんとしてないんだろ?

というの、作ってる俺個人が一番怖いと思っている。
だから、今回プロダクト駆動開発を行うにあたって、セキュリティチェックとライセンスチェックはバカみたいに真面目にやった。

ライセンス関係をちゃんとやってみた話、としてあとでもう一個記事を出します。


そんなワケで。BOOTH で買えちゃう ― 価格・リンク

**BOOTH の https://ena-dri.booth.pm/items/8417370 **から購入できます。

  • 価格: BOOTH の販売ページをご確認。(リリース時にキャンペーン価格を設定予定)
  • 対応 OS: Windows 11
  • GPU: なくても動くがとんでもなく遅くなるぞ!(CPU 生成)。ただし、LoRA 学習については NVIDIA GPU が無いと動かない様にした。
  • 商品に含まれるもの: Umaru 本体(NSIS インストーラー)、Python 3.11 embed 版(自動展開)、QAシートと機能紹介PDF
  • 含まれないもの: モデル・LoRA・タグ辞書(各自 Civitai / HuggingFace からどうぞ)

インストーラーは NSIS 形式で、初回起動時に SmartScreen の警告が出る場合がある。「詳細情報 → 実行」で進めてほしい(SHA256 は checksums.sha256 で確認できる。


今後の展望 ― v2 ロードマップ

v1 でできていないことを正直に書いておくのだ。

機能 時期
Flux 対応 v2.0 以降(別の生成パイプラインとして実装予定)
公式日本語辞書 v2.0 以降(日本語 → タグの自動変換とセット)
複数 LoRA の同時適用 将来バージョンで対応予定
WD14 tagger による自動キャプション 学習データセット作成支援として検討
ControlNet お祈りラボの拡張として検討
アップスケーラー(Real-ESRGAN 等) オプション機能として検討
LLM によるプロンプト補助 Flux対応するなら自然言語翻訳として欲しいかなと思った。 VRAM 排他制御に組み込む形で検討

Flux は生成品質がかなり上がっているので早めに対応したい気持ちはある。ただパイプライン構成が SDXL と結構違うので、v1 の安定化を優先しての v2 対応へ。一応自作の旧プロダクト(表に出てない)では実装済みなんだけど安定性が…(遠い目)

日本語辞書は「日本語でタグを書けるようにする」機能で、個人的にかなり欲しい機能。
そう、個人的にだ。日本語で 上半身 とか打ち込むと upper_body がリアルタイムで出てくるの、最高じゃろ。
※これは個人で作ったSDプロンプト辞書内包型貼り付け用クリップボード監視ツール。配布予定は無いです。
この仕組みをアプリにも組み込んじゃおう、という話。

現状はユーザー自身が CSV を取り込む形だが、公式で用意した翻訳辞書を配布したい。これも v2 に入れる予定。


おわりに ― エコシステムの一員として

Umaru は Ena-dri というブランドで開発しているプロダクト群の一つになった。現在走っているプロダクトをざっと並べると:

  • Tsurezure(旧 StoryForge) ― 小説執筆総合ワークスペース(母艦)
  • Umaru ― キャラクターイラスト制作ワークスペース(今回リリース)
  • Suzuna(開発中) ― キャラクターボイス製作ワークスペース
  • Otofu(開発中) ― 楽曲生成・ミックス
  • Yurari(開発中) ― キャラクターを動かすための編集ソフト

三姉妹という設定で、Umaruは長女、Suzuna と Otofu は双子です(プロダクトにキャラクター設定を持たせるのが好き、これが同人って言い張ってる根拠の一つ)。

Umaru で作ったキャラクターデータは、将来的に Suzuna や Yurari へ JSON でエクスポートして渡せるようにする予定だったりする。Tsurezure を母艦として、各アプリがキャラクターを育てた先を繋いでいく。そういうエコシステムを地道に作ってる。

元々はゲームを通貫で、一人で作ってみたいなと思って始めた開発。

一人で全部作っているので進みは遅いのだけれども、使ってもらえれば気合いが入ります。既に旧StoryForgeも10人近くが手を取ってくれている。コレほど嬉しいものはないです。

バグ報告・感想は BOOTH の連絡窓口か、各 SNS まで気軽にどうぞ。


では、もしこのアプリを手にとっていただけた方が居れば
「キャラクターを育てる」感覚、ぜひ試してみてください。