Files
2026-01-01 14:42:44 +03:00

657 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Пути к файлам в контейнере
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)