mod: планы по моделям, хранению данных, защите от накруток в голосовании.

This commit is contained in:
2026-08-17 23:24:08 +03:00
parent 6c23be1f4b
commit 4005373755
+96 -34
View File
@@ -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 на этапе генерации добавляется мета-комментарий со ссылкой на генератор (`<!-- Generated by Hypn0 Generator (https://hypn0.ru) -->` и `<metadata>`).
- **Страница просмотра (`/v/<hash>`):** Для внешнего шеринга и индексации поисковиками генерируется легкая 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. Структура каталогов