Загрузить файлы в «/»
Upload glintwine v1.1a
This commit is contained in:
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Устанавливаем системные зависимости для шрифтов и изображений
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libgl1-mesa-glx \
|
||||
libglib2.0-0 \
|
||||
libsm6 \
|
||||
libxext6 \
|
||||
libxrender-dev \
|
||||
libfontconfig1 \
|
||||
libfreetype6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Создаём рабочую директорию
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем зависимости
|
||||
COPY requirements.txt .
|
||||
|
||||
# Устанавливаем Python зависимости
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем все файлы проекта
|
||||
COPY . .
|
||||
|
||||
# Создаём папки для данных
|
||||
RUN mkdir -p gallery
|
||||
|
||||
# Открываем порт для Streamlit
|
||||
EXPOSE 8501
|
||||
|
||||
# Команда для запуска
|
||||
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
||||
657
app.py
Normal file
657
app.py
Normal file
@@ -0,0 +1,657 @@
|
||||
# Пути к файлам в контейнере
|
||||
import os
|
||||
|
||||
# Базовый путь для данных
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Путь к файлу для хранения resolutions
|
||||
CSV_FILE = os.path.join(BASE_DIR, 'resolutions.csv')
|
||||
|
||||
# Используем OpenSans шрифт
|
||||
FONT_PATH = os.path.join(BASE_DIR, 'opensans.ttf')
|
||||
|
||||
# Создание папки для галереи
|
||||
GALLERY_DIR = os.path.join(BASE_DIR, 'gallery')
|
||||
os.makedirs(GALLERY_DIR, exist_ok=True)
|
||||
|
||||
# Пути к шаблонам
|
||||
TEMPLATES = {
|
||||
"Еж-тян": os.path.join(BASE_DIR, "ejtan.png"),
|
||||
"Ежунья": os.path.join(BASE_DIR, "ejunya.png"),
|
||||
"Оливьешка": os.path.join(BASE_DIR, "olive.png"),
|
||||
"Опер-тян": os.path.join(BASE_DIR, "opertan.png"),
|
||||
"Фортран & Co": os.path.join(BASE_DIR, "fortranco.png"),
|
||||
"Фунтик": os.path.join(BASE_DIR, "funtik.png")
|
||||
}
|
||||
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import random
|
||||
from datetime import datetime
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import shutil
|
||||
import base64
|
||||
|
||||
# Функция для инициализации CSV
|
||||
def initialize_csv():
|
||||
df = pd.DataFrame(columns=['Resolution', 'Category', 'Mascot', 'Timestamp', 'ImagePath'])
|
||||
df.to_csv(CSV_FILE, index=False)
|
||||
|
||||
# Проверка и инициализация CSV
|
||||
if not os.path.exists(CSV_FILE) or os.path.getsize(CSV_FILE) == 0:
|
||||
initialize_csv()
|
||||
else:
|
||||
try:
|
||||
pd.read_csv(CSV_FILE)
|
||||
except (pd.errors.EmptyDataError, pd.errors.ParserError):
|
||||
initialize_csv()
|
||||
|
||||
# Список категорий и маскотов
|
||||
CATEGORIES = ["Новогоднее поздравление", "Ебать ты молодец", "Эпик фейл"]
|
||||
MASCOTS = ["Еж-тян", "Ежунья", "Оливьешка", "Опер-тян", "Фортран & Co", "Фунтик"]
|
||||
|
||||
# Проверка существования файлов шаблонов
|
||||
def check_templates():
|
||||
missing_templates = []
|
||||
for name, path in TEMPLATES.items():
|
||||
if not os.path.exists(path):
|
||||
missing_templates.append(f"{name}: {path}")
|
||||
return missing_templates
|
||||
|
||||
# Проверка шрифта
|
||||
def check_font():
|
||||
if not os.path.exists(FONT_PATH):
|
||||
return False
|
||||
return True
|
||||
|
||||
# Пул заголовков для каждой категории
|
||||
title_pools = {
|
||||
"Новогоднее поздравление": [
|
||||
"Заверяющий официальное пожелание анона",
|
||||
"Подтверждает новогоднее настроение",
|
||||
"Вручается в честь наступления очередного витка этой вселенной",
|
||||
"Официально разрешает радоваться Новому году",
|
||||
"Подтверждает наличие оливьешки",
|
||||
"На законное распитие всякого под бой курантов",
|
||||
"За сохранение новогоднего духа в условиях треда",
|
||||
"Выдан без очереди, потому что новый год",
|
||||
"Сертифицирует тёплые пожелания от анона к анону",
|
||||
"На выдачу новогоднего настроения в треде"
|
||||
],
|
||||
"Ебать ты молодец": [
|
||||
"Выдаётся охуенному молодцу",
|
||||
"Выдаётся по причине охуеть ты молодец",
|
||||
"Вручается за ультимативную годноту",
|
||||
"Вручается за запредельный уровень молодцовости",
|
||||
"Подтверждает факт: ты реально ебать какой молодец",
|
||||
"Выдан ебать какому молодцу",
|
||||
"За действия, от которых аноны охуели",
|
||||
"Официальное признание: охуенно справился",
|
||||
"За вклад в общее дело и локальный ахуй",
|
||||
"За демонстрацию силы постинга и абсолютной годноты"
|
||||
],
|
||||
"Эпик фейл": [
|
||||
"За полнейший пиздец",
|
||||
"Вручается тотальному серуну",
|
||||
"Вручается за эталонный проёб всего и сразу",
|
||||
"О фиксации эпик фейла",
|
||||
"За вклад в фонд коллективного серунства",
|
||||
"Подтверждает: так обосраться - это надо уметь",
|
||||
"За достижение дна и уверенное его пробитие",
|
||||
"О признании фейлом года без права апелляции",
|
||||
"За демонстрацию того, как делать НЕ надо",
|
||||
"Официальное признание постера позором Ежача"
|
||||
]
|
||||
}
|
||||
|
||||
# Пул комментариев для каждой категории
|
||||
comment_pools = {
|
||||
"Новогоднее поздравление": [
|
||||
"Всем Ежачом верим, что так и будет!",
|
||||
"Зафиксировано и заверенно, теперь сбудется, инфа 146%",
|
||||
"Эти пожелания искренны, я гарантирую это!",
|
||||
"Заверено Оператором, так что точно сбудется",
|
||||
"Пожелания занесены в аналлы Ежача",
|
||||
"Ежач видел - значит так тому и быть",
|
||||
"Так и будет, сомневаться запрещено",
|
||||
"Записано, сохранено, пути назад нет",
|
||||
"Теперь это официальная реальность (в разработке)",
|
||||
"С этого момента считается пророчеством"
|
||||
],
|
||||
"Ебать ты молодец": [
|
||||
"Так держать!",
|
||||
"Ты заслужил уважение всех анонов",
|
||||
"Это было эпично!",
|
||||
"Такого ещё не видел никто",
|
||||
"Твой подвиг войдет в историю",
|
||||
"Оче годно, одобряемо всем тредом",
|
||||
"Мощно, анон",
|
||||
"Это был просто отвал жопы",
|
||||
"Восхищаемся всем Ежачом",
|
||||
"Это просто вау"
|
||||
|
||||
],
|
||||
"Эпик фейл": [
|
||||
"Ты обосрался на глазах у всего треда",
|
||||
"Господи, зачем ты это сделал",
|
||||
"Аноны запомнят это на долгие годы",
|
||||
"Таких фейлов у нас ещё не было",
|
||||
"Даже Оператору стыдно",
|
||||
"Интернет всё помнит",
|
||||
"Аноны в ахуе",
|
||||
"Поздравляем, ты вошёл не в ту историю",
|
||||
"Ну ты и выдал",
|
||||
"Лучше больше не пиши"
|
||||
]
|
||||
}
|
||||
|
||||
# Пул нижнего текста для каждой категории
|
||||
signature_pools = {
|
||||
"Новогоднее поздравление": [
|
||||
"С Новым 2026 годом, анон!",
|
||||
"Желаем исполнения желаний в 2026!",
|
||||
"До встречи в новом году!",
|
||||
"Пусть все обещания сбудутся!",
|
||||
"С наступающим, анончик!",
|
||||
"Новый год уже здесь!",
|
||||
"2026 начинается прямо сейчас!",
|
||||
"Береги себя в новом году!",
|
||||
"С теплом, от анона к анону!",
|
||||
"Увидимся в 2026!"
|
||||
],
|
||||
"Ебать ты молодец": [
|
||||
"Так держать и дальше!",
|
||||
"Продолжай в том же духе!",
|
||||
"Ты - легенда!",
|
||||
"Гордимся тобой, анон!",
|
||||
"За тобой будущее Ежача!",
|
||||
"Это было красиво!",
|
||||
"Ежач одобряе!",
|
||||
"Достойно!",
|
||||
"Не останавливайся!",
|
||||
"Уважение заслужено!"
|
||||
],
|
||||
"Эпик фейл": [
|
||||
"Позор зафиксирован",
|
||||
"Это не забудут",
|
||||
"Удалить уже не получится",
|
||||
"С этим тебе жить",
|
||||
"Фейл принят без возражений",
|
||||
"Не пиши больше",
|
||||
"Это просто пиздец",
|
||||
"Невероятный кринж",
|
||||
"Смеялись всем тредом",
|
||||
"Борды - это не твоё"
|
||||
]
|
||||
}
|
||||
|
||||
# Определение текстовых областей для каждой позиции
|
||||
# Формат: (x, y, max_width, line_height)
|
||||
text_areas = {
|
||||
"title": (840, 610, 800, 50), # Заголовок
|
||||
"content": (840, 740, 800, 40), # Основной текст
|
||||
"comment": (840, 1120, 800, 40), # Комментарий
|
||||
"signature": (1110, 1260, 600, 35) # Подпись/нижний текст
|
||||
}
|
||||
|
||||
# Фиксированные размеры шрифтов
|
||||
font_sizes = {
|
||||
"title": 40,
|
||||
"content": 34,
|
||||
"comment": 34,
|
||||
"signature": 28
|
||||
}
|
||||
|
||||
# Цвет текста
|
||||
FONT_COLOR = (232, 201, 165)
|
||||
|
||||
def wrap_text(text, font, max_width):
|
||||
"""Разбивает текст на строки, чтобы он помещался в заданную ширину"""
|
||||
lines = []
|
||||
|
||||
# Если текст уже содержит переносы строк, обрабатываем каждую часть отдельно
|
||||
text_parts = text.split('\n')
|
||||
|
||||
for part in text_parts:
|
||||
words = part.split()
|
||||
current_line = ""
|
||||
|
||||
for word in words:
|
||||
# Проверяем ширину текущей строки с новым словом
|
||||
test_line = f"{current_line} {word}" if current_line else word
|
||||
test_bbox = font.getbbox(test_line)
|
||||
test_width = test_bbox[2] - test_bbox[0]
|
||||
|
||||
if test_width <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
|
||||
return lines
|
||||
|
||||
def draw_wrapped_text(draw, x, y, text, font, max_width, line_height, color):
|
||||
"""Рисует текст с автоматическим переносом"""
|
||||
lines = wrap_text(text, font, max_width)
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
draw.text((x, y + i * line_height), line, font=font, fill=color)
|
||||
|
||||
return len(lines)
|
||||
|
||||
def generate_certificate_image(category, mascot, user_data):
|
||||
"""Генерация изображения сертификата"""
|
||||
# Проверяем существование шаблона
|
||||
template_path = TEMPLATES.get(mascot)
|
||||
|
||||
if not template_path or not os.path.exists(template_path):
|
||||
st.error(f"Шаблон для маскота '{mascot}' не найден!")
|
||||
st.info(f"Ожидаемый путь: {template_path}")
|
||||
return None
|
||||
|
||||
# Проверяем существование шрифта
|
||||
if not os.path.exists(FONT_PATH):
|
||||
st.error(f"Шрифт не найден по пути: {FONT_PATH}")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Загружаем шаблон
|
||||
img = Image.open(template_path)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Загружаем шрифт OpenSans
|
||||
title_font = ImageFont.truetype(FONT_PATH, font_sizes["title"])
|
||||
content_font = ImageFont.truetype(FONT_PATH, font_sizes["content"])
|
||||
comment_font = ImageFont.truetype(FONT_PATH, font_sizes["comment"])
|
||||
signature_font = ImageFont.truetype(FONT_PATH, font_sizes["signature"])
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка при загрузке ресурсов: {str(e)}")
|
||||
return None
|
||||
|
||||
# Выбираем случайный текст из пулов
|
||||
title = random.choice(title_pools[category])
|
||||
comment = random.choice(comment_pools[category])
|
||||
signature_text = random.choice(signature_pools[category])
|
||||
|
||||
# Формируем основной текст в зависимости от категории
|
||||
if category == "Новогоднее поздравление":
|
||||
main_text = f"Пожелания: {user_data.get('promise', '')}"
|
||||
else:
|
||||
main_text = f"Для анона: {user_data.get('username', '')}\nПричина вручения: {user_data.get('reason', '')}"
|
||||
|
||||
# Получаем параметры областей
|
||||
title_x, title_y, title_width, title_line_height = text_areas["title"]
|
||||
content_x, content_y, content_width, content_line_height = text_areas["content"]
|
||||
comment_x, comment_y, comment_width, comment_line_height = text_areas["comment"]
|
||||
signature_x, signature_y, signature_width, signature_line_height = text_areas["signature"]
|
||||
|
||||
# Рисуем текст на изображении с автоматическим переносом
|
||||
# Заголовок
|
||||
draw_wrapped_text(draw, title_x, title_y, title, title_font, title_width, title_line_height, FONT_COLOR)
|
||||
|
||||
# Основной текст
|
||||
draw_wrapped_text(draw, content_x, content_y, main_text, content_font, content_width, content_line_height, FONT_COLOR)
|
||||
|
||||
# Комментарий
|
||||
draw_wrapped_text(draw, comment_x, comment_y, comment, comment_font, comment_width, comment_line_height, FONT_COLOR)
|
||||
|
||||
# Подпись/нижний текст
|
||||
draw_wrapped_text(draw, signature_x, signature_y, signature_text, signature_font, signature_width, signature_line_height, FONT_COLOR)
|
||||
|
||||
# Сохранение временного файла
|
||||
temp_file = os.path.join(BASE_DIR, 'certificate.png')
|
||||
try:
|
||||
img.save(temp_file)
|
||||
return temp_file
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка при сохранении сертификата: {str(e)}")
|
||||
return None
|
||||
|
||||
# Функция для загрузки изображения и конвертации в base64
|
||||
def get_image_base64(path):
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, "rb") as img_file:
|
||||
return base64.b64encode(img_file.read()).decode()
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка при чтении файла: {str(e)}")
|
||||
return None
|
||||
return None
|
||||
|
||||
# Функция для обновления категории
|
||||
def update_category(new_category):
|
||||
"""Обновляет категорию и очищает соответствующие поля"""
|
||||
st.session_state.category = new_category
|
||||
if new_category == "Новогоднее поздравление":
|
||||
st.session_state.username = ''
|
||||
st.session_state.reason = ''
|
||||
else:
|
||||
st.session_state.promise = ''
|
||||
|
||||
# Настройка страницы
|
||||
st.set_page_config(
|
||||
page_title="Glintwine for ejchan",
|
||||
page_icon="🍷",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
# Кастомный CSS для оформления
|
||||
st.markdown("""
|
||||
<style>
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.logo-img {
|
||||
height: 60px;
|
||||
width: 30px;
|
||||
margin-right: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.service-title {
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
color: #2E8B57;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.service-subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.footer-container {
|
||||
margin-top: 50px;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.footer-text {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background-color: rgba(46, 139, 87, 0.1);
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
.main-title {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 15px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.stButton > button {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.stButton > button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.text-info {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border-left: 3px solid #2E8B57;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Шапка с логотипом и названием сервиса
|
||||
logo_path = None
|
||||
for ext in ['.svg', '.png', '.jpg', '.jpeg', '.webp']:
|
||||
test_path = os.path.join(BASE_DIR, f'logo{ext}')
|
||||
if os.path.exists(test_path):
|
||||
logo_path = test_path
|
||||
break
|
||||
|
||||
# Отображаем шапку
|
||||
st.markdown("""
|
||||
<div class="header-container">
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
col1, col2, col3 = st.columns([1, 5, 1])
|
||||
with col1:
|
||||
if logo_path:
|
||||
st.image(logo_path, width=60)
|
||||
else:
|
||||
st.markdown('<div style="font-size: 48px; margin-right: 20px;">🍷</div>', unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
st.markdown('<p class="service-title">Glintwine for ejchan</p>', unsafe_allow_html=True)
|
||||
st.markdown('<p class="service-subtitle">Drug for dreams does not exists, but you can drink some glintwine. Cause why not?</p>', unsafe_allow_html=True)
|
||||
|
||||
with col3:
|
||||
pass
|
||||
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
|
||||
# Проверка необходимых файлов
|
||||
missing_templates = check_templates()
|
||||
if missing_templates:
|
||||
st.warning("⚠️ Некоторые шаблоны не найдены:")
|
||||
for missing in missing_templates:
|
||||
st.write(f" - {missing}")
|
||||
|
||||
if not check_font():
|
||||
st.error(f"⚠️ Шрифт OpenSans не найден по пути: {FONT_PATH}")
|
||||
|
||||
# Основной заголовок
|
||||
st.markdown('<h1 class="main-title">Генератор сертификатов для ежеанонов 🦔</h1>', unsafe_allow_html=True)
|
||||
st.write("Создай уникальный сертификат для себя или другого анона!")
|
||||
|
||||
# Инициализируем session state для хранения данных формы
|
||||
if 'category' not in st.session_state:
|
||||
st.session_state.category = CATEGORIES[0]
|
||||
if 'promise' not in st.session_state:
|
||||
st.session_state.promise = ''
|
||||
if 'username' not in st.session_state:
|
||||
st.session_state.username = ''
|
||||
if 'reason' not in st.session_state:
|
||||
st.session_state.reason = ''
|
||||
if 'mascot' not in st.session_state:
|
||||
st.session_state.mascot = MASCOTS[0]
|
||||
|
||||
# Используем st.radio для выбора категории
|
||||
st.write("### Выбери категорию:")
|
||||
category = st.radio(
|
||||
"Категория",
|
||||
CATEGORIES,
|
||||
index=CATEGORIES.index(st.session_state.category),
|
||||
key="category_radio",
|
||||
label_visibility="collapsed",
|
||||
horizontal=True
|
||||
)
|
||||
|
||||
# Обновляем session state при изменении категории
|
||||
if category != st.session_state.category:
|
||||
update_category(category)
|
||||
|
||||
# Показываем текущую выбранную категорию
|
||||
st.write(f"**Выбрана категория:** {st.session_state.category}")
|
||||
|
||||
# Используем форму с фиксированным ключом
|
||||
with st.form(key='certificate_form'):
|
||||
st.write("### Заполни данные:")
|
||||
|
||||
# Динамические поля в зависимости от категории
|
||||
if st.session_state.category == "Новогоднее поздравление":
|
||||
promise = st.text_area(
|
||||
"Твоё поздравление",
|
||||
value=st.session_state.promise,
|
||||
key='promise_input',
|
||||
height=100
|
||||
)
|
||||
st.session_state.promise = promise
|
||||
|
||||
else:
|
||||
username = st.text_input(
|
||||
"Вставь номер поста или имя анона, кому будет выдано",
|
||||
value=st.session_state.username,
|
||||
key='username_input'
|
||||
)
|
||||
|
||||
reason_label = "Причина молодца" if st.session_state.category == "Ебать ты молодец" else "Причина фейла"
|
||||
reason_example = "Охуенный пост, познавательный" if st.session_state.category == "Ебать ты молодец" else "'Обосрался в дискуссии о чае'"
|
||||
|
||||
reason = st.text_area(
|
||||
f"{reason_label} (например: {reason_example})",
|
||||
value=st.session_state.reason,
|
||||
key='reason_input',
|
||||
height=80,
|
||||
)
|
||||
|
||||
st.session_state.username = username
|
||||
st.session_state.reason = reason
|
||||
|
||||
# Выбор маскота
|
||||
mascot = st.selectbox(
|
||||
"Выберите оформление",
|
||||
MASCOTS,
|
||||
index=MASCOTS.index(st.session_state.mascot) if st.session_state.mascot in MASCOTS else 0,
|
||||
key='mascot_select'
|
||||
)
|
||||
st.session_state.mascot = mascot
|
||||
|
||||
# Дополнительные опции
|
||||
save_to_gallery = st.checkbox("Сохранить в галерею", key='gallery_checkbox')
|
||||
submit_button = st.form_submit_button(label='🎨 Сгенерировать сертификат')
|
||||
|
||||
if submit_button:
|
||||
# Подготавливаем user_data
|
||||
user_data = {}
|
||||
if st.session_state.category == "Новогоднее поздравление":
|
||||
user_data['promise'] = st.session_state.promise
|
||||
else:
|
||||
user_data['username'] = st.session_state.username
|
||||
user_data['reason'] = st.session_state.reason
|
||||
|
||||
# Проверка заполнения полей
|
||||
is_valid = True
|
||||
error_message = ""
|
||||
|
||||
if st.session_state.category == "Новогоднее поздравление":
|
||||
if not user_data.get('promise'):
|
||||
error_message = "Заполните поле 'Твоё поздравление'!"
|
||||
is_valid = False
|
||||
else:
|
||||
if not user_data.get('username'):
|
||||
error_message = "Заполните поле 'Номер поста или имя анона'!"
|
||||
is_valid = False
|
||||
elif not user_data.get('reason'):
|
||||
field_name = "Причина молодца" if st.session_state.category == "Ебать ты молодец" else "Причина фейла"
|
||||
error_message = f"Заполните поле '{field_name}'!"
|
||||
is_valid = False
|
||||
|
||||
if not is_valid:
|
||||
st.error(error_message)
|
||||
else:
|
||||
# Генерация изображения с индикатором загрузки
|
||||
with st.spinner("Генерируем сертификат..."):
|
||||
cert_path = generate_certificate_image(st.session_state.category, st.session_state.mascot, user_data)
|
||||
|
||||
if cert_path:
|
||||
# Отображение сертификата
|
||||
st.success("Сертификат готов!")
|
||||
st.image(cert_path, caption="Ура!")
|
||||
|
||||
# Кнопка скачивания
|
||||
try:
|
||||
with open(cert_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="💾 Скачать сертификат",
|
||||
data=file,
|
||||
file_name="certificate.png",
|
||||
mime="image/png"
|
||||
)
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка при подготовке файла для скачивания: {str(e)}")
|
||||
|
||||
# Сохранение в галерею
|
||||
if save_to_gallery:
|
||||
try:
|
||||
now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
unique_name = f"cert_{now}.png"
|
||||
gallery_path = os.path.join(GALLERY_DIR, unique_name)
|
||||
shutil.copy(cert_path, gallery_path)
|
||||
|
||||
# Сохраняем данные в CSV
|
||||
new_entry = pd.DataFrame([[str(user_data), st.session_state.category, st.session_state.mascot, now, gallery_path]],
|
||||
columns=['Resolution', 'Category', 'Mascot', 'Timestamp', 'ImagePath'])
|
||||
try:
|
||||
df = pd.read_csv(CSV_FILE)
|
||||
except (pd.errors.EmptyDataError, pd.errors.ParserError):
|
||||
initialize_csv()
|
||||
df = pd.read_csv(CSV_FILE)
|
||||
|
||||
df = pd.concat([df, new_entry], ignore_index=True)
|
||||
df.to_csv(CSV_FILE, index=False)
|
||||
|
||||
st.success("✅ Сертификат сохранён в галерее!")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка при сохранении в галерею: {str(e)}")
|
||||
|
||||
# Галерея сертификатов
|
||||
st.header("Галерея сертификатов")
|
||||
st.caption("Последние сгенерированные сертификаты")
|
||||
|
||||
try:
|
||||
if os.path.exists(CSV_FILE) and os.path.getsize(CSV_FILE) > 0:
|
||||
df = pd.read_csv(CSV_FILE)
|
||||
if not df.empty:
|
||||
# Получаем только существующие файлы
|
||||
valid_images = []
|
||||
for i, row in df.iterrows():
|
||||
if os.path.exists(row['ImagePath']):
|
||||
valid_images.append(row)
|
||||
|
||||
if valid_images:
|
||||
# Показываем последние 9 сертификатов (3 ряда по 3)
|
||||
recent_images = list(reversed(valid_images))[:9]
|
||||
|
||||
# Создаем сетку 3x3
|
||||
for i in range(0, len(recent_images), 3):
|
||||
cols = st.columns(3)
|
||||
for j in range(3):
|
||||
if i + j < len(recent_images):
|
||||
with cols[j]:
|
||||
row = recent_images[i + j]
|
||||
try:
|
||||
st.image(row['ImagePath'], caption=f"{row['Category']}")
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка при загрузке изображения")
|
||||
else:
|
||||
st.write("Нет доступных изображений в галерее.")
|
||||
else:
|
||||
st.write("Пока пусто — будь первым!")
|
||||
else:
|
||||
st.write("Пока пусто — будь первым!")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Ошибка при загрузке галереи: {str(e)}")
|
||||
st.write("Попробуйте сгенерировать новый сертификат.")
|
||||
|
||||
# Футер с надписью
|
||||
st.markdown("""
|
||||
<div class="footer-container">
|
||||
<div class="footer-text">
|
||||
Glintwine v1.1a. The d4d project
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
glintwine-generator:
|
||||
build: .
|
||||
container_name: glintwine-generator
|
||||
ports:
|
||||
- "8501:8501"
|
||||
volumes:
|
||||
- ./gallery:/app/gallery # Галерея сертификатов
|
||||
- ./resolutions.csv:/app/resolutions.csv # CSV файл с данными
|
||||
- ./data:/app/data # Общая папка для данных
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
networks:
|
||||
- glintwine-network
|
||||
|
||||
networks:
|
||||
glintwine-network:
|
||||
driver: bridge
|
||||
Reference in New Issue
Block a user