Download app.py from nvikou/vitrina: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/spaces/nvikou/vitrina/resolve/main/app.py
- Command line
-
hf download hf://spaces/nvikou/vitrina/app.py
-
curl -L -o app.py https://huggingface.co/spaces/nvikou/vitrina/resolve/main/app.py
13.6 kB
| """Витрина — генератор иллюстраций и текстов карточек для маркетплейсов. | |
| Изображения рисует 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""" | |
| <header class="vt-header"> | |
| <div class="vt-mark"></div> | |
| <div> | |
| <h1>{APP_NAME}</h1> | |
| <p>{APP_TAGLINE}</p> | |
| </div> | |
| </header> | |
| """ | |
| ) | |
| 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"<p class='vt-note'>{_keys_status()}</p>") | |
| 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( | |
| """ | |
| <p class="vt-note"> | |
| Картинки генеративные: перед публикацией сверьте товар с реальностью — площадки | |
| штрафуют за расхождение фото и товара. Названия моделей провайдеров меняются, | |
| актуальные задаются переменными окружения без правки кода. | |
| </p> | |
| """ | |
| ) | |
| 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, | |
| ) | |