# Пути к файлам в контейнере 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("""
""", 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('
🍷
', unsafe_allow_html=True) with col2: 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("
", 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('

Генератор сертификатов для ежеанонов 🦔

', 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(""" """, unsafe_allow_html=True)