各種AIとAjaxでブログ記事自動作成アプリ開発

# 概要
音声データを文字起こししたテキストデータを元に、AIに特定のプロンプト群を入力する事でブログ記事作成を行なっているが、各出力に対して逐次的に入力するプロンプトはワンパターンなので、これを自動化してWordPress投稿するところまでワンストップで実現可能な社内用WEBアプリを作ってしまおう！となりました

# アーキテクチャ構成
このWEBアプリは以下の主要コンポーネントで構成されています：
- フロントエンド: HTML/CSS/JavaScript（バニラJS）
- バックエンド: PHP 7.4以上
- 外部API連携: OpenAI GPT-4o, Anthropic Claude 4 Sonnet, Google Gemini 2.0 Flash, ElevenLabs Speech-to-Text
- CMS連携: WordPress REST API
- 認証: JWT（JSON Web Token）
- HTTPクライアント: Guzzle HTTP

# 構成
root/
　├ .htaccess
　├ api/
　│　├ api-chatgpt.php
　│　├ api-claude.php
　│　├ api-elevenlabs.php
　│　├ api-gemini.php
　│　├ api-wp.php
　│　├ prompt-processer.php
　│　└ prompts.php
　├ config/
　├ index.php
　├ logo.png
　├ script.php
　└ style.css

 ※PHP実行環境は事前にインストール済み

# 処理フロー

1. JWT認証によるユーザー認証
2. 音声ファイルのアップロードと文字起こし（ElevenLabs API）
3. 複数段階のプロンプト処理による記事生成（AI APIs）
4. WordPress REST APIを通じた記事投稿

# 全体像

![スクリーンショット 2025-05-18 20.49.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472245/b7270652-b1d9-4b53-a213-d1382b248cbe.png)

```php:index.php
 <?php

// セッションの開始（トークンの保存に使用）
session_start();

require __DIR__ . '/config/vendor/autoload.php';

// 入力値用変数初期化
$message = '';
$username = '';
$password = '';

// ログアウト処理
if (isset($_GET['logout']) && $_GET['logout'] == 'true') {
	session_unset(); //セッション変数を全解除
	session_destroy(); //セッションを破棄
    
	// セッションクッキー削除
	if (ini_get("session.use_cookies")) {
		$params = session_get_cookie_params();
		setcookie(session_name(), '', time() - 42000,
			$params["path"], $params["domain"],
			$params["secure"], $params["httponly"]
		);
	}

	// ログアウト後画面
	header('Location: ' .strtok($_SERVER['REQUEST_URI'], '?')); //パラメータを削除したURLにリダイレクト
}

// フォームが送信された場合
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
	$username = $_POST['username'];
	$password = $_POST['password'];

	try {
		// Guzzleクライアント初期化
		$client = new GuzzleHttp\Client([
			'base_uri' => 'https://philipptarohiltl.com/wp-json/', //WordPressのAPIエンドポイント
			'timeout'  => 10.0,
		]);

		// JWTトークン取得
		$response = $client->post('jwt-auth/v1/token', [
			'form_params' => [
				'username' => $username,
				'password' => $password,
			]
		]);

		// レスポンスボディ
		$data = json_decode($response->getBody(), true);

		// トークン有無確認
		if (isset($data['token'])) {
			$token = $data['token'];
			$_SESSION['jwt_token'] = $token; //セッションに保存
		} else {
			$message = "トークンが取得出来ませんでした（システム管理者にこの画面スクショを送って下さい）";
		}
	} catch (GuzzleHttp\Exception\RequestException $e) {
		if ($e->hasResponse()) {
			$error = json_decode($e->getResponse()->getBody(), true);
			$message = "ユーザー名 or パスワードが間違っています";
		} else {
			$message = "リクエスト失敗: " .$e->getMessage();
		}
		session_unset(); //セッション変数全解除
	}

	// フォームをリセット
	unset($_POST); //POSTデータ削除
}
?>

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ai.philipptarohiltl.com</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1 class="h1-logo">ai.philipptarohiltl.com</h1>
<div id="app" class="wrapper">

	<!-- JWTトークンがない時 -->
	<?php if (empty($_SESSION['jwt_token'])): ?>

		<!-- ログインフォーム -->
		<form method="POST" class="form-login">
			<h2>ログイン</h2>
			<p class="msg">WordPressのログイン情報を入力して下さい</p>
			<?php if (!empty($message)): ?>
				<p class="err-msg"><?php echo htmlspecialchars($message); ?></p>
			<?php endif; ?>
			<label for="username">メールアドレス/ユーザー名：</label>
			<input type="text" id="username" name="username" value="" required>
			<label for="password">パスワード：</label>
			<input type="password" id="password" name="password" value="" required>
			<button type="submit">ログイン</button>
		</form>

	<!-- ログイン済みの場合 -->
	<?php else: ?>
		<a href="?logout=true" class="btn-logout">ログアウト</a>
		<!-- ローディング表示 -->
		<div id="loader-wrapper" class="loader-wrapper is-hidden">
			<div class="loader-circle"></div>
			<div id="loader-progress" class="loader-progress">0%</div>
		</div>
		<!-- テキスト入力欄 -->
		<div class="container-input">
			<form id="form" class="form-transcription" method="POST" action="./index.php">
				<div id="ui" class="ui is-hidden">
					<span class="ui-icon-wrapper"><span class="ui-icon"></span></span>
					<div class="ui-input-wrapper">
						<p><input id="tx-search" type="text" placeholder="検索テキスト" />検出：<span class="ui-counter">0</span></p>
						<p class="is-hidden"><input id="tx-replace" type="text" placeholder="置換テキスト" /><input id="btn-replace" type="button" value="一括置換" /></p>
				</div>
				</div>
				<label for="form-file" id="btn-upload" class="btn-upload">音声ファイルを選択して文字起こし</label>
				<input id="form-file" type="file" accept="audio/*" />
				<textarea id="form-tx" class="textarea" placeholder="※もしくは、ここに音声ファイルをドラッグ＆ドロップ or 文字起こしの内容をコピペして下さい" required></textarea>
				<div class="form-footer">
					<div id="select-ai" class="form-select-ai is-hidden">
						使用するAI：
						<input type="radio" id="gemini" name="ai-model" checked><label for="gemini">Gemini</label>
						<input type="radio" id="claude" name="ai-model"><label for="claude">Claude</label>
						<input type="radio" id="chatgpt" name="ai-model"><label for="chatgpt">ChatGPT</label>
					</div>
					<input id="btn-submit" type="submit" name="submit" value="ブログ記事生成" disabled />
				</div>
			</form>
		</div>
		<div class="container-output">
			<div id="response-container" class="response-container" contenteditable="true">
				<!-- レスポンス -->
			</div>
			<p id="btn-wp-post" class="btn-wp-post is-hidden">記事投稿</p>
		</div>
	<?php endif; ?>
</div>
<!-- マークダウンテキスト表示用 -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<?php include 'script.php'; ?>
</body>
</html>
```

# 詳細技術仕様
### 認証システム（JWT）
WordPressのJWT認証プラグインで認証成功時にトークンをセッションに保存し、以降のAPIリクエストで使用します

### 音声処理システム
ElevenLabs Speech-to-Text API連携
```php
<?php
$response = $client->post('https://api.elevenlabs.io/v1/speech-to-text', [
    'headers' => [
        'Accept' => 'application/json',
        'xi-api-key' => $api_key
    ],
    'multipart' => [
        [
            'name' => 'model_id',
            'contents' => 'scribe_v1'
        ],
        [
            'name' => 'file',
            'contents' => fopen($audio_file['tmp_name'], 'r'),
            'filename' => $audio_file['name'],
            'headers' => ['Content-Type' => 'audio/mpeg']
        ]
    ]
]);
```

音声ファイルはマルチパート形式でElevenLabs APIに送信され、scribe_v1モデルを使用して高精度な文字起こしを実行（進捗表示機能によりユーザーエクスペリエンスを向上）

### マルチAI対応システム
```php
<?php
// プロンプト処理エンジン
function get_prompt($index, $transcription = '', $toc_item = '') {
    global $prompts;
    $prompt = $prompts[$index];
    
    if (isset($transcription) && $transcription !== "") {
        $prompt = str_replace("{TRANSCRIPTION}", $transcription, $prompt);
    }
    
    if (isset($toc_item) && $toc_item !== "") {
        $prompt = str_replace("{TOC_ITEM}", $toc_item, $prompt);
    }
    
    return $prompt;
}
```
動的プロンプト生成により、文字起こしデータや目次項目を適切に置換し各AIモデルに最適化された形式でリクエストを送信

### 各AI APIの実装パターン
```php
<?php
// OpenAI GPT-4o
$response = $client->post('/v1/chat/completions', [
    'json' => [
        'model' => 'gpt-4o',
        'max_tokens' => 1000,
        'messages' => [
            ['role' => 'system', 'content' => $system_prompt],
            ['role' => 'user', 'content' => $user_prompt]
        ]
    ]
]);
```
```php
<?php
// Anthropic Claude
$response = $client->post('/v1/messages', [
    'json' => [
        'model' => 'claude-sonnet-4-20250514',
        'max_tokens' => 1500,
        'messages' => [
            ['role' => 'user', 'content' => [['type' => 'text', 'text' => $prompt]]]
        ],
        'system' => $system_prompt
    ]
]);
```
```php:
<?php
// Google Gemini
php$response = $client->post('/v1/models/gemini-2.0-flash:generateContent?key=' . $api_key, [
    'json' => [
        'contents' => [
            ['role' => 'user', 'parts' => [['text' => $prompt]]]
        ],
        'generationConfig' => [
            'maxOutputTokens' => 1024,
            'temperature' => 0.5
        ]
    ]
]);
```
各AIモデルのAPI仕様に合わせた統一的なインターフェースを提供し、フロントエンドから選択可能な設計にしました

### フロントエンド処理システム
段階的なプロンプト処理により、記事タイトル、目次、各章の内容、要約文を順次生成し、最終的に一つの記事として統合
```javascript:.js
// 非同期記事生成プロセス
async function processStep(index, transcription) {
    const {data, selectedAi} = await apiRequest(index, transcription);
    const aiMessage = handleAiData(data, selectedAi);
    
    if (index === 2) {
        // 目次作成処理
        let tocText = aiMessage.trim()
            .replace(/^```(?:json|javascript)?\s*/i, '')
            .replace(/```$/, '')
            .trim();
        window.tocArray = JSON.parse(tocText);
        return;
    } else if (index === 3) {
        // 目次ベースの繰り返し処理
        for (const [tocIndex, tocItem] of window.tocArray.entries()) {
            const {data, selectedAi} = await apiRequest(3, undefined, tocItem);
            const aiMessage = handleAiData(data, selectedAi);
            markdownOutput(3, data, aiMessage);
        }
        return;
    }
    
    markdownOutput(index, data, aiMessage);
}
```

### 進捗表示とUX改善
リアルタイム進捗表示により、長時間の処理中もユーザーエンゲージメントを維持
```javascript:.js
function updateProgress(current, total) {
    const percentage = ((current / total) * 100).toFixed(1);
    document.getElementById('loader-progress').textContent = `${Math.round(percentage)}%`;
}

function startSimilation(startValue, maxValue, duration) {
    const step = (maxValue - startValue) / (duration / 100);
    progressInterval = setInterval(() => {
        currentProgress += step;
        if (currentProgress >= maxValue) {
            currentProgress = maxValue;
            clearInterval(progressInterval);
        }
        updateProgress(Math.floor(currentProgress), totalSteps);
    }, 100);
}
```

### WordPress統合システム
生成されたHTMLコンテンツをWordPressのGutenbergエディタ形式に変換し、適切なブロック構造で投稿
```php
<?php
// Gutenbergブロック対応
$html_tags = [
    'h1' => 'heading {"level":1}',
    'h2' => 'heading {"level":2}',
    'h3' => 'heading {"level":3}',
    'p' => 'paragraph',
    'ul' => 'list'
];

foreach ($html_tags as $html_tag => $block_name) {
    $content = preg_replace(
        '/<' . $html_tag . '([^>]*)>/i',
        '<!-- wp:' . $block_name . ' --><' . $html_tag . '$1>',
        $content
    );
    
    $content = preg_replace(
        '/<\/' . $html_tag . '>/i',
        '</' . $html_tag . '><!-- /wp:' . explode(' ', $block_name)[0] . ' -->',
        $content
    );
}
```

### REST API投稿処理
```php
<?php
$response = $client->post('posts', [
    'json' => [
        'title' => $title,
        'content' => $content,
        'slug' => $slug,
        'status' => 'draft',
        'meta' => ['meta_description' => $meta_description]
    ]
]);
```
Basic認証（Application Password）を使用し、記事タイトル、本文、スラッグ、メタディスクリプションを含む完全な記事データをWordPressに送信します

### セキュリティ考慮事項
**入力値検証：**
- ファイルタイプの厳密なチェック
- HTMLエスケープ処理の実装
- JSONデータの適切な検証

**APIキー管理：**
- 環境変数による秘匿情報の管理
- .envファイルを使用したローカル開発環境での設定

**セッション管理：**
- 適切なセッション破棄処理
- セッションクッキーの安全な削除

**パフォーマンス最適化：**
- APIレスポンス処理（各AIモデルのレスポンス形式に対応し統一的に処理）
- エラーハンドリング・タイムアウト設定

**フロントエンド最適化：**
- 非同期処理による応答性の向上
- 進捗表示によるUX改善
- DOM操作の最適化

# 拡張性について
- 新しいAIモデルの追加：統一されたAPIインターフェースにより、新しいAIサービスの追加が容易
- プロンプトテンプレートの拡張：prompts.phpの編集により、様々な記事タイプに対応可能
- CMS連携の拡張：WordPress以外のCMSへの対応も可能な設計