Я регулярно выкладываю посты в блог НормЦРМ. На двух языках: русском и английском.
Написал пост, придумал заголовок. Тут всё просто. А дальше неприятный процесс. С помощью ИИ перевести пост на английский — и перенести перевод в блог. А ещё сгенерировать мета-данные и og-данные (это для поисковиков и мессенджеров), тоже перевести их на английский и руками поставить в нужные поля.
Всё это занимает минуты, но такая работа раздражает. А пишу я довольно часто (публикация раз в пару дней). И решил сделать в интерфейсе одну кнопку, которая возьмёт на себя всю эту рутину. Решил — и сделал. Теперь в один клик переводится пост и генерируются все мета-данные.
Сейчас расскажу во всех деталях, как именно это реализовано. Вдруг вы тоже так захотите?
Для начала немного контекста. Меня зовут Егор Камелев. Я проектировщик интерфейсов, но благодаря нейронкам, потихоньку погружаюсь в разработку. У меня есть свой проект — НормЦРМ (повышалка производительности для «взрослых» одиночек), написан на Python, Django, PostgreSQL. И в нём есть блог.
Первая версия блога была очень простой, из коробки. Затем я решил, что пора начинать активно писать и немного её улучшил. Добавил теги на нескольких языках, визуальный редактор, возможность подгружать картинки, вот это всё.
В каждой публикации можно добавлять разные языковые версии со своими адресами. Я пока поддерживаю только русский и английский. И вот настал момент, когда мне надоело заниматься рутинной работой по переносам переводов и мета-данных из соседнего окна с ChatGPT — и я решил сделать кнопку, которая возьмёт эту работу на себя.
В теории я представлял, что нужно делать, но на практике ни разу до этого не работал с API нейросетей.
Первое, с чего начал, — пошёл в ChatGPT и попросил помочь составить план действий. Сформулировал задачу примерно так:
Дальше я попытался понять, сколько это будет стоить, и пошёл регистрировать себе аккаунт в OpenAI Platform. Да, чтобы воспользоваться API, уже не подойдёт мой простой пользовательский аккаунт.
Зарегистрировался — и вдруг понял, что ChatGPT недоступен пользователям из России. Я же всё это через VPN делаю. А на сервере моего проекта никакого VPN нет. И что ничего у меня не получится.
Выбирать какую-то другую нейронку мне не хотелось. Я неплохо освоился с ChatGPT и разобрался с его достоинствами и недостатками — не хотелось повторять весь этот путь с каким-то другим ИИ.
Так что я решил поднять прокси-сервер где-нибудь в Европе. Это такой промежуточный сервер, который делает вот что:
Принимает HTTP-запрос от НормЦРМ
Добавляет API-ключ
Отправляет запрос в OpenAI
Возвращает ответ обратно.
Я выбрал одного из провайдеров в Дубае и арендовал выделенный сервер, территориально находящийся в Амстердаме.
Вот всё, что понадобилось на сервере:
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, он начинает решать вполне приземлённые задачи.
И, честно говоря, это самое интересное в происходящем.
Статья: 20 лет объяснял программистам, что делать. А теперь попробовал сам
Статья: 20 лет объяснял программистам, что делать. А теперь попробовал сам. Часть вторая
Мой канал в Телеграме
Источник


