個人でWebサイトやアプリを作っています(屋号は sesebox です)。
いま、水槽をモチーフにしたパズルアプリ「WaterFill」を作っています。アプリの中で使う魚やコインの絵が何枚も要るので、ChatGPTに課金しているプランの枠で、コマンド1行で画像を作れるようにしました。そのためのスクリプトを自作したので、仕組みと気をつけたところを書きます。
前提:画像生成は Codex CLI そのものの機能だった
OpenAIの Codex CLI(ターミナルで動くAIのコーディングツール)には、画像を作る機能が最初から入っています(公式ドキュメント:https://learn.chatgpt.com/docs/image-generation )。
ChatGPTのアカウントでログインして使えば、ChatGPTのプランの利用枠の中で画像を作れます。APIのように1枚ごとの料金はかかりません。
npm install -g @openai/codex
codex login # ブラウザでChatGPTにログイン
codex login status # → Logged in using ChatGPT ならOK
ネットで調べると、これを便利にする非公式のプラグインもいくつか見つかりました。中を読むと、どれも**codex exec を呼んでいるだけの薄い包み**でした。それなら、知らない人のコードを入れるより、同じことを自分で数十行書くほうが安心だと判断して、自作することにしました。
使い方
node codex-image.mjs --out fish.png "白い背景に、かわいいクマノミを1匹。正方形。"
# 見本の画像を渡して、同じタッチで描かせる
node codex-image.mjs --out new-fish.png --ref fish-01.png --ref fish-02.png "同じタッチでハコフグを1匹"
1枚あたり、だいたい1分でPNGができます。
仕組み
やっていることは3つだけです。
- 一時フォルダを作る
- その中で
codex exec(Codexに1回だけ仕事を頼むコマンド)を走らせて、「画像を1枚作ってout.pngで保存して」と頼む - できた
out.pngを、指定の場所にコピーする
中心の部分はこんな感じです。
import { spawnSync } from "node:child_process";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
const work = mkdtempSync(join(tmpdir(), "codex-image-"));
const env = { ...process.env };
delete env.OPENAI_API_KEY; // ← 大事(後述)
const prompt = [
"$imagegen",
"次の説明どおりの画像を1枚だけ生成して、このフォルダ直下に out.png というファイル名で保存してください。",
"ほかのファイルは作らないでください。",
"",
"説明:",
desc,
].join("\n");
const args = ["exec", "-s", "workspace-write", "--skip-git-repo-check", "--ephemeral", "-C", work];
for (const r of refs) args.push("-i", resolve(r)); // 見本の画像
args.push("-"); // プロンプトは標準入力で渡す
const res = spawnSync(process.execPath, [codexJs, ...args], { env, input: prompt, encoding: "utf8" });
$imagegen は、Codexに「画像生成の機能を使って」と伝える書き方です。-i で画像を添付すると、それを見本にしてくれます。
気をつけたところ
1. OPENAI_API_KEY を子プロセスに渡さない
公式ドキュメントによると、OPENAI_API_KEY が設定されていると、ChatGPTのプランではなくAPIの従量課金で動きます。ほかの開発でAPIキーを環境変数に入れていると、知らないうちにAPI料金になってしまいます。
なので、Codexを呼ぶ直前に delete env.OPENAI_API_KEY で外しています。
2. 一時フォルダの中で走らせる
codex exec は、指定したフォルダの中のファイルを読んだり書いたりできます。リポジトリの中で走らせると、関係ないファイルまで見られてしまいます。
-C で一時フォルダを指定し、-s workspace-write(書き込めるのはそのフォルダだけ)にして、Codexが触れるのは一時フォルダだけにしました。--ephemeral は、この1回の会話を記録に残さないオプションです。
3. codex.cmd をシェル経由で呼ばない
Windowsでは codex の実体が codex.cmd なので、最初は shell: true で呼んでいました。すると Node が次の警告を出しました。
[DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
シェル経由だと引数がエスケープされず、そのままつながれてしまう、という警告です。説明文に記号や改行が入ると壊れる原因になります。
そこで、npmが入れた本体のJavaScript(npm root -g の下の @openai/codex/bin/codex.js)を、node で直接呼ぶようにしました。説明文も、引数ではなく標準入力で渡しています。
4. 作った画像は、失敗作も全部とっておく
うまくいかなかった絵も、「なぜダメだったか」が次の頼み方のヒントになります。スクリプトが毎回、日付ごとのフォルダに画像のコピーと、頼んだ内容・見本・かかった時間を書いたメモを残すようにしました。
使ってみて分かったこと
- 見本なしで頼むと、黒い線とベタ塗りの、よくあるイラスト風になりがちでした。アプリの絵と全然タッチが合いません
- アプリの実際の魚の絵を4枚、見本として渡すと、線の色やヒレの筋、目の描き方までそろった絵が出てきました
- 逆に、タッチの違う絵を見本にすると、その違いまで忠実に真似されます。使う場所が決まっている絵は、その作品の実物を見本にするのが大事でした
- 2枚を同時に走らせても大丈夫でした(一時フォルダが別々なのでぶつからない)
- 画像生成は、ふつうの作業より3〜5倍の速さで利用枠を使うそうです(公式ドキュメント)。試し打ちを連発しないようにしています
まとめ
- Codex CLIには画像生成が入っていて、ChatGPTのプランの枠で使える
-
codex execを一時フォルダで呼ぶだけで、「コマンド1行で画像を作る」道具になる -
OPENAI_API_KEYを外す・一時フォルダに閉じ込める・シェルを通さない、の3つを守れば、自作でも安心して使える
自分で書けば数十行なので、中身の分からないプラグインを入れる前に、一度自作を考えてみるのもおすすめです。