Segmentantor
Универсальный микросервис сегментации изображений. Подключай любую ONNX-модель (SAM, YOLO, SegFormer…) — получай контуры объектов через единый HTTP API.
Архитектура
Segmentantor включает 4 контейнера:
Базовый URL: http://<host>:8090
Поток данных
- Клиент отправляет
POST /segmentс URL тайла и координатами клика - Segmentantor загружает изображение (по URL или из base64)
- Выбранный процессор (SAM по умолчанию) выполняет инференс
- Бинарная маска конвертируется в полигон (контур)
- Контур возвращается клиенту в пиксельных координатах
Жизненный цикл задачи
queued
BRPOPprocessing
success / error
Статусы задачи
| Статус | Описание |
|---|---|
queued | В очереди, ожидает обработки |
processing | Воркер обрабатывает |
success | Успешно, результат доступен |
error | Ошибка при обработке |
Быстрый старт
1. Скачать модели
bash scripts/download_sam.sh
2. Запустить
docker compose up -d
3. Проверить
curl -X POST http://localhost:8090/segment \
-H "Content-Type: application/json" \
-d '{"image_url": "/api/gpkg/file.gpkg/tiles/17/1234/5678/", "click_x": 128, "click_y": 100}'POST /segment
/segment
Основной эндпоинт сегментации. Принимает изображение (URL или base64) и координаты, возвращает контур объекта.
Параметры (JSON body)
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
image_url | string | * | URL изображения. Абсолютный или относительный (резолвится через UPSTREAM_BASE_URL). |
image_b64 | string | * | Изображение в формате base64 (PNG/JPEG). Альтернатива image_url. |
click_x | float | ** | X-координата клика в пикселях (legacy, используйте points). |
click_y | float | ** | Y-координата клика в пикселях (legacy, используйте points). |
points | array | ** | Массив точек [{x, y, label}]. label: 1 = foreground, 0 = background. |
box | object | ** | Ограничивающий прямоугольник {x1, y1, x2, y2}. Можно совмещать с points. |
processor | string | Имя процессора: "sam", "yolo", … По умолчанию из конфига. |
|
cache_key | string | Ключ кэша эмбеддингов. Ускоряет повторные клики по одному тайлу. | |
simplify_tolerance | float | Допуск упрощения контура (Douglas-Peucker), пиксели. По умолчанию: 2.0. |
|
callback_url | string | URL для POST-уведомления при завершении задачи. Разовый — только для этой задачи. |
image_url или image_b64.** Обязателен хотя бы один промпт:
click_x+click_y, points или box.
Ответ (202 Accepted)
{
"task_id": "a1b2c3d4e5f6",
"status": "queued",
"queue_depth": 0
}Параметры результата (после обработки)
| Поле | Тип | Описание |
|---|---|---|
contour | float[][] | Массив точек [[x, y], ...] в пиксельных координатах. |
result_score | float | Уверенность модели (IoU score). |
mask_width | int | Ширина исходного изображения. |
mask_height | int | Высота исходного изображения. |
result_processor | string | Имя процессора. |
duration_ms | float | Время обработки в миллисекундах. |
Коды ответов
| Код | Описание |
|---|---|
202 | Задача поставлена в очередь. |
422 | Невалидные параметры (Pydantic validation). |
Примеры кода
# Одна точка (legacy) curl -X POST http://localhost:8090/segment \ -H "Content-Type: application/json" \ -d '{ "image_url": "/api/gpkg/file.gpkg/tiles/17/1234/5678/", "click_x": 128, "click_y": 100 }' # Несколько точек + box + callback curl -X POST http://localhost:8090/segment \ -H "Content-Type: application/json" \ -d '{ "image_url": "/api/gpkg/file.gpkg/tiles/17/1234/5678/", "points": [ {"x": 128, "y": 100, "label": 1}, {"x": 50, "y": 50, "label": 0} ], "box": {"x1": 30, "y1": 30, "x2": 200, "y2": 200}, "callback_url": "https://my-server.com/webhook" }'
const resp = await fetch('/segment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image_url: '/api/gpkg/map.gpkg/tiles/17/1234/5678/', points: [{ x: 128, y: 100, label: 1 }], cache_key: '17/1234/5678', callback_url: 'https://my-server.com/webhook', }), }); const { task_id, status, queue_depth } = await resp.json();
import requests resp = requests.post('http://localhost:8090/segment', json={ 'image_url': '/api/gpkg/file.gpkg/tiles/17/1234/5678/', 'points': [{'x': 128, 'y': 100, 'label': 1}], 'processor': 'sam', 'cache_key': 'tile-17-1234-5678', 'callback_url': 'https://my-server.com/webhook', }) print(resp.json()) # {"task_id": "...", "status": "queued", "queue_depth": 0}
GET /models
/models
Список зарегистрированных процессоров и их статус.
{
"processors": [
{ "name": "sam", "ready": true, "description": "Segment Anything Model (ViT-B, ONNX quantized)" }
],
"default": "sam"
}GET /health
/health
Healthcheck. Возвращает {"status": "ok"}.
WS /ws/tasks
/ws/tasks
WebSocket стрим событий задач. Каждое сообщение — JSON задачи при смене статуса.
Формат сообщения
{
"id": "a1b2c3d4e5f6",
"created_at_iso": "2026-03-27 18:00:00",
"processor": "sam",
"status": "success",
"duration_ms": 1523.4,
"result_vertices": 42,
"result_score": 0.952
}Пример подключения
const ws = new WebSocket('ws://localhost:8090/ws/tasks'); ws.onmessage = (e) => { const task = JSON.parse(e.data); console.log(`[${task.status}] ${task.id} — ${task.duration_ms?.toFixed(0)}ms`); };
POST /segment/auto
/segment/auto
Автоматическая сегментация — модель сканирует всё изображение сеткой точек и возвращает контуры всех найденных объектов. Промпты (точки, box) не нужны.
Возвращает 202 Accepted с task_id. Результат — через поллинг GET /api/tasks/{'{id}'} или WebSocket.
Тело запроса (JSON)
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
image_b64 | string | — | Base64-кодированное изображение (PNG/JPEG). Одно из image_b64 / image_url обязательно. |
image_url | string | — | URL изображения для загрузки. |
processor | string | "sam" | Имя процессора. Должен поддерживать auto mode. |
cache_key | string | null | Ключ кэша эмбеддинга. |
simplify_tolerance | float | 2.0 | Douglas-Peucker tolerance (пиксели). |
callback_url | string | null | URL для POST-уведомления по завершении. |
points_per_side | int | 32 | Плотность сетки: всего точек = N². Больше — точнее, но медленнее. [2..128] |
pred_iou_thresh | float | 0.88 | Минимальный predicted IoU для сохранения маски. [0..1] |
stability_score_thresh | float | 0.95 | Минимальный stability score маски. [0..1] |
nms_thresh | float | 0.7 | IoU порог для NMS (подавление дубликатов). [0..1] |
min_mask_area | int | 100 | Отбрасывать маски меньше N пикселей. |
Ответ 202
{
"task_id": "abc123...",
"status": "queued",
"queue_depth": 0
}Результат задачи (в GET /api/tasks/{'{id}'})
При status: "success" — поле objects содержит массив найденных объектов:
{
"status": "success",
"objects": [
{
"contour": [[x1,y1], [x2,y2], ...],
"area": 1234,
"score": 0.95
},
...
],
"duration_ms": 28000
}Пример запроса
const resp = await fetch('/segment/auto', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image_b64: '...', processor: 'sam', points_per_side: 32, pred_iou_thresh: 0.88, stability_score_thresh: 0.95, }), }); const { task_id } = await resp.json();
GET /api/tasks
/api/tasks
Список задач с пагинацией (новейшие первыми).
Параметры (query string)
| Поле | Тип | По умолч. | Описание |
|---|---|---|---|
page | int | 1 | Номер страницы (1-based) |
per_page | int | 50 | Кол-во записей (1–200) |
Ответ
{
"tasks": [
{
"id": "a1b2c3d4e5f6",
"created_at_iso": "2026-03-28 12:00:00",
"processor": "sam",
"status": "success",
"duration_ms": 1523.4,
"result_vertices": 42,
"result_score": 0.952
}
],
"total": 128,
"page": 1,
"per_page": 50,
"pages": 3
}Примеры кода
curl "http://localhost:8090/api/tasks?page=1&per_page=20"
import requests resp = requests.get('http://localhost:8090/api/tasks', params={'page': 1, 'per_page': 20}) data = resp.json() print(f"Всего задач: {data['total']}, страниц: {data['pages']}") for task in data['tasks']: print(f" [{task['status']}] {task['id']} — {task.get('duration_ms', '?')}ms")
GET /api/tasks/{'{task_id}'}
/api/tasks/{'{task_id}'}
Статус и результат конкретной задачи.
Ответ (задача завершена)
{
"id": "a1b2c3d4e5f6",
"status": "success",
"processor": "sam",
"duration_ms": 1523.4,
"result_vertices": 42,
"result_score": 0.952,
"result_processor": "sam",
"contour": [[102.0, 85.0], [115.0, 82.0], ...],
"mask_width": 256,
"mask_height": 256,
"preview_input": "/data/previews/a1b2c3d4e5f6_input.png",
"preview_result": "/data/previews/a1b2c3d4e5f6_result.png"
}Примеры кода
curl http://localhost:8090/api/tasks/a1b2c3d4e5f6
import requests resp = requests.get('http://localhost:8090/api/tasks/a1b2c3d4e5f6') if resp.status_code == 404: print('Задача не найдена') else: task = resp.json() print(f"Статус: {task['status']}, вершин: {task.get('result_vertices')}")
Настройки
Настройки по умолчанию для процессоров. Применяются, когда параметры не переданы в запросе.
/api/settings
Получить текущие настройки.
/api/settings
Обновить настройки (deep-merge). Передайте только изменённые поля.
Схема настроек
{
"enabled_processors": ["sam", "yolo"],
"use_gpu": false,
"default_processor": "sam",
"sam": {
"embedding_cache_size": 16,
"simplify_tolerance": 2.0,
"ort_inter_threads": 2,
"ort_intra_threads": 4
},
"yolo": {
"conf_threshold": 0.25,
"iou_threshold": 0.7
}
}Примеры кода
# Получить curl http://localhost:8090/api/settings # Обновить SAM tolerance curl -X PUT http://localhost:8090/api/settings \ -H "Content-Type: application/json" \ -d '{"sam": {"simplify_tolerance": 1.5}}'
import requests # Получить settings = requests.get('http://localhost:8090/api/settings').json() # Обновить updated = requests.put('http://localhost:8090/api/settings', json={ 'sam': {'simplify_tolerance': 1.5} }).json()
Вебхуки
Вебхуки позволяют получать уведомления о завершении задач без поллинга. При регистрации указывается URL, на который будет отправлен POST-запрос с результатом.
Два механизма уведомлений:
- Persistent webhooks — зарегистрированные через API, получают ВСЕ результаты
callback_url— разовый, передаётся в запросеPOST /segment, только для этой задачи
CRUD
/api/webhooks
Зарегистрировать вебхук. Тело — JSON.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
url | string | ✓ | URL для POST-уведомлений |
secret | string | Секрет для HMAC-SHA256 подписи |
/api/webhooks
Список всех зарегистрированных вебхуков (секреты замаскированы).
/api/webhooks/{'{webhook_id}'}
Удалить вебхук по ID.
Payload вебхука
При завершении задачи (статус success или error) на все зарегистрированные URL отправляется POST:
POST https://your-server.com/callback
Content-Type: application/json
X-Webhook-Signature: sha256=a1b2c3d4...
{
"id": "a1b2c3d4e5f6",
"status": "success",
"processor": "sam",
"duration_ms": 1523.4,
"result_vertices": 42,
"result_score": 0.952,
"contour": [[102.0, 85.0], [115.0, 82.0], ...],
"mask_width": 256,
"mask_height": 256
}Подпись HMAC-SHA256
Если при регистрации указан secret, заголовок X-Webhook-Signature содержит HMAC-SHA256 хеш тела.
import hmac, hashlib def verify_signature(body: bytes, secret: str, signature: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)
const crypto = require('crypto'); function verifySignature(body, secret, signature) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); }
callback_url (per-request)
Разовый вебхук — передаётся в POST /segment. Вызывается один раз при завершении только этой задачи.
Формат POST-запроса идентичен persistent-вебхукам, но без X-Webhook-Signature.
Полный пример
# 1. Зарегистрировать вебхук curl -X POST http://localhost:8090/api/webhooks \ -H "Content-Type: application/json" \ -d '{"url": "https://my-server.com/callback", "secret": "my-key"}' # Ответ: {"id": "wh_abc123...", "url": "...", "secret": "***", "created_at": "..."} # 2. Отправить задачу curl -X POST http://localhost:8090/segment \ -H "Content-Type: application/json" \ -d '{"image_url": "/tiles/17/1234/5678/", "click_x": 128, "click_y": 100}' # Результат придёт POST-запросом на https://my-server.com/callback # 3. Посмотреть вебхуки curl http://localhost:8090/api/webhooks # 4. Удалить curl -X DELETE http://localhost:8090/api/webhooks/wh_abc123
import requests BASE = 'http://localhost:8090' # 1. Зарегистрировать hook = requests.post(f'{BASE}/api/webhooks', json={ 'url': 'https://my-server.com/callback', 'secret': 'my-key', }).json() print(hook) # {"id": "wh_...", "url": "...", ...} # 2. Отправить задачу task = requests.post(f'{BASE}/segment', json={ 'image_url': '/tiles/17/1234/5678/', 'click_x': 128, 'click_y': 100, }).json() print(task['task_id']) # 3. Список hooks = requests.get(f'{BASE}/api/webhooks').json() print(hooks) # 4. Удалить requests.delete(f'{BASE}/api/webhooks/{hook["id"]}')
Обработка ошибок
| Код | Описание | Пример |
|---|---|---|
202 | Задача принята в очередь | {"task_id": "...", "status": "queued"} |
404 | Задача или вебхук не найден | {"detail": "Task not found."} |
422 | Невалидные параметры | Pydantic validation error (см. ниже) |
Пример ошибки валидации (422)
{
"detail": [
{
"type": "value_error",
"loc": ["body"],
"msg": "Provide at least one prompt: click_x+click_y, points, or box.",
"input": {}
}
]
}Процессор: SAM (ViT-B)
Segment Anything Model от Meta AI Research. Квантованная ONNX-версия (Xenova/sam-vit-base).
Файлы модели
models/sam/ ├── vision_encoder_quantized.onnx # ~97 MB └── prompt_encoder_mask_decoder_quantized.onnx # ~5 MB
Как работает
- Предобработка: resize longest → 1024px, нормализация, pad до 1024×1024
- Encoder: pixel_values → image_embeddings. Кэшируется по
cache_key. - Decoder: embeddings + point → 3 маски + IoU. Берётся лучшая.
- Постобработка: resize mask, largest component, Moore tracing, Douglas-Peucker.
cache_key повторные клики мгновенные.
Размер кэша: EMBEDDING_CACHE_SIZE (default 64).
Добавление своего процессора
1. Создать файл
# app/processors/yolo.py from .base import BaseProcessor, SegmentResult class YOLOProcessor(BaseProcessor): name = 'yolo' description = 'YOLOv8 instance segmentation' def load(self): self._ready = True def segment(self, image, points, labels=None, *, cache_key=None): return SegmentResult( mask=binary_mask, score=confidence, orig_width=image.shape[1], orig_height=image.shape[0], )
2. Зарегистрировать
# app/processors/__init__.py PROCESSORS = { 'sam': SAMProcessor(), 'yolo': YOLOProcessor(), }
Федерация (Hub ↔ Satellite)
Федерация позволяет подключить удалённые Segmentantor-узлы (satellite) к основному серверу (hub). Satellite-узлы отдают свои воркеры и забирают задачи из общей очереди. GPU-машины, CPU-фермы — всё объединяется в один пул обработки.
/api/federation/.
Satellite общается с Hub по HTTP/HTTPS. Данные хранятся в Redis с TTL.
Архитектура
┌─────────────────────────────────┐
│ HUB │
│ (segmentator.service.ru) │
│ │
│ Worker 1 Worker 2 Worker 3 │
│ (CPU) (CPU) (CPU) │
│ │
│ Redis: │
│ sg:federation:nodes │
│ sg:federation:node:{id} │
│ sg:federation:workers:{id} │
└──────────┬──────────────────────┘
│ HTTPS
┌─────┴─────┐ ┌──────────────┐
│ SATELLITE A│ │ SATELLITE B │
│ (GPU) │ │ (CPU farm) │
│ Worker GPU │ │ Worker 1 CPU │
│ Worker GPU │ │ Worker 2 CPU │
└────────────┘ └──────────────┘Жизненный цикл подключения
список воркеров
(long-poll)
POST /api/federation/connect
/api/federation/connect
Satellite регистрируется на Hub при старте. Отправляет идентификацию, URL и capabilities.
Тело запроса (JSON)
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
node_id | string | ✅ | Уникальный идентификатор узла |
url | string | ✅ | Публичный URL satellite-сервера |
hostname | string | Имя машины (отображается в UI) | |
capabilities | object | Аппаратные возможности | |
capabilities.gpu_available | bool | Есть ли GPU | |
capabilities.gpu_device | string | Название GPU | |
capabilities.max_workers | int | Максимальное число воркеров |
Пример запроса
curl -X POST https://segmentator.service.levkona.ru/api/federation/connect -H "Content-Type: application/json" -d '{
"node_id": "gpu-office-a3f8",
"url": "http://192.168.1.50:8090",
"hostname": "gpu-workstation",
"capabilities": {
"gpu_available": true,
"gpu_device": "NVIDIA RTX 4090",
"max_workers": 4
}
}'Ответ — 200 OK
{
"status": "connected",
"heartbeat_interval": 10,
"node_id": "gpu-office-a3f8"
}Ошибки
| Код | Описание |
|---|---|
400 | node_id и url обязательны |
sg:federation:node:{node_id} с TTL 60 секунд.
Повторный connect с тем же node_id безопасен — данные перезаписываются.
POST /api/federation/heartbeat
/api/federation/heartbeat
Satellite каждые heartbeat_interval секунд сообщает Hub что жив и передаёт полный список своих воркеров.
Тело запроса (JSON)
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
node_id | string | ✅ | Идентификатор зарегистрированного узла |
workers | array | ✅ | Полный список воркеров (или пустой []) |
Поля каждого worker
| Поле | Тип | Описание |
|---|---|---|
id | string | Уникальный ID воркера |
hostname | string | Имя хоста |
pid | int | PID процесса |
status | string | idle, busy, stopped, error |
enabled | bool | Принимает ли задачи |
use_gpu | bool | Использует GPU |
gpu_available | bool | GPU доступна на машине |
gpu_device | string | Название GPU |
current_task | string|null | ID текущей задачи |
tasks_completed | int | Выполнено задач |
tasks_errored | int | Ошибочных задач |
started_at | float | Время запуска (Unix timestamp) |
last_heartbeat | float | Последний heartbeat воркера |
server_url | string | URL satellite (группировка в UI) |
Пример запроса
curl -X POST https://segmentator.service.levkona.ru/api/federation/heartbeat -H "Content-Type: application/json" -d '{
"node_id": "gpu-office-a3f8",
"workers": [
{
"id": "worker-gpu-1",
"hostname": "gpu-workstation",
"pid": 12345,
"status": "idle",
"enabled": true,
"use_gpu": true,
"gpu_available": true,
"gpu_device": "NVIDIA RTX 4090",
"current_task": null,
"tasks_completed": 42,
"tasks_errored": 1,
"started_at": 1776000000.0,
"last_heartbeat": 1776018000.0,
"server_url": "http://192.168.1.50:8090"
}
]
}'Ответ — 200 OK
{
"status": "ok",
"tasks_pending": 3
}Ошибки
| Код | Описание |
|---|---|
400 | node_id обязателен |
404 | Узел не зарегистрирован. Нужно вызвать /api/federation/connect |
sg:federation:workers:{node_id}) и автоматически
включаются в GET /api/workers. При отсутствии heartbeat 30+ секунд — узел offline,
60+ секунд — данные удаляются по TTL.
POST /api/federation/dequeue
/api/federation/dequeue
Satellite запрашивает задачу из очереди Hub. Использует long-polling —
Hub ждёт до timeout секунд, проверяя очередь каждые 0.5 сек.
Тело запроса (JSON)
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
node_id | string | ✅ | Идентификатор узла |
timeout | int | Максимальное время ожидания (сек). По умолчанию 5, максимум 30. |
Ответ — задача найдена
{
"task_id": "seg-20260412-abc123",
"payload": {
"map_id": 42,
"bounds": [[55.75, 37.61], [55.76, 37.62]],
"processor": "sam",
"params": {"threshold": 0.5}
}
}Ответ — задач нет (timeout)
{
"task_id": null,
"payload": null
}broker.enqueue().
Нулевой ответ — нормальная ситуация, satellite продолжает цикл.
POST /api/federation/disconnect
/api/federation/disconnect
Satellite уведомляет Hub при graceful shutdown. Все данные узла мгновенно удаляются из Redis.
Тело запроса (JSON)
{"node_id": "gpu-office-a3f8"}Ответ — 200 OK
{"status": "disconnected"}offline,
через 60 сек Redis TTL удаляет все данные автоматически.
GET /api/federation/nodes
/api/federation/nodes
Список всех зарегистрированных satellite-узлов с их статусом.
Ответ
[
{
"node_id": "gpu-office-a3f8",
"url": "http://192.168.1.50:8090",
"hostname": "gpu-workstation",
"status": "online",
"gpu_available": true,
"gpu_device": "NVIDIA RTX 4090",
"max_workers": 4,
"worker_count": 2,
"connected_at": 1776022330.44,
"last_heartbeat": 1776022339.09
},
{
"node_id": "cpu-farm-b12e",
"url": "http://10.0.1.100:8090",
"hostname": "cpu-server-rack-3",
"status": "offline",
"gpu_available": false,
"gpu_device": "",
"max_workers": 8,
"worker_count": 0,
"connected_at": 1776010000.00,
"last_heartbeat": 1776019900.00
}
]| Поле | Описание |
|---|---|
status | online — heartbeat ≤ 30 сек, offline — устарел |
worker_count | Количество воркеров из последнего heartbeat |
GET /api/workers (объединённый)
/api/workers
Возвращает все воркеры: локальные + с каждого online satellite-узла.
UI группирует их по server_url.
Пример ответа (3 локальных + 1 satellite GPU)
[
{
"id": "worker-pod-dvx6p-1",
"hostname": "worker-pod-dvx6p",
"status": "idle",
"gpu_available": false,
"server_url": "https://segmentator.service.levkona.ru",
"tasks_completed": 150
},
{
"id": "worker-gpu-1",
"hostname": "gpu-workstation",
"status": "idle",
"gpu_available": true,
"gpu_device": "NVIDIA RTX 4090",
"server_url": "http://192.168.1.50:8090",
"tasks_completed": 42
}
]Конфигурация Satellite
Satellite настраивается переменными окружения:
| Переменная | По умолчанию | Описание |
|---|---|---|
PARENT_SERVER_URL | — | URL Hub-сервера. Если не задан — relay не запускается. |
RELAY_SERVER_URL | auto | Публичный URL satellite. Автоопределяется по IP. |
RELAY_NODE_ID | auto | Фиксированный node_id. Автогенерируется как {hostname}-{uuid8}. |
RELAY_POLL_TIMEOUT | 5 | Timeout long-poll dequeue (секунды). |
Пример запуска satellite
docker run -d --name segmentantor-satellite -e PARENT_SERVER_URL=https://segmentator.service.levkona.ru -e RELAY_SERVER_URL=http://192.168.1.50:8090 -e RELAY_NODE_ID=gpu-office-node -e USE_GPU=true -p 8090:8000 reg.git.levkona.ru/services/computer-vision/segmentantor/app:local
Redis-ключи (Hub)
| Ключ | Тип | TTL | Описание |
|---|---|---|---|
sg:federation:nodes | SET | — | Множество всех node_id |
sg:federation:node:{id} | HASH | 60s | Метаданные узла |
sg:federation:workers:{id} | STRING | 60s | JSON-массив воркеров |
Таймауты
| Параметр | Значение | Описание |
|---|---|---|
NODE_TTL | 60 сек | Redis TTL для данных узла |
HEARTBEAT_INTERVAL | 10 сек | Интервал heartbeat |
OFFLINE_THRESHOLD | 30 сек | Порог → offline |
Восстановление при ошибках
Hub вернул 404 на heartbeat
Hub перезапустился и потерял Redis. Satellite автоматически выполняет повторный
connect и продолжает работу.
Satellite упал без disconnect
t = 0 сек — Последний heartbeat
t = 30 сек — Hub помечает узел offline
Воркеры исключаются из GET /api/workers
t = 60 сек — Redis TTL удаляет hash и workers JSONSatellite перезапустился
Повторный connect с тем же node_id безопасен —
данные в Redis просто перезаписываются. Свежий heartbeat обновляет список воркеров.
Exponential backoff
При ошибках соединения satellite увеличивает задержку: 2 сек → 4 сек → 8 сек → … → max 30 сек. Сбрасывается при успешном ответе.
Файлы реализации
| Файл | Роль | Описание |
|---|---|---|
app/federation.py | Hub | Управление узлами, Redis-операции |
app/relay.py | Satellite | Фоновый поток: connect → heartbeat → dequeue |
app/router.py | Hub | FastAPI endpoints /api/federation/* |
app/main.py | Оба | Запуск relay, shutdown handler |
Docker
# Собрать и запустить docker compose up -d # Или вручную docker build -t geo_segmentantor . docker run -d -p 8090:80 geo_segmentantor
Переменные окружения
| Переменная | По умолчанию | Описание |
|---|---|---|
MODELS_DIR | /app/models | Директория с моделями |
DEFAULT_PROCESSOR | sam | Процессор по умолчанию |
UPSTREAM_BASE_URL | http://host.docker.internal:8080 | URL для резолва относительных image_url |
LOG_LEVEL | info | Уровень логирования |
ORT_INTRA_THREADS | 4 | Потоки ONNX внутри оператора |
ORT_INTER_THREADS | 2 | Потоки ONNX между операторами |
EMBEDDING_CACHE_SIZE | 64 | Размер кэша эмбеддингов |
SEGMENTANTOR_PORT | 8090 | Внешний порт nginx |
Структура моделей
models/
├── sam/
│ ├── vision_encoder_quantized.onnx
│ └── prompt_encoder_mask_decoder_quantized.onnx
└── yolo/
└── model.onnxbash scripts/download_sam.sh.