ドット絵ツール:パレット交換、通信変更、ギャラリー追加
ドット絵ツールに NES 16 色パレットを導入し、通信を SSE から WebSocket ストリーミングに変更し、作品ギャラリーを追加した。三回の改造が解決するのは同じ一つの問題。モデルが描画そのものに集中できるようにすることだ。
前にAI がドット絵を描くのを見るを書いた。ツールを公開してから自分でしばらく使い、三箇所を順次改めた。
第一は色だ。元の設計ではモデルが直接 hex 色値を出力していた。例えば #FF0000 で赤、#008751 で緑を描く。理論上は柔軟だが、実際にはモデルがよく間違える。#FF00O0(0 を O と書く)を出力したり、肉眼ではほとんど区別できない近隣色を選んだりする。さらによく見られるのは、モデルが同一画面で十数色を使い、ドット絵本来の控えめさを失うことだ。
問題の根源は自由度が高すぎることだ。ドット絵の魅力はまさに制限から来る。色が少ないほど、輪郭がはっきりし、スタイルが統一される。モデルに数百万色から自由に選ばせるより、固定のパレットを与える方が良い。
NES 16 色パレットを選んだ。NES の 32 色から 16 の代表色を抽出し、黒、白、灰、赤、橙、黄、緑、青、紫、粉をカバーする。各色に短い番号 c1 から c16 を付ける。モデルは「c6 を描く」と言えばよく、「#F83800 を描く」と言う必要がない。
この変更はプロンプトとコードの両層で実装した。プロンプトにはパレット対照表を置き、色番号、色名、hex 値、典型的用途を列挙する。コード層にはマッピング関数を追加し、c1-c16 を実際の hex 値に変換する。フロントエンドは完全に変わらず、受け取るのは引き続き hex だ。
効果は即座に表れた。モデルの出力がずっとクリーンになり、色数は 4-8 種に安定し、「一種の赤で五つの微妙な変種を描く」状況がなくなった。色番号は hex より覚えやすく、モデルがフォーマットエラーを犯す確率も下がった。
第二の改造は通信方式だ。元は SSE(Server-Sent Events)を使い、フロントエンドが fetch で POST リクエストを発し、サーバー側が ReadableStream でイベントを一行ずつプッシュしていた。SSE の問題は単方向なことだ。クライアントは聞くことしかできず、話せない。ユーザーが途中で停止したい場合、フロントエンドは fetch を中断するしかなく、サーバー側の LLM 呼び出しはまだ継続しており、リソースが無駄に消費される。
WebSocket に変更すると、クライアントとサーバー間に双方向のチャンネルができる。フロントエンドが {type: "start", image: "..."} メッセージを送って描画を開始し、サーバー側が各ツール呼び出しの結果を逐次プッシュする。ユーザーが停止をクリックしたとき、フロントエンドは直接 ws.close() し、サーバー側は接続断を即座に感知し、以降の LLM 呼び出しを終了する。
SSE にはもう一つ隠れた問題がある。ハートビートだ。Caddy リバースプロキシにはアイドル接続のタイムアウト機構があり、LLM 呼び出しの時間が長い(例えば 10 秒以上)と中間にデータフローがなく、Caddy が接続を切る。SSE を使っていたときはサーバー側が 3 秒ごとにコメント行を送ってハートビート保活していた。WebSocket には本来 ping/pong 機構があり、追加処理が不要だ。
Bun の WebSocket 実装は uWebSockets に基づき、Node.js の ws ライブラリより性能が約 7 倍高い。このツールにとって性能はボトルネックではない(一度に一人のユーザーしか描かない)が、API 設計がより簡潔だ。server.upgrade(req) 一行でプロトコルアップグレードが完了し、ws.send() でメッセージを送り、ws.close() で接続を切る。
フロントエンドの変更も大きくない。元は SSE 解析ロジックで、空行でイベントフレームを分割し、ハートビート行をフィルタし、data フィールドを抽出していた。WebSocket に変更後、ws.onmessage で完全な JSON オブジェクトを直接取得でき、一度解析するだけで済む。
通信層を変更後、さらに一層の最適化を行った。ストリーミングプッシュだ。元の LLM 呼び出しは stream: false を使い、サーバー側がモデルの全コンテンツ生成を待ってから tool call の処理を開始していた。stream: true に変更後、サーバー側が LLM の SSE delta ストリームをトークンごとに受信し、index で tool_calls の断片を収集し、完全な tool call を組み立てた後に即座に実行してフロントエンドにプッシュする。最初のフレームの遅延が「LLM 総所要時間」から「最初の tool call 完了時」まで下がった。
最後に作品ギャラリーを追加した。毎回の描画終了後、フロントエンドが参照画像、最終キャンバス、完全な操作シーケンス(各ステップの paint / fill_rect)をサーバー側に保存する。ギャラリーページの下部にすべての作品を展示し、クリックで描画プロセスを再生できる。操作シーケンスの存在により「プロセス」自体が作品の一部となり、単なる結果のスクリーンショットではなくなった。
三回の改造は同じ方向を指している。モデルが無関係な決定を減らすことだ。固定パレットでモデルに色選びで迷わせず、WebSocket ストリーミングでフロントエンドにプロトコルと遅延で迷わせず、ギャラリーで作品プロセスを追跡可能にする。良いツールは使用者に創作そのものに集中させ、ツールと格闘させないものだ。
ツールのアドレス:AI ドット絵
関連記事
- AI がドット絵を描くのを見る — ドット絵ツールの初版実装
- Harness Engineering — なぜ AI が間違えたときシステムを問うべきか — システムレベルで AI のエラーを考える
- Bun WebView でブログに Mermaid レンダリングエンジンを構築した — もう一つの Bun 実践プロジェクト