Метод gRPC /analytics.v1.AnalyticsService/ExtractReceiptData

Документация API: Асинхронный контур OCR-распознавания фискальных чеков и парсинга товарной матрицы

Published

July 1, 2026

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

Метод предназначен для глубокого анализа изображений, которые на этапе первичной классификации (Статья 1) были определены ИИ-сервисом как «Тип 2: Фискальный чек». Так как процесс оптического распознавания текста (OCR) и парсинга строк является вычислительно сложным и занимает от 1.5 до 4 секунд, этот контур полностью изолирован от синхронного HTTP-потока пользователя.

Метод решает следующие задачи:

  1. Текстовое распознавание (OCR): Локализация текстовых блоков на фотографии чека и превращение пикселей в сырой массив строк.
  2. Парсинг матрицы товаров: Извлечение из каждой строки наименования товара, его цены, единицы измерения и точного количества/веса (включая дробные значения float для весовых товаров).
  3. Нормализация данных: Подготовка плоского списка позиций чека для записи в дочернюю таблицу буфера pending_receipt_items перед выводом пользователю на экран ручного аудита.

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

  • Протокол: gRPC поверх HTTP/2
  • Тип вызова: Unary RPC (вызывается фоновым воркером FastAPI асинхронно)
  • Пакет (Package): analytics.v1
  • Сервис (Service): AnalyticsService
  • Метод: ExtractReceiptData

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

Контракт описывается в файле analytics.proto. Фоновый воркер передает ИИ-сервису только указатель на файл в S3, чтобы не гонять бинарники по сети повторно.

syntax = "proto3";

package analytics.v1;

message ReceiptExtractRequest {
  string draft_id = 1; // Уникальный UUID черновика чека из таблицы pending_receipts_buffer
  string s3_key = 2;   // Путь к файлу фотографии чека в бакете MinIO S3
}

2.2 Спецификация структуры ответа (Protocol Buffers Response)

ИИ-сервис возвращает структурированный массив распознанных позиций (структура «один-ко-многим»).

message ReceiptItem {
  string raw_name = 1;     // Сырая строка из чека (например, "2110310 БАНАН ЭКВАДОР КГ")
  double quantity = 2;     // Распознанное количество или вес (например, 1.695)
  double price = 3;        // Цена за единицу товара
  double total_sum = 4;    // Общая сумма по позиции
}

message ReceiptExtractResponse {
  string status = 1;               // Статус обработки: "SUCCESS" или "OCR_FAILED"
  string draft_id = 2;             // Идентификатор черновика
  repeated ReceiptItem items = 3;  // Массив распознанных товарных позиций
}

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

Диаграмма описывает асинхронный процесс: пока пользователь видит экран ожидания, фоновый воркер FastAPI и gRPC ИИ-сервис скачивают чек из MinIO S3, распознают матрицу товаров через OCR-движок, парсят веса, записывают данные в PostgreSQL и отправляют WebSocket-сигнал во Flutter.

sequenceDiagram
    autonumber
    participant API as Бэкенд-шлюз (FastAPI)
    participant GRPC as ИИ-сервис (OCR Engine)
    participant S3 as Хранилище (MinIO S3)
    participant DB as Реляционная СУБД (PostgreSQL)
    participant APP as Мобильное приложение (Flutter)

    Note over API: Шаг 1: Фоновый запуск после маршрутизации Типа 2 (Чек)
    API->>GRPC: gRPC ExtractReceiptData(draft_id, s3_key)
    activate GRPC
    
    GRPC->>S3: Скачивает бинарные байты чека по s3_key
    activate S3
    S3-->>GRPC: Возвращает файл изображения чека
    deactivate S3
    
    note over GRPC: Шаг 4: Сегментация и распознавание текста (OCR)
    note over GRPC: Шаг 5: Регулярные выражения (Парсинг веса, цен и количества)
    
    alt Ошибка чтения структуры чека / Смазанное фото
        GRPC-->>API: gRPC error (StatusCode.DATA_LOSS, "OCR_FAILED")
    else Успешный парсинг матрицы товаров
        GRPC-->>API: gRPC ReceiptExtractResponse(status='SUCCESS', items=[...])
        deactivate GRPC
        
        activate API
        note over API: Шаг 7: Старт транзакции записи позиций чека
        API->>DB: INSERT INTO pending_receipt_items (массив строк с qty/price)
        API->>DB: UPDATE pending_receipts_buffer SET status='READY_FOR_AUDIT'
        
        API->>APP: WebSocket /ws/push-stream/{id} -> Сигнал "RECEIPT_OCR_READY"
        note over APP: Экран ожидания исчезает, открывается список товаров чека для аудита
        deactivate API
    end

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

  • Шаг 1 (API -> GRPC): Сразу после того, как классификатор верхнего уровня (из Статьи 1) определил изображение как чек и бэкенд зафиксировал его в таблице pending_receipts_buffer, асинхронный фоновый воркер FastAPI отправляет gRPC-запрос на метод ExtractReceiptData. Передается UUID черновика и ключ файла в S3. Пользователь в этот момент видит во Flutter экран с лоадером «ИИ читает чек…».
  • Шаг 2 (GRPC -> S3): ИИ-сервис (OCR-контур) обращается к объектному хранилищу MinIO S3 по полученному s3_key, чтобы забрать оригинальное изображение чека в высоком разрешении.
  • Шаг 3 (S3 -> GRPC): MinIO S3 отдает бинарный файл изображения на gRPC-сервер аналитики.
  • Шаг 4 (GRPC -> GRPC): Внутри ИИ-сервиса запускается OCR-движок. Он выполняет предобработку снимка (Бинаризация, выравнивание контрастности, дескьюинг — устранение наклона текста) и производит сегментацию изображения на строки, переводя графические пиксели в текстовые строки UTF-8.
  • Шаг 5 (GRPC -> GRPC): Текст передается в детерминированный парсер на базе регулярных выражений (Regex). Парсер разбирает структуру чека строка за строкой. Он ищет разделители умножения (X или *), выделяет цену (например, 595.00) и строго извлекает количество/вес (например, дробное значение 1.695000 для строки БАНАН ЭКВАДОР КГ).
    • Вилка исключений (Критический дефект фото): Если изображение полностью размыто, порвано или OCR-движок не смог распознать ни одной товарной позиции, метод прерывает выполнение и возвращает gRPC-ошибку OCR_FAILED (код DATA_LOSS). Бэкенд-шлюз ловит её, переводит статус чека в FAILED и шлет WebSocket-пуш на фронтенд для вывода модального окна: «Не удалось прочитать чек. Пожалуйста, сфотографируйте четче или введите продукты вручную».
  • Шаг 6 (GRPC -> API): В случае успешного разбора всей матрицы товаров, gRPC-сервер упаковывает результат в структуру ReceiptExtractResponse, содержащую массив repeated ReceiptItem, и возвращает его на бэкенд FastAPI.
  • Шаг 7 (API -> DB): Шлюз FastAPI активируется и открывает SQL-транзакцию. Так как чек содержит несколько позиций (связь 1:M), бэкенд выполняет множественную вставку (Bulk Insert) всех распознанных строк, их цен и весов в дочернюю буферную таблицу: INSERT INTO pending_receipt_items (receipt_draft_id, raw_name, quantity, price) VALUES (...). После этого статус родительского черновика в pending_receipts_buffer обновляется на READY_FOR_AUDIT.
  • Шаг 8 (API -> APP): Бэкенд через открытое WebSocket-соединение отправляет клиенту push-сигнал "RECEIPT_OCR_READY" вместе с UUID черновика. Мобильное приложение Flutter мгновенно скрывает экран ожидания (лоадер) и перенаправляет пользователя на экран ручного аудита чека, где выгружается весь список распознанных товаров с заполненными весами и ценами из таблицы pending_receipt_items. Пользователю остается только подтвердить или скорректировать список, как это описано в Шаге 15 Версии 1.

5 Спецификация структур данных буфера и вилок исключений

5.1 1. Спецификация буферных таблиц PostgreSQL (Связь 1:M)

Для хранения результатов распознавания чека до момента ручного подтверждения пользователем используется классическая реляционная схема со связью «один-ко-многим» (One-to-Many). Это позволяет хранить метаданные чека изолированно от его позиций.

-- Таблица 1: Шапка чека (Родительская структура)
CREATE TABLE pending_receipts_buffer (
    draft_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL,
    home_group_id VARCHAR(50) NOT NULL,
    s3_key VARCHAR(512) NOT NULL,
    status VARCHAR(30) NOT NULL DEFAULT 'PROCESSING', -- PROCESSING, READY_FOR_AUDIT, CONFIRMED, FAILED
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Таблица 2: Распознанные позиции чека (Дочерняя структура)
CREATE TABLE pending_receipt_items (
    item_id BIGSERIAL PRIMARY KEY,
    receipt_draft_id UUID NOT NULL REFERENCES pending_receipts_buffer(draft_id) ON DELETE CASCADE,
    raw_name VARCHAR(255) NOT NULL,    -- Пример: "2110310 БАНАН ЭКВАДОР КГ"
    quantity NUMERIC(10, 4) NOT NULL, -- Пример: 1.6950 (дробный float/numeric для веса)
    price NUMERIC(10, 2) NOT NULL,    -- Пример: 595.00
    is_verified BOOLEAN DEFAULT FALSE -- Флаг подтверждения пользователем при аудите
);

-- Индекс для быстрой выборки позиций конкретного чека при рендеринге во Flutter
CREATE INDEX idx_receipt_items_draft_id ON pending_receipt_items(receipt_draft_id);

5.2 2. Спецификация WebSocket-сигнала об успешном завершении OCR (Шаг 8)

После завершения Bulk Insert в базу данных, FastAPI отправляет текстовое JSON-сообщение в канал /ws/push-stream/{id}. Клиент ловит его, гасит лоадер и запрашивает данные для экрана аудита.

{
  "event_type": "RECEIPT_OCR_READY",
  "timestamp": "2026-07-01T21:21:40.890Z",
  "data": {
    "draft_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "total_items_parsed": 3,
    "ui_action": "REDIRECT_TO_RECEIPT_AUDIT_SCREEN",
    "fetch_url": "/api/v1/fridge/receipt-draft/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/items"
  }
}

5.3 3. Вилки исключений и стратегии обработки ошибок (Failures & Fail-Safe)

Асинхронный OCR-контур чеков работает в фоновом режиме, поэтому обработка ошибок строится на изменении статусов в БД и отправке алертов через WebSockets.

5.3.1 Ошибка 1: Смазанный снимок или критический сбой OCR-движка (OCR_FAILED)

  • Условие срабатывания (Шаг 5): Картинка чека не сфокусирована, текст размыт, или Regex-парсер не смог выделить структуру цен и весов. gRPC возвращает ошибку со статус-кодом DATA_LOSS.
  • Действие бэкенда: FastAPI переводит статус в таблице pending_receipts_buffer в значение FAILED. В топик Kafka analytics.dlq.v1 отправляется лог инцидента.
  • WebSocket-пакет ошибки для клиента:
{
  "event_type": "RECEIPT_OCR_FAILED",
  "timestamp": "2026-07-01T21:21:42.110Z",
  "data": {
    "draft_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "error_code": "ERR-RECEIPT-UNREADABLE",
    "message": "Не удалось распознать текст чека. Пожалуйста, введите продукты вручную или сделайте более четкий снимок."
  }
}

5.3.2 Ошибка 2: Таймаут или падение S3 при попытке скачать оригинальное фото (UNAVAILABLE)

  • Условие срабатывания (Шаг 2): gRPC-сервер ИИ пытается скачать бинарник из MinIO S3, но объектное хранилище не отвечает по таймауту.
  • Стратегия Fail-Safe: ИИ-сервис делает 3 автоматические попытки перезапроса файла (Retry Policy) с интервалом в 500мс. Если все попытки исчерпаны, gRPC выбрасывает статус StatusCode.UNAVAILABLE. Бэкенд выставляет чеку статус FAILED, логирует системную ошибку инфраструктуры и отправляет клиенту push-уведомление: «Внутренний сбой хранилища. Повторите попытку позже».