Files
Mavis 2394eff1c0 Initial commit: OmniSVG-Lite MVP before live-streaming work
- LM Studio client (httpx-based, OpenAI-compatible)
- SVG validator (lxml, whitelist tags, no <script>/<foreignObject>/http refs)
- PNG renderer (resvg-py primary, cairosvg fallback - no native cairo dep)
- History (SQLite, tracks raw/validated/preview paths)
- Gradio UI on 127.0.0.1:8788 with:
  * mode radio (icon/illustration)
  * n_candidates slider (default 1)
  * image upload for image-to-SVG
  * LM Studio URL/token inputs
  * model dropdown + refresh button
- prompts/ with system_icon.txt, system_illustration.txt, few_shot_examples.txt
- docs/spec.md, docs/design.md
- 122 unit/integration tests passing
2026-06-13 15:32:54 +03:00

252 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OmniSVG-Lite — Spec
> MVP-спецификация. Цель: дать пользователю веб-приложение, в котором можно
> из текста или картинки получить N кандидатов SVG в одном из двух режимов
> (icon / illustration), посмотреть PNG-превью и сохранить историю генераций.
---
## 1. Цели и не-цели
**Цель MVP:**
- Локально запускаемый Gradio UI, который ходит в LM Studio (OpenAI-compatible)
и возвращает валидный SVG + PNG-превью.
- Один пользователь, без авторизации, без облака.
- История генераций хранится в локальной SQLite.
**Не-цели (Out of Scope для MVP):**
- Редактирование SVG в браузере (paint / drag).
- Экспорт в Lottie / GIF / видео.
- Batch-генерация (CSV промптов, генерация по расписанию).
- Авторизация, multi-user, разграничение прав.
- Деплой / Docker / CI.
- Per-mode температура — единый слайдер на UI.
- Fine-tune, RLHF, оценка качества моделей.
- Поддержка моделей, отличных от LM Studio (Ollama, vLLM, OpenAI cloud) — клиент
пишется под OpenAI-compatible chat completions, но другие провайдеры не
тестируются.
---
## 2. Режимы генерации
### 2.1. Mode: `icon`
**Назначение:** маленькие, узнаваемые, плоские или с лёгкой стилизацией иконки
для UI. Допустимы монохром и 2–3 цвета. Геометрия упрощённая, без мелких деталей.
**Требования к SVG:**
- `viewBox="0 0 64 64"` (фиксировано, чтобы иконки были консистентны).
- Геометрия привязана к пиксельной сетке (целочисленные координаты ±0.5).
- Без градиентов со множеством stops, без фильтров (`feGaussianBlur` и т.п.).
- Допустимы: `<path>`, `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>`,
`<polyline>`, `<g>`, `<text>`, базовые `<linearGradient>`/`<radialGradient>`.
**Промпт-стиль (направление для пользователя):**
"outline battery", "filled magnifying glass", "settings gear, line style".
### 2.2. Mode: `illustration`
**Назначение:** полноценные сцены / объекты / паттерны. Больше деталей, больше
цветов, допустимы градиенты и простые фильтры.
**Требования к SVG:**
- `viewBox="0 0 512 512"` (фиксировано).
- Допустимы все валидные теги SVG 1.1 кроме `<script>` и `<foreignObject>`.
- Допустимы простые `<filter>` (без `<feImage>` на внешний URL).
- Палитра ограничена заявленной пользователем (если указана).
**Промпт-стиль:**
"flat illustration of a fox in a forest, autumn colors",
"isometric server room, blue and purple palette".
### 2.3. Сводная таблица
| Параметр | icon | illustration |
|----------------------|---------------------------------|----------------------------------|
| viewBox | `0 0 64 64` | `0 0 512 512` |
| Размер файла (лимит) | 32 KB | 256 KB |
| Допустимые теги | Базовые shapes + g + text | Полный SVG 1.1 минус опасные |
| Градиенты | ≤ 2 stops | без ограничений |
| Фильтры | нет | простые (без feImage на URL) |
| Текст | допустим, но не основной канал | допустим |
| Дефолтный n_candidates | 4 | 2 |
---
## 3. Типы задач
### 3.1. text-to-SVG
- Вход: `prompt: str` (1–1000 символов), `mode ∈ {icon, illustration}`,
`n_candidates: int ∈ [1, 8]`.
- Опционально: палитра (поле в UI, не обязательное).
- Модель получает только текст.
### 3.2. image-to-SVG
- Вход: `prompt: str` (1–1000 символов), `image: PIL.Image | file path`,
`mode ∈ {icon, illustration}`, `n_candidates: int ∈ [1, 8]`.
- Допустимые форматы картинки: PNG, JPEG, WEBP. Максимум 10 MB, до 4096×4096.
- Картинка передаётся в модель как `image_url` (data: URL, base64).
- В промпте добавляется явная инструкция: "Recreate the visual content of the
attached image as SVG. Do not describe, just generate the markup."
---
## 4. Поведение N кандидатов
**Логика выбора лучшего кандидата (для MVP — простая, детерминированная):**
1. Из N ответов модели берём только валидные (прошедшие lxml-валидацию).
2. Если валидных 0 — показываем ошибку `"no_valid_candidates"` (см. §6).
3. Каждому валидному кандидату считаем **score = размер_файла_KB** (инвертированный:
меньше = лучше). Это прокси "компактность", не качество.
4. Лучший = кандидат с минимальным score.
5. **Показ в UI:**
- `Gallery` отображает PNG-превью всех валидных кандидатов в сетке.
- Лучший помечается визуально: бейдж "★ best" над PNG.
- В выпадашке/таблице истории сохраняются все валидные SVG + пути к PNG.
**Замечание:** для MVP не используем VLM-as-judge или pairwise — это сильно
увеличит latency и стоимость. Выбор по размеру файла — намеренно простой.
---
## 5. User Stories + Acceptance Criteria
### US-1. Сгенерировать иконку по текстовому промпту
**As a** дизайнер,
**I want** ввести текстовое описание иконки и получить N вариантов SVG,
**so that** я могу быстро подобрать визуал для кнопки/меню.
**Acceptance Criteria (Given/When/Then):**
- **Given** UI запущен и LM Studio отвечает, **when** я выбираю mode=`icon`,
ввожу `prompt="filled magnifying glass"`, `n_candidates=4`, **then**:
- отправляется ровно 1 запрос в LM Studio с `n=4`,
- получаю 4 PNG-превью в Gallery (или меньше, если часть невалидна),
- у лучшего есть бейдж "★ best",
- история в SQLite содержит запись со всеми raw/validated SVG.
- **Given** промпт пустой или длиннее 1000 символов, **when** я нажимаю
Generate, **then** вижу `gr.Warning` "Prompt must be 11000 characters",
запрос в LM Studio НЕ отправляется.
- **Given** LM Studio не отвечает 30+ секунд, **when** идёт генерация, **then**
запрос отменяется, UI показывает ошибку "LM Studio timeout".
### US-2. Сгенерировать иллюстрацию по картинке
**As a** иллюстратор,
**I want** загрузить референс-картинку и текстовое описание, получить N SVG,
**so that** я могу быстро сделать vector-версию понравившегося эскиза.
**Acceptance Criteria:**
- **Given** UI запущен, **when** я загружаю PNG/JPEG/WEBP, ввожу
`prompt="flat illustration, blue and teal palette"`, выбираю
mode=`illustration`, `n_candidates=2`, **then**:
- картинка кодируется в base64 data: URL и отправляется в LM Studio,
- получаю до 2 PNG в Gallery,
- все raw-ответы и валидные SVG попадают в историю.
- **Given** загружена картинка > 10 MB или > 4096×4096, **when** я нажимаю
Generate, **then** UI отклоняет с ошибкой валидации до отправки в LM Studio.
### US-3. Посмотреть и скачать результат
**As a** пользователь,
**I want** увидеть PNG-превью, скачать выбранный SVG и PNG,
**so that** я могу сразу использовать результат в своих задачах.
**Acceptance Criteria:**
- **Given** генерация завершилась с ≥ 1 валидным кандидатом, **when** я смотрю
на Gallery, **then**:
- каждая картинка кликабельна,
- под/возле картинки — кнопки `Download SVG` и `Download PNG`,
- скачиваются именно те файлы, которые видны на превью (source of truth —
путь в `preview_paths`).
- **Given** я нажимаю `Download SVG`, **then** браузер получает файл с MIME
`image/svg+xml` и расширением `.svg`.
### US-4. Просмотреть историю генераций
**As a** пользователь,
**I want** видеть таблицу последних генераций и кликнуть строку, чтобы
повторно открыть результаты,
**so that** я могу вернуться к удачному варианту.
**Acceptance Criteria:**
- **Given** UI запущен, **when** я смотрю на History, **then**:
- таблица показывает последние 50 записей (id, prompt, mode, model, n,
status, created_at),
- новые записи появляются вверху,
- строки сортируются по `created_at DESC`.
- **Given** я кликаю строку в History, **when** выбираю запись, **then**
Gallery обновляется превью из `preview_paths` этой записи, raw SVG
подгружается в отдельный code-block для копирования.
- **Given** приложение перезапущено, **when** я открываю History, **then**
данные сохранились (SQLite на диске).
### US-5. Контроль параметров (температура, n)
**As a** пользователь,
**I want** управлять количеством кандидатов и креативностью,
**so that** балансировать скорость/стоимость и разнообразие.
**Acceptance Criteria:**
- **Given** UI запущен, **when** я двигаю `n_candidates` slider, **then**
диапазон 1–8, шаг 1, дефолт зависит от mode (icon=4, illustration=2).
- **Given** UI запущен, **when** я двигаю `temperature` slider, **then**
диапазон 0.0–1.5, шаг 0.05, дефолт 0.4.
- **Given** `temperature=0`, **when** идёт генерация, **then** отправляется
параметр `temperature=0` (детерминированный режим).
---
## 6. Edge Cases (таблица)
| # | Сценарий | Поведение |
|---|---------------------------------------------|--------------------------------------------------------------------------------------------------------|
| 1 | Пустой промпт | `gr.Warning` "Prompt must be 11000 characters", запрос не отправляется. |
| 2 | Промпт > 1000 символов | То же, что (1). |
| 3 | Картинка > 10 MB / > 4096×4096 / не PNG/JPEG/WEBP | Отклоняется до LM Studio, `gr.Warning` с указанием ограничения. |
| 4 | LM Studio не отвечает (timeout 30s) | `gr.Error` "LM Studio timeout at <url>", статус генерации `error`, в лог пишется traceback. |
| 5 | LM Studio вернул 5xx | `gr.Error` "LM Studio error: <code> <body>", статус `error`. |
| 6 | Модель вернула не-SVG (текст/JSON/мусор) | Парсер пытается вытащить `<svg>...</svg>` regex'ом; если не нашёл — кандидат помечается invalid, идёт в history со status=`invalid`, в Gallery не попадает. |
| 7 | SVG не well-formed XML | lxml бросает `XMLSyntaxError` → кандидат invalid, в лог `WARNING`, в history. |
| 8 | Отсутствует / кривой `viewBox` | `ValidatorError("missing_viewbox")` → invalid. |
| 9 | Внутри `<script>` или `<foreignObject>` | `ValidatorError("disallowed_tag")` → invalid. |
| 10| `http://` / `https://` в `href` / `xlink:href` / `url(...)` | `ValidatorError("external_ref")` → invalid. |
| 11| Размер файла > лимита (32 KB / 256 KB) | `ValidatorError("too_large")` → invalid. |
| 12| Все N кандидатов invalid | `gr.Info` "All N candidates failed validation. See history for details.", в history — запись со status=`all_invalid`. |
| 13| Картинка в image-to-SVG повреждена | PIL при открытии бросает `UnidentifiedImageError``gr.Warning` "Cannot read image", запрос не отправляется. |
| 14| Пользователь жмёт Generate повторно во время генерации | Кнопка `interactive=False` на время выполнения, повторный клик игнорируется. |
---
## 7. Не-функциональные требования
| Категория | Требование |
|----------------|------------------------------------------------------------------------------------------|
| Performance | Генерация 1 иконки (n=1) — p95 ≤ 20 секунд на одной 5090 / Ryzen 9. |
| Performance | PNG-рендер одного SVG — p95 ≤ 200 ms. |
| Privacy | Никакие промпты/картинки не уходят за пределы LM Studio на этой машине. |
| Reliability | Падение LM Studio не валит UI: ошибка показывается, остальные кнопки работают. |
| Storage | SQLite WAL mode, `data/omnisvg.db`. Превью PNG — `data/previews/<id>_<n>.png`. |
| Logging | `INFO` для старта/конца генерации, `WARNING` для invalid, `ERROR` для LM Studio errors. |
| Concurrency | MVP — однопользовательский, очередь не требуется. UI блокирует кнопку на время генерации. |
---
## 8. Открытые вопросы
- Требуется ли streaming в UI (как в LM Studio)? **Для MVP — нет**, выбран non-stream
режим: n>1 плохо сочетается со streaming, плюс парсить инкрементально тяжелее.
- Нужна ли кнопка "regenerate" с теми же параметрами? **Не вошла в MVP**, но UI
должен позволять скопировать промпт обратно в textbox.
- Нужен ли экспорт PNG в высоком разрешении (1024×1024)? **Нет**, достаточно
рендера в нативный viewBox.