"""Витрина — генератор иллюстраций и текстов карточек для маркетплейсов. Изображения рисует 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_NAME}

{APP_TAGLINE}

""" ) with gr.Row(equal_height=False): # ---------------- Левая колонка: бриф ---------------- with gr.Column(scale=4, min_width=360, elem_classes="vt-panel"): product = gr.Textbox( label="Что за товар", placeholder="Термокружка из нержавеющей стали, 450 мл, матовый графит", lines=2, ) details = gr.Textbox( label="Детали, которые должны попасть в кадр", placeholder="Двойные стенки, крышка-поилка, логотипа нет", lines=2, ) with gr.Row(): style = gr.Dropdown( label="Стиль съёмки", choices=list(STYLE_PRESETS), value="Белый фон (главное фото)", ) category = gr.Dropdown( label="Категория", choices=list(CATEGORY_PRESETS), value="Универсально", ) light = gr.Radio( label="Свет", choices=list(LIGHT_PRESETS), value="Мягкий студийный", ) with gr.Accordion("Фото товара", open=False): reference = gr.Image( label="Загрузи фото, если товар уже существует", type="pil", height=200, ) keep_product = gr.Checkbox( label="Сохранить товар с фото (пересъёмка сцены)", value=False, info="Без галочки фото используется только как описание для промпта.", ) with gr.Accordion("Формат и качество", open=True): with gr.Row(): size_preset = gr.Dropdown( label="Размер под площадку", choices=list(SIZE_PRESETS), value="Wildberries — 900×1200", ) fit_mode = gr.Radio( label="Кадрирование", choices=["Обрезать по краям", "Вписать целиком"], value="Обрезать по краям", ) with gr.Row(): n_images = gr.Slider( label="Сколько вариантов", minimum=1, maximum=settings.max_images, step=1, value=2, ) quality = gr.Radio( label="Качество", choices=["Черновик", "Рабочее", "Финал"], value="Рабочее", ) background = gr.ColorPicker(label="Цвет полей", value="#FFFFFF") with gr.Accordion("Плашка и подпись", open=False): badge_text = gr.Textbox( label="Текст плашки", placeholder="Держит тепло 12 часов", lines=2, info="Текст рисуем поверх картинки шрифтом — генератор буквы пишет плохо.", ) with gr.Row(): badge_position = gr.Dropdown( label="Где", choices=list(BADGE_POSITIONS), value="Слева сверху" ) badge_color = gr.ColorPicker(label="Цвет плашки", value="#E4287C") watermark = gr.Textbox(label="Водяной знак", placeholder="черновик · не для публикации") with gr.Accordion("Тексты и ключи", open=False): write_copy = gr.Checkbox( label="Написать тексты карточки", value=True, info="Заголовок, буллеты, alt-текст и ключевые слова. Нужен ключ Groq.", ) gr.Markdown(f"

{_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, )