511 lines
28 KiB
Markdown
511 lines
28 KiB
Markdown
# План: 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] │
|
||
│ Анимация: [✓] │
|
||
│ Опубликовать: [ ] — показать ссылку │
|
||
├──────────────────────────────────────────────┤
|
||
│ [Сгенерировать] │
|
||
├──────────────────────────────────────────────┤
|
||
│ <div id="preview"></div> │
|
||
│ <div id="actions" class="hidden"> │
|
||
│ <a id="download-link">Скачать</a> │
|
||
│ <a id="public-link" class="hidden">🔗 │
|
||
│ Поделиться</a> │
|
||
│ </div> │
|
||
└──────────────────────────────────────────────┘
|
||
```
|
||
|
||
Важно: данная форма -- черновик. Настроек генерации будет больше:
|
||
- Фигуры (не только кружки, но и квадратики, кольца, треугольники, буквы-цифры, линии (переменной толщины), волнистые линии.
|
||
- Наклон сетки (угол)
|
||
- Цветовые модели (дуотон/трейтон с разным наклоном сетки для каждого цвета как в полиграфии, полноцвет)
|
||
- Возможно разные настройки анимации (групп)
|
||
|
||
Рядом с панелью настроек, превью ("на лету") и кнопка скачать.
|
||
Галерея отобранных изображений. Под картинками в галере кнопки "понравилось" и "подаваться"
|
||
|
||
## СЕКЦИЯ B: Алгоритмы и фигуры
|
||
|
||
### F: Типы фигур
|
||
|
||
| ID | Фигура | Параметры | SVG-реализация |
|
||
|------------|------------------------|------------------------------|-------------------------------------------------|
|
||
| `circle` | Круг | `max_radius` | `<circle r="..."/>` |
|
||
| `square` | Квадрат | `max_size` | `<rect x="-r" y="-r" width="2r" height="2r"/>` |
|
||
| `ring` | Кольцо | `max_radius`, `stroke_width` | `<circle r="..." fill="none" stroke="..."/>` |
|
||
| `diamond` | Ромб | `max_size` | `<polygon points="0,-r -r,0 0,r r,0"/>` |
|
||
| `star` | Звезда 5-лучевая | `outer_r`, `inner_r` | `<polygon points="..."/>` |
|
||
| `cross` | Крест | `max_size`, `arm_width` | `<path d="..."/>` |
|
||
| `line-h` | Горизонтальная полоска | `max_width`, `thickness` | `<rect x="-r" y="-t/2" width="2r" height="t"/>` |
|
||
| `line-v` | Вертикальная полоска | `max_height`, `thickness` | `<rect x="-t/2" y="-r" width="t" height="2r"/>` |
|
||
| `line-d` | Диагональная полоска | `max_length`, `thickness` | Повернутый `<rect/>` на angle |
|
||
| `wave` | Волна | `amplitude`, `frequency` | `<path d="M 0 0 Q r/2 -a r 0 ..."/>` |
|
||
| `hex` | Шестигранник | `max_radius` | `<polygon points="..."/>` (6 вершин) |
|
||
| `triangle` | Треугольник | `max_size` | `<polygon points="0,-r -r,r r,r"/>` |
|
||
| `typeface` | Буква/цифра | `font_family`, `font_size` | `<text>...</text>` |
|
||
|
||
**Сложность реализации:**
|
||
- **Быстро** (фигура по центру, один параметр масштаба): 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 ключевых кадров в `<style>` блока SVG
|
||
- SVG inline — всё работает без внешних ресурсов
|
||
|
||
## СЕКЦИЯ C: UI/UX
|
||
|
||
### Лэаут страницы
|
||
|
||
```
|
||
┌───────────────────────────────────────────────────────┐
|
||
│ 🧠 HypnoSVG [Галерея] [О] │
|
||
├───────────────────┬───────────────────────────────────┤
|
||
│ ◀ Настройки │ ▶ Превью │
|
||
│ │ │
|
||
│ ┌──────────────┐ │ ┌─────────────────────────────┐ │
|
||
│ │ 📷 Загрузить │ │ │ <svg viewBox="..."> │ │
|
||
│ └──────────────┘ │ │ <style>...</style> │ │
|
||
│ │ │ </svg> │ │
|
||
│ ─── Стиль ───── │ │ │ │
|
||
│ ┌─┐┌─┐┌─┐┌─┐ │ │ [♥ 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 (`<script src="https://cdn.tailwindcss.com"></script>`) + 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)
|