"""Витрина — генератор иллюстраций и текстов карточек для маркетплейсов.
Изображения рисует OpenAI, промпт и тексты собирает Groq, финальный монтаж
под размеры площадок делает Pillow. Запуск: python app.py (или docker compose up).
"""
from __future__ import annotations
import logging
import os
from pathlib import Path
os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
import gradio as gr # noqa: E402
from src.config import APP_NAME, APP_TAGLINE, settings
from src.pipeline import Brief, run
from src.postprocess import BADGE_POSITIONS, SIZE_PRESETS
from src.prompts import CATEGORY_PRESETS, LIGHT_PRESETS, STYLE_PRESETS
logging.basicConfig(
level=settings.log_level,
format="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s",
)
log = logging.getLogger("app")
CSS = Path("style.css").read_text(encoding="utf-8") if Path("style.css").exists() else ""
THEME = gr.themes.Soft(
primary_hue=gr.themes.colors.pink,
neutral_hue=gr.themes.colors.slate,
radius_size=gr.themes.sizes.radius_lg,
font=[gr.themes.GoogleFont("Manrope"), "ui-sans-serif", "system-ui", "sans-serif"],
)
EXAMPLES = [
[
"Термокружка из нержавеющей стали, 450 мл, матовый графит",
"Двойные стенки, крышка-поилка, держит тепло 12 часов",
"Белый фон (главное фото)",
"Товары для дома",
],
[
"Крем для лица с гиалуроновой кислотой, 50 мл",
"Белая стеклянная банка с золотой крышкой, минималистичная этикетка",
"Бьюти: макро-текстура",
"Косметика и парфюмерия",
],
[
"Беспроводные наушники-вкладыши в зарядном кейсе",
"Чёрный soft-touch пластик, светодиодный индикатор",
"Премиум на тёмном",
"Электроника",
],
[
"Детский рюкзак с динозаврами, объём 12 л",
"Плотный текстиль, светоотражающие вставки, две боковые сетки",
"Лайфстайл в интерьере",
"Детские товары",
],
]
def _keys_status() -> str:
openai_ok = bool(settings.openai_api_key)
groq_ok = bool(settings.groq_api_key)
if openai_ok and groq_ok:
return "Ключи взяты из настроек сервера. Свои вводить не нужно."
if openai_ok and not groq_ok:
return "Ключ OpenAI есть на сервере. Без ключа Groq промпт собирается по шаблону, тексты карточки не пишутся."
return "Ключи на сервере не заданы — введи свои ниже. Они живут только в этой сессии и никуда не сохраняются."
def handle_generate(
product,
details,
style,
category,
light,
extra,
reference,
keep_product,
size_preset,
fit_mode,
background,
n_images,
quality,
badge_text,
badge_position,
badge_color,
watermark,
write_copy,
openai_key_in,
groq_key_in,
progress=gr.Progress(), # noqa: B008 — так требует Gradio
):
openai_key = settings.resolve_openai_key(openai_key_in)
groq_key = settings.resolve_groq_key(groq_key_in)
brief = Brief(
product=product or "",
details=details or "",
style=style,
category=category,
light=light,
extra=extra or "",
size_preset=size_preset,
fit_mode=fit_mode,
background=background or "#FFFFFF",
n_images=int(n_images),
quality=quality,
keep_product=bool(keep_product),
badge_text=badge_text or "",
badge_position=badge_position,
badge_color=badge_color or "#E4287C",
watermark=watermark or "",
write_copy=bool(write_copy),
)
progress(0.15, desc="Собираю промпт")
try:
result = run(brief, openai_key=openai_key, groq_key=groq_key, reference=reference)
except ValueError as exc:
raise gr.Error(str(exc)) from exc
except Exception as exc: # noqa: BLE001
log.exception("Прогон не удался")
raise gr.Error(f"Генерация не прошла: {exc}") from exc
progress(1.0, desc="Готово")
notes = " · ".join(result.notes)
copy_md = result.copy_markdown or "_Тексты карточки не запрашивались._"
return (
result.images,
copy_md,
result.prompt,
result.meta,
result.archive_path,
f"Готово: {len(result.images)} шт. за {result.meta['секунд']} с. {notes}",
)
with gr.Blocks(title=f"{APP_NAME} — {APP_TAGLINE}", theme=THEME, css=CSS, fill_width=True) as demo:
gr.HTML(
f"""
{APP_TAGLINE}{APP_NAME}
{_keys_status()}
") openai_key_in = gr.Textbox( label="Ключ OpenAI", type="password", placeholder="sk-...", visible=settings.allow_user_keys, ) groq_key_in = gr.Textbox( label="Ключ Groq", type="password", placeholder="gsk_...", visible=settings.allow_user_keys, ) extra = gr.Textbox( label="Что добавить или чего избежать", placeholder="Добавь зелёное растение слева, без людей в кадре", lines=2, ) go = gr.Button("Сгенерировать", variant="primary", size="lg", elem_classes="vt-go") # ---------------- Правая колонка: результат ---------------- with gr.Column(scale=6, elem_classes="vt-panel"): status = gr.Markdown("", elem_classes="vt-status") gallery = gr.Gallery( label="Варианты", columns=2, height=620, object_fit="contain", show_download_button=True, preview=True, ) with gr.Row(): archive = gr.File(label="Скачать всё: PNG, meta.json, тексты") with gr.Tab("Тексты карточки"): copy_out = gr.Markdown("Здесь появятся заголовок, буллеты, alt-текст и ключевые слова.") with gr.Tab("Промпт"): prompt_out = gr.Textbox( label="Что ушло в генератор", lines=8, show_copy_button=True ) with gr.Tab("Параметры прогона"): meta_out = gr.JSON(label="meta.json") gr.Examples( examples=EXAMPLES, inputs=[product, details, style, category], label="Примеры брифов", ) gr.Markdown( """Картинки генеративные: перед публикацией сверьте товар с реальностью — площадки штрафуют за расхождение фото и товара. Названия моделей провайдеров меняются, актуальные задаются переменными окружения без правки кода.
""" ) go.click( fn=handle_generate, inputs=[ product, details, style, category, light, extra, reference, keep_product, size_preset, fit_mode, background, n_images, quality, badge_text, badge_position, badge_color, watermark, write_copy, openai_key_in, groq_key_in, ], outputs=[gallery, copy_out, prompt_out, meta_out, archive, status], api_name="generate", concurrency_limit=settings.enable_queue_concurrency, ) if __name__ == "__main__": demo.queue(default_concurrency_limit=settings.enable_queue_concurrency).launch( server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")), show_api=False, ssr_mode=False, )