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

220 lines
14 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 обезличенных рецептах.
## 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.