{
  "openapi": "3.0.3",
  "info": {
    "title": "Scanderm Face Visualization API — sber_demo",
    "version": "1.0.0",
    "x-logo": { "altText": "Scanderm" },
    "description": "Сервис визуализации кожи лица. Принимает **фото лица** и возвращает набор **визуализаций** (heatmap-оверлеи и маски проблемных зон) вместе с баллами ML-моделей по каждому признаку.\n\nЭта документация описывает **только** публичный endpoint визуализации по фотографии — остальной функционал платформы Scanderm сюда не включён.\n\n## Аутентификация\nВсе запросы требуют заголовок `X-Internal-Key` с ключом проекта. Для этого демо-стенда уже прошит публичный ключ проекта **`sber_demo`** (нажмите **Authorize** — он подставлен). Этот ключ ограничен **только** API визуализации.\n\n## Два способа получить результат\n\n| Способ | Endpoint | Когда использовать |\n|--------|----------|--------------------|\n| **REST (sync)** | `POST /v1/visualize` | Простой кейс: один HTTP-запрос → полный JSON со всеми визуализациями (~15–25 c) |\n| **SSE (stream)** | `GET /v1/visualize/stream/{session_id}` | Прогрессивный рендер: каждая визуализация прилетает событием по мере готовности, не дожидаясь остальных |\n\n### Как работает SSE\n1. Сгенерируйте `session_id` (UUID) на клиенте.\n2. Откройте SSE-поток `GET /v1/visualize/stream/{session_id}` (заголовок `X-Internal-Key`).\n3. Параллельно отправьте фото `POST /v1/visualize?session_id={session_id}`.\n4. По мере рендера каждой карточки в поток прилетает событие `{ task_id, status:\"done\", affected_percent, score, image_url }`.\n5. Завершение потока — событие `{ \"type\": \"complete\" }`.\n\n> **Swagger «Try it Out» и SSE.** OpenAPI/Swagger *описывает* SSE-endpoint (тип ответа `text/event-stream`), но кнопка «Try it Out» **не умеет** отрисовывать живой поток — она ждёт закрытия соединения и показывает накопленный текст. Поэтому для наглядной демонстрации SSE используйте блок **«Live SSE demo»** на странице документации (он шлёт реальные события и рисует визуализации в реальном времени).\n\n## Каталог визуализаций (module = face)\nЗа один запрос возвращается до 16 карточек. Каждая карточка — это `image_url` (готовая склейка фото + оверлей, JPEG, presigned-ссылка на S3) + балл `score` (0–10, где больше = сильнее выражен признак) + `affected_percent` (для дисплея).\n\n| id | Признак (RU) | Признак (EN) | Тип визуализации |\n|----|--------------|--------------|------------------|\n| `skin_uniformity` | Однородность тона | Skin uniformity | Тепловая карта + легенда |\n| `oily_shine` | Жирный блеск | Oily shine | Тепловая карта |\n| `scars` | Постакне | Post-acne | Маска зон |\n| `wrinkles` | Морщины | Wrinkles | Маска линий |\n| `redness` | Покраснения | Redness | Маска + легенда |\n| `pores` | Поры | Pores | Маска |\n| `papules` | Воспаления (папулы) | Papules | Маска элементов |\n| `pustules` | Пустулы | Pustules | Маска элементов |\n| `comedons` | Комедоны | Comedones | Маска + легенда (open/closed) |\n| `dark_circles` | Тёмные круги под глазами | Dark circles | Контур зоны + шкала |\n| `dehydration` | Обезвоженность | Dehydration | Тепловая карта |\n| `dullness` | Тусклость кожи | Dullness | Тепловая карта + легенда |\n| `erythem` | Эритема | Erythema | Маска |\n| `tele` | Купероз | Telangiectasia | Маска + легенда |\n| `orientation_field` | Поле ориентации пор | Pore orientation field | Векторный оверлей |\n| `wrinkle_forecast` | Прогноз морщин | Wrinkle forecast | Прогнозный оверлей |\n\nНабор карточек зависит от качества фото и детекции лица: если признак не обнаружен, карточка может отсутствовать."
  },
  "servers": [
    { "url": "https://sber-demo.scanderm.pro", "description": "sber_demo (production demo)" }
  ],
  "tags": [
    { "name": "visualize", "description": "Визуализация кожи по фото" }
  ],
  "components": {
    "securitySchemes": {
      "ProjectKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Internal-Key",
        "description": "Ключ проекта. Для демо-стенда уже подставлен ключ проекта sber_demo."
      }
    },
    "schemas": {
      "TaskResult": {
        "type": "object",
        "description": "Одна карточка-визуализация.",
        "properties": {
          "task_id": { "type": "string", "example": "wrinkles", "description": "Идентификатор признака (см. каталог)." },
          "image_url": { "type": "string", "nullable": true, "description": "Presigned-ссылка (S3, JPEG) на готовую визуализацию: кроп лица + оверлей. Действует ~1 час.", "example": "https://storage.clo.ru/neuro-default-bucket/viz/sber_demo/<sid>/wrinkles.jpg?X-Amz-..." },
          "image_b64": { "type": "string", "nullable": true, "description": "JPEG в base64 (только в debug-режиме)." },
          "overlay_url": { "type": "string", "nullable": true, "description": "RGBA-оверлей с прозрачным фоном (если карточка отдаёт отдельный слой)." },
          "overlay_b64": { "type": "string", "nullable": true },
          "score": { "type": "number", "nullable": true, "example": 6.0, "description": "Балл ML-модели 0–10 (больше = сильнее выражен признак). null для производных карточек." },
          "affected_percent": { "type": "number", "example": 60.0, "description": "Значение для отображения (0–100)." },
          "colors": { "type": "array", "items": {}, "description": "Палитра [[r,g,b,a], …] для фронт-рендера." },
          "legend": { "type": "array", "items": {}, "description": "Легенда [{label,color}, …] для карточек с категориями (redness, comedons, …)." },
          "layers": { "type": "array", "items": {}, "description": "Переключаемые слои визуализации." },
          "error": { "type": "string", "nullable": true }
        }
      },
      "VisualizeResponse": {
        "type": "object",
        "properties": {
          "session_id": { "type": "string", "example": "0b9c2c2e-2f8a-4d77-9b1f-7d2c1d4e9a10" },
          "status": { "type": "string", "enum": ["done", "error"], "example": "done" },
          "backend": { "type": "string", "example": "both", "description": "ML-бэкенд: localbotnet | ml_direct | both." },
          "age": { "type": "number", "nullable": true, "example": 40.0, "description": "Предсказанный возраст кожи." },
          "scores": {
            "type": "object",
            "additionalProperties": { "type": "number" },
            "description": "Сырые баллы ML по нейро-именам (0–10).",
            "example": { "Wrinkles_number": 6.0, "Greasy_shine_number": 5.0, "Redness_number": 1.0, "uneven_tone_texture_number": 5.0, "Enlarged_pores_number": 3.0 }
          },
          "results": {
            "type": "object",
            "additionalProperties": { "$ref": "#/components/schemas/TaskResult" },
            "description": "Карта id-визуализации → карточка."
          },
          "cropped_face_url": { "type": "string", "nullable": true, "description": "Presigned-ссылка на выровненный кроп лица (базовое изображение для всех оверлеев)." },
          "cropped_face_b64": { "type": "string", "nullable": true, "description": "Кроп лица в base64 (для demo/debug)." },
          "warning": { "type": "string", "nullable": true },
          "error": { "type": "string", "nullable": true }
        }
      },
      "SSEEvent": {
        "type": "object",
        "description": "Одно SSE-событие (строка `data: <json>`). Бывает четырёх видов.",
        "properties": {
          "task_id": { "type": "string", "description": "id визуализации (для событий прогресса/готовности).", "example": "wrinkles" },
          "status": { "type": "string", "enum": ["done", "error"], "example": "done" },
          "affected_percent": { "type": "number", "example": 60.0 },
          "score": { "type": "number", "nullable": true, "example": 6.0 },
          "image_url": { "type": "string", "nullable": true, "description": "Готовая визуализация — приходит в «image-ready» событии.", "example": "https://storage.clo.ru/.../wrinkles.jpg?X-Amz-..." },
          "type": { "type": "string", "enum": ["complete", "error"], "description": "Присутствует только в финальных событиях потока.", "example": "complete" },
          "session_id": { "type": "string" },
          "error": { "type": "string", "nullable": true }
        }
      }
    }
  },
  "security": [ { "ProjectKey": [] } ],
  "paths": {
    "/v1/visualize": {
      "post": {
        "tags": ["visualize"],
        "summary": "Визуализировать кожу по фото (REST, sync)",
        "description": "Загрузите фото лица — получите JSON со всеми визуализациями и баллами. Один синхронный запрос, занимает ~15–25 секунд (ML-инференс). \n\nЕсли вы заранее открыли SSE-поток с тем же `session_id`, карточки также прилетят туда по мере готовности.",
        "operationId": "visualize",
        "parameters": [
          { "name": "module", "in": "query", "required": false, "schema": { "type": "string", "enum": ["face", "skin"], "default": "face" }, "description": "Модуль анализа. `face` — лицо (16 визуализаций)." },
          { "name": "session_id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Свяжите запрос с открытым SSE-потоком. Если не задан — сгенерируется автоматически." },
          { "name": "visualizations", "in": "query", "required": false, "schema": { "type": "string", "example": "wrinkles,redness,pores" }, "description": "**Какие визуализации вернуть** — список id через запятую (см. каталог). Фильтрует и REST-ответ (`results`), и SSE-события того же `session_id`. Пусто = все доступные для модуля. Пример: `wrinkles,redness,pores,oily_shine,dark_circles`." },
          { "name": "project_id", "in": "query", "required": false, "schema": { "type": "string", "default": "sber_demo" }, "description": "Идентификатор проекта (используется в пути S3 и для inline-кропа)." }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": { "type": "string", "format": "binary", "description": "Фото лица: JPEG / PNG / WebP, до 10 МБ." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Готовый набор визуализаций.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/VisualizeResponse" },
                "example": {
                  "session_id": "0b9c2c2e-2f8a-4d77-9b1f-7d2c1d4e9a10",
                  "status": "done",
                  "backend": "both",
                  "age": 40.0,
                  "scores": { "Wrinkles_number": 6.0, "Greasy_shine_number": 5.0, "uneven_tone_texture_number": 5.0, "Enlarged_pores_number": 3.0, "Comedones_number": 2.0, "Redness_number": 1.0 },
                  "cropped_face_url": "https://storage.clo.ru/neuro-default-bucket/viz/sber_demo/<sid>/cropped_face.png?X-Amz-...",
                  "results": {
                    "wrinkles": { "task_id": "wrinkles", "score": 6.0, "affected_percent": 60.0, "image_url": "https://storage.clo.ru/.../wrinkles.jpg?X-Amz-...", "legend": [], "layers": [] },
                    "oily_shine": { "task_id": "oily_shine", "score": 5.0, "affected_percent": 50.0, "image_url": "https://storage.clo.ru/.../oily_shine.jpg?X-Amz-..." },
                    "skin_uniformity": { "task_id": "skin_uniformity", "score": 5.0, "affected_percent": 50.0, "image_url": "https://storage.clo.ru/.../skin_uniformity.jpg?X-Amz-...", "legend": [{"label": "однородно", "color": [80,200,120]}] }
                  }
                }
              }
            }
          },
          "400": { "description": "Неверный тип файла (только JPEG/PNG/WebP)." },
          "403": { "description": "Неверный или отсутствующий X-Internal-Key." },
          "413": { "description": "Файл больше 10 МБ." }
        }
      }
    },
    "/v1/visualize/stream/{session_id}": {
      "get": {
        "tags": ["visualize"],
        "summary": "SSE-поток визуализаций (progressive)",
        "description": "Server-Sent Events (`text/event-stream`). Откройте поток **до** или сразу после `POST /v1/visualize` с тем же `session_id`. \n\nЕсли в POST передан параметр `visualizations=a,b,c` — в поток прилетят события **только** по этим визуализациям. \n\nКаждое событие — строка `data: <json>`:\n\n```\ndata: {\"task_id\": \"wrinkles\", \"status\": \"done\", \"affected_percent\": 60.0}\\n\\n\ndata: {\"task_id\": \"wrinkles\", \"status\": \"done\", \"affected_percent\": 60.0, \"score\": 6.0, \"image_url\": \"https://storage.clo.ru/.../wrinkles.jpg?X-Amz-...\"}\\n\\n\ndata: {\"type\": \"complete\", \"session_id\": \"<sid>\"}\\n\\n\n```\n\nВиды событий: **progress** (балл готов, без картинки) → **image-ready** (есть `image_url`) → **complete** (поток завершён). При ошибке — `{ \"task_id\", \"status\":\"error\", \"error\" }` или фатальное `{ \"type\":\"error\", \"error\" }`.\n\n⚠️ Браузерный `EventSource` не умеет слать кастомные заголовки — используйте `fetch()` + чтение `ReadableStream` (как в блоке «Live SSE demo»), либо серверный SSE-клиент.",
        "operationId": "streamResults",
        "parameters": [
          { "name": "session_id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Тот же session_id, что и в POST /v1/visualize." }
        ],
        "responses": {
          "200": {
            "description": "Поток SSE-событий.",
            "content": {
              "text/event-stream": {
                "schema": { "$ref": "#/components/schemas/SSEEvent" },
                "example": "data: {\"task_id\": \"wrinkles\", \"status\": \"done\", \"affected_percent\": 60.0, \"score\": 6.0, \"image_url\": \"https://storage.clo.ru/.../wrinkles.jpg?X-Amz-...\"}\n\ndata: {\"type\": \"complete\", \"session_id\": \"<sid>\"}\n\n"
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": ["visualize"],
        "summary": "Health-check",
        "security": [],
        "operationId": "health",
        "responses": {
          "200": {
            "description": "Сервис жив.",
            "content": { "application/json": { "example": { "status": "ok", "service": "viz" } } }
          }
        }
      }
    }
  }
}
