Add isolated Gemini image proxy

This commit is contained in:
“Naeel”
2026-08-28 19:20:51 +03:00
parent ec57cf5d57
commit df5afcec8f
11 changed files with 849 additions and 0 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

+184
View File
@@ -0,0 +1,184 @@
# План архитектуры 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 обезличенных рецептах.