mod: планы по моделям, хранению данных, защите от накруток в голосовании.
This commit is contained in:
+96
-34
@@ -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. Структура каталогов
|
||||
|
||||
Reference in New Issue
Block a user