# Итерация 3: Распознавание продуктов (Gemini Vision) **Цель:** пользователь фотографирует чек, холодильник или готовое блюдо — Gemini Vision автоматически определяет продукты и их количество, после чего пользователь подтверждает список и добавляет в запасы. **Зависимости:** Итерация 2 (управление продуктами, ingredient_mappings). --- ## Структура задач ``` 3.1 Backend: AI-распознавание чека ├── 3.1.1 POST /ai/recognize-receipt (multipart: image) ├── 3.1.2 Промпт для OCR чека └── 3.1.3 Fuzzy match результатов по ingredient_mappings 3.2 Backend: AI-распознавание фото продуктов ├── 3.2.1 POST /ai/recognize-products (multipart: 1–3 images) ├── 3.2.2 Параллельные Gemini-запросы для нескольких фото └── 3.2.3 Дедупликация и объединение результатов 3.3 Backend: AI-распознавание блюда ├── 3.3.1 POST /ai/recognize-dish (multipart: image) └── 3.3.2 Промпт для определения блюда и КБЖУ 3.4 Backend: нераспознанные продукты → Gemini └── 3.4.1 Если fuzzy match не найден → запрос к Gemini для классификации → сохранение нового маппинга в ingredient_mappings 3.5 S3: загрузка фото ├── 3.5.1 Конфигурация S3-совместимого хранилища (MinIO/Cloud Storage) └── 3.5.2 Сохранение фото на период обработки (TTL 24h) 3.6 Flutter: экран сканирования ├── 3.6.1 ScanScreen (камера или галерея) ├── 3.6.2 Выбор режима: чек / продукты / блюдо ├── 3.6.3 Экран подтверждения списка продуктов └── 3.6.4 Экран результата распознавания блюда ``` --- ## 3.1 Распознавание чека ### Промпт ``` Ты — OCR-система для чеков из продуктовых магазинов. Проанализируй фото чека и извлеки список продуктов питания. Для каждого продукта определи: - name: название на русском языке (очисти от артикулов и кодов) - quantity: количество (число) - unit: единица (г, кг, мл, л, шт, уп) - category: dairy | meat | produce | bakery | frozen | beverages | other - confidence: 0.0–1.0 Позиции, которые не являются едой (бытовая химия, табак), пропусти. Позиции с непонятным текстом добавь в unrecognized. Верни ТОЛЬКО валидный JSON без markdown: { "items": [ {"name": "Молоко 2.5%", "quantity": 1, "unit": "л", "category": "dairy", "confidence": 0.95} ], "unrecognized": [ {"raw_text": "ТОВ АРТИК 1ШТ", "price": 89.0} ] } ``` ### Fuzzy match После получения списка от Gemini — для каждого item: ```go // 1. Поиск в ingredient_mappings mapping, found := ingredientRepo.FuzzyMatch(ctx, item.Name) // 2. Нашли — используем mapping_id, подставляем дефолты (unit, storage_days) // 3. Не нашли → разовый запрос к Gemini для классификации: if !found { mapping = gemini.ClassifyIngredient(ctx, item.Name) ingredientRepo.Save(ctx, mapping) // новая строка в ingredient_mappings } ``` ### Response ```json { "items": [ { "name": "Куриная грудка", "quantity": 500, "unit": "г", "category": "meat", "mapping_id": "uuid", "storage_days": 3, "confidence": 0.95 } ], "unrecognized": [ { "raw_text": "ТОВ АРТИК 1ШТ" } ] } ``` Клиент показывает список для подтверждения, затем вызывает `POST /products/batch`. --- ## 3.2 Распознавание фото продуктов ### Промпт (на каждое фото) ``` Ты — система распознавания продуктов питания. Посмотри на фото и определи все видимые продукты питания. Для каждого продукта оцени: - name: название на русском языке - quantity: приблизительное количество (число) - unit: единица (г, кг, мл, л, шт) - category: dairy | meat | produce | bakery | frozen | beverages | other - confidence: 0.0–1.0 Только продукты питания. Упаковки без содержимого — пропусти. Верни ТОЛЬКО валидный JSON: { "items": [ {"name": "Яйца", "quantity": 10, "unit": "шт", "category": "dairy", "confidence": 0.9} ] } ``` ### Обработка нескольких фото ```go // Параллельные запросы к Gemini (по одному на фото) results := make([][]RecognizedItem, len(images)) var wg sync.WaitGroup for i, img := range images { wg.Add(1) go func(i int, img []byte) { defer wg.Done() results[i], _ = gemini.RecognizeProducts(ctx, img) }(i, img) } wg.Wait() // Дедупликация: если canonical_name совпадает → суммируем quantity merged := mergeAndDeduplicate(results) ``` --- ## 3.3 Распознавание блюда ### Промпт ``` Ты — диетолог и кулинарный эксперт. Посмотри на фото блюда и определи: - dish_name: название блюда на русском языке - weight_grams: приблизительный вес порции в граммах - calories: калорийность порции (приблизительно) - protein_g, fat_g, carbs_g: БЖУ на порцию - confidence: 0.0–1.0 - similar_dishes: до 3 похожих блюд (для поиска рецептов) Верни ТОЛЬКО валидный JSON: { "dish_name": "Паста Карбонара", "weight_grams": 350, "calories": 520, "protein_g": 22, "fat_g": 26, "carbs_g": 48, "confidence": 0.85, "similar_dishes": ["Паста с беконом", "Спагетти"] } ``` ### Response-flow ``` POST /ai/recognize-dish → Gemini → {dish_name, calories≈, КБЖУ≈} ↓ Поиск в saved_recipes по dish_name (optional) ↓ Response: {dish_name, nutrition≈, matched_recipe?: {id, title}} ``` Клиент показывает результат с возможностью добавить в дневник питания. --- ## 3.4 Классификация нераспознанного ингредиента Когда fuzzy match не нашёл маппинг: ``` Промпт: "Классифицируй продукт питания: '{name}'. Ответь JSON: { 'canonical_name': 'turkey_breast', 'canonical_name_ru': 'грудка индейки', 'category': 'meat', 'default_unit': 'g', 'calories_per_100g': 135, 'protein_per_100g': 29, 'fat_per_100g': 1, 'carbs_per_100g': 0, 'storage_days': 3, 'aliases': ['грудка индейки', 'филе индейки', 'turkey breast'] }" ``` Результат сохраняется в `ingredient_mappings`. Следующий пользователь с тем же продуктом — AI не вызывается. --- ## 3.5 S3-хранилище для фото Фотографии загружаются на S3 (MinIO локально / Cloud Storage в проде): ``` 1. Клиент: PUT /upload → presigned URL 2. Клиент загружает фото напрямую на S3 3. Клиент: POST /ai/recognize-receipt { s3_key: "..." } 4. Backend: скачивает фото с S3, отправляет в Gemini, удаляет (TTL 24h) ``` Альтернатива для MVP: принимать base64 в теле запроса (для начала проще). --- ## 3.6 Flutter: экран сканирования ### ScanScreen (точка входа) ``` ┌─────────────────────────────────────┐ │ [←] Добавить продукты │ ├─────────────────────────────────────┤ │ │ │ Выберите способ │ │ │ │ ┌───────────────────────────────┐ │ │ │ 🧾 Сфотографировать чек │ │ │ │ Распознаем все продукты │ │ │ └───────────────────────────────┘ │ │ │ │ ┌───────────────────────────────┐ │ │ │ 🥦 Сфотографировать продукты │ │ │ │ Холодильник, стол, полка │ │ │ └───────────────────────────────┘ │ │ │ │ ┌───────────────────────────────┐ │ │ │ ✏️ Добавить вручную │ │ │ └───────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ ``` ### Экран подтверждения После распознавания — список с возможностью: - Редактировать количество/единицу каждого пункта - Удалить нераспознанные или ошибочные - Добавить вручную - Кнопка «Добавить всё» → POST /products/batch ### Экран результата блюда ``` ┌─────────────────────────────────────┐ │ [←] Распознано блюдо │ ├─────────────────────────────────────┤ │ [фото блюда] │ │ │ │ Паста Карбонара │ │ Уверенность: 85% │ │ │ │ ≈ 520 ккал (приблизительно) │ │ Б: 22г · Ж: 26г · У: 48г │ │ Вес порции: ~350г │ │ │ │ ┌───────────────────────────────┐ │ │ │ 📓 Добавить в дневник │ │ │ └───────────────────────────────┘ │ │ │ │ ┌───────────────────────────────┐ │ │ │ 📖 Открыть рецепт │ │ │ └───────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ ``` --- ## Новые конфигурационные переменные ``` S3_ENDPOINT=http://localhost:9000 S3_BUCKET=food-ai-uploads S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin ``` ## Оценка нагрузки на Gemini | Действие | Gemini-вызовов | |----------|---------------| | Чек (1 фото) | 1 vision | | Фото продуктов (3 фото) | 3 vision (параллельно) | | Блюдо | 1 vision | | Нераспознанный ингредиент | 1 текст (разово) | При 100 активных пользователях × 1 сканирование/день: - 100–300 дополнительных Gemini-запросов/день - Free tier (1 500/день) остаётся в запасе