nanobanana pro×AIジェネレータのコード公開!

この記事の内容について、業務や開発でお困りの場合は個別に対応できます。

【結論:P / Point】

今回は WordPress のテーマに設置するだけで動く「完全クライアントサイド版・漫画AIジェネレーター」をコード付きで公開します。

さらに今回は nanobanana pro(Fal.ai Hosted)を強く意識したプロンプト設計で、
Gemini 3 系の画像生成を **「漫画特化」**の品質で出せるよう調整しました。

  • APIキーはサーバーに送信されない(100% クライアント)
  • WordPress にアップするだけで使える
  • nanobanana pro の “線画・コマ割り・漫画タッチ” がきれいに出る
  • 少年漫画 / 少女漫画 / 4コマ / SNS漫画 / 劇画 / Webtoon のプリセット搭載

Gemini 単体より nanobanana pro の “漫画安定性” が高いため、
漫画生成ツールとして実用的に使えるレベルになっています。


**【理由:R / Reason】

なぜ nanobanana pro なのか?**

最近は Gemini / GPT-4o などで漫画生成が注目されていますが、
実際の現場で使うと次の問題が出ます。

  • 顔崩れ・指崩壊が頻発する
  • コマ割りが消える
  • コマ内部の情報密度が揺れる
  • ポーズが安定しない
  • 同じキャラが維持されない

ところが **nanobanana pro(Fal.ai Hosted Model)**を使うと、次の強みがあります:

線画が安定している

モノクロ漫画の相性が異常に良い

“漫画タッチの構造” が落ちにくい

構図が過剰に崩れない

商業漫画の密度寄りに出る

そして今回のコードは クライアントサイドで Gemini API を叩く形式なので、

  • WordPress のサーバー側に APIキーが残らない
  • セキュリティ的に安全
  • サーバー負荷ゼロ
  • 高速 & 即導入

というメリットも得られます。


**【具体例:E / Example】

今回公開するコードの特徴(要点だけ)**

1. WordPress ページテンプレートとして即使える

/* Template Name: Manga AI Generator */
→ 固定ページでテンプレートに選べばすぐ動く。

2. すべてブラウザ側で処理(APIキーは LocalStorage)

APIキーはサーバーを通過しません。

const apiUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent?key=" + apiKey;

WordPress に一切残らないので安心。


3. nanobanana pro を前提にした「漫画プリセット」付き

const PRESETS = {
    shonen: {...},
    shoujo: {...},
    koma4: {...},
    gekiga: {...},
    sns: {...},
    webtoon: {...}
};

nanobanana pro の強みである “線画・コマ割り” を最大限に活かすためのプリセットになっています。


4. Atmosphere / Pacing / Density / Style の細かい調整も可能

プロンプトが自動生成される UI
編集可能 / プロンプトプレビュー付き
漫画のクオリティを調整できる


5. Gemini を使うが、漫画生成は nanobanana pro 前提

あなたがこのツールを自分用や案件用で使うなら、
画像生成のバックエンドを nanobanana pro に切り替えるだけで品質が激変します。

漫画用途では本気でおすすめ。

<?php
/*
Template Name: Manga AI Generator
*/
/**
 * Manga AI Generator for WordPress (Client-Side Only Version)
 * 
 * Usage:
 * 1. Upload this file to your server or include it in a WordPress plugin/theme.
 * 2. If using as a standalone file, access it directly.
 * 3. If embedding in WP, you can use the HTML/JS part.
 * 
 * Note: This version performs all API calls directly from the browser to Google's servers.
 * Your server (WordPress) is NOT involved in the API communication, ensuring the API key
 * never passes through your server logic.
 */
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>漫画AIジェネレーター (Gemini)</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        /* Custom styles if needed */
        .loader {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            width: 24px;
            height: 24px;
            animation: spin 2s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body class="bg-gray-50 min-h-screen py-8 px-4 sm:px-6 lg:px-8">

<div class="max-w-3xl mx-auto">
    <!-- Header -->
    <div class="flex justify-between items-center mb-8">
        <h1 class="text-3xl font-bold text-gray-900">漫画AIジェネレーター (Gemini)</h1>
        <div class="flex space-x-4">
            <button id="manualBtn" class="text-sm text-gray-600 hover:text-gray-900">
                使い方・免責事項
            </button>
            <button id="apiKeyBtn" class="text-sm text-indigo-600 hover:text-indigo-900">
                APIキー設定
            </button>
        </div>
    </div>

    <!-- Manual Modal -->
    <div id="manualModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
        <div class="relative top-10 mx-auto p-5 border w-11/12 max-w-2xl shadow-lg rounded-md bg-white">
            <div class="mt-3">
                <h3 class="text-xl font-bold text-gray-900 mb-4 text-center">使い方・免責事項</h3>
                <div class="mt-2 px-4 py-3 text-sm text-gray-600 space-y-4 max-h-[70vh] overflow-y-auto">
                    
                    <section>
                        <h4 class="font-bold text-gray-800 mb-2">1. APIキーの取得方法</h4>
                        <ol class="list-decimal list-inside space-y-1 ml-2">
                            <li><a href="https://aistudio.google.com/app/apikey" target="_blank" class="text-indigo-600 hover:underline">Google AI Studio</a> にアクセスします。</li>
                            <li>Googleアカウントでログインします。</li>
                            <li>「Create API key」ボタンをクリックします。</li>
                            <li>作成されたキー(AIzaSy...で始まる文字列)をコピーします。</li>
                            <li>本ツールの右上の「APIキー設定」をクリックし、キーを貼り付けて保存します。</li>
                        </ol>
                    </section>

                    <section>
                        <h4 class="font-bold text-gray-800 mb-2">2. セキュリティとプライバシーについて</h4>
                        <ul class="list-disc list-inside space-y-1 ml-2">
                            <li><strong>クライアントサイド処理:</strong> 本ツールは、あなたのブラウザから直接Googleのサーバーへ通信を行います。</li>
                            <li><strong>サーバー非経由:</strong> 入力したAPIキーや生成された画像データが、本ツールを設置しているWebサーバー(WordPress等)に送信・保存されることは一切ありません。</li>
                            <li><strong>データ保存:</strong> APIキーはブラウザの `LocalStorage`(ローカルストレージ)に保存されます。これはあなたの端末内にのみ存在する領域です。</li>
                        </ul>
                    </section>

                    <section>
                        <h4 class="font-bold text-gray-800 mb-2">3. 免責事項</h4>
                        <div class="bg-gray-100 p-3 rounded border border-gray-200">
                            <p>本ツールは、Google Gemini APIを利用した実験的なアプリケーションです。</p>
                            <ul class="list-disc list-inside mt-2 space-y-1">
                                <li>生成される画像の品質や内容について、開発者および提供者は一切の責任を負いません。</li>
                                <li>APIの利用料金(無料枠を超えた場合など)は、APIキー所有者の負担となります。</li>
                                <li>Googleの利用規約やポリシーに従ってご利用ください。</li>
                                <li>本ツールの利用により生じた損害について、提供者は責任を負いかねます。</li>
                            </ul>
                        </div>
                    </section>

                </div>
                <div class="items-center px-4 py-3 text-center">
                    <button id="closeManualModal" class="px-4 py-2 bg-indigo-600 text-white text-base font-medium rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-300">
                        閉じる
                    </button>
                </div>
            </div>
        </div>
    </div>

    <!-- API Key Modal -->
    <div id="apiKeyModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
        <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
            <div class="mt-3 text-center">
                <h3 class="text-lg leading-6 font-medium text-gray-900">APIキー設定</h3>
                <div class="mt-2 px-7 py-3">
                    <p class="text-sm text-gray-500 mb-4">
                        Gemini APIキーを入力してください。<br>
                        キーはブラウザ(LocalStorage)に保存され、<br>
                        <strong>サーバーには送信されません。</strong>
                    </p>
                    <input type="password" id="apiKeyInput" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" placeholder="AIzaSy...">
                </div>
                <div class="items-center px-4 py-3">
                    <button id="saveApiKey" class="px-4 py-2 bg-indigo-600 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-300">
                        保存
                    </button>
                    <button id="closeModal" class="mt-3 px-4 py-2 bg-gray-100 text-gray-700 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-300">
                        閉じる
                    </button>
                </div>
            </div>
        </div>
    </div>

    <!-- Main Generator UI -->
    <div class="bg-white shadow sm:rounded-lg p-6 mb-8">
        
        <!-- 1. Preset Selector -->
        <div class="mb-6">
            <label class="block text-sm font-bold text-gray-700 mb-2">1. スタイル選択 (プリセット)</label>
            <select id="presetSelector" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white">
                <option value="shonen">少年漫画 (バトル/アクション)</option>
                <option value="shoujo">少女漫画 (恋愛/エモ)</option>
                <option value="koma4">4コマ漫画 (コメディ/日常)</option>
                <option value="gekiga">劇画 (シリアス/ハードボイルド)</option>
                <option value="sns">SNSバズ漫画 (共感/エッセイ)</option>
                <option value="webtoon">Webtoon風 (縦スクロール/フルカラー)</option>
            </select>
            <p class="text-xs text-gray-500 mt-1">スタイルを選ぶと、下のオプションが自動で推奨設定に切り替わります。</p>
        </div>

        <!-- 2. Character Description -->
        <div class="mb-6">
            <label class="block text-sm font-bold text-gray-700 mb-2">2. キャラクター・状況説明</label>
            <textarea id="characterInput" rows="3" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white" placeholder="例: 黒髪の高校生、制服を着ている、剣を持っている、必殺技を叫んでいる"></textarea>
        </div>

        <!-- 3. Detailed Options -->
        <div class="mb-6">
            <label class="block text-sm font-bold text-gray-700 mb-2">3. 全体の雰囲気・演出 (ざっくり指定)</label>
            <div class="grid grid-cols-1 md:grid-cols-2 gap-4 bg-gray-50 p-4 rounded-md border border-gray-200">
                
                <!-- Atmosphere -->
                <div>
                    <label class="block text-xs font-medium text-gray-500 mb-1">ストーリーの雰囲気</label>
                    <select id="optAtmosphere" class="w-full p-2 border border-gray-300 rounded text-sm">
                        <option value="default">指定なし (おまかせ)</option>
                        <option value="serious">シリアス・重厚 (緊張感)</option>
                        <option value="comedy">コメディ・明るい (楽しい)</option>
                        <option value="emotional">エモーショナル (感動・ドラマ)</option>
                        <option value="horror">ホラー・サスペンス (恐怖)</option>
                        <option value="peaceful">ほのぼの・日常 (リラックス)</option>
                    </select>
                </div>

                <!-- Pacing -->
                <div>
                    <label class="block text-xs font-medium text-gray-500 mb-1">展開のテンポ・構成</label>
                    <select id="optPacing" class="w-full p-2 border border-gray-300 rounded text-sm">
                        <option value="default">指定なし (おまかせ)</option>
                        <option value="dynamic">動的・アクション多め (スピード感)</option>
                        <option value="static">静的・会話中心 (心理描写)</option>
                        <option value="impact">衝撃的な展開 (大ゴマ・決めシーン)</option>
                        <option value="slow">ゆったり・情緒的 (余韻)</option>
                    </select>
                </div>

                <!-- Density -->
                <div>
                    <label class="block text-xs font-medium text-gray-500 mb-1">画面の密度・描き込み</label>
                    <select id="optDensity" class="w-full p-2 border border-gray-300 rounded text-sm">
                        <option value="default">指定なし (おまかせ)</option>
                        <option value="simple">シンプル (読みやすさ重視・余白多め)</option>
                        <option value="standard">標準バランス</option>
                        <option value="high">高密度 (描き込み重視・背景詳細)</option>
                    </select>
                </div>

                <!-- Art Style -->
                <div>
                    <label class="block text-xs font-medium text-gray-500 mb-1">描画タッチ</label>
                    <select id="optStyle" class="w-full p-2 border border-gray-300 rounded text-sm">
                        <option value="default">指定なし (おまかせ)</option>
                        <option value="bold">力強い・荒々しい (少年漫画的)</option>
                        <option value="delicate">繊細・美麗 (少女漫画的)</option>
                        <option value="pop">ポップ・デフォルメ (親しみやすい)</option>
                        <option value="realistic">リアル・劇画調 (写実的)</option>
                    </select>
                </div>

            </div>
        </div>

        <!-- Advanced: Generated Prompt Preview -->
        <div class="mb-6">
            <details>
                <summary class="text-sm text-indigo-600 cursor-pointer hover:text-indigo-800 select-none">詳細設定・生成されるプロンプトを確認</summary>
                <div class="mt-2">
                    <label class="block text-sm font-medium text-gray-700 mb-1">生成プロンプト (自動生成)</label>
                    <textarea id="finalPrompt" rows="4" class="w-full p-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 text-gray-600 text-xs font-mono" readonly></textarea>
                    <p class="text-xs text-gray-500 mt-1">※上のオプションを変更すると自動で更新されます。</p>
                </div>
            </details>
        </div>

        <!-- Settings -->
        <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">アスペクト比</label>
                <select id="aspectRatio" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white">
                    <option value="1:1">1:1 (正方形)</option>
                    <option value="3:4">3:4 (縦長 - スマホ/単行本)</option>
                    <option value="4:3">4:3 (横長 - PC/動画)</option>
                    <option value="16:9">16:9 (ワイド - サムネ)</option>
                </select>
            </div>
            <div>
                <label class="block text-sm font-medium text-gray-700 mb-1">画像サイズ</label>
                <select id="imageSize" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-gray-900 bg-white">
                    <option value="1K">1K (標準)</option>
                    <option value="2K">2K (高画質)</option>
                </select>
            </div>
        </div>

        <!-- Generate Button -->
        <button id="generateBtn" class="w-full flex justify-center items-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:bg-indigo-400">
            <span id="btnText">漫画コマを生成</span>
            <div id="btnLoader" class="loader ml-2 hidden"></div>
        </button>
    </div>

    <!-- Results -->
    <div id="resultArea" class="hidden">
        <h2 class="text-xl font-bold text-gray-900 mb-4">生成結果</h2>
        <div id="imageContainer" class="bg-white p-4 rounded-lg shadow flex justify-center">
            <!-- Image will be inserted here -->
        </div>
    </div>

</div>

<script>
// 1. Presets Configuration
const PRESETS = {
    shonen: {
        name: "少年漫画",
        baseTemplate: `少年漫画スタイル, 漫画のページレイアウト, コマ割り, 複数のコマ,
ダイナミックなアクション, 大胆な線画, 集中線, インパクトのある構図,
高コントラストの白黒, 筋肉のバランス, モーションブラー, 激しい表情`,
        defaults: {
            atmosphere: "default",
            pacing: "dynamic",
            density: "default",
            style: "bold"
        }
    },
    shoujo: {
        name: "少女漫画",
        baseTemplate: `少女漫画スタイル, 漫画のページレイアウト, コマ割り, 複数のコマ,
繊細で綺麗な線画, 優しい目, キラキラエフェクト, 細い輪郭線,
ロマンチックな照明, 白っぽいトーン, 花の背景, 感情的な表情, 柔らかい雰囲気`,
        defaults: {
            atmosphere: "emotional",
            pacing: "default",
            density: "default",
            style: "delicate"
        }
    },
    koma4: {
        name: "4コマ漫画",
        baseTemplate: `4コマ漫画スタイル, 4つのコマ, 縦に並んだコマ割り,
太い枠線, シンプルな背景, ミニマリストな線画, コミカルな表情,
デフォルメ(ちびキャラ)可, きれいなコマ割り, 読みやすい構図`,
        defaults: {
            atmosphere: "comedy",
            pacing: "static",
            density: "simple",
            style: "pop"
        }
    },
    gekiga: {
        name: "劇画",
        baseTemplate: `劇画スタイル, 漫画のページレイアウト, コマ割り, 複数のコマ,
リアルな描写, 濃い影, 緻密な書き込み, シリアスな雰囲気,
筆ペンタッチ, 荒々しい線, 重厚なストーリー性, 大人向け`,
        defaults: {
            atmosphere: "serious",
            pacing: "default",
            density: "high",
            style: "realistic"
        }
    },
    sns: {
        name: "SNSバズ漫画",
        baseTemplate: `SNSエッセイ漫画風, 漫画のページレイアウト, コマ割り, 複数のコマ,
シンプルで親しみやすい, 共感を呼ぶ表情, 余白多め, 読みやすさ重視,
デジタル作画, 今風の絵柄`,
        defaults: {
            atmosphere: "peaceful",
            pacing: "static",
            density: "simple",
            style: "pop"
        }
    },
    webtoon: {
        name: "Webtoon風",
        baseTemplate: `Webtoonスタイル, 縦スクロール漫画, 縦長のコマ割り,
フルカラー(または高精細モノクロ), アニメ塗り, 鮮やかな照明,
映画的な演出, スマホで読みやすい構図`,
        defaults: {
            atmosphere: "default",
            pacing: "dynamic",
            density: "default",
            style: "default"
        }
    }
};

// Option Labels (for prompt construction)
const OPTION_LABELS = {
    atmosphere: {
        default: null,
        serious: "シリアスな雰囲気, 緊張感のある展開, 重厚なストーリー",
        comedy: "コメディタッチ, 明るい雰囲気, 笑える展開, 楽しい",
        emotional: "感動的な雰囲気, エモーショナル, 心温まる, ドラマチック",
        horror: "恐怖, ホラーテイスト, 不気味な雰囲気, サスペンス",
        peaceful: "ほのぼのとした日常, 穏やかな雰囲気, リラックス"
    },
    pacing: {
        default: null,
        static: "会話シーン中心, 静的な構図, キャラクターの表情重視, 落ち着いた展開",
        dynamic: "激しいアクション, スピード感のある展開, ダイナミックな動き, 戦闘シーン",
        impact: "衝撃的な展開, インパクトのある大ゴマ, クライマックス, 劇的",
        slow: "ゆったりとした間, 情緒的な演出, 余韻を感じさせる, スローテンポ"
    },
    density: {
        default: null,
        simple: "シンプルな画面構成, 余白を活かす, 読みやすさ最優先, すっきりとした背景",
        standard: "標準的な描き込み, バランスの取れた画面",
        high: "緻密な描き込み, 高密度な画面, 背景の細部まで描写, 圧倒的な情報量"
    },
    style: {
        default: null,
        delicate: "繊細な線画, 美麗なタッチ, 細かいトーン処理, 少女漫画的",
        bold: "力強い線画, 荒々しいタッチ, ハイコントラスト, 少年漫画的",
        pop: "ポップな絵柄, デフォルメ表現, 丸みのある線, 親しみやすい",
        realistic: "リアルな描写, 劇画タッチ, 写実的, 陰影の強調"
    }
};

// Storage Helpers
function setApiKey(key) {
    localStorage.setItem('gemini_api_key', key);
}

function getApiKey() {
    return localStorage.getItem('gemini_api_key');
}

// UI Logic
document.addEventListener('DOMContentLoaded', () => {
    // Elements
    const apiKeyModal = document.getElementById('apiKeyModal');
    const apiKeyInput = document.getElementById('apiKeyInput');
    const apiKeyBtn = document.getElementById('apiKeyBtn');
    const saveApiKeyBtn = document.getElementById('saveApiKey');
    const closeModalBtn = document.getElementById('closeModal');
    
    const manualModal = document.getElementById('manualModal');
    const manualBtn = document.getElementById('manualBtn');
    const closeManualModalBtn = document.getElementById('closeManualModal');
    
    const presetSelector = document.getElementById('presetSelector');
    const characterInput = document.getElementById('characterInput');
    
    // New Options
    const optAtmosphere = document.getElementById('optAtmosphere');
    const optPacing = document.getElementById('optPacing');
    const optDensity = document.getElementById('optDensity');
    const optStyle = document.getElementById('optStyle');
    
    const finalPrompt = document.getElementById('finalPrompt');
    const generateBtn = document.getElementById('generateBtn');
    const btnText = document.getElementById('btnText');
    const btnLoader = document.getElementById('btnLoader');
    const resultArea = document.getElementById('resultArea');
    const imageContainer = document.getElementById('imageContainer');

    // --- Logic Functions ---

    function updatePrompt() {
        const presetKey = presetSelector.value;
        const preset = PRESETS[presetKey];
        
        const charDesc = characterInput.value.trim() || "人物";
        
        // Helper to get option text only if not default
        const getOpt = (cat, val) => OPTION_LABELS[cat][val];

        const atmosphere = getOpt('atmosphere', optAtmosphere.value);
        const pacing = getOpt('pacing', optPacing.value);
        const density = getOpt('density', optDensity.value);
        const style = getOpt('style', optStyle.value);

        // Build option lines conditionally
        let optionsBlock = "";
        if (atmosphere) optionsBlock += `雰囲気: ${atmosphere}\n`;
        if (pacing) optionsBlock += `展開・テンポ: ${pacing}\n`;
        if (density) optionsBlock += `画面密度: ${density}\n`;
        if (style) optionsBlock += `描画スタイル: ${style}\n`;

        // Construct the prompt
        const prompt = `
${preset.baseTemplate}

【キャラクター・状況】
${charDesc}

【全体演出・構成】
${optionsBlock}
【品質保持】
漫画のコマ割り(Panel Layout), 複数のコマ(Multiple Panels),
顔崩れなし, 指の異常なし, アーティファクト禁止, 
自然な身体バランス, 商業漫画クオリティ,
高解像度, ノイズ除去, 鮮明な線画
`.trim();

        finalPrompt.value = prompt;
    }

    function applyPresetDefaults(presetKey) {
        const defaults = PRESETS[presetKey].defaults;
        if (defaults) {
            optAtmosphere.value = defaults.atmosphere;
            optPacing.value = defaults.pacing;
            optDensity.value = defaults.density;
            optStyle.value = defaults.style;
        }
        updatePrompt();
    }

    // --- Event Listeners ---

    // Preset Change
    presetSelector.addEventListener('change', (e) => {
        applyPresetDefaults(e.target.value);
    });

    // Input Changes
    [characterInput, optAtmosphere, optPacing, optDensity, optStyle].forEach(el => {
        el.addEventListener('input', updatePrompt);
    });

    // Initialize
    applyPresetDefaults('shonen'); // Default preset
    
    // Check API Key
    const currentKey = getApiKey();
    if (!currentKey) {
        apiKeyModal.classList.remove('hidden');
    } else {
        apiKeyInput.value = currentKey;
    }

    // Modal Handlers
    apiKeyBtn.addEventListener('click', () => {
        apiKeyInput.value = getApiKey() || '';
        apiKeyModal.classList.remove('hidden');
    });

    closeModalBtn.addEventListener('click', () => {
        apiKeyModal.classList.add('hidden');
    });

    manualBtn.addEventListener('click', () => {
        manualModal.classList.remove('hidden');
    });

    closeManualModalBtn.addEventListener('click', () => {
        manualModal.classList.add('hidden');
    });

    saveApiKeyBtn.addEventListener('click', () => {
        const key = apiKeyInput.value.trim();
        if (key) {
            setApiKey(key);
            apiKeyModal.classList.add('hidden');
            alert('APIキーを保存しました');
        } else {
            alert('APIキーを入力してください');
        }
    });

    // Generate
    generateBtn.addEventListener('click', async () => {
        const apiKey = getApiKey();
        if (!apiKey) {
            alert('APIキーが設定されていません。右上の「APIキー設定」から設定してください。');
            apiKeyModal.classList.remove('hidden');
            return;
        }

        const prompt = finalPrompt.value; // Use the generated prompt
        const aspectRatio = document.getElementById('aspectRatio').value;
        const imageSize = document.getElementById('imageSize').value;

        if (!prompt) {
            alert('プロンプトが空です。');
            return;
        }

        // UI Loading State
        generateBtn.disabled = true;
        btnText.textContent = '生成中...';
        btnLoader.classList.remove('hidden');
        resultArea.classList.add('hidden');
        imageContainer.innerHTML = '';

        try {
            // Direct API Call to Gemini (Client-Side)
            const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent?key=${apiKey}`;
            
            const payload = {
                contents: [
                    {
                        parts: [{ text: prompt }]
                    }
                ],
                generationConfig: {
                    imageConfig: {
                        aspectRatio: aspectRatio,
                        imageSize: imageSize
                    }
                }
            };

            const response = await fetch(apiUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(payload)
            });

            if (!response.ok) {
                const errorText = await response.text();
                let errorMsg = `API Error: ${response.status}`;
                try {
                    const errorJson = JSON.parse(errorText);
                    if (errorJson.error && errorJson.error.message) {
                        errorMsg = errorJson.error.message;
                    }
                } catch (e) {}
                throw new Error(errorMsg);
            }

            const data = await response.json();
            
            // Parse Response
            const candidate = data.candidates?.[0];
            const part = candidate?.content?.parts?.find(p => p.inlineData?.data);

            if (!part) {
                throw new Error('画像が生成されませんでした。プロンプトを変更して再試行してください。');
            }

            const base64 = part.inlineData.data;
            const mime = part.inlineData.mimeType || 'image/png';
            const dataUrl = `data:${mime};base64,${base64}`;

            const img = document.createElement('img');
            img.src = dataUrl;
            img.className = 'max-w-full h-auto rounded shadow-lg';
            imageContainer.appendChild(img);
            resultArea.classList.remove('hidden');

        } catch (error) {
            console.error(error);
            alert('エラーが発生しました: ' + error.message);
        } finally {
            generateBtn.disabled = false;
            btnText.textContent = '漫画コマを生成';
            btnLoader.classList.add('hidden');
        }
    });
});
</script>
</body>
</html>

ZIDOOKA!

この記事の内容について、対応できます

この記事に関連する技術トラブルや開発上の問題について個別対応を行っています。

個別対応は3,000円〜 内容・工数により事前にお見積りします
最後までお読みいただきありがとうございました

One thought on “nanobanana pro×AIジェネレータのコード公開!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

AI活用に関するポリシー

当サイトでは、記事の執筆補助にAIを活用する場合がありますが、全面的な委任は行いません。