1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Gemini API×WordPress】モモンガ構文メーカーを作った:プロンプトで消えない一人称を正規表現で消す【PHP】

1
Last updated at Posted at 2026-10-26

はじめに

入力した文章を、『ちいかわ』のモモンガ風の「わがままで、あざとい」口調に変換する モモンガ構文メーカー を作りました(非公式のファンツールです)。

WordPressの固定ページにフロントを置き、admin-ajax.php 経由でサーバー側から Gemini API を呼ぶ、よくある構成です。この記事では、その実装に加えて、プロンプトだけではどうしても消えなかった出力を、正規表現の後処理で消した話 と、1日10回の利用制限にかからずにプロンプトを調整するためのローカルテスト をまとめます。

本記事について

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

この記事で分かること

  • WordPressの admin-ajax.php で Gemini API を安全に呼ぶプロキシの書き方
  • 複数モデルのフォールバックと、合計時間の上限での打ち切り
  • 「口調の型」を指示するプロンプトの組み立て方
  • プロンプトで禁止しても出てくる語を、決定的な後処理で消す方法
  • 本番のプロンプトをそのまま読んで回す、Pythonの10文テスト

この記事の対象者

  • 生成AIで文章の言い回しを変えるツールを作りたい方
  • WordPressにAPIキーを隠したままAI機能を載せたい方
  • プロンプトの調整を、毎回同じ入力で比べながら進めたい方

動作環境・使用技術

  • 開発環境: Windows 11
  • 言語: PHP(WordPressの functions.php), JavaScript, Python(テスト用)
  • CMS: WordPress(固定ページにカスタムHTMLとして配置)
  • AI: Gemini API(gemini-3.1-flash-lite を第1候補に、5モデルでフォールバック)
  • 開発AI: Claude Code

完成品・デモ

例えば次の文章を入れると、

プリン買ってきて

こんな一文が返ってきます(生成のたびに変わります)。

プリン買ってこいッ!!いちばん大きいやつだぞッ!!早くしろッ!!

技術構成

固定ページ(カスタムHTML)
  └ fetch('/wp-admin/admin-ajax.php', action=momonga_generate)
      ↓
functions.php
  ├ check_maker_rate_limit()     … IPごとに1日10回(16:00区切り)
  ├ momonga_generate_proxy()     … プロンプト組み立て → Gemini を順に試す
  ├ format_momonga_text()        … 後処理(ラベル除去・一人称の置き換え)
  └ increment_maker_rate_limit() … 成功したときだけ +1

APIキーは wp-config.php に GEMINI_API_KEY_MOMONGA として定義し、ブラウザには一切出しません。利用制限やカウンターは、既存の構文メーカーと共通の関数を使っています。

実装手順

1. AJAXの受け口を用意する

ログインしていない訪問者からも呼べるように、wp_ajax_ と wp_ajax_nopriv_ の両方に登録します。

add_action('wp_ajax_momonga_generate', 'momonga_generate_proxy');
add_action('wp_ajax_nopriv_momonga_generate', 'momonga_generate_proxy');

受け取った文章は、空チェック・サニタイズ・長さチェックの順で通します。

function momonga_generate_proxy() {
    $user_ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
    check_maker_rate_limit('momonga', $user_ip);

    if (!isset($_POST['text']) || empty(trim($_POST['text']))) {
        wp_send_json_error(['message' => 'モモンガ構文にしたい文章を入力してください。']);
        return;
    }
    $input_text = sanitize_textarea_field(trim($_POST['text']));
    if (mb_strlen($input_text) > 1000) {
        wp_send_json_error(['message' => '文章が長すぎます。1000文字以内で入力してください。']);
        return;
    }
    // ……
}

キーは専用のものを優先し、なければ共通のキーに落とします。

if (defined('GEMINI_API_KEY_MOMONGA') && !empty(GEMINI_API_KEY_MOMONGA)) {
    $momonga_api_key = GEMINI_API_KEY_MOMONGA;
} elseif (defined('GEMINI_API_KEY') && !empty(GEMINI_API_KEY)) {
    $momonga_api_key = GEMINI_API_KEY;
}

2. 複数モデルを順に試し、合計時間で打ち切る

無料枠のモデルは混み合うと失敗することがあるので、モデルの一覧を上から順に試します。ただ、1モデル10秒で5モデル試すと最悪50秒待たせてしまうため、合計45秒の上限が近づいたら次のモデルを試さずに打ち切る ようにしています。

$gemini_started = microtime(true);
foreach (GEMINI_FALLBACK_MODELS as $model) {
    $remaining = GEMINI_TIMEOUT_TOTAL - (microtime(true) - $gemini_started);
    if ($remaining < 3) {
        break; // 残り3秒未満なら諦める
    }
    $response = wp_remote_post($api_url, [
        'headers' => ['Content-Type' => 'application/json'],
        'body'    => json_encode($request_body),
        'timeout' => min(GEMINI_TIMEOUT_PER_MODEL, $remaining),
    ]);
    // 200 かつ candidates[0] に text があれば採用して break
}

生成の設定は temperature: 0.9 / maxOutputTokens: 300 です。言い回しに毎回ばらつきが欲しいので、温度は高めにしています。

3. プロンプトは「核」「要素」「意味を守る」「安全」に分ける

プロンプトは次の5つのブロックで組み立てました。

モモンガ構文らしさの核:      … 主役はいつも自分/かわいさを武器にする/照れずに言い切る
使う要素(毎回2〜3個だけ):   … 命令形+「ッ!!」/語尾を伸ばす/かわいさで押す/ほめろ・かまえ
文体・トーン:                 … 2〜4文/絵文字は使わない/一人称を使わない/ツンデレの言い回しを使わない
意味を守るルール:             … 日時・数字は変えない/報告を依頼に作り変えない
安全ルール(厳守):           … 原作の台詞・固有名詞を出さない/キャラを貶めない/暴言を使わない

ポイントは 「使う要素は毎回2〜3個だけ」 と書いたことです。要素を並べるだけだと、AIは毎回すべての要素を盛り込み、どの出力も同じ型になります。

もう一つは、禁止には必ず「×→○」の例を付ける ことです。

- 一人称は絶対に使わない。(×ボクをほめろッ!! → ○ほめろッ!!)
- ツンデレの言い回しは1回も使わない(×おめでとうなんだからねッ!! → ○おめでとうだぞッ!!)

4. 消えない語は後処理で消す

それでも、一人称の「ボク」だけは10本に2本ほど出続けました。そこで、プロンプトで消すのを諦めて、Geminiの出力を返す前に正規表現で置き換える ことにしました。

function format_momonga_text($text) {
    $text = trim($text);
    $text = preg_replace('/```[a-zA-Z]*|```/', '', $text);
    $text = preg_replace('/^(出力|生成結果|モモンガ構文)[::]\s*/u', '', $text);
    // プロンプトで禁止しても一人称が混ざるため、最後に「こっち」へ置き換える
    $text = preg_replace('/(この)?(ボク|ぼく|僕|オレ|俺)(様|さま)?/u', 'こっち', $text);
    $text = preg_replace('/(この)?モモンガ(様|さま)/u', 'こっち', $text);
    return trim($text);
}

削除ではなく 「こっち」に置き換える のがコツです。「ボクをほめろッ!!」の「ボク」を消すだけだと「をほめろッ!!」になってしまいますが、「こっち」なら「こっちをほめろッ!!」と文が崩れません。「このボク様」のような自称もまとめて拾えるよう、前後の「この」「様」も含めています。

/u 修飾子を付けないと、マルチバイト文字を正しく扱えないので注意してください。

テスト・デバッグ

本番のプロンプトをそのまま読む10文テスト

本番の画面で試すと、1日10回の利用制限にすぐかかります。そこで、functions.php からプロンプトを正規表現で抜き出し、同じ設定で Gemini に投げる Python のテストを作りました。プロンプトを2か所で管理しないので、テストと本番がずれません。

src = open(PHP, encoding='utf-8').read()
body = src[src.index('function momonga_generate_proxy'):]
tmpl = re.search(r"\$prompt = sprintf\(\s*'(.*?)',\s*\$input_text", body, re.S).group(1)

for t in inputs:  # 例ボタンの7文+3文
    req = {"contents": [{"parts": [{"text": tmpl.replace('%s', t, 1)}]}],
           "generationConfig": {"temperature": 0.9, "topK": 40, "topP": 0.95, "maxOutputTokens": 300}}
    r = urllib.request.Request(
        f"https://generativelanguage.googleapis.com/v1/models/{MODEL}:generateContent",
        data=json.dumps(req).encode(),
        headers={"Content-Type": "application/json", "x-goog-api-key": key})

APIキーはURLに入れず、x-goog-api-key ヘッダーで渡しています。ログや画面にキーが残らないようにするためです。

出力ごとに、機械的に判定できるものだけ旗を立てます。

flags = []
if emoji_count(o): flags.append(f"絵文字{emoji_count(o)}")
hit = [n for n in NAMES if n in o]          # 一人称・自称・ツンデレ・キャラ名
if hit: flags.append("名前:" + "/".join(hit))
if not (30 <= len(o) <= 100): flags.append(f"長さ{len(o)}")
if 'ッ!!' not in o: flags.append("ッ!!なし")

「らしいかどうか」は人の目で見るしかありませんが、「絵文字が入った」「一人称が出た」は機械で数えられます。数えられるものを旗にしておくと、目視は旗の立った出力と「らしさ」だけに集中できます。

4回の調整で何が変わったか

同じ10文を、プロンプトを直すたびに回した結果です。

回 主な変更 「こんなにかわいい」 ツンデレ 一人称・自称 「ほめろ」 平均文字数
1 初版(要素を「2つ以上」、各要素に言い回しの例つき) 10/10 8/10 3/10 0/10 90字
2 「らしさの核」を追加・照れ隠しの要素を外す・要素は2〜3個だけ 0/10 0/10 9/10 8/10 55字
3 一人称・自称・ツンデレの禁止を追加 0/10 1/10 2/10 4/10 53字
4 禁止に×→○の例・要求の中身を入力に合わせて変える 3/10 0/10 2/10 3/10 52字

1回目の原因は、プロンプトに書いた例そのものでした。要素の説明に「こんなにかわいいんだから〜」「…べつに、さみしくなんかないんだぞッ」と例を添えたところ、Geminiはそれを10本中10本・8本でほぼそのまま使いました。プロンプトの例は「参考」ではなく「お手本」として写される と考えたほうがよさそうです。

2回目で例を外すと、その2つは消えましたが、今度は「ボク」が9本、「ほめろ」が8本に出ました。1つの型を消すと、別の型に偏りが移る、という動きです。3回目に禁止を足すと「ボク」は減ったものの、「このモモンガ様」「ぼく様」と別の形の自称が出てきました。4回目でも残った2本の一人称は、前述の後処理で消しています。

1回目で目立ったのは、お知らせの文を依頼に作り変えてしまうことでした。「会議の時間が15時に変わりました」が「15時に変えてよォ〜」になっていたため、「報告やお知らせの文は、依頼に作り変えない」を足しています。4回目では「会議は15時に変更だぞッ!!」と、事実を残したまま言い切る形になりました。

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

  • 成功したときだけ回数を減らす:Gemini が混んで失敗したときに利用回数を消費しないよう、increment_maker_rate_limit() は応答を整形したあとに呼んでいます。
  • WindowsのcurlでUTF-8が化ける:本番のAJAXをWindowsのcurlで叩いたところ、日本語の本文が化けて空の入力として届き、Geminiが入力なしで文章を作ってしまいました。本番の確認はPythonの urllib に切り替えています。
  • 絵文字を使わない:同じサイトのおじさん構文メーカーとの見分けがつくよう、絵文字は禁止し、「ッ!!」「ー」「〜」の記号だけで口調を出しています。

まとめ

  • 口調の変換は、要素を並べるより「毎回2〜3個だけ選ぶ」と書くほうが型の繰り返しが減る
  • 禁止の指示には「×→○」の例を付ける
  • それでも消えない語は、プロンプトで粘らず後処理で決定的に消す(削除ではなく置き換え)
  • 本番のプロンプトをそのまま読むテストで、同じ入力を比べながら調整する

プロンプトは「お願い」で、後処理は「保証」です。両方を組み合わせると、調整がぐっと楽になりました。

サービスはこちら

免責事項

モモンガ構文メーカーは、『ちいかわ』の原作者・公式とは一切関係のない非公式のファンツールです。キャラクターの画像・原作の台詞は使用していません。変換結果はAIが生成したもので、特定の人をからかったり傷つけたりする目的での利用はご遠慮ください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?