政治家言い訳メーカーを作ってみた - Gemini API × WordPress実装ガイド
はじめに
AIの「無駄遣い」をテーマに、政治家風の言い訳を自動生成するWebサービスを開発しました。日常のちょっとした「やらかし」を、政治家のような巧妙な言い回しで切り抜けられる(?)エンターテイメントツールです。
本記事では、Google Gemini APIとWordPressを使った実装方法を詳しく解説します。
開発背景
日々のニュースで見る政治家の絶妙な言い回しや巧みな言い訳。「この技術を、もっと身近に体験できたら面白いのでは?」と考え、最新のAI技術をあえて"無駄遣い"し、ブラックジョークとして楽しめるサービスを作りました。
本記事について
今回のサービス開発および記事執筆は、Claude(Anthropic社のAI)との協業で進めました。AI時代の新しい開発スタイルの実践例として参考になれば幸いです。
自己紹介
ホネグミ代表、応用情報技術者の資格を持つエンジニア×マーケターです。これまでIT系の会社役員を4年、独立して4年目になります。クライアントワークでは「こうしたい」を技術で形にすることを専門としていますが、最近は個人開発でAIを「無駄に」使った社会風刺的なサービスも作っています。
完成品

サービスURL:
主な機能
- 状況入力による言い訳自動生成: テキストエリアに状況を入力
- 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に設定します。
// APIキーを安全に管理
define('GEMINI_API_KEY', 'your_gemini_api_key_here');
functions.phpでAJAXアクション登録
// 政治家言い訳生成の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. バックエンド実装
メインのプロキシ関数
/**
* 政治家言い訳生成プロキシ関数
* 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')
]);
}
プロンプト構築関数
function build_politician_prompt($situation, $style, $politician_type) {
// スタイル別とタイプ別の設定を組み合わせて
// 政治家らしい言い訳を生成するプロンプトを構築
// 詳細なプロンプト技術は企業機密のため省略
return $final_prompt;
}
フォールバック機能付きAPI呼び出し
/**
* 複数モデル対応の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; // 全モデル失敗
}
カウンター機能の実装
/**
* 診断ツールカウンターシステム
* 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