Files
recipe/PLAN/receipt-service-plan.md
T

14 KiB
Raw Blame History

План архитектуры obdai.ru/receipt

1. Цель

Создать независимый Python API-сервис для распознавания рукописных рецептов. API принимает изображение рецепта или фрагменты с названиями лекарств и дозировками, а возвращает структурированный текст для отображения в Android-приложении.

ФИО пациента, дата рождения, адрес и другие идентификаторы не должны извлекаться, сохраняться или возвращаться.

2. Размещение

Android
  -> HTTPS
nginx: obdai.ru/receipt/
  -> 127.0.0.1:8768
receipt.service
  -> FastAPI/Gunicorn
  -> OCR/vision-модель
  -> нормализация по справочнику лекарств
  -> JSON

elmer остаётся полностью независимым:

  • код: /opt/receipt/;
  • virtualenv: /opt/receipt/venv/;
  • пользователь systemd: отдельный receipt;
  • порт: 127.0.0.1:8768;
  • юнит: /etc/systemd/system/receipt.service;
  • отдельные логи и секреты;
  • отдельный deploy/restart;
  • отсутствие общих импортов, virtualenv и файлов с elmer.

3. API

Основной endpoint: POST /receipt/recognize.

Формат: multipart/form-data.

Поля:

  • image — JPEG, PNG или WEBP, до 10 MB;
  • request_id — UUID клиента;
  • masked_zones — координаты зон, которые нужно закрыть до OCR;
  • при необходимости отдельный тип запроса для уже выделенных фрагментов.

Ответ должен содержать:

  • request_id;
  • дату рецепта, если она распознана;
  • список лекарств;
  • исходный распознанный фрагмент без PII;
  • bounding box;
  • дозировку и режим приёма как написано;
  • confidence;
  • uncertain;
  • кандидатов из справочника при сомнительном совпадении.

Ошибки: 400, 401, 413, 422, 429, 503.

4. Pipeline

  1. Проверить API-ключ, размер, magic bytes и разрешение изображения.
  2. Применить маскирование зон до передачи изображения модели.
  3. Выполнить масштабирование и deskew.
  4. Получить OCR-текст и bounding boxes.
  5. Выделить роли: лекарство, дозировка, режим, дата, прочее.
  6. Удалить PII из результата.
  7. Сопоставить название со справочником через нормализацию и rapidfuzz.
  8. При близких или слабых совпадениях установить uncertain=true.
  9. Валидировать ответ Pydantic.
  10. Вернуть JSON без записи изображения и медицинского текста на диск.

5. Модель и OCR

Сначала проверить, поддерживает ли используемый endpoint gpt-oss-120b изображения. Размер 120B сам по себе не означает наличие vision-возможностей.

  • Если модель vision-capable: изображение -> модель -> строгий JSON.
  • Если text-only: Tesseract 5 или облачный OCR с русским языком и bounding boxes -> gpt-oss-120b для структурного разбора -> справочник.
  • Финальное название лекарства нельзя принимать только от LLM: источник истины — справочник и fuzzy matching.
  • Дозировка возвращается как распознанный текст, без медицинской интерпретации.

6. Справочник лекарств

Для MVP использовать локальную read-only SQLite БД.

Минимальные сущности:

  • препарат;
  • МНН;
  • торговое название;
  • форма выпуска;
  • дозировка;
  • производитель;
  • синонимы и распространённые варианты написания;
  • поисковый индекс;
  • пары опасно похожих названий.

Возможный базовый источник — ГРЛС. Перед использованием необходимо проверить формат, условия использования, полноту данных и порядок обновления. Коммерческие справочники требуют отдельной лицензии.

Рекомендуемая логика:

  • высокий score: совпадение;
  • средний score: uncertain=true и список кандидатов;
  • низкий score: совпадение не подтверждать;
  • при близости двух похожих препаратов принудительно требовать подтверждение пользователя.

7. Персональные данные

  • Android маскирует ФИО/ДР до отправки.
  • Сервер повторно применяет masked_zones до OCR.
  • В response schema отсутствуют поля пациента.
  • Изображения и raw OCR не записываются на диск.
  • В логах только request_id, статус, размер, latency и количество сомнительных позиций.
  • TLS обеспечивается nginx.
  • API требует Bearer-ключ.
  • Должны быть TTL, rate limiting и аудит без медицинского содержимого.

8. Надёжность

  • Жёсткие timeout для nginx, Gunicorn, HTTP-клиента модели и SQLite.
  • На MVP синхронная обработка без Redis/Celery.
  • При недоступности модели возвращать 503.
  • Ограничить размер запроса и частоту запросов.
  • Health endpoint не должен требовать API-ключ.
  • Metrics endpoint доступен только локально.
  • Бэкапить только справочник и конфигурацию ключей, не изображения.

9. Этапы

MVP

  1. FastAPI, health и recognize.
  2. Отдельные /opt/receipt, virtualenv, systemd и nginx location.
  3. Загрузка справочника в SQLite.
  4. OCR и базовый matcher.
  5. Pydantic response без PII.
  6. API-ключ, лимиты и journald.

Следующий этап

  • интеграция vision-модели;
  • calibrated confidence;
  • подтверждение кандидатов в Android;
  • автообновление справочника;
  • Prometheus metrics;
  • circuit breaker;
  • расширенная защита от похожих названий.

10. Основные риски

  • gpt-oss-120b может оказаться text-only;
  • рукописный русский текст может распознаваться с низкой точностью;
  • fuzzy matching может выбрать похожий препарат;
  • ГРЛС может быть неполным или неудобным для автоматической синхронизации;
  • клиент может случайно отправить PII;
  • медицинский сценарий требует явного отказа от диагностики и назначения лечения.

11. Утверждённый вариант распознавания

Сервис не строится как OCR -> исправление строки. Основной принцип: vision-модель возвращает несколько гипотез наблюдаемого текста, после чего локальный справочник участвует в выборе допустимого препарата.

crop лекарственной строки
  -> несколько вариантов preprocessing
  -> DeepSeek Vision: observed_text + alternatives
  -> exact/alias/fuzzy candidate resolver
  -> проверка дозировки и лекарственной формы
  -> scoring
  -> CONFIDENT / NEED_CONFIRMATION / UNKNOWN

Ключевые правила:

  • не просить модель угадывать единственный препарат без альтернатив;
  • не отправлять весь словарь в prompt;
  • сначала получать ограниченный набор кандидатов локальным поиском, затем передавать модели только top-N;
  • название, strength и схему приёма распознавать отдельными полями;
  • несовместимую дозировку использовать для снижения уверенности, а не для автоматического исправления;
  • при близких названиях показывать варианты пользователю;
  • добавить endpoint подтверждения результата, чтобы собирать обезличенные correction samples;
  • начать с SQLite и перейти на отдельный PostgreSQL только при появлении требований к параллельной записи, админке или нескольким экземплярам сервиса.

Перед фиксацией порогов и коэффициентов scoring провести эксперимент на 50–200 обезличенных рецептах.

12. Критический план по результатам валидации Opus (2026-08-31)

Подтвержденные проблемы и приоритет

  1. recipe_service/metrics.py: убрать initialize() из record(); инициализация должна быть только на старте сервиса.
  2. recipe_service/app.py: устранить дублирование record(...) через единый финализатор/обертку завершения запроса.
  3. recipe_service/app.py: считать duration_ms во всех ветках, а не только при 401.
  4. recipe_service/app.py: заменить сравнение токена на constant-time (hmac.compare_digest).
  5. recipe_service/app.py: минимизировать детализацию 502 для клиента, подробности оставлять в логах.
  6. recipe_service/app.py: добавить учет заголовков прокси (ProxyFix) для корректного client IP.
  7. Тесты: добавить recipe_service/test_app.py с кейсами 401/400/415/413/200.
  8. requirements.txt: выровнять root и recipe_service/requirements.txt по version bounds.

Android-блок (после подтверждения модели безопасности)

  1. Убрать долговременный статический секрет из APK или перейти на схему краткоживущих токенов.
  2. Оптимизировать pipeline камеры: снизить частоту тяжелой конвертации или заменить способ получения bitmap.
  3. Убрать двойной вывод результата (Column и ResultOverlay) и оставить один источник отображения.
  4. Пересмотреть containsPatientData в MedicationZoneDetector: дата сама по себе не должна блокировать распознавание.

Вопросы к Opus (требуют уточнения до правок)

  1. api_key_override в gemini_proxy/app.py: удалять полностью или оставить только для локального debug-режима за флагом окружения?
  2. По 502: какой клиентский формат ошибки нужен — единое сообщение или кодизированные причины без деталей провайдера?
  3. Для ProxyFix: доверяем ровно одному прокси (x_for=1) или нужен более строгий trust chain?
  4. Для Android: подтверждаем перенос от статического API-токена к короткоживущему серверному token exchange?
  5. По privacy-гейту: дата рецепта в medication-зоне допустима или должна только понижать confidence без блокировки?

План выполнения после подтверждения

  1. Backend фаза A: metrics.py + app.py (инициализация, duration, record-finalizer, compare_digest, ProxyFix).
  2. Backend фаза B: тесты recipe_service/test_app.py + проверка pytest.
  3. Backend фаза C: harmonize requirements.
  4. Android фаза D: безопасность токена + оптимизация камеры + privacy-gate.