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
Метод POST /api/v1/purchase/upload-voice
Документация API: Добавление продуктов через готовый снимок с автоматической gRPC ИИ-модерацией
- IAD-MIGRATION-116 Backlog — Краткое Описание задачи 1.
- IAD-MIGRATION-117 Refinement (Уточнение) — Краткое Описание задачи 2.
- GW-1 Ready for Development — Подключение эндпоинта в шлюзе.
- AID-012 Ready for Development — создать метож ProcessVoiceStream.
- AID-013 Ready for Development — создать метож ProcessUploadText.
- INV-FRONTEND-222 Refinement (Уточнение) — Экран управления B2B-интеграциями
- INV-FRONTEND-223 Refinement (Уточнение) — Экран управления B2B-интеграциями
В открытом доступе представлена демонстрационная версия метода. В настоящей публичной документации отображены не все шаги, технические сценарии и приватные эндпоинты для системы цифровых симуляторов бизнес-процессов.
- Полная спецификация метода: Будет доступна только во внутреннем контуре разработки (Confluence / Swagger Enterprise).
1 Функциональное назначение
Метод предназначен для асинхронной обработки голосовых заметок пользователя, их автоматического перевода в текст с помощью нейросети распознавания речи (Whisper Tiny) и последующего добавления продуктов в холодильник.
В рамках новой архитектуры этот метод выполняет роль поставщика аудио-контекста (Audio Context Provider):
- Изоляция медиа-потока: Бинарный аудио-файл сохраняется шлюзом
FastAPIв S3-бакетMinIO. - Асинхронная транскрибация (Speech-to-Text): Тяжелый ИИ-инференс вынесен из API-слоя в изолированный воркер
grpc-analytics, который транслирует аудиозапись в текстовую строку. - Переиспользование контура фильтрации: Воркер отправляет извлеченную строку в общий топик
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)
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: BucketNotFoundMinIOException: AccessDenied (истекли или неверны секретные ключи доступа шлюза)StorageFullException |
Шаг 2 (MinIO -> Gateway) |
Объектное хранилище успешно сохраняет медиафайл на дисковом массиве и возвращает шлюзу постоянную или временную (presigned) URL-ссылку на объект. | S3 API PUT Object Response: HTTP Статус: 200 OKETag: "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 ForbiddenConnectTimeout: 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: DeliveryTimeoutKafkaException: RecordTooLargeException |
Шаг 7 (T_Results -> Censor) |
Воркер цензуры считывает распознанный текстовый payload из аудиозаписи для выполнения лингвистической фильтрации и валидации. | Kafka Consumer Poll Request:Group_ID: "censorship-workers", Topic: bpds.inventory.in.receipt.uploadPayload: { "raw_text_input": "Текст с матом из аудиозаписи", "app_lang": "ru-RU", "x_request_id": "trace-stt-censor-uuid" } |
KafkaException: CommitFailedExceptionSerializationException (ошибка парсинга 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 unreachableHTTP 500 Internal Server ErrorReadTimeout: Inference duration exceeded |
Шаг 9 (Censor -> Redis) |
Параллельно выполняется сверка по локальным словарям мата для app_lang и условное инкрементирование счетчика нарушений для ID пользователя. |
Redis Command Pipeline:1. SISMEMBER "dict:profanity:ru-RU" "выделенное_слово"2. MULTI3. INCRBY "abuse:counter:user_456" 14. EXPIRE "abuse:counter:user_456" 864005. EXEC |
Redis.ConnectionError: Connection refusedRedis.TimeoutError: Command timed outRedis.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: DeliveryTimeoutKafkaException: 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: LeaderNotAvailableExceptionKafkaException: 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: BrokerNotAvailableKafkaException: MessageTimedOut |
Шаг 11 (T_Draft -> App) |
Из топика черновиков сомнительный текст транскрипции выводится на экран приложения в интерфейс модератора для ручной корректировки. | gRPC / HTTP Stream (Вкладка модерации):Payload: { "draft_id": "uuid-draft-stt", "raw_text_input": "Текст для исправления", "app_lang": "ru-RU" } |
DioException: connection errorHTTP 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: QueueFullExceptionFastAPI.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: DeliveryTimeoutKafkaException: 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: LeaderNotAvailableExceptionInvalidTopicException |
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": "Текст не прошел модерацию: обнаружена ненормативная лексика или спам."
}