🏛️

政治家言い訳メーカーを作ってみた - Gemini API × WordPress実装ガイド

に公開

はじめに

AIの「無駄遣い」をテーマに、政治家風の言い訳を自動生成するWebサービスを開発しました。日常のちょっとした「やらかし」を、政治家のような巧妙な言い回しで切り抜けられる(?)エンターテイメントツールです。

本記事では、Google Gemini APIとWordPressを使った実装方法を詳しく解説します。

開発背景
日々のニュースで見る政治家の絶妙な言い回しや巧みな言い訳。「この技術を、もっと身近に体験できたら面白いのでは?」と考え、最新のAI技術をあえて"無駄遣い"し、ブラックジョークとして楽しめるサービスを作りました。

本記事について
今回のサービス開発および記事執筆は、Claude(Anthropic社のAI)との協業で進めました。AI時代の新しい開発スタイルの実践例として参考になれば幸いです。

自己紹介

ホネグミ代表、応用情報技術者の資格を持つエンジニア×マーケターです。これまでIT系の会社役員を4年、独立して4年目になります。クライアントワークでは「こうしたい」を技術で形にすることを専門としていますが、最近は個人開発でAIを「無駄に」使った社会風刺的なサービスも作っています。

https://zenn.dev/5naokichi/articles/8f9446a136a874

完成品

政治家言い訳メーカーのスクリーンショット

サービスURL:
https://hone-gumi.com/seijika-iiwake/

主な機能

  • 状況入力による言い訳自動生成: テキストエリアに状況を入力
  • 6つの言い訳スタイル: 謝罪型、開き直り型、はぐらかし型、完全否定型、ブチギレ型、同情誘い型
  • 4つの政治家タイプ: 若手議員、中堅議員、ベテラン議員、大臣クラス
  • SNSシェア機能: X(Twitter)、LINEでの共有
  • 累計使用回数表示: WordPress wp_optionsテーブルを活用
  • レスポンシブ対応: モバイル・デスクトップ両対応

技術構成

アーキテクチャ概要

使用技術

  • フロントエンド: HTML, Vanilla JavaScript, CSS
  • バックエンド: WordPress (PHP)
  • AI API: Google Gemini API
  • データベース: WordPress wp_options(カウンター機能)
  • セキュリティ: サーバーサイドAPIキー管理

実装手順

1. 環境構築

Gemini APIキーの取得

Google AI StudioからAPIキーを取得し、WordPressに設定します。

wp-config.php
// APIキーを安全に管理
define('GEMINI_API_KEY', 'your_gemini_api_key_here');

functions.phpでAJAXアクション登録

functions.php
// 政治家言い訳生成のAJAXアクション登録
add_action('wp_ajax_politician_excuse_generate', 'politician_excuse_proxy');
add_action('wp_ajax_nopriv_politician_excuse_generate', 'politician_excuse_proxy');

2. フロントエンド実装

HTML構造

<div class="excuse-generator">
  <h2>政治家言い訳メーカー</h2>
  <p>スキャンダルの状況を入力すると、政治家風の言い訳を自動生成します</p>
  
  <!-- 累計使用回数表示 -->
  <div class="usage-stats">
    <p class="total-usage">累計使用回数: <span id="total-count">読み込み中...</span>回</p>
  </div>
  
  <!-- 例文ボタン -->
  <div class="example-buttons">
    <p>以下の例をクリックすると自動入力されます:</p>
    <div class="example-btn-container">
      <button class="example-btn" data-example="国会で居眠りしている写真が撮られた">国会で居眠り</button>
      <button class="example-btn" data-example="秘書が選挙区で現金を配っていた">秘書の現金配布</button>
      <!-- その他の例文ボタン -->
    </div>
  </div>
  
  <!-- 入力エリア -->
  <div class="input-section">
    <label for="scandal-situation">言い訳が必要な状況を入力してください:</label>
    <textarea id="scandal-situation" rows="4" placeholder="例:公金を私的な飲食に使用していたことがバレた..."></textarea>
    
    <div class="settings">
      <div class="setting-item">
        <label for="excuse-style">言い訳のスタイル:</label>
        <select id="excuse-style">
          <option value="apologetic">謝罪型</option>
          <option value="aggressive">開き直り型</option>
          <option value="evasive">はぐらかし型</option>
          <option value="denial">完全否定型</option>
          <option value="furious">ブチギレ型</option>
          <option value="pity">同情誘い型</option>
        </select>
      </div>
      
      <div class="setting-item">
        <label for="politician-type">政治家タイプ:</label>
        <select id="politician-type">
          <option value="young">若手議員</option>
          <option value="middle">中堅議員</option>
          <option value="veteran">ベテラン議員</option>
          <option value="minister">大臣クラス</option>
        </select>
      </div>
    </div>
    
    <button id="generate-excuse-btn">言い訳を生成する</button>
  </div>
  
  <!-- 結果表示エリア -->
  <div class="result-section" style="display: none;">
    <div class="politician-icon">
      <img id="politician-image" src="" alt="政治家">
    </div>
    <div class="excuse-content">
      <p id="excuse-text"></p>
    </div>
    <div class="share-buttons">
      <button id="twitter-share" class="share-btn">Xでシェア</button>
      <button id="line-share" class="share-btn">LINEで送る</button>
      <button id="copy-excuse" class="share-btn">コピー</button>
      <button id="new-excuse" class="share-btn">別の言い訳</button>
    </div>
  </div>
  
  <!-- ローディング表示 -->
  <div class="loading" style="display: none;">
    <div class="spinner"></div>
    <p>政治家が言い訳を考えています...</p>
  </div>
</div>

JavaScript実装

document.addEventListener('DOMContentLoaded', function() {
  const generateBtn = document.getElementById('generate-excuse-btn');
  const situationInput = document.getElementById('scandal-situation');
  const styleSelect = document.getElementById('excuse-style');
  const politicianTypeSelect = document.getElementById('politician-type');
  const excuseText = document.getElementById('excuse-text');
  const resultSection = document.querySelector('.result-section');
  const loadingSection = document.querySelector('.loading');
  
  // 例文ボタンのイベントリスナー設定
  const exampleBtns = document.querySelectorAll('.example-btn');
  exampleBtns.forEach(btn => {
    btn.addEventListener('click', function() {
      situationInput.value = this.getAttribute('data-example');
      situationInput.focus();
    });
  });
  
  // 言い訳生成のメイン処理
  generateBtn.addEventListener('click', generateExcuse);
  
  async function generateExcuse() {
    const situation = situationInput.value.trim();
    if (!situation) {
      alert('言い訳が必要な状況を入力してください。');
      return;
    }
    
    const style = styleSelect.value;
    const politicianType = politicianTypeSelect.value;
    
    // ローディング表示
    resultSection.style.display = 'none';
    loadingSection.style.display = 'block';
    
    try {
      const response = await fetch('/wp-admin/admin-ajax.php', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: `action=politician_excuse_generate&situation=${encodeURIComponent(situation)}&style=${encodeURIComponent(style)}&politician_type=${encodeURIComponent(politicianType)}`
      });
      
      const result = await response.json();
      
      if (!result.success) {
        throw new Error(result.data?.message || 'APIエラーが発生しました。');
      }
      
      const excuse = result.data.excuse_text;
      
      // 結果表示
      excuseText.textContent = excuse;
      loadingSection.style.display = 'none';
      resultSection.style.display = 'block';
      
      // 使用回数カウンター更新
      updateUsageCounter();
      
      // シェアボタンの設定
      updateShareButtons(excuse);
      
    } catch (error) {
      console.error('言い訳生成エラー:', error);
      alert('言い訳の生成中にエラーが発生しました。\n詳細: ' + error.message);
      loadingSection.style.display = 'none';
    }
  }
  
  // シェアボタンの機能設定
  function updateShareButtons(excuse) {
    // Twitter/Xシェア
    document.getElementById('twitter-share').onclick = function() {
      const shareText = excuse.length > 120 ? excuse.substring(0, 117) + '...' : excuse;
      const tweetText = encodeURIComponent(`【政治家風言い訳ジェネレーター】\n\n${shareText}`);
      const tweetUrl = encodeURIComponent(window.location.href);
      window.open(`https://twitter.com/intent/tweet?text=${tweetText}&url=${tweetUrl}`, '_blank');
    };
    
    // LINEシェア
    document.getElementById('line-share').onclick = function() {
      const shareText = excuse.length > 450 ? excuse.substring(0, 447) + '...' : excuse;
      const lineText = encodeURIComponent(`【政治家風言い訳ジェネレーター】\n\n${shareText}\n\n${window.location.href}`);
      window.open(`https://line.me/R/msg/text/?${lineText}`, '_blank');
    };
    
    // コピー機能
    document.getElementById('copy-excuse').onclick = function() {
      navigator.clipboard.writeText(excuse)
        .then(() => {
          const originalText = this.innerHTML;
          this.innerHTML = 'コピーしました!';
          setTimeout(() => {
            this.innerHTML = originalText;
          }, 2000);
        })
        .catch(err => console.error('コピーに失敗しました:', err));
    };
  }
  
  // 使用回数カウンター更新
  function updateUsageCounter() {
    fetch('/wp-admin/admin-ajax.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: 'action=politician_counter'
    })
    .then(response => response.text())
    .then(count => {
      document.getElementById('total-count').textContent = count;
    })
    .catch(error => {
      console.error('カウンター更新エラー:', error);
    });
  }
  
  // ページ読み込み時のカウント表示
  function loadCurrentCount() {
    fetch('/wp-admin/admin-ajax.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: 'action=politician_get_count'
    })
    .then(response => response.text())
    .then(count => {
      document.getElementById('total-count').textContent = count;
    })
    .catch(error => {
      console.error('カウント取得エラー:', error);
      document.getElementById('total-count').textContent = '0';
    });
  }
  
  // 初期化
  loadCurrentCount();
});

3. バックエンド実装

メインのプロキシ関数

functions.php
/**
 * 政治家言い訳生成プロキシ関数
 * Gemini APIを呼び出してクライアントに結果を返す
 */
function politician_excuse_proxy() {
    // 1. 入力検証
    if (!isset($_POST['situation']) || empty(trim($_POST['situation']))) {
        wp_send_json_error([
            'message' => '言い訳が必要な状況を入力してください。'
        ]);
        return;
    }
    
    $situation = sanitize_text_field(trim($_POST['situation']));
    $style = sanitize_text_field($_POST['style'] ?? 'apologetic');
    $politician_type = sanitize_text_field($_POST['politician_type'] ?? 'young');
    
    // 文字数制限(1000文字)
    if (mb_strlen($situation) > 1000) {
        wp_send_json_error([
            'message' => '状況が長すぎます。1000文字以内で入力してください。'
        ]);
        return;
    }
    
    // 2. APIキー確認
    if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
        wp_send_json_error([
            'message' => 'APIキーが設定されていません。管理者にお問い合わせください。'
        ]);
        return;
    }
    
    // 3. プロンプト構築
    $prompt = build_politician_prompt($situation, $style, $politician_type);
    
    // 4. API呼び出し(フォールバック機能付き)
    $excuse = call_gemini_api_with_fallback($prompt);
    
    if (!$excuse) {
        wp_send_json_error([
            'message' => '全てのAIモデルが利用できません。しばらく後でお試しください。'
        ]);
        return;
    }
    
    // 5. 成功レスポンス
    wp_send_json_success([
        'excuse_text' => $excuse,
        'original_situation' => $situation,
        'style' => $style,
        'politician_type' => $politician_type,
        'timestamp' => current_time('timestamp')
    ]);
}

プロンプト構築関数

functions.php
function build_politician_prompt($situation, $style, $politician_type) {
    // スタイル別とタイプ別の設定を組み合わせて
    // 政治家らしい言い訳を生成するプロンプトを構築
    // 詳細なプロンプト技術は企業機密のため省略
    
    return $final_prompt;
}

フォールバック機能付きAPI呼び出し

functions.php
/**
 * 複数モデル対応のGemini API呼び出し
 * 使用量制限対策として複数モデルを順次試行
 */
function call_gemini_api_with_fallback($prompt) {
    $models = [
        'gemini-2.0-flash-lite',    // 最も軽量で制限が緩い
        'gemini-2.0-flash',         // バックアップ
        'gemini-1.5-flash'          // 最終手段
    ];
    
    $request_body = [
        'contents' => [
            [
                'parts' => [
                    ['text' => $prompt]
                ]
            ]
        ],
        'generationConfig' => [
            'temperature' => 0.8,
            'topK' => 40,
            'topP' => 0.95,
            'maxOutputTokens' => 800,
        ]
    ];

    foreach ($models as $model) {
        $api_url = "https://generativelanguage.googleapis.com/v1/models/{$model}:generateContent?key=" . GEMINI_API_KEY;
        
        $response = wp_remote_post($api_url, [
            'headers' => [
                'Content-Type' => 'application/json',
            ],
            'body' => json_encode($request_body),
            'timeout' => 30,
            'sslverify' => true,
        ]);
        
        // エラーでない かつ HTTP 200 かつ 正常なレスポンス構造
        if (!is_wp_error($response) && 
            wp_remote_retrieve_response_code($response) === 200) {
            
            $body = wp_remote_retrieve_body($response);
            $data = json_decode($body, true);
            
            if ($data && isset($data['candidates'][0]['content']['parts'][0]['text'])) {
                error_log("✅ Successfully used model: {$model}");
                return trim($data['candidates'][0]['content']['parts'][0]['text']);
            }
        }
        
        error_log("❌ Model {$model} failed, trying next...");
    }

    return false; // 全モデル失敗
}

カウンター機能の実装

functions.php
/**
 * 診断ツールカウンターシステム
 * wp_optionsテーブルを使用して軽量かつ安全に管理
 */
class DiagnosisCounterSystem {
    
    private static $tools = [
        'politician_excuse' => [
            'name' => '政治家言い訳メーカー',
            'option_key' => 'politician_excuse_usage_count'
        ]
    ];
    
    /**
     * カウンターを1増加させる
     */
    public static function increment_counter($tool_key) {
        if (!isset(self::$tools[$tool_key])) {
            wp_die('無効なツールです');
        }
        
        $option_key = self::$tools[$tool_key]['option_key'];
        $current_count = get_option($option_key, 0);
        $new_count = intval($current_count) + 1;
        update_option($option_key, $new_count);
        
        wp_die($new_count);
    }
    
    /**
     * 現在のカウンターを取得
     */
    public static function get_counter($tool_key) {
        if (!isset(self::$tools[$tool_key])) {
            wp_die('0');
        }
        
        $option_key = self::$tools[$tool_key]['option_key'];
        $count = get_option($option_key, 0);
        wp_die($count);
    }
}

// カウンター関連のAJAXアクション
function politician_excuse_counter_increment() {
    DiagnosisCounterSystem::increment_counter('politician_excuse');
}

function politician_excuse_counter_get() {
    DiagnosisCounterSystem::get_counter('politician_excuse');
}

add_action('wp_ajax_politician_counter', 'politician_excuse_counter_increment');
add_action('wp_ajax_nopriv_politician_counter', 'politician_excuse_counter_increment');
add_action('wp_ajax_politician_get_count', 'politician_excuse_counter_get');
add_action('wp_ajax_nopriv_politician_get_count', 'politician_excuse_counter_get');

工夫したポイント・苦労した点

1. APIキーのセキュリティ管理

問題: フロントエンドからGemini APIを直接呼び出すとAPIキーが丸見えになり、悪用されるリスクがある

解決策: WordPressのサーバーサイドプロキシ機能を実装

// wp-config.phpでAPIキーを安全に管理
define('GEMINI_API_KEY', 'your_api_key_here');

// functions.phpでプロキシ関数を実装
function politician_excuse_proxy() {
    // APIキーの存在確認
    if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
        wp_send_json_error(['message' => 'APIキーが設定されていません。']);
        return;
    }
    
    // サーバーサイドでGemini APIを呼び出し
    $api_url = "https://generativelanguage.googleapis.com/v1/models/{$model}:generateContent?key=" . GEMINI_API_KEY;
}

この方法により、APIキーを完全に秘匿化できました。

2. API使用量制限への対策

問題: Google Gemini APIには無料枠での使用回数制限があり、1つのモデルに集中するとすぐに制限に達する

解決策: 複数のGeminiモデルを順次試行するフォールバック機能を実装

function call_gemini_api_with_fallback($prompt) {
    // 使用制限を分散させるため複数モデルを準備
    $models = [
        'gemini-2.0-flash-lite',    // 最も軽量で制限が緩い
        'gemini-2.0-flash',         // バックアップ
        'gemini-1.5-flash'          // 最終手段
    ];
    
    foreach ($models as $model) {
        $response = wp_remote_post($api_url, $request_params);
        
        if (api_call_successful($response)) {
            return extract_response($response);
        }
        
        // 失敗したら次のモデルを試行
        error_log("Model {$model} failed, trying next...");
    }
    
    return false; // 全モデル失敗
}

これにより、1つのモデルが制限に達しても他のモデルでサービス継続が可能になりました。

3. エラーハンドリングの充実

API呼び出しの失敗パターンを網羅的にハンドリングする必要がありました。

// 入力値検証
if (mb_strlen($situation) > 1000) {
    wp_send_json_error(['message' => '状況が長すぎます。1000文字以内で入力してください。']);
    return;
}

// API応答の検証
if (!$data || !isset($data['candidates'][0]['content']['parts'][0]['text'])) {
    wp_send_json_error(['message' => 'APIからの応答が不正です。しばらく後でお試しください。']);
    return;
}

4. ユーザビリティの向上

  • ローディング画面: API呼び出し中の待ち時間を可視化
  • 例文ボタン: 使い方を直感的に理解できる仕組み
  • レスポンシブ対応: モバイル・デスクトップ両対応
  • SNSシェア機能: 生成結果の簡単共有

5. 軽量なカウンター機能

WordPress標準のwp_optionsテーブルを活用した使用回数カウンター

class DiagnosisCounterSystem {
    public static function increment_counter($tool_key) {
        $option_key = self::$tools[$tool_key]['option_key'];
        $current_count = get_option($option_key, 0);
        $new_count = intval($current_count) + 1;
        update_option($option_key, $new_count);
        return $new_count;
    }
}

パフォーマンス最適化

API呼び出しの最適化

  • タイムアウト設定: 30秒で適切なバランス
  • リクエストサイズの最小化: 不要なパラメータの除去
  • レスポンスの効率的な処理: 必要な部分のみ抽出

フロントエンドの軽量化

  • Vanilla JavaScriptの採用: ライブラリ依存なしで高速化
  • CSS最適化: 不要なスタイルの削除
  • 画像最適化: 適切なフォーマットとサイズ

今後の発展可能性

機能拡張案

  • 言い訳の評価機能: トートロジー度などの採点
  • 履歴機能: 過去の言い訳の保存・参照
  • カスタマイズ機能: ユーザー独自の政治家タイプ追加
  • 音声読み上げ: Web Speech APIを活用

技術的改善案

  • キャッシュ機能: 同一入力に対する高速レスポンス
  • A/Bテスト: プロンプト最適化のための実験機能
  • 分析機能: 使用パターンの詳細分析

まとめ

AI技術を「無駄遣い」したエンターテイメントサービスでしたが、実装過程で多くの技術的学びがありました。

技術的収穫

  • Google Gemini APIの実践的活用方法: 複数モデルの使い分けと制限対策
  • WordPressでのAJAXプロキシパターン: セキュアなAPI呼び出し設計
  • API使用量制限を考慮したフォールバック設計: サービス安定性の確保
  • 軽量なデータ管理手法: wp_optionsテーブルの効果的活用

開発での気づき

  • セキュリティ: APIキー管理の重要性を実感
  • ユーザビリティ: 待ち時間の可視化がユーザー体験に大きく影響
  • エラーハンドリング: 外部API依存サービスでの堅牢性確保の難しさ

このようなユニークなサービス開発を通じて、技術力向上とユーザー体験設計の両方を学ぶことができました。

皆さんも、AIを「真面目に無駄遣い」して、面白いサービスを作ってみてはいかがでしょうか。技術的な学びと楽しさの両方を得られるはずです。

注意事項・免責事項

利用上の注意

  • 生成された文章を公的な場面で使用することは推奨しません
  • SNSシェア時は上記の趣旨をご理解の上でお楽しみください
  • APIの仕様変更により予告なく動作が停止する場合があります

参考リンク

Discussion