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