185 lines
10 KiB
Markdown
185 lines
10 KiB
Markdown
# План архитектуры 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 обезличенных рецептах.
|