# План архитектуры 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.