Метод POST /api/v1/purchase/upload-voice

Документация API: Добавление продуктов через готовый снимок с автоматической gRPC ИИ-модерацией

Published

June 11, 2026

1 Функциональное назначение

Метод реализует логику быстрого добавления продуктов питания в «цифровой холодильник» через отправку снимка (jpg, png). Фотография продукта либо чек со списком продуктов.

2 Протокол взаимодействия (HTTP Контракт)

  • Метод: POST
  • Маршрут: /api/v1/purchase/upload-voice
  • Формат данных: multipart/(.mp3/.ogg)

2.1 Спецификация тела запроса (Request Body)

Поле Тип Обязательный Описание
TBD String Да Файл (например: "*.mp3/.ogg")

3 Схема обработки запроса пользователя (Mermaid)

sequenceDiagram
    autonumber
    actor User as Мобильный клиент (App)
    participant GW as FastAPI Gateway (backend-api)
    participant S3 as MinIO Object Storage
    participant K as Apache Kafka Cluster
    participant AudioAI as grpc-analytics (AID)
    participant Censor as censorship-control-worker (SECURITY)

    %% ШАГ 1: ЗАГРУЗКА ФАЙЛА И ПУШ ЗАДАЧИ
    User->>GW: POST /api/v1/purchase/upload-voice (Multipart Form Data)
    GW->>S3: PutObject: Сохранение .mp3/.ogg бинарного потока
    S3-->>GW: Возврат audio_file_url
    Note over GW: Шаг 3: Генерация X-Request-ID (trace_id)<br/>Сборка payload с app_lang
    GW->>K: Пуш в топик: bpds.inventory.in.voice.stream
    GW-->>User: HTTP 202 Accepted (Файл загружен, сессия в обработке)

    %% ШАГ 2: ТРАНСКРИБАЦИЯ
    K->>AudioAI: Handler: ProcessVoiceStream()
    AudioAI->>S3: GetObject: Скачивание бинарного аудио по URL
    S3-->>AudioAI: Бинарный аудио-поток
    Note over AudioAI: Шаг 5: Инференс локального Whisper Tiny<br/>Перевод звуковых волн в сырую строку текста
    AudioAI->>K: Пуш в топик: bpds.inventory.in.receipt.upload

    %% ШАГ 3: ЦЕНЗУРА И ВЕТВЛЕНИЕ
    K->>Censor: Handler: ProcessUploadText() (Общий контур фильтрации)
    Note over Censor: Анализ строки, инкремент Redis сессий<br/>и валидация через Ollama Container
    alt Исход 10а: Чисто + ИИ уверен
        Censor->>K: Пуш в топик: bpds.inventory.out.receipt.parsed
    else Исход 10б: Чисто + ИИ НЕ уверен
        Censor->>K: Пуш в топик: bpds.mdm.in.product.process
        K-->>User: Трансляция черновика (draft_data) во Вкладку модерации
    else Исход 10в / 12б: Обнаружен мат/спам
        Censor->>K: Пуш в топик: bpds.aid.out.profanity.violate
    end

4 Расшифровка шагов

4.1 Шаги 1–6: Загрузка файла и первичная фиксация

  • Шаг 1 (User -> APP): Пользователь во Flutter-интерфейсе нажимает кнопку «ПАКЕТ ЧЕКА v1» и выбирает фотографию из галереи или камеры.
  • Шаг 2 (APP -> NGINX): Приложение через клиент Dio отправляет бинарный файл изображения методом HTTP POST /product-photo (формат multipart/form-data) на прокси-сервер.
  • Шаг 3 (NGINX -> API): Nginx принимает запрос и перенаправляет (проксирует) его на бэкенд-шлюз FastAPI. В бэкенде активируется контекст обработки (activate API).
  • Шаг 4 (API -> S3): FastAPI вызывает асинхронный метод s3_client.upload_fileobj(), отправляя бинарный поток фотографии апельсина/чека на сохранение в бакет MinIO S3.
  • Шаг 5 (API -> DB): Бэкенд делает запись в базу данных PostgreSQL: INSERT INTO pending_receipts_buffer (status='PROCESSING'). Этим он резервирует буфер и генерирует уникальный UUID (draft_id) для черновика.
  • Шаг 6 (API -> NGINX): Бэкенд мгновенно, не дожидаясь ИИ, возвращает прокси-серверу ответ HTTP 202 Accepted вместе со сгенерированным draft_id = UUID.

4.2 Шаги 7–13: Асинхронный ИИ-инференс, генерация тикета и Push-уведомление

  • Шаг 7 (NGINX -> APP): Nginx транслирует ответ HTTP 202 в мобильное приложение. Экран смартфона освобождается для пользователя, он может идти дальше по приложению.
  • Шаг 8 (API -> GRPC): На бэкенде фоновый воркер async_bupar_ai_worker инициирует удаленный вызов к ИИ-сервису через метод gRPC CheckProduct(draft_id, s3_key, crop_type). Шлюз FastAPI временно деактивируется, а gRPC-сервер активируется (activate GRPC).
  • Шаг 9 (GRPC -> S3): ИИ-сервис на базе PyTorch обращается к хранилищу MinIO S3 и скачивает сырые байты картинки по переданному адресу s3_key.
  • Шаг 10 (GRPC -> GRPC): Внутренний шаг ИИ-сервиса: нейросеть PyTorch (MobileNetV2) рассчитывает тензор весов. На выходе определяется ID класса 950, который автоматически маппится в строку "ORANGE".
  • Шаг 12 (GRPC -> API): ИИ-сервис возвращает структурированный gRPC-ответ ImageResponse(status='PENDING', info='ORANGE') обратно на бэкенд FastAPI. Работа gRPC-сервера завершается.
  • Шаг 12 (API -> DB): FastAPI снова активен. Он валидирует ответ (что пришла именно строка, а не JSON) и создаёт запись в БД: INSERT INTO product_moderation_tickets (status='PENDING') для ручного подтверждения.
  • Шаг 13 (API -> KAFKA): Бэкенд отправляет асинхронное сообщение send_to_kafka_async("analytics.dlq.v1"). Этот инцидент SKU логируется в брокер сообщений для команды MLOps (чтобы позже дообучать нейросеть).

4.3 Шаги 14–21: Ручное исправление, транзакция в БД и обновление остатков

  • Шаг 14 (API -> APP): Бэкенд через открытый WebSocket-канал /ws/push-stream/{id} отправляет клиенту push-сигнал "NEW_MODERATION_TICKET". У пользователя во Flutter мгновенно обновляется вкладка «Модерация ИИ» и выводится карточка ORANGE. Контекст API засыпает.
  • Шаг 15 (User -> APP): Пользователь видит карточку ORANGE, вручную стирает её, пишет "АПЕЛЬСИНЫ" и нажимает кнопку «Утвердить».
  • Шаг 16 (APP -> NGINX): Приложение отправляет запрос HTTP POST /receipt-draft/{UUID}/confirm с телом "qty": 1.0 (тип double) на прокси-сервер.
  • Шаг 17 (NGINX -> API): Nginx перенаправляет этот POST-запрос на бэкенд FastAPI. Бэкенд снова активируется.
  • Шаг 18 (API -> DB): Бэкенд идет в базу данных с запросом SELECT FROM product_moderation_tickets, чтобы проверить статус тикета (защита от повторного подтверждения). В этот момент стартует атомарная SQL-транзакция, внутри которой последовательно выполняются еще три операции:
    1. INSERT INTO bupar_event_log (activity='PURCHASE') — логирование для Process Mining в R.
    2. INSERT INTO fridge_inventory ON CONFLICT DO UPDATE — инкремент количества апельсинов в холодильнике, если они уже там были, или создание новой записи.
    3. UPDATE product_moderation_tickets (status='CONFIRMED') — закрытие тикета.
  • Шаг 19 (API -> KAFKA): После успешного коммита транзакции в БД, бэкенд отправляет событие send_to_kafka_async("bupar.fridge.events.add"), сигнализируя всей системе, что остатки продуктов на складе изменились.
  • Шаг 20 (API -> NGINX): Бэкенд сообщает об успехе, возвращая статус HTTP 201 Created прокси-серверу, и закрывает свой контекст.
  • Шаг 21 (NGINX -> APP): Nginx транслирует статус HTTP 201 Created в мобильное приложение. Приложение ловит этот статус, делает автоматический фоновый запрос GET /fridge/stock (для вычитывания актуального склада), карточка тикета исчезает, и на экране пользователя появляются реальные апельсины.

5 Сквозная бизнес-логика и ИИ-модерация

Процесс обработки запроса живого пользователя разделен на три этапа:

5.1 Этап : HTTP POST (Контур отправки)

MediaType('image', 'jpeg') асинхронно перенаправляется во внутреннее хранилище на метод upload_fileobj() для сохранения в выделенном бакете.

  • Если метод возвращает статус REJECTED, бэкенд прерывает транзакцию и выбрасывает ошибку цензуры ERR-BUCKET (HTTP 400). Ошибка маппится с текстом для модалки.

5.2 Этап : Запись в PostgreSQL

Запись вносится в базу данных. В:

INSERT INTO pending_receipts_buffer (status="PROCESSING", draft_id=uuid)
VALUES (\$1, \$2)
  • Если база данных не доступна возвращает
  • Возвращает HTTP 202 Accepted.

5.3 Этап : gRPC Цензурирование (ИИ-контур модерации)

MediaType('image', 'jpeg') асинхронно перенаправляется во внутренний gRPC-сервер аналитики на метод CheckProduct для проверки на спам и ненормативную лексику.

  • Если ИИ-модератор возвращает статус REJECTED, бэкенд прерывает транзакцию и выбрасывает ошибку цензуры ERR-BUPAR-CENSOR (HTTP 400).
  • Стратегия Fail-Safe: Если gRPC-сервер оффлайн, система пишет предупреждение [Moderation Warning] в системный лог и пропускает запрос по сценарию ограниченного доверия, чтобы не блокировать действия реального человека.

5.4 Этап : Парсинг строки

Чистый текст разбивается по разделителям (пробелам). Первое слово интерпретируется как наименование продукта, второе — преобразуется в тип float как количество (дефолтное значение = 1.0).

5.5 Этап : Фиксация в PostgreSQL

Запись вносится в базу данных. В случае коллизии (если такой продукт уже добавлен в рамках этой семьи), срабатывает логика инкремента остатков:

INSERT INTO fridge_inventory (user_id, home_group_id, product_name, quantity, unit)
VALUES (\$1, \$2, \$3, \$4, 'шт')
ON CONFLICT (home_group_id, product_name) 
DO UPDATE SET quantity = fridge_inventory.quantity + EXCLUDED.quantity;

5.6 Этап : gRPC Цензурирование (ИИ-контур модерации)

Строка raw_text_input асинхронно перенаправляется во внутренний gRPC-сервер аналитики на метод CheckProduct для проверки на спам и ненормативную лексику.

  • Если ИИ-модератор возвращает статус REJECTED, бэкенд прерывает транзакцию и выбрасывает ошибку цензуры ERR-BUPAR-CENSOR (HTTP 400).
  • Стратегия Fail-Safe: Если gRPC-сервер оффлайн, система пишет предупреждение [Moderation Warning] в системный лог и пропускает запрос по сценарию ограниченного доверия, чтобы не блокировать действия реального человека.

5.7 Этап : Фиксация в PostgreSQL

Запись вносится в базу данных. В случае коллизии (если такой продукт уже добавлен в рамках этой семьи), срабатывает логика инкремента остатков:

INSERT INTO fridge_inventory (user_id, home_group_id, product_name, quantity, unit)
VALUES (\$1, \$2, \$3, \$4, 'шт')
ON CONFLICT (home_group_id, product_name) 
DO UPDAT


## Спецификация ответов сервера

### Успешное добавление (201 Created)
```json
{
  "status": "SUCCESS",
  "message": "Продукт 'Яблоки' успешно отмодерирован и добавлен текстовым контуром."
}

5.8 Ошибка модерации (400 Bad Request)

Возвращается, если ИИ-контур зафиксировал мат или спам в текстовом вводе реального пользователя.

{
  "code": "ERR-BUPAR-CENSOR",
  "detail": "Текст не прошел модерацию: обнаружена ненормативная лексика или спам."
}