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

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

Published

June 11, 2026

WarningОграничение публичной документации

В открытом доступе представлена демонстрационная версия метода. В настоящей публичной документации отображены не все шаги, технические сценарии и приватные эндпоинты для системы цифровых симуляторов бизнес-процессов.

  • Полная спецификация метода: Будет доступна только во внутреннем контуре разработки (Confluence / Swagger Enterprise).

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

Метод предназначен для асинхронной обработки голосовых заметок пользователя, их автоматического перевода в текст с помощью нейросети распознавания речи (Whisper Tiny) и последующего добавления продуктов в холодильник.

В рамках новой архитектуры этот метод выполняет роль поставщика аудио-контекста (Audio Context Provider):

  1. Изоляция медиа-потока: Бинарный аудио-файл сохраняется шлюзом FastAPI в S3-бакет MinIO.
  2. Асинхронная транскрибация (Speech-to-Text): Тяжелый ИИ-инференс вынесен из API-слоя в изолированный воркер grpc-analytics, который транслирует аудиозапись в текстовую строку.
  3. Переиспользование контура фильтрации: Воркер отправляет извлеченную строку в общий топик bpds.inventory.in.receipt.upload. Воркер цензуры проверяет распознанный текст на обсценную лексику (censorship-control-worker) и повторное использование обсценной лексики при редактуре текста.

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

  • Метод: POST
  • Маршрут: /api/v1/purchase/upload-voice
  • Формат данных: multipart/form-data

2.1 Спецификация заголовков (HTTP Headers)

Заголовок Обязательный Описание Пример значения
Content-Type Да Указывает на передачу составных данных формы (мультипарт) multipart/form-data; boundary=----WebKitFormBoundary...
Authorization Да Токен авторизации для извлечения user_id и Multi-Tenancy клейма home_group_id Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Request-ID Да Сквозной ID запроса для трассировки (FastAPI \(\rightarrow\) MinIO \(\rightarrow\) Kafka \(\rightarrow\) Workers) req-voice-77bb-99aa

2.2 Спецификация параметров запроса (Query Parameters)

Параметр Тип Обязательный Описание Пример значения
app_lang String Да Целевой язык приложения для адаптации системного промпта на этапе цензуры ru

2.3 Спецификация тела запроса (Request Body — Multipart FormData)

Поле Тип Обязательный Описание Пример значения
file Binary (File) Да Бинарный аудиофайл голосовой заметки (audio/mpeg, audio/ogg). Ограничение: до 5 МБ. voice_note.ogg

2.3.1 Пример сырого HTTP-запроса (Payload):

POST /api/v1/purchase/upload-voice?app_lang=ru HTTP/1.1
Host: foodlifecycle.com
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID: req-voice-77bb-99aa
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="voice_note.ogg"
Content-Type: audio/ogg

[БИНАРНЫЕ ДАННЫЕ АУДИОПОТОКА]
------WebKitFormBoundary7MA4YWxkTrZu0gW--

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 Расшифровка шагов

Шаг Действие Параметры / Запросы / DTO Ошибки (Исключения / Статусы)
Шаг 1 (Gateway -> MinIO) API-шлюз принимает от клиента бинарный поток аудиосообщения (.mp3 или .ogg), валидирует заголовки и загружает файл в бакет объектного хранилища S3. S3 API PUT Object Request:
Bucket: "user-voice-streams", Key: "uploads/2026/07/voice_audio_99.ogg"
Headers: Content-Type: audio/ogg, Content-Length: 1048576
MinIOException: BucketNotFound
MinIOException: AccessDenied (истекли или неверны секретные ключи доступа шлюза)
StorageFullException
Шаг 2 (MinIO -> Gateway) Объектное хранилище успешно сохраняет медиафайл на дисковом массиве и возвращает шлюзу постоянную или временную (presigned) URL-ссылку на объект. S3 API PUT Object Response:
HTTP Статус: 200 OK
ETag: "68b329da9893e34099c7d8ad5cb9c940", URL: https://minio.internal
MinIOException: ConnectionTimeout (сетевой сбой во время финализации записи блока данных)
DiskWriteError
Шаг 3 (Gateway -> T_Voice) Шлюз формирует стартовый DTO задачи на транскрибацию, внедряет метаданные языка интерфейса, сквозной трассировочный ID и отправляет событие в шину. Kafka Message (Topic: bpds.inventory.in.voice.stream):
Payload: { "audio_file_url": "https://minio.internal", "app_lang": "ru-RU", "x_request_id": "trace-stt-censor-uuid" }
FastAPI.ValidationError (невалидный формат URL)
KafkaException: QueueFullException (буфер отправки брокера переполнен)
MessageTimedOut
Шаг 4 (T_Voice -> AudioAI) Специализированный микросервис анализа речи grpc-analytics, подписанный на топик аудиозадач, вычитывает событие из очереди для обработки. Kafka Consumer Poll Request:
Group_ID: "voice-stt-processors", Получен payload шага 3.
Partition: 0, Offset: 12054
KafkaException: CommitFailedException (сервис завис на предобработке, консьюмер выпал из группы)
SerializationException
Шаг 5 (AudioAI -> MinIO) Сервис транскрибации инициирует внутреннее скачивание файла по полученной ссылке из бакета S3 для его последующей загрузки в память модели. HTTP GET /user-voice-streams/uploads/2026/07/voice_audio_99.ogg:
Headers: Internal Auth Tokens. На выходе: бинарный поток байт аудиофайла во временный буфер.
HTTP 404 Not Found (файл был удален или ссылка сформирована некорректно)
HTTP 403 Forbidden
ConnectTimeout: MinIO endpoint dead
Шаг 6 (AudioAI -> T_Results) Локальная нейросеть Whisper Tiny переводит голос в текст. Полученная сырая строка вместе с контекстом публикуется в общий топик распознанных чеков. Kafka Message (Topic: bpds.inventory.in.receipt.upload):
Payload: { "raw_text_input": "Текст, который распознал Whisper из аудиозаписи с матом", "app_lang": "ru-RU", "x_request_id": "trace-stt-censor-uuid" }
WhisperInferenceError (сбой декодирования кодека или сильный шум)
KafkaException: DeliveryTimeout
KafkaException: RecordTooLargeException
Шаг 7 (T_Results -> Censor) Воркер цензуры считывает распознанный текстовый payload из аудиозаписи для выполнения лингвистической фильтрации и валидации. Kafka Consumer Poll Request:
Group_ID: "censorship-workers", Topic: bpds.inventory.in.receipt.upload
Payload: { "raw_text_input": "Текст с матом из аудиозаписи", "app_lang": "ru-RU", "x_request_id": "trace-stt-censor-uuid" }
KafkaException: CommitFailedException
SerializationException (ошибка парсинга JSON после этапа STT)
Шаг 8 (Censor -> Ollama) Цензор запрашивает семантический анализ и флаг чистоты у контейнера LLM, чтобы выявить завуалированный мат, спам или оскорбления. HTTP POST /api/generate (Ollama API):
{ "model": "llama3", "prompt": "Analyze the following text for profanity. Return exact JSON: { 'is_clean': boolean, 'confidence': float }. Text: 'Текст с матом...'", "stream": false }
ConnectTimeout: Ollama unreachable
HTTP 500 Internal Server Error
ReadTimeout: Inference duration exceeded
Шаг 9 (Censor -> Redis) Параллельно выполняется сверка по локальным словарям мата для app_lang и условное инкрементирование счетчика нарушений для ID пользователя. Redis Command Pipeline:
1. SISMEMBER "dict:profanity:ru-RU" "выделенное_слово"
2. MULTI
3. INCRBY "abuse:counter:user_456" 1
4. EXPIRE "abuse:counter:user_456" 86400
5. EXEC
Redis.ConnectionError: Connection refused
Redis.TimeoutError: Command timed out
Redis.ClusterDownException
Шаг 10а (Censor -> T_Clean_Text) Ветка «Успех»: Если стоп-слова не обнаружены, а ИИ полностью уверен в чистоте транскрибированного текста, данные уходят далее по контуру. Kafka Message (Topic: bpds.inventory.out.receipt.parsed):
Payload: { "clean_text_input": "Чистый текст аудиозаписи", "app_lang": "ru-RU", "x_request_id": "trace-stt-censor-uuid" }
KafkaException: DeliveryTimeout
KafkaException: NotCoordinatorForException
Шаг 10б (Censor -> T_Draft) Ветка «Сомнение»: Если явного мата нет, но ИИ не уверен в контексте, текст аудиозаписи изолируется в топик черновиков для ручной проверки. Kafka Message (Topic: bpds.mdm.in.product.process):
Payload: { "draft_data": { "raw_id": "uuid-stt", "text": "Текст для проверки" }, "app_lang": "ru-RU", "x_request_id": "trace-stt-censor-uuid" }
KafkaException: LeaderNotAvailableException
KafkaException: RecordTooLargeException
Шаг 10в (Censor -> Abuse_Check -> T_Notif) Первичное нарушение из аудио: Обнаружен явный мат. Логический разветвитель видит в Redis Счетчик == 1 и пушит в шину инцидент первого предупреждения. Kafka Message (Topic: bpds.aid.out.profanity.violate):
Payload: { "user_id": "user_456", "violation_count": 1, "app_lang": "ru-RU", "notification_type": "AUDIO_PROFANITY_WARNING" }
KafkaException: BrokerNotAvailable
KafkaException: MessageTimedOut
Шаг 11 (T_Draft -> App) Из топика черновиков сомнительный текст транскрипции выводится на экран приложения в интерфейс модератора для ручной корректировки. gRPC / HTTP Stream (Вкладка модерации):
Payload: { "draft_id": "uuid-draft-stt", "raw_text_input": "Текст для исправления", "app_lang": "ru-RU" }
DioException: connection error
HTTP 401 Unauthorized (истек сессионный токен модератора)
Шаг 12 (App -> T_Results) Модератор отправляет исправленную вручную чистую текстовую версию обратно в систему на повторный цикл цензуры. Kafka Message (Topic: bpds.inventory.in.receipt.upload):
Payload: { "raw_text_input": "Исправленный чистый текст руками модератора", "app_lang": "ru-RU", "x_request_id": "trace-stt-censor-uuid" }
KafkaException: QueueFullException
FastAPI.ValidationError
Шаг 12а (Censor -> T_Clean_Text) Успешный исход модерации: Повторная автоматическая проверка подтверждает чистоту текста после рук модератора. Запись пушится в топик чистых данных. Kafka Message (Topic: bpds.inventory.out.receipt.parsed):
Payload: { "clean_text_input": "Исправленный чистый текст руками модератора", "app_lang": "ru-RU", "x_request_id": "trace-stt-censor-uuid" }
KafkaException: DeliveryTimeout
KafkaException: ConcurrentModificationException
Шаг 12б (Abuse_Check -> T_Notif) Повторный мат при модерации: Если в отредактированном тексте черновика снова найден мат, разветвитель фиксирует Счетчик >= 2 и отправляет триггер блокировки. Kafka Message (Topic: bpds.aid.out.profanity.violate):
Payload: { "user_id": "user_456", "violation_count": 2, "app_lang": "ru-RU", "notification_type": "STRICT_BAN_USER" }
KafkaException: LeaderNotAvailableException
InvalidTopicException

5 Спецификация ответов сервера, вилок исключений и таймаутов

5.1 Успешный асинхронный ответ (HTTP 202 Accepted)

{
  "status": "PROCESSING",
  "data": {
    "trace_id": "req-voice-77bb-99aa",
    "message": "Аудиофайл успешно загружен. Запущен асинхронный контур ИИ-транскрибации Whisper Tiny.",
    "processed_at": "2026-07-21T02:30:00Z"
  }
}

5.2 Исключение: Неразборчивая речь / Сбой транскрибации (Асинхронный Исход 10б)

Формируется воркером, если коэффициент уверенности распознавания аудиомодели Whisper упал ниже 0.85.

{
  "error_code": "ERR-LOW-AUDIO-CONFIDENCE",
  "message": "Не удалось четко распознать голосовую заметку. Требуется ручное подтверждение.",
  "details": {
    "whisper_score": 0.58,
    "audio_snapshot_url": "s3://voice-streams/uuid-88.ogg",
    "action": "Render interactive draft card on UI with voice transcript placeholder field."
  }
}

5.3 Исключение: Ошибка ИИ-цензурирования (Асинхронный Исход 10в)

{
  "error_code": "ERR-BUPAR-CENSOR",
  "message": "Текст не прошел модерацию: обнаружена ненормативная лексика или спам.",
  "details": {
    "reason": "Spam or explicit content detected by Ollama Container within audio transcript."
  }
}

5.4 Политика распределенных таймаутов (Deadlines)

  • Таймаут инференса Whisper Tiny (Audio Worker): timeout = 3.0s (3000 миллисекунд). Выделяется на скачивание и транскрибацию 5-секундного аудиофайла. При превышении лимита — автоматический Fallback в MDM-топик.

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

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

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

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

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

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

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

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

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

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

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

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

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

6.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;

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

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

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

6.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": "Продукт 'Яблоки' успешно отмодерирован и добавлен текстовым контуром."
}

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

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

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