2014 年から個人でアプリを作り続けてきて、累計ダウンロードが 5,000 万を超えるあたりから「素材の補充」がいちばん重い作業になりました。とくに壁紙アプリは、新作が止まると即座に DAU が落ちます。これまでは外部の素材販売サイトと自分で描いた絵を併用してしのいできましたが、今年に入って Gemini 3.2 Pro と Imagen 4 が同じ Google AI ファミリーで揃ったタイミングを見計らって、本気で「自分のアート作品を起点にした壁紙生成パイプライン」を組んでみました。
3 月の終わりから 4 月いっぱいまで、ちょうど 30 日間。1 枚の元絵から 8 〜 16 枚の派生バリエーションを生成し、品質ゲートを通った 120 枚を実際にアプリに投入しました。本記事は、その実装記録と運用数字を、コードと一緒に丸ごと公開する内容です。
ここで言う「自分のアート作品」というのは、わたし廣川政樹がここ数年取り組んできた、日本特有の祈りや集団心理をテーマにした抽象画のことです。国際的なアート賞を 17 受けたあたりから「この世界観をスマホの待ち受けに落とし込めないか」という相談をいただくようになり、自分でもずっと気になっていた領域でした。
アーティストとアプリ開発者を兼ねている人だけが直面する制約
絵を描く側の人なら分かってもらえると思うのですが、自分の作品をそのまま壁紙にすると 2 つの問題が出ます。1 つは比率です。アート作品はキャンバスサイズに最適化されており、スマホの 9:19.5 に切り出すと構図が崩れます。もう 1 つは解像度で、SNS に出した低解像度版しか手元にないことが多く、Retina ディスプレイには耐えられません。
外注して切り出してもらったり再描画したりすることもできるのですが、個人開発の単価ではペイしません。社外の方に依頼すると 1 枚あたり 8,000 〜 15,000 円。アプリ 1 本あたり毎月 30 〜 50 枚の補充が要るので、月 30 万円のコストを払い続けるのは無理がありました。
そこで「自分の元絵 1 枚から、世界観を保ったままバリエーションだけ広げる」という方針に切り替えました。これなら著作の出発点が自分にあり、世界観の責任もこちらに残ります。Imagen 4 のような生成モデルは便利ですが、何もないところから「祈り」のようなニュアンスを出すのは現状むずかしいので、構造の核は自分の絵でつくり、派生だけ機械に任せる、という分業が現実的だと判断しました。
設計の出発点 — 「分析」と「生成」を別のモデルに分けた理由
最初の設計で大きく悩んだのが、Imagen 4 にプロンプトだけ与えて生成させるか、Gemini 3.2 Pro にいったん絵を解析させてから生成に渡すか、という分岐でした。
結論としては、Gemini 3.2 Pro と Imagen 4 をはっきり役割分担させる方法を採りました。前者は「自分の絵を読み解いて、構造記述(structured description)を JSON で返す係」、後者は「その構造記述を入力に、解像度と縦横比をきっちり合わせて画像を生成する係」です。
なぜ分けたかというと、Imagen 4 に画像を直接見せて「この感じで派生を出して」と頼むと、表面的な色や形は寄りますが「祈り」「静けさ」「気配」のような抽象軸がぶれやすかったからです。逆に Gemini 3.2 Pro は、絵の構造を「中央に菱形の余白」「全体の彩度を 30% 落としている」「視線を画面右上に逃がしている」のように言葉で要素分解してくれるため、Imagen 4 への指示が安定します。
これは Function Calling の使いどころとしても素直で、Gemini API Function Calling 完全ガイド に書いた基本パターンの応用がそのまま効きました。
実装ステップ 1: 元絵を Gemini 3.2 Pro に「読んで」もらう
まずは元絵 1 枚を Gemini 3.2 Pro に渡して、構造記述を JSON で返してもらいます。google-genai SDK を使います。
# requirements: google-genai>=0.5
from google import genai
from google.genai import types
import base64, json, pathlib
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
SYSTEM_INSTRUCTION = """
あなたはアートディレクターです。入力された 1 枚の絵を、後段の画像生成モデルが
派生バリエーションを作りやすい形に分解してください。出力は次の JSON スキーマに従ってください。
{
"palette": ["主要な色 5 色を hex 表記"],
"saturation_bias": "low|mid|high",
"composition": "中央構図 / 三分割 / 余白主体 / 対角線 など",
"negative_space_ratio": 0.0-1.0,
"mood_axes": ["静謐", "祈り", "畏れ", "高揚" などから 2-3 個"],
"forbidden_elements": ["人物の顔", "ロゴ", "テキスト" などを必要に応じて"]
}
"""
def analyze_artwork(image_path: str) -> dict:
image_bytes = pathlib.Path(image_path).read_bytes()
response = client.models.generate_content(
model="gemini-3.2-pro",
contents=[
types.Part.from_bytes(data=image_bytes, mime_type="image/png"),
"この絵の構造を JSON で返してください。",
],
config=types.GenerateContentConfig(
system_instruction=SYSTEM_INSTRUCTION,
response_mime_type="application/json",
temperature=0.2,
),
)
return json.loads(response.text)
if __name__ == "__main__":
spec = analyze_artwork("artworks/inori_001.png")
print(json.dumps(spec, ensure_ascii=False, indent=2))temperature=0.2 まで下げているのは、同じ絵を複数回読ませても結果がほぼ一致してほしいからです。ここで揺らぐと、後段の Imagen 4 への入力が変わってしまい、シリーズの統一感が崩れます。
期待する出力はこんな形です(実際のログから抜粋)。
{
"palette": ["#1A1F2E", "#2E3A50", "#7891A6", "#C8B79A", "#E8DCC0"],
"saturation_bias": "low",
"composition": "中央構図",
"negative_space_ratio": 0.62,
"mood_axes": ["静謐", "祈り"],
"forbidden_elements": ["人物の顔", "ロゴ"]
}ここまで構造化できれば、絵を渡して感覚で頼むよりも、明らかに Imagen 4 の出力が落ち着きました。
実装ステップ 2: 構造記述を Imagen 4 用プロンプトに翻訳する
次に、上で得た JSON を Imagen 4 のプロンプトに落とし込みます。Imagen 4 は自然文プロンプトを受け取るので、palette や composition を英語の散文に再構成します。これも Gemini 3.2 Pro に任せると、表現のゆらぎが減ります。
def build_imagen_prompt(spec: dict, variation_seed: int) -> str:
palette_str = ", ".join(spec["palette"])
mood_str = " and ".join(spec["mood_axes"])
forbidden = ", ".join(spec.get("forbidden_elements", []))
prompt = (
f"Abstract artwork with a {spec['composition']} composition, "
f"using a restrained color palette of {palette_str}. "
f"Saturation should be {spec['saturation_bias']}, "
f"with approximately {int(spec['negative_space_ratio'] * 100)}% negative space. "
f"Evoke a sense of {mood_str}, suitable for a phone wallpaper. "
f"Variation hint #{variation_seed}: subtle shift in light direction "
f"and texture grain, while preserving the overall composition. "
f"Strictly avoid: {forbidden}."
)
return prompt
def generate_variation(spec: dict, seed: int, out_path: str):
prompt = build_imagen_prompt(spec, seed)
result = client.models.generate_images(
model="imagen-4.0-generate-001",
prompt=prompt,
config=types.GenerateImagesConfig(
number_of_images=1,
aspect_ratio="9:16", # スマホ縦
output_mime_type="image/png",
safety_filter_level="block_low_and_above",
),
)
pathlib.Path(out_path).write_bytes(result.generated_images[0].image.image_bytes)aspect_ratio="9:16" は壁紙用途で必須です。最初に 1:1 で生成して後でクロップしたところ、構図の重心がずれて「元絵の祈りの感じ」が消えるバリエーションが半数近く出てしまい、撮り直しになりました。生成段階で縦比率を確定させるのが正解でした。
safety_filter_level は block_low_and_above にしています。アートの抽象表現でも「強い陰影+赤系の彩度」がたまたま安全フィルタに触れるケースがあり、ここを緩めにしないとシリーズの 1 〜 2 割が無音で落ちる事故が起きました。これは公式ドキュメントだけ読んでいると気づきにくい運用上の注意点です。
実装ステップ 3: 品質ゲートを 4 枚目に置く
生成した画像をそのままアプリに入れると、たまに「視線誘導が崩れている」「中央に妙な記号のような造形が残っている」といった不具合が混じります。Imagen 4 は十分に高品質ですが、120 枚のうち 5 〜 8 枚は人間の目で見て一発で「これは違う」と分かるものが出ます。
そこで再び Gemini 3.2 Pro に登場してもらい、品質判定をさせます。元絵と生成物を並べて読み込ませ、4 つの観点でスコアを返してもらう構成です。
QC_INSTRUCTION = """
2 枚の画像を比較してください。1 枚目が元絵、2 枚目が派生案です。
次の 4 軸を 0.0 〜 1.0 で採点し、JSON で返してください。
{
"palette_match": 色域の一致度,
"composition_consistency": 構図の継承度,
"mood_consistency": ムードの保持度,
"wallpaper_safety": 文字や顔・ロゴが入っていないか
}
全軸 0.7 以上なら採用、いずれか 0.5 未満なら却下、その間なら人間レビュー、と扱います。
"""
def qc_check(original: str, candidate: str) -> dict:
parts = [
types.Part.from_bytes(data=pathlib.Path(original).read_bytes(), mime_type="image/png"),
types.Part.from_bytes(data=pathlib.Path(candidate).read_bytes(), mime_type="image/png"),
"上記の指示に従って JSON で返してください。",
]
response = client.models.generate_content(
model="gemini-3.2-pro",
contents=parts,
config=types.GenerateContentConfig(
system_instruction=QC_INSTRUCTION,
response_mime_type="application/json",
temperature=0.1,
),
)
return json.loads(response.text)このゲートを 4 枚目に挟んだところ、最終的に 120 枚を残すまでに生成した総数は 168 枚で済みました。ゲートがなかった初週は 247 枚生成して 120 枚採用(採用率 48.6%)でしたが、ゲート導入後は 71.4% まで上がり、コストも 30% 下がっています。
実装ステップ 4: アプリへの納品形式に揃える
採用された画像は、わたしの場合 iOS / Android 双方の壁紙アプリに入れるので、3 種類の解像度(1290×2796、1170×2532、1080×2340)に書き出します。Imagen 4 は 1024 や 2048 の正方形ベースで返してくるので、後処理として Pillow でリサイズと余白付与を行います。
from PIL import Image
TARGET_SIZES = [(1290, 2796), (1170, 2532), (1080, 2340)]
def export_for_app(src: str, slug: str, out_dir: str):
img = Image.open(src).convert("RGB")
for w, h in TARGET_SIZES:
canvas = Image.new("RGB", (w, h), color=(20, 24, 36))
# 元画像を縦合わせでフィット、余白は元絵 palette[0] で塗る
ratio = min(w / img.width, h / img.height)
new_w, new_h = int(img.width * ratio), int(img.height * ratio)
resized = img.resize((new_w, new_h), Image.LANCZOS)
canvas.paste(resized, ((w - new_w) // 2, (h - new_h) // 2))
canvas.save(f"{out_dir}/{slug}_{w}x{h}.jpg", quality=92, optimize=True)余白色を「元絵の palette[0]」に揃えるのは小さな工夫ですが、ホーム画面でアイコンを置いたときの違和感がぐっと減ります。アプリのストア画面に乗せた印象もこれで全体トーンが揃います。
30 日運用で見えた数字 — コスト・採用率・アプリ KPI
ここからは実数で振り返ります。元絵 1 枚から派生 8 〜 16 枚を作る単位を「シリーズ」と呼んでいます。30 日でシリーズ 12 本、合計 168 枚生成、採用 120 枚という結果でした。
API コストの内訳は次のとおりです。Gemini 3.2 Pro の構造解析と QC を合わせた呼び出しが 408 回で約 1,720 円、Imagen 4 Standard の生成が 168 回で約 6,720 円、合計 8,440 円。1 枚の最終納品あたり 70 円前後です。これまで外注していた 1 枚 8,000 〜 15,000 円と比べると、計算上 99% 以上のコスト圧縮になりました。
アプリ側の KPI も計測しました。新シリーズ投入後 7 日間の DAU が、過去 12 週の同様カテゴリ平均と比べて +14.3% 上振れ、平均セッション時間も +8.7% 改善しています。AdMob の eCPM 自体は変わっていないので、滞在時間の増加分がそのまま広告収益の押し上げに効いています。1 シリーズあたり月数万円規模で見えるようになり、累計 5,000 万 DL を超えてからやや停滞気味だったアプリ群に、想定以上のテコ入れになりました。
逆に苦戦した点もきちんと書いておきます。1 つはモチーフが「人型」に近づいた瞬間に Imagen 4 の安全フィルタが反応し、シリーズ 1 本が丸ごとリトライになったことが 30 日で 2 回ありました。プロンプト側で「figurative human form」を明示的に避ける一文を入れて回避しましたが、最初は原因が読めずに半日溶かしました。もう 1 つは、QC スコアが「全部 0.7 以上だが、人が見ると微妙」という中間ゾーンが 1 割あった点です。ここはやはり目視で 1 枚 5 秒の最終確認をはさむのがいまのところ正解です。
参考になりそうなトピックでは、個人開発で同じ規模の API オーケストレーションを安定させたい場合は Vertex AI Gemini を本番運用するためのガイド の運用パターン、状態を持たせたエージェント設計に踏み込むなら Gemini 2.5 Pro × ADK で永続アシスタントを作る本番ガイド のセッション設計が地続きで効きました。今回の本番運用ノウハウとは別系統ですが、生成プロセスを内側から理解しておくと安全フィルタや構図の事故を回避しやすくなります。
これから同じことを試す個人開発者・アーティストへ
最後に、ここから動き始める人が最初に手をつけるとよさそうな一手を共有します。元絵を 3 枚だけ用意して、構造解析 → 派生 4 枚 → QC → 納品の最小ループを 1 シリーズ作ってみてください。コストはおそらく 200 〜 300 円で済みます。1 シリーズ通すと、自分の作品のどの部分が「機械に任せて伸びる」のか、どこは「人間が描かないと出ない」のか、肌感覚で分かれます。
わたし自身は 16 歳でインターネットに出会って独学でプログラミングを始め、宮大工だった両祖父から引き継いだ「手を動かすことが一つの信心」という感覚を、いまも創作に持ち込んでいます。生成 AI の登場で「アーティストはもう不要」という論調を聞くこともありますが、自分の手で出した一筆を起点にして、世界観の責任を持ったまま仕組みに任せる部分だけ任せる、というやり方なら、むしろ作家が拡張できる手段が増えたと感じています。
同じことに挑戦している方の参考になれば嬉しいです。