Генерация картинок по шаблону: Genviso и BetterToken
Как отделить визуальный поиск от серверной генерации, превратить удачный prompt в шаблон и безопасно запустить выпуск изображений через API.
Содержание
Удачная генерация ещё не становится производственным процессом. Для одного изображения можно несколько раз переписать prompt, открыть ответы и выбрать лучший вариант вручную. При сотнях SKU тот же подход превращается в длинную цепочку дорогих проб. Каждое изменение света, ракурса или материала запускает новый API-запрос; причины удачного результата остаются в голове у автора.
Рабочую схему удобнее разделить на два контура. Сначала команда проверяет визуальную идею и фиксирует повторяемые правила. Затем backend подставляет бизнес-данные в утверждённый шаблон, отправляет запросы, сохраняет результаты и учитывает ошибки. Творческий поиск остаётся вне очереди production-задач. Серверный код не приходится менять после каждой правки композиции.
Почему отладка prompt в коде быстро выходит из-под контроля
Слишком много связанных переменных
Модель одновременно реагирует на объект, окружение, свет, положение камеры, материал, глубину резкости и палитру. Для фотографии флакона сыворотки заметно различаются, например:
- фронтальный кадр и съёмка под углом 45°;
- жёсткий направленный свет и мягкий рассеянный;
- стекло с выраженными отражениями и матовая поверхность;
- фон из травертина, металла или однотонной бумаги;
- макросъёмка на 85 mm и широкий угол.
Если менять несколько параметров за раз, команда не понимает, какая формулировка улучшила результат. Если менять по одному, число запросов быстро растёт. Backend через BetterToken может выполнять Image API-вызовы по шаблону, но преждевременный запуск очереди только размножит непроверенную визуальную гипотезу.
У разных задач разная визуальная грамматика
Карточке товара нужны читаемый силуэт, контролируемые отражения и место под верстку. У 3D-иллюстрации другие требования к форме и материалу. Постеру для соцсетей важны иерархия, контраст и безопасные зоны. Универсальный prompt для всех этих случаев обычно обрастает противоречивыми прилагательными.
Практичнее хранить несколько семейств шаблонов:
skincare_product
luxury_watch
food_photography
3d_illustration
social_poster
Каждое семейство задаёт собственные обязательные поля и критерии приёмки. Программа выбирает подходящий шаблон по категории, а затем подставляет данные конкретного товара или кампании.
Исследование и production требуют разных правил
Во время поиска допустимы десятки вариантов и субъективное сравнение. В production важны предсказуемый контракт, версия шаблона, ограниченные повторы, идентификатор задачи и понятный результат проверки.
Плохая схема выглядит так:
изменить prompt в коде
→ отправить API-запрос
→ открыть изображение
→ снова изменить код
→ снова отправить запрос
В ней нет точки, после которой визуальное решение считается утверждённым. Из-за этого любое обсуждение дизайна затрагивает backend и очередь задач.
Архитектура процесса: от визуальной гипотезы до файла в CMS
На этапе визуального поиска команда работает в Genviso: сравнивает варианты через визуальную галерею prompts, проверяет композицию, свет и стиль, затем сохраняет структуру удачного prompt. На серверном этапе приложение обращается через BetterToken к OpenAI-compatible Base URL с собственным API Key пользователя, подставляет данные, вызывает доступную модель и записывает результат. Между этими этапами передаётся версия Prompt Template, а не выбранная картинка и не набор устных пожеланий.
Чтобы проверить эту границу до подключения очереди, нужен собственный API Key. Создать аккаунт BetterToken
Выполните один контрольный запрос с утверждённым шаблоном и сразу сопоставьте модель, status и фактическое списание в Dashboard. Такой тест подтверждает серверный маршрут, не превращая визуальный поиск в серию production-запросов.
визуальный поиск
↓
проверка Prompt Template
↓
фиксация переменных и ограничений
↓
подстановка данных из PIM / CMS / базы SKU
↓
Image API-вызов через backend
↓
сохранение файла, статуса и метаданных
↓
визуальная приёмка результата
У каждого перехода должен быть проверяемый артефакт:
| Этап | Результат | Условие перехода |
|---|---|---|
| Визуальный поиск | Набор удачных и неудачных вариантов | Понятно, какие параметры влияют на результат |
| Проверка шаблона | Prompt с именованными переменными | Шаблон работает на нескольких типичных объектах |
| Интеграция | Функция рендеринга и схема входных данных | Все обязательные поля валидируются до API-вызова |
| Тестовый запрос | Один сохранённый файл и запись запроса | Файл открывается, модель и status совпадают с ожиданием |
| Production | Задача с template_id, версией и job_id | Повторы ограничены, результат связан с исходным SKU |
Этап 1. Превращаем визуальное решение в шаблон
Для студийной фотографии косметики исходная структура может выглядеть так:
Commercial studio product photography of {subject}.
Environment:
{environment}
Visual style:
{visual_style}
Lighting:
{lighting}
Composition:
{composition}
Color palette:
{color_palette}
Crisp reflections, premium material texture,
high-end commercial editorial photography.
Здесь стабилизируется порядок описания и набор визуальных измерений. Значения меняются отдельно:
subject
environment
visual_style
lighting
composition
color_palette
До передачи шаблона разработчикам полезно зафиксировать ещё четыре вещи:
- Обязательные поля. Без
subjectилиcompositionзапрос не должен уходить в API. - Допустимые значения. Если ракурс выбирается из трёх вариантов, лучше хранить enum, чем свободный текст из CMS.
- Запрещённые сочетания. Например, прозрачная упаковка на зеркальном фоне может потребовать отдельного шаблона.
- Критерии приёмки. Силуэт товара читается, логотип не искажён, объект не обрезан, фон подходит для дальнейшей верстки.
Сам prompt можно хранить рядом с машинно-читаемым контрактом:
{
"template_id": "skincare_product_v3",
"required_variables": [
"subject",
"environment",
"visual_style",
"lighting",
"composition",
"color_palette"
],
"output_size": "1024x1024"
}
Версия в template_id нужна для воспроизводимости. Если дизайнер меняет свет или композицию, новые задачи получают следующую версию; уже созданные материалы остаются связанными со старой.
Этап 2. Подключаем шаблон к backend
Для первого теста достаточно официального Python SDK openai, собственного BetterToken API Key и актуального Model ID из текущей документации Image API. Ключ и Model ID хранятся в окружении:
python -m pip install openai
export BETTERTOKEN_API_KEY="your_api_key_here"
export BETTERTOKEN_IMAGE_MODEL="current_image_model_id"
Не добавляйте настоящий ключ в репозиторий, prompt, скриншот или логи. Для production используйте менеджер секретов и отдельные ключи для разных приложений или окружений.
Следующий пример рендерит шаблон, отправляет один запрос и сохраняет PNG из b64_json:
import base64
import os
from pathlib import Path
from typing import Mapping
from openai import OpenAI
client = OpenAI(
base_url="https://www.bettertoken.ai/v1",
api_key=os.environ["BETTERTOKEN_API_KEY"],
)
def render_product_prompt(variables: Mapping[str, str]) -> str:
return f"""
Commercial studio product photography of {variables['subject']}.
Environment:
{variables['environment']}
Visual style:
{variables['visual_style']}
Lighting:
{variables['lighting']}
Composition:
{variables['composition']}
Color palette:
{variables['color_palette']}
Crisp reflections, premium material texture,
high-end commercial editorial photography.
""".strip()
product = {
"subject": (
"frosted amber glass serum bottle "
"with a minimalist gold dropper"
),
"environment": (
"organic travertine pedestal "
"surrounded by subtle water ripples"
),
"visual_style": "high-end botanical skincare editorial",
"lighting": (
"warm directional morning rim light "
"with soft diffused fill"
),
"composition": (
"centered 85mm macro product photography "
"with shallow depth of field"
),
"color_palette": "earthy amber, warm beige and subtle gold",
}
response = client.images.generate(
model=os.environ["BETTERTOKEN_IMAGE_MODEL"],
prompt=render_product_prompt(product),
size="1024x1024",
n=1,
output_format="png",
response_format="b64_json",
)
image_base64 = response.data[0].b64_json
if not image_base64:
raise RuntimeError("Image API response does not contain b64_json")
output_path = Path("serum-product.png")
output_path.write_bytes(base64.b64decode(image_base64))
print(f"Saved: {output_path}")
Синтаксис client.images.generate(...) и декодирование b64_json соответствуют текущему контракту OpenAI Python SDK. Model ID вынесен в BETTERTOKEN_IMAGE_MODEL, поскольку список доступных моделей и параметры нужно проверять перед запуском. Замена модели тогда не требует переписывать Prompt Template и бизнес-логику.
Минимальный контур пакетной задачи
Ниже — намеренно явный псевдокод интеграционного слоя. save_job, generate_image и ApiError обозначают адаптеры вашего хранилища и API-клиента, а не дополнительные методы SDK. Важна последовательность состояний и решений:
MAX_ATTEMPTS = 3
RETRYABLE_STATUS = {429, 500, 502, 503, 504}
for sku in sku_rows:
variables = validate_variables(sku) # до API-вызова
prompt = render_product_prompt(variables)
job_id = uuid4().hex
prompt_hash = sha256(prompt.encode()).hexdigest()
save_job(
job_id=job_id,
sku_id=sku["id"],
template_id="skincare_product_v3",
prompt_hash=prompt_hash,
status="pending",
)
for attempt in range(1, MAX_ATTEMPTS + 1):
save_job(job_id=job_id, status="running", attempt=attempt)
try:
result = generate_image(prompt)
except ApiError as error:
if error.status_code in {400, 401}:
save_job(job_id=job_id, status="failed", error_code=error.status_code)
break # сначала исправить данные, Key или конфигурацию
if error.status_code not in RETRYABLE_STATUS:
save_job(job_id=job_id, status="failed", error_code=error.status_code)
break
if attempt == MAX_ATTEMPTS:
save_job(job_id=job_id, status="failed", error_code=error.status_code)
break
sleep(min(2 ** attempt, 8))
continue
except TimeoutError:
save_job(job_id=job_id, status="unknown", error_code="timeout")
break # проверить Dashboard и хранилище до повторной отправки
if not result.b64_json:
save_job(job_id=job_id, status="failed", error_code="empty_output")
break
output_path = persist_png(job_id, result.b64_json)
save_job(
job_id=job_id,
status="succeeded",
output_path=output_path,
model=result.model,
attempt=attempt,
)
break
Локальный job_id связывает SKU, шаблон и файл, но сам по себе не делает запрос идемпотентным. После timeout статус остаётся unknown: сначала найдите запрос по времени в Dashboard и проверьте объектное хранилище, затем решите, нужна ли одна повторная отправка. Так очередь не маскирует возможный дубликат.
Порядок диагностики
| Симптом | Что проверить сначала | Исправление | Как перепроверить |
|---|---|---|---|
| 400 | Обязательные переменные, текущий Model ID, поддерживаемый size | Исправить данные или параметр; не повторять тот же запрос автоматически | Запустить один контрольный SKU и открыть PNG |
| 401 | Загружена ли переменная Key, принадлежит ли ключ пользователю и подходит ли текущему протоколу | Заменить или пересоздать Key, не выводя его в лог | Отправить минимальный запрос и найти его status в Dashboard |
| 429 | Текущую конкуренцию и частоту задач | Остановить набор новых задач, снизить конкуренцию, применить ограниченный backoff | Сначала пропустить один запрос, затем постепенно вернуть нагрузку |
| 5xx | Время запроса и число уже сделанных попыток | Повторять только до MAX_ATTEMPTS; при сохранении ошибки зафиксировать время и status | Проверить один новый запрос после задержки, не меняя утверждённый шаблон |
| timeout | Dashboard и хранилище: результат мог быть создан без ответа клиенту | Оставить unknown, не считать запрос автоматически неуспешным | Если записи и файла нет, разрешить одну новую отправку с тем же локальным job_id |
Пустой b64_json или ошибка декодирования | Текущий формат ответа, модель и параметры по документации | Сохранить код ошибки без содержимого Key, исправить разбор или конфигурацию | Повторить на одном SKU и убедиться, что PNG декодируется и открывается |
Что добавить перед пакетной генерацией
Один успешный файл подтверждает базовый маршрут, но не готовность очереди к сотням задач. Перед масштабированием добавьте следующие проверки.
Валидируйте данные до запроса
Пустой material, неожиданная разметка в product_name или свободный текст вместо утверждённой палитры меняют prompt. Проверяйте обязательные поля, длину строк и допустимые значения до обращения к модели. Сохраняйте итоговый prompt hash, template_id и идентификатор SKU рядом с задачей.
Ограничьте повторы
Повтор после timeout может создать ещё одно изображение, даже если приложение не успело получить первый ответ. Задайте конечное число попыток, используйте задержку и помечайте каждый запуск собственным job_id. Не запускайте бесконечный retry на 400, 401 или ошибке модели: сначала исправьте данные, ключ или конфигурацию.
Разделите техническую и визуальную приёмку
HTTP 200 и корректный PNG подтверждают технический успех. Композицию, искажение товара и соответствие бренду оценивают отдельно. Автоматическая задача сохраняет файл и метаданные; следующий этап применяет визуальные критерии шаблона.
Сопоставьте запрос с учётом использования
После контрольной генерации найдите запрос в Dashboard по времени. Сверьте модель, status и соответствующее списание; доступные поля использования показывают input, output и cache Token. Dashboard предназначен для метаданных использования и расхода, не для хранения полного prompt или ответа. Перед расчётом бюджета берите текущую ставку со страницы моделей и цен. Фактический расход тестового вызова смотрите в записи запроса.
Пример потока для каталога товаров
Для магазина с несколькими сотнями SKU задача может проходить через такую цепочку:
PIM / база SKU
↓
категория товара → template_id
↓
название / материал / цвет / фон
↓
валидация обязательных полей
↓
рендер Prompt Template
↓
задача генерации с job_id
↓
Image API
↓
объектное хранилище
↓
визуальная приёмка
↓
CMS / медиатека
В этой схеме prompt становится версионируемым производственным объектом. Можно увидеть, какой шаблон создал конкретный файл, сравнить долю отклонённых результатов по версиям и откатить неудачное изменение без правки всей интеграции.
Контрольный список перед запуском
- Prompt Template проверен на типичных товарах и сложных пограничных случаях.
- Для шаблона заданы
template_id, обязательные переменные и критерии приёмки. - API Key хранится вне исходного кода и не попадает в логи.
- Model ID читается из окружения или конфигурации, а не зашит в бизнес-логику.
- Один тестовый запрос создаёт открываемый файл ожидаемого размера.
- Ошибки 400/401 требуют исправления данных или конфигурации до нового запроса.
- Для 429/5xx задан ограниченный retry.
- Каждая задача связана с SKU,
job_id, версией шаблона и местом хранения результата. - Техническая проверка и визуальная приёмка выполняются отдельно.
- Модель, status и расход контрольного запроса сверены в Dashboard.
Как разделение ролей упрощает совместную работу
Genviso отвечает за интерактивный контур: быстрый поиск визуального направления, сравнение prompts и проверку шаблона до передачи разработчикам. BetterToken отвечает за серверный контур: API Key, OpenAI-compatible подключение, вызов текущей доступной модели и запись использования. Команды согласуют один контракт — Prompt Template с переменными, версией и критериями приёмки.
Чтобы перенести утверждённый шаблон в работающий backend и проверить расход на одном типичном SKU, выполните минимальный запрос по Image API reference, затем сверьте модель, status и списание в Dashboard до подключения очереди. Такая граница оставляет визуальный поиск в интерактивном контуре, а production-код занимается воспроизводимым исполнением.