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

185 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План архитектуры obdai.ru/receipt
## 1. Цель
Создать независимый Python API-сервис для распознавания рукописных рецептов. API принимает изображение рецепта или фрагменты с названиями лекарств и дозировками, а возвращает структурированный текст для отображения в Android-приложении.
ФИО пациента, дата рождения, адрес и другие идентификаторы не должны извлекаться, сохраняться или возвращаться.
## 2. Размещение
```text
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-модель возвращает несколько гипотез наблюдаемого текста, после чего локальный справочник участвует в выборе допустимого препарата.
```text
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 обезличенных рецептах.