Я регулярно выкладываю посты в блог НормЦРМ. На двух языках: русском и английском.

Написал пост, придумал заголовок. Тут всё просто. А дальше неприятный процесс. С помощью ИИ перевести пост на английский — и перенести перевод в блог. А ещё сгенерировать мета-данные и og-данные (это для поисковиков и мессенджеров), тоже перевести их на английский и руками поставить в нужные поля.

Всё это занимает минуты, но такая работа раздражает. А пишу я довольно часто (публикация раз в пару дней). И решил сделать в интерфейсе одну кнопку, которая возьмёт на себя всю эту рутину. Решил — и сделал. Теперь в один клик переводится пост и генерируются все мета-данные.

Сейчас расскажу во всех деталях, как именно это реализовано. Вдруг вы тоже так захотите?

Для начала немного контекста. Меня зовут Егор Камелев. Я проектировщик интерфейсов, но благодаря нейронкам, потихоньку погружаюсь в разработку. У меня есть свой проект — НормЦРМ (повышалка производительности для «взрослых» одиночек), написан на Python, Django, PostgreSQL. И в нём есть блог.

Первая версия блога была очень простой, из коробки. Затем я решил, что пора начинать активно писать и немного её улучшил. Добавил теги на нескольких яз��ках, визуальный редактор, возможность подгружать картинки, вот это всё.

В каждой публикации можно добавлять разные языковые версии со своими адресами. Я пока поддерживаю только русский и английский. И вот настал момент, когда мне надоело заниматься рутинной работой по переносам переводов и мета-данных из соседнего окна с ChatGPT — и я решил сделать кнопку, которая возьмёт эту работу на себя.

В теории я представлял, что нужно делать, но на практике ни разу до этого не работал с API нейросетей.

Первое, с чего начал, — пошёл в ChatGPT и попросил помочь составить план действий. Сформулировал задачу примерно так:

А как ты думаешь, можно ли как-то встроить ИИ в редактор публикаций НормЦРМ (питон, джанго, ckeditor), чтобы он мне автоматически делал версию на английском? Что для этого нужно в целом сделать и сколько займёт по времени?

Дальше я попытался понять, сколько это будет стоить, и пошёл регистрировать себе аккаунт в OpenAI Platform. Да, чтобы воспользоваться API, уже не подойдёт мой простой пользовательский аккаунт.

Прокси-сервер

Зарегистрировался — и вдруг понял, что ChatGPT недоступен пользователям из России. Я же всё это через VPN делаю. А на сервере моего проекта никакого VPN нет. И что ничего у меня не получится.

Выбирать какую-то другую нейронку мне не хотелось. Я неплохо освоился с ChatGPT и разобрался с его достоинствами и недостатками — не хотелось повторять весь этот путь с каким-то другим ИИ.

Так что я решил поднять прокси-сервер где-нибудь в Европе. Это такой промежуточный сервер, который делает вот что:

  1. Принимает HTTP-запрос от НормЦРМ

  2. Добавляет API-ключ

  3. Отправляет запрос в OpenAI

  4. Возвращает ответ обратно.

Я выбрал одного из провайдеров в Дубае и арендовал выделенный сервер, территориально находящийся в Амстердаме.

Вот всё, что понадобилось на сервере:

  • Ubuntu

  • Python venv

  • FastAPI

  • Uvicorn

  • Nginx

  • systemd

Дальше я попросил ИИ помочь мне с кодом, который будет управлять проксированием. Вот что получилось.

import os
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()

app = FastAPI()

AI_PROXY_TOKEN = os.getenv("AI_PROXY_TOKEN")
AI_MODEL = os.getenv("AI_MODEL", "gpt-5-mini")

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

class GenerateRequest(BaseModel):
    prompt: str

@app.post("/generate")
async def generate(
    request: GenerateRequest,
    x_proxy_token: str | None = Header(default=None)
):
    if not AI_PROXY_TOKEN or x_proxy_token != AI_PROXY_TOKEN:
        raise HTTPException(status_code=401, detail="Unauthorized")

    response = client.responses.create(
        model=AI_MODEL,
        input=request.prompt,
    )

    return {
        "output": response.output_text
    }

Как видите, всё умещается в 34 строки. По коду вы можете понять, что настройки токена, ключа и модели ChatGPT я вынес в .env файл. Ключ получил в OpenAI Platform, модель выбрал самую дешёвую на момент создания (любая справится с переводами), а токен уже добавил чуть позже, когда проверил, что всё работает. Токен нужен для того, чтобы никто не мог прийти ко мне на сервер и использовать его в качестве бесплатного входа в API (и потратить мои драгоценные пять баксов).

Я не стал делать полноценную async-архитектуру, потому что прокси используется только мной и нагрузка минимальна.

Дальше, чтобы это всё работало, необходим systemd-сервис. Потому что когда я зап��скаю Python-скрипт в терминале, он работает только пока открыт терминал. Но в продакшене сервер должен:

  • Стартовать автоматически при загрузке системы;

  • Перезапускаться при падении;

  • Работать в фоне.

Настройки (файл INI) выглядят так:

[Unit]
Description=AI Proxy (FastAPI)
After=network.target

[Service]
User=root
WorkingDirectory=/opt/ai-proxy
ExecStart=/opt/ai-proxy/venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000
Restart=always

[Install]
WantedBy=multi-user.target

Что это значит?

  • ExecStart — какую команду запускать

  • Restart=always — если приложение упало, запустить снова

  • WorkingDirectory — из какой папки запускать

Как итог: прокси работает как полноценная серверная служба, а не как «скрипт в терминале».

После этого я настроил Nginx.

server {
    listen 80;
    server_name _;

    location /generate {
        proxy_pass http://127.0.0.1:8000/generate;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Что тут происходит? FastAPI запущен на локальном адресе внутри сервера (127.0.0.1:8000). Но пользователи (и мой НормЦРМ в их числе) обращаются к серверу по обычному HTTP-порту 80. И вот для таких случаев, когда приходят запросы на адрес /generate, Nginx берёт и перенаправляет их внутреннему приложению. Так что Nginx — это reverse proxy

И всё. Прокси-сервер настроен, автономен и базово защищён от злоумышленников.

Доработки в НормЦРМ

Дальше я сформулировал задачу уже для Codex. Это нейронка от ChatGPT, в которой я вайб-кодю НормЦРМ. В результате получилось всего три правки в код НормЦРМ.

Первая отвечает за то, чтобы по API отправлялся запрос к ИИ, а ответ обрабатывался и встраивался в публикацию в блоге.

Вторая — добавление в файл с настройками всего проекта пары переменных: адреса прокси-сервера, а также токена, который используется для обращения к нему.

Наконец, третья — это небольшая правка в шаблон страницы создания/редактирования поста в админке.

Внешний вид меня вообще не волновал — это инструмент сугубо для меня. Поэтому я просто попросил сделать мне кнопку под полем с текстом публикации. Получилось вот что:

Это скриншот из админки. Видите синюю страшную кнопку по-середине экрана? Вот это оно.
Это скриншот из админки. Видите синюю страшную кнопку по-середине экрана? Вот это оно.

Промпт

Как отправляется запрос к ИИ? Да таким же промптом, какие мы отправляем, общаясь с чат-ботами. Вот промпт, который сейчас используется в моём коде:

AI_PROMPT_TEMPLATE = """You are a professional bilingual editor and SEO specialist.

Your task is to:

1) Generate Russian SEO metadata for the original Russian blog post.
2) Generate a full English version of the blog post.
3) Preserve HTML formatting.
4) Return STRICT JSON only. No explanations. No markdown. No extra text.

--------------------------------------------------
ORIGINAL RUSSIAN DATA:

Title (RU):
{{RU_TITLE}}

Body (RU, HTML):
{{RU_BODY_HTML}}

--------------------------------------------------

REQUIREMENTS:

=== RUSSIAN SEO ===

Generate:

- ru_meta_title (50–60 characters preferred)
- ru_meta_description (140–160 characters preferred)
- ru_og_title (can match meta_title)
- ru_og_description (can match meta_description)

Meta fields must:
- Be natural Russian
- Not repeat the full title verbatim unless appropriate
- Be concise and clickable
- Not contain quotation marks unless necessary

=== ENGLISH VERSION ===

Generate:

- en_title
- en_slug (lowercase, hyphen-separated, latin only)
- en_body_html (valid HTML)
- en_meta_title (50–60 characters preferred)
- en_meta_description (140–160 characters preferred)
- en_og_title
- en_og_description

EN rules:

- Translate naturally, not word-for-word.
- Preserve ALL HTML structure.
- DO NOT modify:
  - <a href="">
  - <img src="">
- Preserve paragraphs, lists, strong, em, headings.
- Translate image alt attributes into English.
- Do not invent new links.
- Do not add scripts or styles.
- Do not wrap result in markdown.
- Do not add explanations.
- Ensure en_body_html is a JSON string (escape quotes and newlines).

Return ONLY a single JSON object (not an array). Output must be valid JSON that can be parsed by json.loads().

Critical JSON rules:
- Use double quotes for all keys and string values.
- Escape any double quotes inside strings as \"
- Escape newlines as \n
- Do not include trailing commas.
- Do not wrap the JSON in markdown fences.
- Do not include any text before or after the JSON.

If you are unsure about any field, return an empty string for it. Never add explanations.

JSON FORMAT:

{
  "ru_meta_title": "",
  "ru_meta_description": "",
  "ru_og_title": "",
  "ru_og_description": "",
  "en_title": "",
  "en_slug": "",
  "en_body_html": "",
  "en_meta_title": "",
  "en_meta_description": "",
  "en_og_title": "",
  "en_og_description": ""
}
"""

Как видите, в промпте указан и контекст, и задача, и формат, в котором должен приходить ответ. Обратите внимание на то, что отдельно формируется просьба не ломать ничего в html-разметке, не присылать ничего, помимо JSON (не сопровождать ответы дополнительными объяснениями) и так далее.

Я бы и рад поделиться с вами историями о том, как ИИ прислал мне ответ в неверном формате — и ничего не сработало, но у меня их нет. Пока что всё работает, как задумано, и без сбоев.

В коде также предусмотрены сценарии которые описывают, что делать с уже заполненными полями, а также подробное логирование. Это уже результат моей работы в роли обычного проектировщика интерфейсов (хотя здесь интерфейсов и нет, как таковых, но как по мне UX-дизайнеры должны разбираться в движении и обработке данных не хуже аналитиков).

В общем, я это всё задеплоил, протестировал — и оно не заработало :)

Единственная неполадка

На уровне интерфейса всё выглядело хаотично: то 500, то 502, то сообщение о том, что сервер вернул не JSON. Причём ошибка возникала не всегда, что сбивало с толку.

Логи показали, что прокси отрабатывает корректно: запрос до OpenAI уходит, ответ генерируется и возвращается. Проблема оказалась в Gunicorn. По умолчанию его timeout — 30 секунд. Если воркер не вернул ответ за это время, он считается зависшим и перезапускается.

Мой промпт довольно объёмный: перевод статьи, генерация slug, SEO-метаданных и OG-данных, плюс строгий JSON и сохранение HTML. В среднем модель отвечала за 40–50 секунд. То есть OpenAI честно формировал результат, но Gunicorn уже успевал «похоронить» воркер. Ответ приходил — а принимать его было уже некому.

В продакшене такую задачу правильнее выносить в очередь и обрабатывать асинхронно. Но в моём случае кнопкой пользуюсь только я, нагрузка минимальная, а ожидание в 40 секунд некритично. Поэтому я просто увеличил timeout до 120 секунд. Для соло-проекта это оказалось самым рациональным решением.

Итоги

Что по деньгам? Сервер в Амстердаме — пять евро в месяц. На баланс OpenAI Platform я закинул пять долларов. Генерация одного поста обходится примерно в один цент. Даже если публиковать по 30 постов в месяц, денег хватит надолго.

Что по времени? От идеи до работающего решения прошло около трёх часов (это включая регистрацию в OpenAI Platform и закидывание денег в неё и на хостинг). Ещё пара часов ушла на разбор с тайм-аутами и закрытие прямого доступа к прокси.

И да, всё работает. Подготовил текст на русском, нажал кнопку — через 40 секунд появляются перевод, slug, SEO- и OG-метаданные. Экономия — около пяти минут на пост. При 10–15 публикациях в месяц это уже час времени. Стоимость моего часа перекрывает все расходы на сервер и токены, а затраченные на разработку пять часов окупятся довольно быстро.

Интеграция OpenAI в блог оказалась не столько задачей про «ИИ», сколько задачей про инфраструктуру. Основные сложности возникли не на уровне модели, а на уровне сетевой архитектуры и тайм-аутов. Сам вызов API занял несколько строк кода. Всё остальное — продакшен.

Пару лет назад я вряд ли поверил бы, что буду поднимать прокси, настраивать systemd и разбираться с Gunicorn ради кнопки в админке. А сейчас это просто ещё один инструмент в арсенале.

ИИ оказался не магией, а обычным внешним сервисом — просто очень мощным. И если относиться к нему как к любому другому API, он начинает решать вполне приземлённые задачи.

И, честно говоря, это самое интересное в происходящем.

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