diff --git a/_blueprint/plan-draft.md b/_blueprint/plan-draft.md index c1b7597..eb2f213 100644 --- a/_blueprint/plan-draft.md +++ b/_blueprint/plan-draft.md @@ -327,48 +327,110 @@ pixels = image.quantize(colors=256) # или custom median-cut HTMX: форма с `hx-post`, `hx-encoding="multipart/form-data"` → SVG вставляется в `#preview`. -### 4. Политика хранения +### 4. Политика хранения, приватность и модели -**Исходные изображения:** удаляются сразу после генерации (не хранятся вообще). +#### 4.1. Принципы приватности и Zero-PII (Без регистрации) +- **Полное отсутствие регистрации для посетителей:** Пользователи не создают аккаунты, не вводят email, пароли или личные данные. Единственный пользователь в `auth.User` — администратор сайта для входа в Django Admin. +- **Исходные растровые изображения (JPG/PNG/WEBP):** Обрабатываются исключительно в оперативной памяти (через буфер `BytesIO`) и **не сохраняются на диск сервера вообще**. +- **Лайки и учет голосов:** + - **Бэкенд:** Голоса учитываются анонимно через связку `(item_id, fingerprint)`. Поле `fingerprint` формируется как необратимый SHA-256 хэш: `sha256(IP + User-Agent + SECRET_KEY)`. Это защищает от примитивной накрутки через очистку кук или инкогнито и не сохраняет персональные данные (Zero-PII). + - **Фронтенд:** Для исключения переполнения cookies и сетевого оверхеда список лайкнутых картин хранится локально на клиенте в **`localStorage`** (до 5–10 МБ). На базе Alpine.js выполняется мгновенная индикация проголосованных работ. +- **Соответствие GDPR / 152-ФЗ:** Персональные данные не собираются и не хранятся. -**Публичные SVG (опционально, по желанию пользователя):** -``` -public/media/temp/{hash_16символов}.svg -``` -- TTL: **14 дней** -- URL: `/view/{hash}` — прямая ссылка, без индексации -- Автоматическая очистка: `manage.py cleanup_temp --older-than 1209600` → crontab +#### 4.2. Генерация названий (AI & Fallback Title) +У каждой сгенерированной картины обязательно должно быть название (title): +- **Основной способ:** Вызов через **OpenRouter API** с отправкой сильно сжатой миниатюры (thumbnail) исходного изображения в мультимодальную LLM (VLM) для распознавания сюжета и генерации забавного гипно-названия (в стиле Гипножабы/мемов). +- **Резервный способ (Fallback):** Если API OpenRouter недоступен, закончились квоты или таймаут — название генерируется локально из случайного набора гипнотических слов, эпитетов и фраз («Мерцающий транс #42», «Глаз Гипножабы», «Астральный вихрь» и т.п.). -**Галерея (FeaturedSVG):** -- Администратор/promote: лучшие SVG переезжают из `temp/` в `gallery/` -- Хранятся вечно (лучшая коллекция халфтонов) -- Прямая ссылка: `/gallery/{hash}` -- `/gallery` — страница галереи со всеми (пагинация) +#### 4.3. Стратегия хранения и умной очистки (Smart Retention) +Проект не выступает постоянным файлообменником или бесплатным CDN. Пользователь скачивает сгенерированный SVG для личного использования. +Для оптимизации диска на сервере используется динамическая очистка на основе рейтинга популярности («гравитации»), а не слепой 14-дневный таймер: + +$$\text{Score} = \frac{\text{Likes} + \text{Bonus}_{\text{moderator}}}{(\text{Age in hours} + 2)^\gamma}$$ + +- **Квота хранилища:** Задается жесткий лимит объема (например, 500 МБ или N тысяч файлов). +- **Очистка (`manage.py cleanup_storage`):** При превышении лимита удаляются файлы с наименьшим `Score`. +- **Иммунитет к удалению:** Работы, отмеченные модератором (`is_curated=True` / `Bonus`), и закрепленные промо-картины сохраняются бессрочно. + +#### 4.4. SEO, шеринг и коммерциализация +- **Вшивание ссылки в SVG (Zero-cost Branding):** В скачиваемый SVG на этапе генерации добавляется мета-комментарий со ссылкой на генератор (`` и ``). +- **Страница просмотра (`/v/`):** Для внешнего шеринга и индексации поисковиками генерируется легкая HTML-страница с OpenGraph разметкой и кнопкой «Открыть настройки в генераторе». +- **Возможность промо/коммерциализации:** Поддержка опциональных полей (промо-ссылка, бейдж автора/спонсора, счетчик кликов). + +#### 4.5. Блог, статические страницы и типографика (HTML + `etpgrf`) +- **Отказ от конвертации Markdown «на лету»:** Вместо оверхеда с парсингом Markdown статьи и инфо-страницы хранятся напрямую в формате **HTML**. +- **Очистка HTML в админке:** При вводе/сохранении в Django Admin контент проходит валидацию и санитизацию (очистку от вредоносного или мусорного HTML/инлайн-стилей). +- **Типографирование через `etpgrf`:** Для безупречной верстки текста (неразрывные пробелы, висячая пунктуация, кавычки-елочки, тире) используется библиотека типографирования **`etpgrf`**. + +#### 4.6. Архитектура моделей -**Модель `FeaturedSVG`:** ```python -class FeaturedSVG(models.Model): - svg_hash = models.CharField(max_length=16, unique=True) - original_temp_path = models.CharField(max_length=512) - gallery_path = models.CharField(max_length=512) - params_json = models.JSONField(default=dict) - votes = models.PositiveIntegerField(default=0) - promoted_at = models.DateTimeField(auto_now_add=True) - promoted_by = models.ForeignKey('auth.User', null=True, blank=True) +class HalftoneItem(models.Model): + """ + Единая модель для генераций, публичных шеров и витрины галереи. + """ + hash_id = models.CharField(max_length=16, unique=True, db_index=True) + title = models.CharField(max_length=150, verbose_name="Название картины") + svg_file = models.FileField(upload_to="svg/%Y/%m/") + + # Снимок параметров для возможного клонирования настроек + params_json = models.JSONField(default=dict, blank=True) + file_size = models.PositiveIntegerField(default=0, help_text="Размер файла в байтах") + + # Популярность и модерация + likes_count = models.PositiveIntegerField(default=0, db_index=True) + views_count = models.PositiveIntegerField(default=0) reports_count = models.PositiveIntegerField(default=0) + score = models.FloatField(default=0.0, db_index=True, help_text="Рейтинг для Smart Retention") + + is_curated = models.BooleanField(default=False, help_text="Выбор модератора / галерея (иммунитет к удалению)") + is_public = models.BooleanField(default=True, help_text="Доступность по прямой ссылке") + + # Промо-блок (на перспективу) + promo_url = models.URLField(blank=True, default="", verbose_name="Промо-ссылка") + promo_title = models.CharField(max_length=100, blank=True, default="", verbose_name="Бейдж/автор") + promo_clicks = models.PositiveIntegerField(default=0) + + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + + +class HalftoneVote(models.Model): + """ + Анонимный учет голосов (лайков) без сохранения ПДн. + """ + item = models.ForeignKey(HalftoneItem, on_delete=models.CASCADE, related_name="votes") + fingerprint = models.CharField( + max_length=64, + db_index=True, + help_text="SHA256(IP + User-Agent + SECRET_KEY) для базовой защиты от накрутки" + ) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=["item", "fingerprint"], name="unique_item_fingerprint_vote") + ] + + +class BlogPost(models.Model): + """ + Статьи блога, документация и инфо-страницы (Privacy Policy и др.). + + + виртуальные поля для управления типографом etpgrf + """ + title = models.CharField(max_length=200, help_text="HTML-заголовок, очищенный и типографированный etpgrf") + slug = models.SlugField(max_length=200, unique=True) + summary = models.TextField(max_length=500, blank=True, help_text="HTML-контент тизера, очищенный и типографированный etpgrf") + content = models.TextField(help_text="HTML-контент статьи, очищенный и типографированный etpgrf") + cover_image = models.ImageField(upload_to="blog/covers/%Y/", blank=True, null=True) + + is_published = models.BooleanField(default=False) + published_at = models.DateTimeField(null=True, blank=True, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + ``` -**Report / жалоба:** -- `/report/{hash}` — кнопка "Пожаловаться" на странице просмотра -- Просто счётчик `reports_count` в модели (или просто scan для ручного удаления) -- После жалобы — админ проверяет и удаляет вручную - -**Публичный режим:** -Все генерации публичны: -- Любой SVG доступен к скачиванию -- SVG показываются в браузере по `temp/` или `/gallery`, но доступны только `/hypn0/` с hash-именем -- В браузере, показывается "Поделиться ссылкой" - ## Шаги реализации ### Шаг 1. Структура каталогов