Создание AI-копирайтера на PHP: от идеи до 200+ текстов в день

Технические решения, проблемы интеграции OpenAI API и оптимизация работы с GPT-моделями

В этой статье разберу техническую реализацию веб-сервиса для генерации текстов с использованием OpenAI API. Покажу, с какими проблемами столкнулся при интеграции, как решал вопросы производительности и как построил систему умного выбора AI-моделей.

Почему не просто ChatGPT?

Задача была простая: автоматизировать создание однотипных текстов — описания товаров, посты, email-рассылки. ChatGPT для этого не подходил по нескольким причинам:

  1. Отсутствие API без VPN — каждый запрос через прокси добавлял 2-5 секунд задержки

  2. Нет переиспользования промптов — каждый раз формулировать запрос заново

  3. Нет контроля над выбором модели — переплата за простые задачи

  4. Невозможность встроить в свой workflow — копировать из интерфейса неудобно

Решил сделать обёртку над OpenAI API с системой шаблонов и автоматическим выбором модели.

Техническая архитектура

Стек технологий

Backend: PHP 8.1 + WordPress (как CMS для быстрого старта)
Database: MySQL 5.7
Frontend: Vanilla JS (без фреймворков для простоты)
API: OpenAI GPT-4o, GPT-4o-mini, o1-mini
Hosting: VPS в РФ (для работы без VPN)
Payment: ЮKassa через WooCommerce

Выбор WordPress был осознанным — нужна была готовая система пользователей, подписок и админ-панели. Написать всё с нуля заняло бы месяцы.

Схема работы

[Пользователь] 
    ↓ (выбирает шаблон)
[Frontend: форма с параметрами]
    ↓ (AJAX-запрос)
[PHP: построение промпта]
    ↓ (выбор модели по правилам)
[OpenAI API]
    ↓ (streaming response)
[Frontend: отображение в реальном времени]
    ↓ (JS-анализ SEO)
[Результат + метрики]

Проблема 1: Интеграция с OpenAI API

Первая попытка — cURL

Начал с простого cURL-запроса:

function generate_text_simple($prompt) {
    $api_key = get_option('openai_api_key');
    
    $data = [
        'model' => 'gpt-4o-mini',
        'messages' => [
            ['role' => 'user', 'content' => $prompt]
        ],
        'temperature' => 0.7
    ];
    
    $ch = curl_init('https://api.openai.com/v1/chat/completions');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $api_key
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($http_code !== 200) {
        return ['error' => 'API request failed'];
    }
    
    $result = json_decode($response, true);
    return $result['choices'][0]['message']['content'];
}

Проблемы:

  • Пользователь ждёт 30-60 секунд без обратной связи

  • Таймауты PHP (30 сек по умолчанию)

  • Невозможность отменить запрос

Решение — Server-Sent Events (SSE)

Переписал на streaming для отображения текста по мере генерации:

function generate_text_streaming($prompt, $model) {
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    header('X-Accel-Buffering: no'); // Для nginx
    
    $api_key = get_option('openai_api_key');
    
    $data = [
        'model' => $model,
        'messages' => [
            ['role' => 'system', 'content' => 'Ты профессиональный копирайтер...'],
            ['role' => 'user', 'content' => $prompt]
        ],
        'stream' => true,
        'temperature' => 0.7,
        'max_tokens' => 4000
    ];
    
    $ch = curl_init('https://api.openai.com/v1/chat/completions');
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $api_key
    ]);
    
    // Ключевой момент — функция обработки потока
    curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) {
        $lines = explode("\n", $data);
        
        foreach ($lines as $line) {
            if (strpos($line, 'data: ') === 0) {
                $json = substr($line, 6);
                
                if ($json === '[DONE]') {
                    echo "data: [DONE]\n\n";
                    ob_flush();
                    flush();
                    continue;
                }
                
                $parsed = json_decode($json, true);
                if (isset($parsed['choices'][0]['delta']['content'])) {
                    $content = $parsed['choices'][0]['delta']['content'];
                    echo "data: " . json_encode(['content' => $content]) . "\n\n";
                    ob_flush();
                    flush();
                }
            }
        }
        
        return strlen($data);
    });
    
    curl_exec($ch);
    curl_close($ch);
}

Результат:

  • Текст появляется через 1-2 секунды

  • Пользователь видит прогресс

  • Можно отменить генерацию

Важный момент для nginx:

location /wp-admin/admin-ajax.php {
    fastcgi_buffering off;  # Отключаем буферизацию
    proxy_buffering off;
    # остальная конфигурация
}

Без этого nginx буферизует ответ и streaming не работает.

Проблема 2: Выбор оптимальной модели

GPT-4o стоит в 15 раз дороже GPT-4o-mini, но не всегда нужен. Для "Напиши пост на 200 символов" достаточно mini, а для "Создай SEO-статью на 10000 знаков с анализом конкурентов" нужна мощная модель.

Система умного выбора модели

Создал систему правил на основе параметров задачи:

class ModelSelector {
    
    private $model_costs = [
        'gpt-4o' => 5.00,           // за 1M токенов
        'gpt-4o-mini' => 0.15,      // за 1M токенов  
        'o1-mini' => 3.00           // за 1M токенов
    ];
    
    public function select_model($params) {
        $length = intval($params['length']);
        $complexity = $params['complexity']; // simple, medium, complex
        $content_type = $params['content_type']; // post, article, description
        $user_tier = $params['user_tier']; // free, start, pro, business
        
        // Правило 1: Бесплатный тариф = только mini
        if ($user_tier === 'free') {
            return 'gpt-4o-mini';
        }
        
        // Правило 2: Короткие тексты (< 500 символов) = mini
        if ($length < 500) {
            return 'gpt-4o-mini';
        }
        
        // Правило 3: Сложные задачи = мощная модель
        if ($complexity === 'complex') {
            return ($user_tier === 'business') ? 'gpt-4o' : 'o1-mini';
        }
        
        // Правило 4: SEO-статьи > 3000 символов = o1-mini минимум
        if ($content_type === 'seo_article' && $length > 3000) {
            return ($user_tier === 'business') ? 'gpt-4o' : 'o1-mini';
        }
        
        // Правило 5: Средняя сложность = o1-mini
        if ($complexity === 'medium') {
            return 'o1-mini';
        }
        
        // По умолчанию
        return 'gpt-4o-mini';
    }
    
    public function estimate_cost($prompt_tokens, $completion_tokens, $model) {
        $total_tokens = $prompt_tokens + $completion_tokens;
        $cost_per_million = $this->model_costs[$model];
        return ($total_tokens / 1000000) * $cost_per_million;
    }
}

Метрики эффективности выбора

Собрал статистику за месяц:

Запросов с gpt-4o-mini: 75% (средняя стоимость $0.002)
Запросов с o1-mini: 20% (средняя стоимость $0.015)
Запросов с gpt-4o: 5% (средняя стоимость $0.08)

Средняя стоимость запроса: $0.006
Экономия vs всегда gpt-4o: 92%

Важный инсайт: 75% задач отлично решаются дешёвой моделью. Переплачивать не имеет смысла.

Проблема 3: Система шаблонов

Каждый раз формулировать промпт с нуля — трата времени. Нужна библиотека готовых шаблонов.

Архитектура шаблонов

class TemplateManager {
    
    private $templates = [];
    
    public function __construct() {
        $this->load_templates();
    }
    
    private function load_templates() {
        // Базовый шаблон для описания товара
        $this->templates['product_description'] = [
            'name' => 'Описание товара для маркетплейса',
            'category' => 'ecommerce',
            'complexity' => 'simple',
            'fields' => [
                'product_name' => [
                    'label' => 'Название товара',
                    'type' => 'text',
                    'required' => true,
                    'placeholder' => 'Беспроводные наушники TWS'
                ],
                'features' => [
                    'label' => 'Характеристики',
                    'type' => 'textarea',
                    'required' => true,
                    'placeholder' => 'ANC, 30 часов работы, IPX4'
                ],
                'target_audience' => [
                    'label' => 'Целевая аудитория',
                    'type' => 'text',
                    'required' => false,
                    'placeholder' => 'Спортсмены 25-35 лет'
                ],
                'keywords' => [
                    'label' => 'Ключевые слова (через запятую)',
                    'type' => 'text',
                    'required' => false,
                    'placeholder' => 'беспроводные наушники, TWS, спорт'
                ],
                'marketplace' => [
                    'label' => 'Маркетплейс',
                    'type' => 'select',
                    'options' => ['Wildberries', 'Ozon', 'Яндекс.Маркет'],
                    'required' => true
                ]
            ],
            'system_prompt' => 'Ты эксперт по написанию продающих описаний для маркетплейсов. Твоя задача — создать привлекательное и SEO-оптимизированное описание товара.',
            'prompt_template' => 'Создай описание для товара "{product_name}" на маркетплейсе {marketplace}.

Характеристики: {features}
Целевая аудитория: {target_audience}
Ключевые слова для SEO: {keywords}

Требования:
- Длина 800-1200 символов
- Структура: заголовок, описание преимуществ, характеристики, призыв к действию
- Естественное вхождение ключевых слов (плотность 2-3%)
- Учитывай особенности маркетплейса {marketplace}'
        ];
        
        // SEO-статья
        $this->templates['seo_article'] = [
            'name' => 'SEO-статья',
            'category' => 'content',
            'complexity' => 'complex',
            'fields' => [
                'topic' => [
                    'label' => 'Тема статьи',
                    'type' => 'text',
                    'required' => true
                ],
                'keywords' => [
                    'label' => 'Ключевые запросы',
                    'type' => 'textarea',
                    'required' => true,
                    'placeholder' => 'Один ключ на строку'
                ],
                'length' => [
                    'label' => 'Объём (символов)',
                    'type' => 'select',
                    'options' => ['3000', '5000', '7000', '10000'],
                    'required' => true
                ],
                'tone' => [
                    'label' => 'Тон повествования',
                    'type' => 'select',
                    'options' => ['Деловой', 'Разговорный', 'Экспертный', 'Продающий'],
                    'required' => true
                ]
            ],
            'system_prompt' => 'Ты SEO-копирайтер с 10-летним опытом. Специализируешься на текстах для Яндекса. Знаешь требования Баден-Баден и пишешь естественные, полезные тексты.',
            'prompt_template' => 'Напиши SEO-статью на тему: "{topic}"

Ключевые запросы (используй естественно):
{keywords}

Объём: {length} символов
Тон: {tone}

Структура:
1. Введение с проблемой читателя (5-7% текста)
2. 3-5 подзаголовков H2 с раскрытием темы
3. Практические советы и примеры
4. Заключение с выводами

Требования:
- Плотность ключей 2-3% (не переспам!)
- Читабельность: индекс Флеша > 50
- Естественный язык без "воды"
- Структурированность: списки, таблицы где уместно
- Экспертность и глубина раскрытия темы'
        ];
        
        // Ещё 20+ шаблонов...
    }
    
    public function build_prompt($template_id, $user_data) {
        $template = $this->templates[$template_id];
        $prompt = $template['prompt_template'];
        
        // Замена плейсхолдеров на данные пользователя
        foreach ($user_data as $key => $value) {
            $prompt = str_replace('{' . $key . '}', $value, $prompt);
        }
        
        return [
            'system' => $template['system_prompt'],
            'user' => $prompt,
            'complexity' => $template['complexity']
        ];
    }
    
    public function get_template_form($template_id) {
        return $this->templates[$template_id]['fields'];
    }
}

Frontend для работы с шаблоном

class TemplateForm {
    constructor(templateId) {
        this.templateId = templateId;
        this.form = document.getElementById('template-form');
        this.loadTemplate();
    }
    
    async loadTemplate() {
        // Загружаем структуру шаблона
        const response = await fetch('/wp-admin/admin-ajax.php', {
            method: 'POST',
            headers: {'Content-Type': 'application/x-www-form-urlencoded'},
            body: new URLSearchParams({
                action: 'get_template_structure',
                template_id: this.templateId
            })
        });
        
        const template = await response.json();
        this.renderForm(template.fields);
    }
    
    renderForm(fields) {
        this.form.innerHTML = '';
        
        for (const [key, field] of Object.entries(fields)) {
            const wrapper = document.createElement('div');
            wrapper.className = 'form-field';
            
            const label = document.createElement('label');
            label.textContent = field.label;
            if (field.required) label.classList.add('required');
            
            let input;
            if (field.type === 'textarea') {
                input = document.createElement('textarea');
                input.rows = 4;
            } else if (field.type === 'select') {
                input = document.createElement('select');
                field.options.forEach(opt => {
                    const option = document.createElement('option');
                    option.value = opt;
                    option.textContent = opt;
                    input.appendChild(option);
                });
            } else {
                input = document.createElement('input');
                input.type = field.type;
            }
            
            input.name = key;
            input.id = `field-${key}`;
            input.placeholder = field.placeholder || '';
            input.required = field.required;
            
            wrapper.appendChild(label);
            wrapper.appendChild(input);
            this.form.appendChild(wrapper);
        }
        
        // Кнопка генерации
        const button = document.createElement('button');
        button.type = 'submit';
        button.textContent = 'Генерировать текст';
        button.className = 'btn-generate';
        this.form.appendChild(button);
        
        this.form.addEventListener('submit', (e) => this.handleSubmit(e));
    }
    
    async handleSubmit(e) {
        e.preventDefault();
        
        const formData = new FormData(this.form);
        const data = Object.fromEntries(formData);
        
        // Streaming генерация
        this.startStreaming(data);
    }
    
    startStreaming(data) {
        const resultDiv = document.getElementById('generated-text');
        resultDiv.innerHTML = '';
        
        const eventSource = new EventSource(
            '/wp-admin/admin-ajax.php?action=generate_text_stream&' + 
            new URLSearchParams({
                template_id: this.templateId,
                ...data
            })
        );
        
        eventSource.onmessage = (event) => {
            if (event.data === '[DONE]') {
                eventSource.close();
                this.analyzeText(resultDiv.textContent);
                return;
            }
            
            const chunk = JSON.parse(event.data);
            resultDiv.textContent += chunk.content;
            
            // Автоскролл
            resultDiv.scrollTop = resultDiv.scrollHeight;
        };
        
        eventSource. => {
            eventSource.close();
            console.error('Streaming error');
        };
    }
    
    analyzeText(text) {
        // SEO-анализ (об этом ниже)
        const analyzer = new SEOAnalyzer();
        const metrics = analyzer.analyze(text);
        this.displayMetrics(metrics);
    }
}

Проблема 4: SEO-анализ текста

Генерация текста — это полдела. Нужно проверить качество с точки зрения SEO.

Реализация SEO-анализатора на JavaScript

class SEOAnalyzer {
    
    analyze(text) {
        return {
            length: this.getLength(text),
            keywords: this.analyzeKeywords(text),
            readability: this.calculateReadability(text),
            water: this.calculateWater(text),
            spam: this.calculateSpam(text)
        };
    }
    
    getLength(text) {
        return {
            characters: text.length,
            charactersNoSpaces: text.replace(/\s/g, '').length,
            words: text.split(/\s+/).filter(w => w.length > 0).length,
            sentences: text.split(/[.!?]+/).filter(s => s.trim().length > 0).length
        };
    }
    
    analyzeKeywords(text, keywords = []) {
        const words = text.toLowerCase().match(/\b[\wа-яё]+\b/gi) || [];
        const totalWords = words.length;
        
        const keywordStats = {};
        
        keywords.forEach(keyword => {
            const keywordLower = keyword.toLowerCase();
            const regex = new RegExp('\\b' + keywordLower + '\\b', 'gi');
            const count = (text.match(regex) || []).length;
            const density = (count / totalWords) * 100;
            
            keywordStats[keyword] = {
                count: count,
                density: density.toFixed(2),
                status: this.getKeywordStatus(density)
            };
        });
        
        return keywordStats;
    }
    
    getKeywordStatus(density) {
        if (density < 1) return 'low'; // Мало
        if (density <= 3) return 'good'; // Оптимально
        if (density <= 5) return 'high'; // Много
        return 'spam'; // Переспам
    }
    
    calculateReadability(text) {
        const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
        const words = text.split(/\s+/).filter(w => w.length > 0);
        const syllables = this.countSyllables(text);
        
        if (sentences.length === 0 || words.length === 0) return 0;
        
        // Индекс Флеша для русского языка
        const ASL = words.length / sentences.length; // Average Sentence Length
        const ASW = syllables / words.length; // Average Syllables per Word
        
        const fleschScore = 206.835 - (1.015 * ASL) - (84.6 * ASW);
        
        return {
            score: Math.max(0, Math.min(100, fleschScore)).toFixed(1),
            level: this.getReadabilityLevel(fleschScore),
            avgWordsPerSentence: ASL.toFixed(1),
            avgSyllablesPerWord: ASW.toFixed(2)
        };
    }
    
    countSyllables(text) {
        // Упрощённый подсчёт слогов для русского языка
        const vowels = 'аеёиоуыэюя';
        let count = 0;
        let prevIsVowel = false;
        
        for (let char of text.toLowerCase()) {
            const isVowel = vowels.includes(char);
            if (isVowel && !prevIsVowel) {
                count++;
            }
            prevIsVowel = isVowel;
        }
        
        return count || 1;
    }
    
    getReadabilityLevel(score) {
        if (score >= 80) return 'Очень лёгкий';
        if (score >= 70) return 'Лёгкий';
        if (score >= 60) return 'Средний';
        if (score >= 50) return 'Сложноватый';
        if (score >= 30) return 'Сложный';
        return 'Очень сложный';
    }
    
    calculateWater(text) {
        // "Водность" — процент стоп-слов
        const stopWords = [
            'а', 'без', 'более', 'бы', 'был', 'была', 'были', 'было', 
            'быть', 'в', 'вам', 'вас', 'весь', 'во', 'вот', 'все', 
            'всё', 'всего', 'всех', 'вы', 'где', 'да', 'даже', 'для',
            'до', 'его', 'ее', 'её', 'ей', 'ему', 'если', 'есть',
            'ещё', 'же', 'за', 'здесь', 'и', 'из', 'или', 'им', 
            'их', 'к', 'как', 'ко', 'когда', 'кто', 'ли', 'либо',
            'мне', 'может', 'мы', 'на', 'надо', 'наш', 'не', 'него',
            'неё', 'нет', 'ни', 'них', 'но', 'ну', 'о', 'об', 'один',
            'он', 'она', 'они', 'оно', 'от', 'очень', 'по', 'под',
            'при', 'с', 'со', 'так', 'также', 'такой', 'там', 'те',
            'тем', 'то', 'того', 'тоже', 'той', 'только', 'том', 'ты',
            'у', 'уже', 'хотя', 'чего', 'чей', 'чем', 'что', 'чтобы',
            'чьё', 'эта', 'эти', 'это', 'я'
        ];
        
        const words = text.toLowerCase().match(/\b[\wа-яё]+\b/gi) || [];
        const stopWordCount = words.filter(w => stopWords.includes(w)).length;
        
        const waterPercent = (stopWordCount / words.length) * 100;
        
        return {
            percent: waterPercent.toFixed(1),
            status: waterPercent < 15 ? 'good' : waterPercent < 30 ? 'medium' : 'high',
            label: waterPercent < 15 ? 'Низкая' : waterPercent < 30 ? 'Средняя' : 'Высокая'
        };
    }
    
    calculateSpam(text) {
        // "Тошнота" — частота самого частого слова
        const words = text.toLowerCase().match(/\b[\wа-яё]{4,}\b/gi) || [];
        const frequency = {};
        
        words.forEach(word => {
            frequency[word] = (frequency[word] || 0) + 1;
        });
        
        const maxFrequency = Math.max(...Object.values(frequency));
        const spamPercent = (maxFrequency / words.length) * 100;
        
        return {
            percent: spamPercent.toFixed(2),
            maxWord: Object.keys(frequency).find(k => frequency[k] === maxFrequency),
            status: spamPercent < 3 ? 'good' : spamPercent < 5 ? 'medium' : 'high'
        };
    }
}

Отображение метрик

function displayMetrics(metrics) {
    const container = document.getElementById('seo-metrics');
    
    container.innerHTML = `
        <div class="metrics-grid">
            <div class="metric">
                <h4>Объём текста</h4>
                <p>${metrics.length.characters} символов</p>
                <p>${metrics.length.words} слов</p>
            </div>
            
            <div class="metric">
                <h4>Читаемость</h4>
                <p class="score">${metrics.readability.score}</p>
                <p>${metrics.readability.level}</p>
            </div>
            
            <div class="metric ${metrics.water.status}">
                <h4>Водность</h4>
                <p>${metrics.water.percent}%</p>
                <p>${metrics.water.label}</p>
            </div>
            
            <div class="metric ${metrics.spam.status}">
                <h4>Тошнота</h4>
                <p>${metrics.spam.percent}%</p>
                <p>Частое слово: ${metrics.spam.maxWord}</p>
            </div>
        </div>
        
        <div class="keywords-analysis">
            <h4>Плотность ключевых слов</h4>
            ${Object.entries(metrics.keywords).map(([key, data]) => `
                <div class="keyword-stat ${data.status}">
                    <span class="keyword">${key}</span>
                    <span class="count">${data.count}x</span>
                    <span class="density">${data.density}%</span>
                </div>
            `).join('')}
        </div>
    `;
}

Проблема 5: Оптимизация затрат на API

За первую неделю работы потратил $25 на API. При 200 запросах в день это $750/месяц — неприемлемо.

Решение 1: Кэширование популярных запросов

class RequestCache {
    
    private $cache_ttl = 86400; // 24 часа
    
    public function get_cache_key($template_id, $params) {
        // Убираем уникальные параметры типа "название компании"
        $cache_params = $params;
        unset($cache_params['company_name']);
        unset($cache_params['product_name']);
        
        return md5($template_id . serialize($cache_params));
    }
    
    public function get($cache_key) {
        global $wpdb;
        
        $cached = $wpdb->get_row($wpdb->prepare(
            "SELECT content, created_at FROM {$wpdb->prefix}gpt_cache 
             WHERE cache_key = %s AND created_at > %s",
            $cache_key,
            date('Y-m-d H:i:s', time() - $this->cache_ttl)
        ));
        
        return $cached ? $cached->content : null;
    }
    
    public function set($cache_key, $content) {
        global $wpdb;
        
        $wpdb->replace(
            $wpdb->prefix . 'gpt_cache',
            [
                'cache_key' => $cache_key,
                'content' => $content,
                'created_at' => current_time('mysql')
            ],
            ['%s', '%s', '%s']
        );
    }
}

Результат: 40% запросов попали в кэш, экономия $300/месяц.

Решение 2: Ограничение max_tokens

function calculate_max_tokens($requested_length, $user_tier) {
    // Запрашиваемая длина в символах → токены
    $base_tokens = ceil($requested_length / 3); // ~3 символа на токен для русского
    
    // Добавляем запас на промпт (обычно 200-500 токенов)
    $prompt_tokens = 500;
    
    // Ограничения по тарифу
    $tier_limits = [
        'free' => 800,
        'start' => 1500,
        'pro' => 4000,
        'business' => 8000
    ];
    
    $max_allowed = $tier_limits[$user_tier];
    $calculated = min($base_tokens + $prompt_tokens, $max_allowed);
    
    return $calculated;
}

Результат: снижение затрат на 30% за счёт отсутствия "лишних" токенов.

Решение 3: Rate limiting

class RateLimiter {
    
    public function check_limit($user_id, $user_tier) {
        $limits = [
            'free' => ['requests' => 2, 'period' => 'day'],
            'start' => ['requests' => 30, 'period' => 'month'],
            'pro' => ['requests' => 100, 'period' => 'month'],
            'business' => ['requests' => 200, 'period' => 'month']
        ];
        
        $limit = $limits[$user_tier];
        $count = $this->get_request_count($user_id, $limit['period']);
        
        if ($count >= $limit['requests']) {
            return [
                'allowed' => false,
                'remaining' => 0,
                'reset_at' => $this->get_reset_time($limit['period'])
            ];
        }
        
        $this->increment_count($user_id);
        
        return [
            'allowed' => true,
            'remaining' => $limit['requests'] - $count - 1,
            'reset_at' => $this->get_reset_time($limit['period'])
        ];
    }
    
    private function get_request_count($user_id, $period) {
        global $wpdb;
        
        $since = ($period === 'day') 
            ? date('Y-m-d 00:00:00')
            : date('Y-m-01 00:00:00');
        
        return $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->prefix}gpt_requests 
             WHERE user_id = %d AND created_at >= %s",
            $user_id, $since
        ));
    }
    
    private function increment_count($user_id) {
        global $wpdb;
        
        $wpdb->insert(
            $wpdb->prefix . 'gpt_requests',
            [
                'user_id' => $user_id,
                'created_at' => current_time('mysql')
            ],
            ['%d', '%s']
        );
    }
    
    private function get_reset_time($period) {
        return ($period === 'day')
            ? strtotime('tomorrow midnight')
            : strtotime('first day of next month midnight');
    }
}

Итоговая архитектура

┌─────────────────────────────────────────────────────────┐
│                    Пользователь                          │
└────────────────────┬────────────────────────────────────┘
                     │
         ┌───────────▼────────────┐
         │   Frontend (JS)        │
         │ - Выбор шаблона        │
         │ - Заполнение формы     │
         │ - SSE для streaming    │
         │ - SEO-анализ           │
         └───────────┬────────────┘
                     │ AJAX
         ┌───────────▼────────────┐
         │   WordPress Backend    │
         │ - TemplateManager      │
         │ - ModelSelector        │
         │ - RateLimiter          │
         │ - RequestCache         │
         └───────────┬────────────┘
                     │
         ┌───────────▼────────────┐
         │    OpenAI API          │
         │ - GPT-4o               │
         │ - GPT-4o-mini          │
         │ - o1-mini              │
         └────────────────────────┘

Результаты и метрики

Производительность

После 3 месяцев работы и 2000+ сгенерированных текстов:

Среднее время генерации:
- Короткий текст (< 500 символов): 8 секунд
- Средний текст (500-2000): 18 секунд  
- Длинный текст (2000-5000): 35 секунд
- SEO-статья (5000+): 60-90 секунд

Надёжность:
- Успешных запросов: 98.7%
- Ошибок API: 0.8%
- Таймаутов: 0.5%

Экономика:
- Средняя стоимость запроса: $0.006
- Затраты на API: $450/месяц (при 2500 запросах)
- Доход от подписок: ~$1800/месяц
- Маржинальность: 75%

Паттерны использования

Самые популярные шаблоны:
1. Описание товара (42%)
2. Пост для соцсетей (28%)
3. Email-письмо (15%)
4. SEO-статья (10%)
5. Остальные (5%)

Распределение по тарифам:
- Free: 60% пользователей, 15% запросов
- Start: 25% пользователей, 30% запросов
- Pro: 12% пользователей, 35% запросов
- Business: 3% пользователей, 20% запросов

Проблемы, которые ещё не решил

  1. Fine-tuning моделей
    Хочу обучить модель на успешных примерах, но OpenAI пока не открыл fine-tuning для GPT-4o.

  2. Мультиязычность
    Сейчас только русский. Расширение на английский/казахский требует новых промптов.

  3. A/B тестирование промптов
    Нет инструментов для сравнения эффективности разных формулировок.

  4. Автоматическая оценка качества
    Сейчас оценка только от пользователей. Хочу автоматический scoring.

Технические выводы

Что сработало:

  • WordPress как основа — быстрый старт

  • SSE для streaming — отличный UX

  • Умный выбор модели — экономия 92%

  • Система шаблонов — основная ценность продукта

  • JS для SEO-анализа — работает быстро

Что не сработало:

  • Попытка использовать GPT для категоризации — слишком дорого

  • Автоматическое определение сложности текста — AI ошибается

  • Кэширование с учётом параметров — ключи коллизий

Технический долг:

  • Миграция с WordPress на чистый PHP (производительность)

  • Переписать frontend на React (сейчас 1500 строк ванильного JS)

  • Добавить очередь запросов (сейчас синхронная обработка)

  • Улучшить систему логирования

Полезные ссылки

Если есть вопросы по реализации или идеи по улучшению — пишите в комментариях. Особенно интересно обсудить:

  • Опыт работы с OpenAI API и оптимизацию затрат

  • Альтернативные подходы к выбору модели

  • Методы оценки качества AI-текстов