vitrina / app.py
nvikou's picture
Витрина: первый деплой
4448e70 verified
Raw History Blame Contribute Delete
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,
)