# План: Django микросайт для генерации гипно-SVG ## Стек - **Django 5.x** — бэкенд - **HTMX** — интерактивность без перезагрузки - **Pillow** — обработка изображений, color clustering - **scikit-learn** — k-means для дуотон/трейтон/квантование - **numpy** — векторизованные вычисления - **Gunicorn** — WSGI-сервер - **Poetry** — управление зависимостями ## Структура проекта (как в cadpoint-ru) ``` 2026-hypn0-site/ ├── pyproject.toml # Poetry — общие зависимости ├── poetry.lock ├── .env.sample # Переменные окружения ├── .env # Локальные секреты (не в VCS) ├── Dockerfile # Многостадийная сборка ├── docker-compose.local.yml # Dev: bind-mount, --reload ├── docker-compose.prod.yml # Prod: образ из registry │ ├── config/ │ └── nginx/ # Nginx-конфиги (хост, не в Docker) │ └── hypn0-app--external-nginx.conf │ ├── public/ # Статика + медиа (shared dev/prod) │ ├── static/ │ │ ├── css/ │ │ │ └── site.css # Tailwind CDN + кастомные стили │ │ └── js/ │ │ └── site.js # Alpine/минимальный JS │ └── media/ │ ├── temp/ # Публичные SVG (TTL 14 дней) │ └── gallery/ # Отфильтрованные лучшие │ ├── database/ # SQLite БД │ └── .gitignore │ ├── hypn0/ # Django project root │ ├── manage.py │ ├── hypn0/ # Settings package │ │ ├── __init__.py │ │ ├── settings.py # django-environ для конфига │ │ ├── urls.py │ │ ├── wsgi.py │ │ └── templates/ │ │ ├── base.html │ │ └── errors/ │ │ ├── 404.html │ │ └── 500.html │ │ │ └── halftone/ # Django app │ ├── __init__.py │ ├── urls.py │ ├── views.py # главная + генерация │ ├── forms.py # формы параметров │ ├── models.py # FeaturedSVG + votes │ ├── services/ │ │ ├── __init__.py │ │ └── halftone.py # ядро из main.py │ ├── templates/ │ │ └── halftone/ │ │ └── index.html │ └── management/ │ └── commands/ │ ├── cleanup_temp.py # очистка temp (14 дней) │ └── cleanup_reports.py # удаление по жалобам │ ├── .gitea/workflows/ # CI/CD │ └── docker-publish.yaml │ └── main.py # [удаляем] ``` ## Ключевые принципы (перенос из cadpoint-ru) | Принцип | Реализация | |---------------|------------------------------------------| | Django-проект | `hypn0/` — подкаталог в корне | | Статика/медиа | `public/static/` и `public/media/` | | БД | `database/db.sqlite3` | | Конфиги | `config/nginx/` — внешний nginx | | Docker | многостадийный, bind-mount для dev | | Зависимости | Poetry, `pyproject.toml` в корне | | Env | `django-environ`, `.env.sample` → `.env` | ## Архитектура приложения ### 1. Рефакторинг ядра (`hypn0/halftone/services/halftone.py`) Извлечь из `main.py` чистую функцию: ```python def generate_halftone_svg( image: Image.Image, *, cols: int = 80, max_radius: int = 8, color: str = "#800000", opacity: str = "90", # hex alpha для CSS animation_variants: int = 12, ) -> str: # возвращает SVG-строку ``` **Что убираем из CLI-версии:** - Аргументы `image_path` / `output_path` - Форматирование чисел для CSS-переменных - Запись в файл - `print()` ### 2. View-слой (`hypn0/halftone/views.py`) ```python # Главная — GET → рендерим index.html # Генерация — POST (HTMX) → возвращаем SVG фрагмент # Скачивание — GET /download/{temp_id} → отдаём файл + удаляем исходник # Просмотр публичного — GET /view/{hash} → отдаём SVG из temp/ # Галерея — GET /gallery → список FeaturedSVG # Просмотр в галерее — GET /gallery/{hash} # Report — POST /report/{hash} → increment reports_count # Promote (admin) — админ переносит SVG из temp → gallery ``` ### 3. Фронтенд (`hypn0/halftone/templates/halftone/index.html`) ``` ┌──────────────────────────────────────────────┐ │ 📷 [Загрузить изображение] │ │ hx-post="/generate" │ │ hx-target="#preview" │ ├──────────────────────────────────────────────┤ │ Ширина колонок: [80] │ │ Макс. размер: [8] │ │ Цвет: [#800000] │ │ Анимация: [✓] │ │ Опубликовать: [ ] — показать ссылку │ ├──────────────────────────────────────────────┤ │ [Сгенерировать] │ ├──────────────────────────────────────────────┤ │
│ │ │ └──────────────────────────────────────────────┘ ``` Важно: данная форма -- черновик. Настроек генерации будет больше: - Фигуры (не только кружки, но и квадратики, кольца, треугольники, буквы-цифры, линии (переменной толщины), волнистые линии. - Наклон сетки (угол) - Цветовые модели (дуотон/трейтон с разным наклоном сетки для каждого цвета как в полиграфии, полноцвет) - Возможно разные настройки анимации (групп) Рядом с панелью настроек, превью ("на лету") и кнопка скачать. Галерея отобранных изображений. Под картинками в галере кнопки "понравилось" и "подаваться" ## СЕКЦИЯ B: Алгоритмы и фигуры ### F: Типы фигур | ID | Фигура | Параметры | SVG-реализация | |------------|------------------------|------------------------------|-------------------------------------------------| | `circle` | Круг | `max_radius` | `` | | `square` | Квадрат | `max_size` | `` | | `ring` | Кольцо | `max_radius`, `stroke_width` | `` | | `diamond` | Ромб | `max_size` | `` | | `star` | Звезда 5-лучевая | `outer_r`, `inner_r` | `` | | `cross` | Крест | `max_size`, `arm_width` | `` | | `line-h` | Горизонтальная полоска | `max_width`, `thickness` | `` | | `line-v` | Вертикальная полоска | `max_height`, `thickness` | `` | | `line-d` | Диагональная полоска | `max_length`, `thickness` | Повернутый `` на angle | | `wave` | Волна | `amplitude`, `frequency` | `` | | `hex` | Шестигранник | `max_radius` | `` (6 вершин) | | `triangle` | Треугольник | `max_size` | `` | | `typeface` | Буква/цифра | `font_family`, `font_size` | `...` | **Сложность реализации:** - **Быстро** (фигура по центру, один параметр масштаба): circle, square, ring, diamond, hex, triangle - **Средне** (дополнительные параметры): line-h/v/d, cross, star, wave - **Сложно** (загрузка шрифта): typeface ### G: Типы сеток | ID | Описание | Параметры | Сложность | |-------------|-----------------------------------|---------------------|-----------| | `grid` | Прямая прямоугольная (как сейчас) | `cols`, `rows` | — | | `rotated` | Прямая сетка с наклоном | `cols`, `angle_deg` | Низкая | | `hex_grid` | Шестиугольная сетка | `cols`, `radius` | Средняя | | `isometric` | Изометрическая проекция 60° | `cols` | Средняя | | `polar` | Полярные координаты | `rings`, `spokes` | Высокая | | `bend` | Изогнутая сетка | `cols`, `curve` | Высокая | **Реализация `rotated`:** ```python angle = math.radians(angle_deg) cos_a, sin_a = math.cos(angle), math.sin(angle) rotated_x = x * cos_a - y * sin_a rotated_y = x * sin_a + y * cos_a ``` ### P: Цветовые модели | ID | Описание | Параметры | Сложность | |----------------|-----------------------|-------------------------------|-----------| | `single` | Один цвет | `color_hex`, `opacity` | — | | `duotone` | 2 цвета | `color_a`, `color_b`, `blend` | Средняя | | `tritone` | 3 цвета | `colors[3]`, `blend` | Средняя | | `quad` | 4 цвета | `colors[4]`, `blend` | Средняя | | `palette_auto` | Авто-палитра n цветов | `palette_name`, `n` | Низкая | | `fullcolor` | Квантование цветов | `quantization_levels` | Высокая | **Реализация `duotone/tritone/quad`:** ```python from sklearn.cluster import KMeans pixels = image.convert('RGB').resize((100, 100)).getdata() kmeans = KMeans(n_clusters=n, random_state=42, n_init=1) kmeans.fit(pixels) centers = kmeans.cluster_centers_ # RGB-координаты кластеров # Каждый пиксель маппим на ближайший кластер ``` **Реализация `fullcolor`:** ```python # Median-cut квантование pixels = image.quantize(colors=256) # или custom median-cut ``` ### Q: Анимации | ID | Описание | Параметры | |-----------|----------------------------------|-----------------------------------------------| | `pulse` | Пульсация (как сейчас) | `speed`, `variants`, `scale_min`, `scale_max` | | `rotate` | Медленное вращение (покачивание) | `speed`, `direction` (cw/ccw) | | `wave` | Волна по вертикали (покачивание) | `amplitude`, `frequency`, `speed` | | `sparkle` | Мерцание fade in/out | `variants`, `speed_range` | | `flow` | Поток в направлении | `direction`, `speed` | | `breathe` | Всё вместе "дышит" | `speed`, `scale_range` | | `static` | Без анимации | — | **Реализация:** - Каждая группа (CSS-класс) получает свой `@keyframes` + `animation-delay` - N ключевых кадров в ` │ │ │ │ │ │ │ │ ─── Стиль ───── │ │ │ │ │ ┌─┐┌─┐┌─┐┌─┐ │ │ [♥ liked] [Скачать] │ │ │ │●│┌─┐┌─┐┌─┐ │ └─────────────────────────────┘ │ │ └─┘└─┘└─┘└─┘ │ [Поделиться] 🔗 │ │ circle │ │ │ │ │ │ ─── Сетка ───── │ │ │ ┌─┐┌─┐┌─┐┌─┐ │ │ │ │●│┌─┐┌─┐┌─┐ │ │ │ └─┘└─┘└─┘└─┘ │ │ │ grid │ │ │ │ │ │ ─── Цвет ────── │ │ │ ┌─┐┌─┐┌─┐┌─┐ │ │ │ │●│┌─┐┌─┐┌─┐ │ │ │ └─┘└─┘└─┘└─┘ │ │ │ single │ │ │ │ │ │ ─── Параметры ─ │ │ │ Колонки: [80]│ │ │ Макс. размер: [8]│ │ │ Цвет: #[800000]│ │ │ Прозрачность:[.5]│ │ │ Угол сетки: [0°]│ │ │ Анимация:[▼pulse]│ │ │ │ │ │ ───────────────── │ │ │ │ │ │ [Сгенерировать] │ │ │ │ │ ├───────────────────┴───────────────────────────────────┤ │ ───────────── Галерея ───────────── │ │ [thumb] [thumb] [thumb] [thumb] ... │ │ [♥ 12] [♥ 8] [♥ 23] [♥ 5] │ └───────────────────────────────────────────────────────┘ ``` ### Табы настроек - На десктопе: 2 колонки — слева настройки, справа превью - На мобильном: вертикально — превью сверху, настройки снизу - Табы переключают секции: "Стиль / Сетка / Цвет / Параметры" - HTMX: `hx-trigger="change"` на контролах → превью обновляется без перезагрузки ### Галерея - `GET /gallery` — CSS Grid сетка миниатюр (3-4 колонки) - Каждая карточка: SVG-превью (inline), кнопка ♥ (HTMX vote), кнопка поделиться - `POST /gallery/{hash}/vote` — HTMX запрос, обновляет ♥ счётчик - Пагинация: 24 карточки на странице - `GET /gallery/{hash}` — полноэкранный просмотр SVG с кнопками "Скачать" и "Поделиться" **CSS:** - Dev: Tailwind CDN (``) + Purge content в конфиге - Prod: `collectstatic` → `whitenoise` обслуживает скомпилированную статику HTMX: форма с `hx-post`, `hx-encoding="multipart/form-data"` → SVG вставляется в `#preview`. ### 4. Политика хранения **Исходные изображения:** удаляются сразу после генерации (не хранятся вообще). **Публичные SVG (опционально, по желанию пользователя):** ``` public/media/temp/{hash_16символов}.svg ``` - TTL: **14 дней** - URL: `/view/{hash}` — прямая ссылка, без индексации - Автоматическая очистка: `manage.py cleanup_temp --older-than 1209600` → crontab **Галерея (FeaturedSVG):** - Администратор/promote: лучшие SVG переезжают из `temp/` в `gallery/` - Хранятся вечно (лучшая коллекция халфтонов) - Прямая ссылка: `/gallery/{hash}` - `/gallery` — страница галереи со всеми (пагинация) **Модель `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) reports_count = models.PositiveIntegerField(default=0) ``` **Report / жалоба:** - `/report/{hash}` — кнопка "Пожаловаться" на странице просмотра - Просто счётчик `reports_count` в модели (или просто scan для ручного удаления) - После жалобы — админ проверяет и удаляет вручную **Публичный режим:** Все генерации публичны: - Любой SVG доступен к скачиванию - SVG показываются в браузере по `temp/` или `/gallery`, но доступны только `/hypn0/` с hash-именем - В браузере, показывается "Поделиться ссылкой" ## Шаги реализации ### Шаг 1. Структура каталогов Создать: - `hypn0/` — Django project root - `hypn0/hypn0/` — settings package - `hypn0/halftone/` — Django app - `public/` — static + media - `public/media/temp/` — публичные SVG (TTL 14 дней) - `public/media/gallery/` — лучшие SVG - `database/` — БД - `config/nginx/` — nginx config - `.gitea/workflows/` — CI/CD ### Шаг 2. Зависимости в `pyproject.toml` Добавить: ```toml [tool.poetry.dependencies] django = "^5.2" django-environ = "^0.13" gunicorn = "^25.3" whitenoise = "^6.12" django-htmx = "^1.21" ``` ### Шаг 3. Django settings (`hypn0/hypn0/settings.py`) ```python import environ env = environ.Env(DEBUG=(bool, False)) environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env')) DEBUG = env('DEBUG') SECRET_KEY = env('SECRET_KEY') DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR.parent / 'database' / 'db.sqlite3', } } STATIC_ROOT = BASE_DIR.parent / 'public' / 'static' MEDIA_ROOT = BASE_DIR.parent / 'public' / 'media' MEDIA_URL = '/media/' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' INSTALLED_APPS += ['halftone'] ROOT_URLCONF = 'hypn0.urls' ``` ### Шаг 4. Docker **Dockerfile** (многостадийный, как в cadpoint-ru): - Stage 1 (builder): Poetry → export → pip install - Stage 2 (runtime): gunicorn, non-root user 1000:1000, `collectstatic` **docker-compose.local.yml**: - Bind-mount: `./hypn0`, `./public`, `./database` - Gunicorn `--reload` - Port `8051:8000` **docker-compose.prod.yml**: - Образ из Gitea registry - Host port: `127.0.0.1:8051:8000` - Watchtower для автообновления ### Шаг 5. Nginx-конфиг (`config/nginx/hypn0-app--external-nginx.conf`) Reverse proxy: `hypn0.local` → `127.0.0.1:8051` - Static/media раздается через Nginx (не Django) - GZip, HTTPS (если есть cert) ### Шаг 6. CI/CD (`.gitea/workflows/docker-publish.yaml`) Trigger: tags `v*` → build → push to Gitea registry. Паттерн как в cadpoint-ru. ### Шаг 7. Удаление `main.py` CLI больше не нужен. ## Файлы для создания (итого) | Файл | Назначение | |---------------------------------------------------------|------------------------| | `hypn0/manage.py` | Django CLI | | `hypn0/hypn0/__init__.py` | Settings pkg | | `hypn0/hypn0/settings.py` | Конфиг | | `hypn0/hypn0/urls.py` | URL routing | | `hypn0/hypn0/wsgi.py` | WSGI entry | | `hypn0/hypn0/templates/base.html` | Базовый шаблон | | `hypn0/halftone/__init__.py` | App pkg | | `hypn0/halftone/urls.py` | URL app | | `hypn0/halftone/views.py` | Views | | `hypn0/halftone/forms.py` | Forms | | `hypn0/halftone/models.py` | FeaturedSVG | | `hypn0/halftone/services/halftone.py` | Ядро | | `hypn0/halftone/templates/halftone/index.html` | Главная | | `hypn0/halftone/management/commands/cleanup_temp.py` | Очистка temp (14 дней) | | `hypn0/halftone/management/commands/cleanup_reports.py` | Очистка по жалобам | | `public/static/css/site.css` | Стили | | `public/static/js/site.js` | JS | | `.env.sample` | Env template | | `Dockerfile` | Сборка | | `docker-compose.local.yml` | Dev | | `docker-compose.prod.yml` | Prod | | `config/nginx/hypn0-app--external-nginx.conf` | Nginx | | `.gitea/workflows/docker-publish.yaml` | CI/CD | | `database/.gitignore` | Игнор БД | ## Файлы для модификации 1. `pyproject.toml` — новые зависимости 2. `poetry.lock` — после `poetry lock` ## Файлы для удаления 1. `main.py` — CLI заменён микросайтом ## Верификация 1. `docker compose -f docker-compose.local.yml up` → сайт открывается 2. Загрузка картинки → параметризация → генерация SVG через HTMX 3. SVG отображается в preview с анимацией 4. Скачивание SVG работает 5. `docker compose -f docker-compose.prod.yml` → prod деплой работает 6. CI/CD: тег `v0.1.0` → сборка образа → push ## Производительность: 1 CPU / 1 GB RAM / 10 GB disk **Storage:** - Один SVG: ~50–200 КБ (зависит от cols/max_radius) - 500 публичных SVG в день × 14 дней = ~700 МБ - 10 ГБ — более чем достаточно (галерея + кэш + ОС) **CPU:** - Генерация SVG: Pillow + цикл по пикселям (~6400 точек при cols=80) - Python: ~0.1–0.5 сек на генерацию - Gunicorn: 2–4 воркера → ~10–30 req/min на генерацию - Просмотры галереи: Nginx раздает статику напрямую (минимум нагрузки) **Трафик:** - 100–200 пользователей/день на генерацию — без проблем - Сотни просмотров галереи — без проблем - 500+ генераций/день → вертикальное масштабирование (2 CPU / 2 GB)