Загрузить файлы в «/»

Upload glintwine v1.1a
This commit is contained in:
2026-01-01 14:42:44 +03:00
parent 970ba033a4
commit 3f53916c16
4 changed files with 711 additions and 0 deletions

33
Dockerfile Normal file
View 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
View 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
View 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

BIN
ejtan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB