Назад к блогу

Автогенерация изображений: шаблоны, jobs и webhooks

E2X Team··13 мин чтения
automationimage-generationapiwebhooksbatch-processingnano-banana

Большинство ответов на этот вопрос — скриншот текстового поля. Набрать prompt, нажать кнопку, подождать, скачать, повторить. Это не автоматизация. Это вы, работающие руками, только быстрее.

Автоматически — значит без человека в контуре. Приходит строка данных, выходит изображение, и никто за этим не смотрел. Для этого нужны четыре части: шаблон prompt с дырками, данные, которыми дырки заполняются, очередь, которая принимает работу не блокируя, и надёжное место, куда класть результат. Мы держим API в середине этой конструкции, так что видим и пайплайны, которые люди строят, и те, что тихо ломаются через три недели после запуска.

Одинаковые пустые панели ровно движутся по ленте конвейера из нержавеющей стали на чистом современном производстве

То, чего вам не говорят первым: теперь это дёшево

Начнём с цифры, потому что она меняет то, что вы готовы построить.

Тысяча изображений через Nano Banana 2 Lite стоит $23.80. Это 1000 × $0.0238, по нашей цене на 26 августа 2026 года. Сто тысяч предметных кадров — настоящий каталог, каждый SKU, каждый цвет — это $2,380.

Те же объёмы по уровням, между которыми вы выбираете:

ИзображенийNano Banana 2 LiteNano Banana 2Nano Banana Pro
100$2.38$4.00$7.50
1 000$23.80$40.00$75.00
10 000$238.00$400.00$750.00
100 000$2,380.00$4,000.00$7,500.00

Цены и условия продуктов со временем меняются; проверьте актуальные цены каждого провайдера, прежде чем принимать решение о покупке. Цены Google Batch или Flex не эквивалентны напрямую стандартному on-demand API request, поскольку планирование, доступность и условия обработки отличаются; поэтому они исключены из этого сравнения. Это ограниченное по охвату сравнение, а не утверждение, что E2X — самый дешёвый вариант в мире при любой конфигурации.

Большинство тех, кто спрашивает, как автоматизировать генерацию изображений, про себя уже оценили это как «слишком дорого, чтобы пробовать». Вопрос стоит $23.80. Стройте.

Шаг 1: перестаньте писать prompt, начните писать шаблон

Prompt, который вы набираете один раз, — это предложение. Prompt, который машина гоняет десять тысяч раз, — это шаблон со слотами, и в этой разнице вся работа.

Правило: всё, что меняется, идёт в слот, всё, что обязано остаться одинаковым, — в неподвижный текст. Ошибитесь с этой границей — и набор поплывёт: половина изображений на белом, половина на сером, потому что «фон» протёк в переменную часть.

TEMPLATE = (
    "Студийная предметная фотография: {product_name}, цвет {colour}, "
    "по центру на ровном тёпло-сером фоне, мягкий направленный рисующий свет "
    "сверху слева, малая глубина резкости, редакционная каталожная подача, "
    "без текста, без логотипов, без людей"
)

Два слота. Свет, фон, кадрирование и запреты заморожены. Именно эта замороженная половина делает 400 изображений одной съёмкой, а не 400 случайностями.

Держите четыре вещи вне переменной половины: медиум, направление света, фон и негативные инструкции. Это ваша визуальная идентичность. Если они гуляют от строки к строке, её у вас нет.

Ряд взаимозаменяемых прецизионных металлических штампов, разложенных на верстаке из шлифованной стали, каждый подходит к одной и той же внешней рамке

Само ремесло prompt — отдельная статья. Для автоматизации важна форма: неподвижная рамка, именованные дырки, никаких сюрпризов.

Шаг 2: данные и есть настоящий продукт

Шаблон тривиален. Работа живёт в данных, и обычно они у вас уже есть.

  • Таблица товарного каталога. Строка на SKU, колонки под название, цвет, материал, категорию.
  • Выгрузка постов из CMS, где заголовок и тема становятся слотами для картинки-шапки к каждой статье.
  • Строки в базе вашего приложения — пользовательские объявления, страницы событий, рецепты.
  • Таблица рекламных вариантов: шесть заголовков на четыре фона — это двадцать четыре изображения, и брифовать их руками никто не станет.

Приведите любой источник к списку словарей до того, как он коснётся API, и держите на каждой строке стабильный ID. Он нужен, чтобы сопоставлять результаты с записями, и его отсутствие бьёт ровно в тот момент, когда прилетает webhook.

Шаг 3: отправляйте, а не ждите

Наш API асинхронен намеренно. Вы делаете POST job, немедленно получаете job ID, а генерация происходит на нашей стороне.

curl -X POST https://api.e2x.ai/v1/jobs/submit \
  -H "Authorization: Bearer $E2X_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/nano-banana-2-lite/text-to-image",
    "input": {
      "prompt": "Студийная предметная фотография: настольная лампа из ореха, цвет матовый чёрный...",
      "aspect_ratio": "1:1"
    },
    "webhookUrl": "https://your-app.example.com/hooks/e2x"
  }'

Обходчик, превращающий таблицу в jobs, занимает около тридцати строк. Он читает CSV, форматирует шаблон по строке, отправляет и записывает job ID напротив ID строки, чтобы ничего не осиротело.

import csv
import os
import time
import requests

API = "https://api.e2x.ai/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['E2X_API_KEY']}",
    "Content-Type": "application/json",
}

TEMPLATE = (
    "Студийная предметная фотография: {product_name}, цвет {colour}, "
    "по центру на ровном тёпло-сером фоне, мягкий направленный рисующий свет "
    "сверху слева, малая глубина резкости, редакционная каталожная подача, "
    "без текста, без логотипов, без людей"
)

MAX_IMAGES = 500          # жёсткий потолок, см. раздел про стоимость
UNIT_COST = 0.0238        # Nano Banana 2 Lite, проверено 26 августа 2026 года


def submit(row):
    body = {
        "model": "google/nano-banana-2-lite/text-to-image",
        "input": {
            "prompt": TEMPLATE.format(**row),
            "aspect_ratio": "1:1",
        },
        "webhookUrl": "https://your-app.example.com/hooks/e2x",
    }
    for attempt in range(4):
        r = requests.post(f"{API}/jobs/submit", json=body, headers=HEADERS, timeout=30)
        if r.status_code < 300:
            return r.json()["data"]["jobId"]
        if r.status_code == 429 or r.status_code >= 500:
            time.sleep(2 ** attempt)
            continue
        raise RuntimeError(f"row {row['id']} rejected: {r.status_code} {r.text}")
    raise RuntimeError(f"row {row['id']} still failing after 4 attempts")


with open("catalog.csv") as fh:
    rows = list(csv.DictReader(fh))[:MAX_IMAGES]

print(f"submitting {len(rows)} jobs, budget ${len(rows) * UNIT_COST:.2f}")

submitted = {}
for row in rows:
    try:
        submitted[row["id"]] = submit(row)
    except RuntimeError as err:
        print(f"skipped: {err}")

print(f"{len(submitted)}/{len(rows)} accepted")

Обратите внимание, чего этот цикл не делает: не останавливается на упавшей строке. Батч из 500, где в строке 212 пустой цвет, должен дать вам 499 изображений и один залогированный пропуск, а не стектрейс в три часа ночи и ноль изображений.

Slug важнее, чем кажется. У google/nano-banana-2-lite/text-to-image есть суффикс возможности; text-слаг Nano Banana 2 — голый google/nano-banana-2, без него. Угадывание стоит вам 404 на пайплайне, который в остальном выглядит готовым. Каждая модель публикует список своих параметров в машиночитаемом файле спецификации.

Шаг 4: webhook вместо polling, и вот почему

Опрашивать можно. GET /v1/jobs/{JOB_ID} возвращает pending, processing, completed, failed или cancelled, и для разового скрипта это нормально.

Для пайплайна это неверный выбор, и причина не в изяществе. Polling связывает время жизни вашего процесса с самым медленным job в батче. Генерация на Lite идёт около 60 секунд на нашей стороне. Отправьте 500 — и опрашивающий процесс обязан оставаться живым, удерживая состояние, столько, сколько тянется хвост: через деплой, перезапуск контейнера, 15-минутный таймаут той serverless-площадки, на которой вы сидите. Когда он умирает, он умирает, держа единственную запись о том, какие jobs были в полёте.

Webhook переворачивает это. Вы отправляете и выходите. Позже прилетает POST с завершённым job, и вы обрабатываете ровно этот один job, без состояния.

// Express handler. Idempotent: the same jobId can arrive twice.
app.post("/hooks/e2x", express.json(), async (req, res) => {
  res.status(200).end(); // acknowledge first, work after

  const job = req.body.data;
  if (job.status !== "completed") {
    await markFailed(job.jobId, job.error?.message ?? "unknown");
    return;
  }

  const already = await findAsset(job.jobId);
  if (already) return;

  await storeImage(job.jobId, job.outputs[0].url);
});

Опрашивайте, когда гоняете скрипт руками и смотрите на него. Берите webhook, когда не смотрит никто, — а это и есть определение автоматизации.

Шаг 5: URL вывода временный, и вот так ломаются пайплайны

Вот оно. Если вы всё остальное просматриваете по диагонали, этот раздел прочитайте дважды.

data.outputs[0].url — это URL доставки, а не хранилище. Сейчас он работает. Вечно работать не будет. Всё, что относится к нему как к постоянному — колонка product_image_url, поле в CMS, ссылка, отправленная клиенту письмом, — прекрасно рендерится на стейджинге и показывает иконки битых картинок через две недели.

Отказ мерзкий, потому что отложенный. В момент записи ничего не падает. Тесты проходят. Батч отчитывается о 500 успехах. Потом изображения начинают исчезать, начиная с самых старых записей, и к тому моменту, как кто-то заметит, перегенерировать их без повторного прогона всей задачи вы не сможете — потому что prompt тоже никто не сохранил.

Скачивайте байты. Кладите их в свой бакет. Храните свой URL.

Одинаковые ящики на роликовых дорожках у погрузочной платформы в сумерках, несколько уже переставлены на массивные стальные складские стеллажи позади

import requests
import boto3

s3 = boto3.client("s3")


def store_image(job_id, row_id, source_url):
    img = requests.get(source_url, timeout=60)
    img.raise_for_status()

    key = f"products/{row_id}.jpg"
    s3.put_object(
        Bucket="my-assets",
        Key=key,
        Body=img.content,
        ContentType=img.headers.get("content-type", "image/jpeg"),
    )
    return f"https://cdn.example.com/{key}"

Раз уж вы там, сохраните рядом с изображением его prompt. Одна текстовая колонка — и это разница между «перегенерируй этот вариант» и «начинай заново».

Ещё две привычки, которые стоит завести с первого дня. Проверяйте, что скачивание вернуло байты изображения, а не страницу с ошибкой: 200 с HTML внутри с удовольствием запишется в ваш бакет. И ключуйте хранилище по ID вашей строки, а не по нашему job ID, чтобы перегенерация перезаписывала нужный объект, а не плодила сирот.

Шаг 6: что реально ломается на объёме

Маленькие батчи прячут всё. Вот что вылезает, когда вы переваливаете за несколько сотен.

Частичные батчи — норма. Какая-то доля любого большого прогона падает: prompt, задевший контентный фильтр, кривая строка, разовая ошибка выше по стеку. Считайте 497 из 500 успехом и заведите себе повтор в одну команду ровно по этим трём строкам.

Ретраям нужен потолок расходов. Цикл ретраев без ограничителя — это баг, который выставляет счёт. $0.0238 за выстрел звучит безобидно, пока залипший цикл не отработает сорок тысяч раз. Вбейте жёсткое число в код — строка MAX_IMAGES выше и есть самая дешёвая страховка.

Идемпотентность не опциональна. Webhook может быть доставлен больше одного раза. Обработчик, который небезопасно запускать дважды на одном job ID, даст вам дубли ассетов и дубли строк, и узнаете вы об этом в самую загруженную неделю.

Расходам нужен живой счётчик, а не месячный инвойс. Денежные значения в нашем API возвращаются в микроцентах: 1 000 000 равно $1. Суммируйте их по батчу и логируйте итог при закрытии. Пайплайн, который каждую ночь пишет «412 изображений, $9.81», — это пайплайн, который никому не надо аудировать.

Значения aspect ratio по умолчанию вас удивят. У Lite по умолчанию 1:1, а у Nano Banana 2 и Pro — 9:16. Зафиксируйте его в шаблоне, иначе половина каталога молча станет вертикальной.

Водяной знак едет вместе с файлом. Google впечатывает SynthID во всё, что производят его модели изображений. Он невидим, это не флаг, и он переживает всё, что ваш пайплайн потом делает с байтами. Тот, кто у вас отвечает за бренд или за юридическую сторону, должен согласовать ИИ-ассеты до того, как пайплайн появится, а не после того, как он записал в бакет 20 000 объектов.

Пневматические толкатели сбрасывают несколько панелей с движущейся ленты в боковой жёлоб отбраковки, остальные едут дальше по линии

На какую модель нацелить пайплайн

По умолчанию — Nano Banana 2 Lite, и уходите с неё, только когда что-то конкретное вас к этому вынудит.

Это арифметика, а не осторожность. За $0.0238 это самое дешёвое, что мы продаём из делающего изображения, она на целое поколение новее оригинальной Nano Banana, которую заменила, а на пайплайновых объёмах разрыв в цене складывается в единственную цифру, на которую смотрит финансовый отдел. У неё же четыре aspect ratio, которых нет больше нигде, — 1:4, 4:1, 1:8 и 8:1, ровно то, что нужно, если на выходе баннеры и вертикальные полосы. Одна особенность: параметра resolution нет вовсе, и отправка его — ошибка, а не молчаливый no-op.

Уводите работу с неё в этих четырёх случаях и ни в каких других:

  • Вывод выше 1K. Lite — только 1K. Nano Banana 2 достаёт до 4K при базе $0.04, где 2K идёт ×1,5, а 4K — ×2.
  • Читаемый текст в изображении. Товарные карточки с ценами, локализованная инфографика. Это живёт на Nano Banana Pro за $0.075, где 1K и 2K стоят одинаково — берите 2K, он бесплатный.
  • Прозрачный фон. Вырезы и листы стикеров требуют background: transparent, и GPT Image 2 — единственная модель у нас, которая делает это одним вызовом. Закладывайте $0.0525 на 1K, а не сырую каталожную цифру: quality умножает на 7 на каждом уровне.
  • Одно и то же лицо или одна и та же бутылка в множестве кадров. Это задача про референсные изображения, а не про объём, и у неё своя статья про консистентность.

Для смешанной нагрузки гоняйте основную массу на Lite, а те 2%, которым нужен текст или точность, ответвляйте на Pro по колонке в ваших данных. Один пайплайн, две модели, цена Pro только там, где она что-то зарабатывает.

Разбор контракта submit и poll параметр за параметром — в руководстве по API Nano Banana Pro, а разбор по уровням — в сравнении моделей. Обе статьи исходят из того, что модель вы уже выбрали. Если нет — модели text-to-image перечислены с ценами, а каталог добавляет к ним видеосторону.

Всё целиком, по порядку

Шаблон со слотами. Данные со стабильными ID. Отправка jobs под жёстким потолком. Приём webhook. Скачивание байтов в собственное хранилище. Логирование отказов и расходов. Шесть шагов, ни один не сложный, и пропускают всегда пятый.

Частые вопросы

Как автоматически генерировать изображения?

Напишите свой prompt как шаблон с именованными слотами, заполните эти слоты из источника данных — товарного каталога или CSV — и отправьте каждый заполненный prompt асинхронным job в API изображений. Забирайте результат по webhook, а не опросом, затем скачивайте байты изображения в собственное хранилище. Ни одному звену этого цикла человек не нужен, и это делает его автоматическим, а не быстрой ручной работой.

Сколько стоит автоматически сгенерировать 1000 изображений?

По ценам E2X на 26 августа 2026 года 1000 изображений через Nano Banana 2 Lite — это $23.80, поскольку каждый request стоит $0.0238. Тот же объём — $40.00 на Nano Banana 2 и $75.00 на Nano Banana Pro. Более высокие разрешения умножаются сверху, но для стандартного вывода в 1K цифра за изображение и есть весь расчёт.

Что выбрать для jobs генерации изображений — webhook или polling?

Для всего, что работает без присмотра, — webhook. Polling привязывает время жизни вашего процесса к самому медленному job в батче, так что деплой или serverless-таймаут убивает опрашивающий процесс ровно тогда, когда он держит единственную запись о том, какие jobs ещё идут. Webhook позволяет отправить и выйти, а затем обрабатывать каждый завершённый job без состояния. Polling нормален для скрипта, за прогоном которого вы наблюдаете.

Почему мои сгенерированные изображения через какое-то время перестают загружаться?

Потому что вы сохранили URL вывода из API вместо самого изображения. Этот URL — временная доставка, а не хранилище, так что колонка с ним сначала рендерится правильно, а потом ломается. Скачивайте байты сразу по завершении job, пишите их в свой бакет, храните свой URL. Это самый частый способ отправить в прод сломанный пайплайн изображений.

Какая модель должна быть по умолчанию в автоматическом пайплайне?

Nano Banana 2 Lite, по $0.0238 за изображение. Это самая дешёвая модель изображений в нашем каталоге и на поколение новее оригинальной Nano Banana, а на батчевых объёмах цена перебивает любые другие соображения. Переходите на Nano Banana 2 только ради вывода выше 1K, на Nano Banana Pro — только ради читаемого текста в кадре, на GPT Image 2 — только ради прозрачного фона.

Что делать, когда часть изображений в батче падает?

Часть будет падать всегда, и пайплайн должен считать это нормой, а не фатальной ошибкой. Продолжайте отправку после отклонённой строки, логируйте ID отказов с их ошибками и постройте путь повтора, который прогоняет только упавшее. Ограничивайте число попыток — каждая из них реальные деньги.