sequenceDiagram
autonumber
actor User as Пользователь (Камера)
participant APP as Мобильное приложение (Flutter)
participant API as Бэкенд-шлюз (FastAPI)
participant KAFKA as Брокер событий (Kafka)
participant GRPC as ИИ-сервис (PyTorch)
participant DB as Реляционная СУБД (PostgreSQL)
%% УСТАНОВЛЕНИЕ СОЕДИНЕНИЯ
User->>APP: Наводит камеру на продукт (Томат)
APP->>API: HTTP GET /stream-capture (Handshake Upgrade)
activate API
API-->>APP: 101 Switching Protocols (WebSocket открыт)
%% ПОТОКОВАЯ ПЕРЕДАЧА КАДРОВ
loop Кадр за кадром (Streaming FPS)
APP->>API: Binary Frame (Сырые байты JPEG кадра)
API->>KAFKA: Отправляет байты в топик "media.camera.stream.v2"
end
deactivate API
%% АСИНХРОННЫЙ ИИ-КОНТУР
activate GRPC
KAFKA->>GRPC: Вычитка кадров из топика (Consumer Group)
note over GRPC: Шаг 10: PyTorch (MobileNetV2) расчет весов
GRPC->>GRPC: Объект идентифицирован (TOMATO, confidence: 0.94)
%% ОБРАТНАЯ СВЯЗЬ ЧЕРЕЗ RPC
GRPC->>API: gRPC StreamResponse(predicted_sku='TOMATO')
deactivate GRPC
activate API
API->>APP: JSON: STREAM_PREDICTION_UPDATED (TOMATO)
note over APP: UI подсвечивает рамку и пишет "Томат (94%)"
deactivate API
%% КЛИЕНТСКАЯ ФИКСАЦИЯ И МАРШРУТИЗАЦИЯ
User->>APP: Нажимает на кнопку затвора "Зафиксировать"
APP->>API: Текстовый фрейм: {"action": "FREEZE_FRAME", "sku": "TOMATO"}
activate API
note over API: Шаг 16: Определение типа и ветвление таблиц
API->>DB: INSERT INTO pending_products_buffer (status='PROCESSING')
API->>APP: JSON: SESSION_FROZEN (Переход на экран ввода веса)
deactivate API
note over APP: Стрим камеры закрывается, юзер вводит вес вручную
Метод WS /api/v2/fridge/stream-capture
Документация API: Потоковая верификация продуктов через реальное время (Camera Stream) с использованием буферной очереди Apache Kafka
1 Функциональное назначение
Метод предназначен для обработки потока изображений (кадров) в реальном времени, поступающих непосредственно с камеры мобильного приложения во время наведения на продукт. В отличие от пакетной загрузки готовых файлов, данный контур минимизирует задержку ввода (Latency) и защищает вычислительные ресурсы ИИ-сервера (PyTorch/GPU) от перегрузки при высокой частоте кадров (FPS).
Бэкенд-шлюз FastAPI в этой схеме выступает в роли ультрабыстрого легковесного ретранслятора, который сгружает бинарные кадры в брокер сообщений Apache Kafka, выполняющий роль амортизатора нагрузки (Load Smoother). ИИ-сервис асинхронно вычитывает кадры из очереди, производит инференс и возвращает результат распознавания обратно в WebSocket-сессию пользователя.
2 Протокол взаимодействия (Потоковый Контракт)
- Протокол:
WebSockets (WSS) - Маршрут (Эндпоинт):
/api/v2/fridge/stream-capture/{session_id} - Тип обмена: Дуплексный (Двунаправленный асинхронный поток)
- Базовый формат кадров (Frame Payload): Бинарный поток сжатых изображений (
image/jpegилиimage/webp) + JSON-управляющие сообщения.
2.1 Спецификация заголовков при установлении соединения (Handshake Headers)
Перед переключением протокола (Upgrade) с HTTP на WebSockets клиент Dio / WebSocketChannel обязан передать валидные метаданные.
| Заголовок | Обязательный | Описание | Пример значения |
|---|---|---|---|
Upgrade |
Да | Системный заголовок для переключения протокола | websocket |
Connection |
Да | Системный заголовок для переключения протокола | Upgrade |
Authorization |
Да | Короткоживущий Access Token для валидации сессии пользователя | Bearer eyJhbGciOiJIUzI1Ni... |
X-Request-ID |
Да | Сквозной ID WebSocket-сессии для агрегации логов в Jaeger/Loki | stream-7a2b-9c1d-00ef |
2.2 Спецификация структуры потоковых сообщений (Stream Payload)
В рамках одной открытой сессии клиент отправляет бинарные данные, а сервер возвращает структурированный JSON с результатами ИИ-классификации.
2.2.1 Входящий поток от клиента (Client-to-Server, Binary/Text)
Мобильное приложение отправляет кадры в бинарном формате для экономии трафика. Кадры сжимаются на стороне Flutter до разрешения 640x480 (дефолт для MobileNetV2).
| Тип фрейма | Обязательный | Описание | Формат данных |
|---|---|---|---|
Binary Frame |
Да | Сырые байты текущего кадра с камеры смартфона | ByteBuf (image/jpeg) |
Text Frame (Ping) |
Нет | Системный фрейм поддержания соединения (каждые 5 сек) | {"type": "PING"} |
2.2.2 Исходящий поток от сервера (Server-to-Client, JSON)
Бэкенд возвращает результаты распознавания только тогда, когда ИИ-сервис преодолевает порог уверенности (Confidence Threshold > 0.85).
| Поле | Тип | Обязательный | Описание |
|---|---|---|---|
event_type |
String | Да | Тип события потока |
confidence |
Float | Да | Степень уверенности ИИ в распознавании объекта |
predicted_sku |
String | Да | Распознанный технический код продукта питания |
frame_status |
String | Да | Инструкция для UI (HOLD — удерживать, DETECTED — зафиксирован) |
3 Схема обработки запроса пользователя (Mermaid)
На диаграмме представлена сквозная архитектура стрим-контура (Версия 2). Бэкенд-шлюз FastAPI изолирован от ИИ-нагрузки: он работает как транслятор фреймов в Apache Kafka. Kafka выступает амортизатором (буфером), из которого ИИ-сервис асинхронно забирает кадры на инференс, предотвращая падение системы при высоком FPS.
4 Расшифровка шагов
- Шаг 1 (
User -> APP): Пользователь открывает экран «Умная камера» во Flutter-приложении и наводит объектив смартфона на продукт (например, лежащий на столе томат). - Шаг 2 (
APP -> API): Приложение инициируетHTTP GETзапрос на эндпоинт/stream-capture, передавая авторизационный токен и уникальныйX-Request-ID. Запрашивается обновление протокола до WebSockets. - Шаг 3 (
API -> APP):FastAPIпроизводит валидацию токена. Если сессия подтверждена, сервер возвращает статусHTTP 101 Switching Protocols. Устанавливается постоянное дуплексное TCP-соединение. Бэкенд переходит в режим ожидания фреймов (activate API). - Шаг 4 (
APP -> API): Внутри бесконечного цикла мобильное приложение захватывает кадры с видеобуфера камеры, сжимает их до разрешения640x480в форматJPEGи непрерывно отправляет в WebSocket-канал в виде бинарных фреймов (Binary Frame). - Шаг 5 (
API -> KAFKA): Бэкенд-шлюзFastAPIработает в асинхронном неблокирующем режиме. Он не сохраняет файлы на диск и не вызывает ИИ-модели. Получив бинарный фрейм, он моментально упаковывает его в массив байт и выполняет методproducer.send(), отправляя кадр в топикmedia.camera.stream.v2. На этом контекст шлюза освобождается (deactivate API). - Шаг 6 (
KAFKA -> GRPC): ИИ-сервис на базе PyTorch работает как изолированный потребитель (Consumer). Он вычитывает бинарные кадры из топикаmedia.camera.stream.v2. Если приложение шлет 30 кадров в секунду, а видеокарта (GPU) успевает обработать только 15, Kafka удерживает оставшиеся 15 кадров в очереди, не перегружая память ИИ-сервиса. - Шаг 7 (
GRPC -> GRPC): Внутренний шаг ИИ-сервиса: каждый извлеченный из очереди кадр пропускается через сверточную нейросетьMobileNetV2. Модель рассчитывает вектор вероятностей. - Шаг 8 (
GRPC -> GRPC): Модель фиксирует совпадение: на картинке обнаружен объект с кодом класса, соответствующим техническому SKU"TOMATO", с высоким коэффициентом уверенности (confidence = 0.94). - Шаг 9 (
GRPC -> API): ИИ-сервис открывает обратный gRPC-канал к шлюзу и отправляет сообщениеStreamResponse(predicted_sku='TOMATO', confidence=0.94). Воркер ИИ-сервиса уходит на чтение следующего кадра из Kafka. - Шаг 10 (
API -> APP):FastAPIпринимает gRPC-ответ, находит нужную WebSocket-сессию пользователя поsession_idи отправляет текстовый JSON-пакет с событиемSTREAM_PREDICTION_UPDATED. - Шаг 11 (
APP -> APP): Интерфейс мобильного приложения мгновенно реагирует на сигнал: поверх превью камеры отрисовывается зеленая рамка-таргет, а пользователю выводится подсказка: «Томат (94%). Удерживайте для фиксации». - Шаг 12 (
User -> APP): Пользователь видит, что ИИ правильно распознал продукт, и нажимает на экране физическую кнопку затвора «Зафиксировать продукт». - Шаг 13 (
APP -> API): Вместо отправки тяжелой фотографии, приложение шлет в открытый WebSocket легковесный текстовый JSON-фрейм команды:{"action": "FREEZE_FRAME", "sku": "TOMATO"}. - Шаг 14 (
API -> DB): ШлюзFastAPIперехватывает команду фиксации, останавливает трансляцию кадров из Kafka и обращается к PostgreSQL. Так как ИИ определил тип как одиночный продукт, срабатывает логика маршрутизации: бэкенд выполняетINSERT INTO pending_products_bufferс дефолтным статусомPROCESSING. - Шаг 15 (
API -> APP): Сервер отправляет в вебсокет финальный статусSESSION_FROZEN. Соединение закрывается. На стороне Flutter экран камеры блокируется, и приложение автоматически перенаправляет пользователя на форму ручного ввода, где ему остается только указать примерный вес томата (например,0.3 кг), как это описано в стандарте Версии 1.
5 Спецификация сообщений брокера и вилок исключений
5.1 Спецификация топика Apache Kafka: media.camera.stream.v2
В этот топик бэкенд-шлюз FastAPI непрерывно сбрасывает кадры, полученные из бинарных фреймов WebSocket. Для оптимизации пропускной способности данные передаются в бинарном виде (сериализация ByteArray), а метаданные упаковываются в заголовки сообщения (Kafka Headers).
- Стратегия партиционирования (Partition Key):
session_id(гарантирует, что все кадры в рамках одной сессии камеры попадают на одну партицию и обрабатываются ИИ строго в хронологическом порядке). - Формат Key:
String(значениеsession_id). - Формат Value:
Byte[](сырой бинарный поток сжатого JPEG-изображения). - Конфигурация заголовков сообщения (Kafka Headers):
| Ключ заголовка | Тип данных | Описание | Пример значения |
|---|---|---|---|
x-request-id |
String | Сквозной ID сессии для распределенного трейсинга | stream-7a2b-9c1d-00ef |
frame_timestamp |
String (ISO) | Время захвата кадра на мобильном устройстве | 2026-07-01T21:10:05.123Z |
user_id |
String | Идентификатор владельца сессии | usr-8822-fa41 |
5.2 Спецификация управляющих текстовых фреймов (JSON)
5.2.1 Команда фиксации кадра от клиента (Шаг 13)
Отправляется в WebSocket-канал в текстовом формате при нажатии пользователем кнопки затвора.
{
"action": "FREEZE_FRAME",
"timestamp": "2026-07-01T21:10:12.450Z",
"payload": {
"verified_sku": "TOMATO",
"confidence_at_freeze": 0.942
}
}5.2.2 Ответ сервера о заморозке сессии (Шаг 15)
Финальное системное сообщение от FastAPI, подтверждающее успешную запись в буфер и закрытие стрима.
{
"event_type": "SESSION_FROZEN",
"timestamp": "2026-07-01T21:10:13.110Z",
"data": {
"draft_id": "8a71d11e-9500-4b11-9a2c-d2b0d7b3dcba",
"target_buffer_table": "pending_products_buffer",
"next_ui_step": "NAVIGATE_TO_MANUAL_WEIGHT_INPUT"
}
}5.3 Вилки исключений и стратегии обработки ошибок (Failures & Fail-Safe)
Стриминговый контур чувствителен к сетевым задержкам и стабильности соединений. Система обрабатывает три основных аварийных сценария:
5.3.1 Сценарий А: Отказ ИИ-сервиса или падение gRPC (Зависание инференса)
Если ИИ-сервис уходит в offline или перестает отвечать на gRPC-запросы шлюза FastAPI, срабатывает стратегия Client-Side Timeout / Fallback.
- Поведение системы: Если в течение 3 секунд от сервера не приходит ни одного обновления
STREAM_PREDICTION_UPDATED, Flutter-приложение мягко переключает интерфейс в режим ручной съемки. На экран выводится тост-уведомление, а кнопка затвора меняет логику: при нажатии отправляется стандартный пакетныйHTTP POST /product-photoпо сценарию Версии 1. Система не блокирует пользователя.
5.3.2 Сценарий Б: Ошибка переполнения буфера или отказ Kafka (HTTP 503)
Выбрасывается на Шаге 5, если брокер Kafka недоступен или внутренний буфер бэкенда переполнен из-за аномально высокого FPS от клиента.
{
"error_code": "ERR-STREAM-BUFFER-OVERFLOW",
"message": "Поток камеры временно перегружен. Пожалуйста, удерживайте камеру неподвижно.",
"details": {
"reason": "Kafka broker partition leader is unavailable or local producer memory buffer is full."
}
}5.3.3 Сценарий В: Разрыв сетевого соединения в процессе стриминга (Close Code)
Если у пользователя пропадает 4G/Wi-Fi соединение на Шаге 4, WebSocket-сессия аварийно закрывается операционной системой.
- Поведение системы:
FastAPIловит системное событиеWebSocketDisconnect. Бэкенд немедленно отправляет команду в Kafka на удаление промежуточных необработанных кадров этой сессии, очищая партицию. В базе данныхPostgreSQLтранзакция черновика аннулируется. При восстановлении сети Flutter-приложение генерирует новыйsession_idи начинает процесс с Шага 2.