diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75b0bfb --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..940749d --- /dev/null +++ b/app.py @@ -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(""" + +""", 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(""" +
Glintwine for ejchan
', unsafe_allow_html=True) + st.markdown('Drug for dreams does not exists, but you can drink some glintwine. Cause why not?
', unsafe_allow_html=True) + +with col3: + pass + +st.markdown("