Files
food-ai/docs/plans/Iteration_3.md
dbastrikin e57ff8e06c feat: implement Iteration 1 — AI recipe recommendations
Backend:
- Add Groq LLM client (llama-3.3-70b) for recipe generation with JSON
  retry strategy (retries only on parse errors, not API errors)
- Add Pexels client for parallel photo search per recipe
- Add saved_recipes table (migration 004) with JSONB fields
- Add GET /recommendations endpoint (profile-aware prompt building)
- Add POST/GET/GET{id}/DELETE /saved-recipes CRUD endpoints
- Wire gemini, pexels, recommendation, savedrecipe packages in main.go

Flutter:
- Add Recipe, SavedRecipe models with json_serializable
- Add RecipeService (getRecommendations, getSavedRecipes, save, delete)
- Add RecommendationsNotifier and SavedRecipesNotifier (Riverpod)
- Add RecommendationsScreen with skeleton loading and refresh FAB
- Add RecipeDetailScreen (SliverAppBar, nutrition tooltip, steps with timer)
- Add SavedRecipesScreen with Dismissible swipe-to-delete and empty state
- Update RecipesScreen to TabBar (Recommendations / Saved)
- Add /recipe-detail route outside ShellRoute (no bottom nav)
- Extend ApiClient with getList() and deleteVoid()

Project:
- Add CLAUDE.md with English-only rule for comments and commit messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:43:29 +02:00

13 KiB
Raw Permalink Blame History

Итерация 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: 13 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.01.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:

// 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

{
  "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.01.0

Только продукты питания. Упаковки без содержимого — пропусти.

Верни ТОЛЬКО валидный JSON:
{
  "items": [
    {"name": "Яйца", "quantity": 10, "unit": "шт",
     "category": "dairy", "confidence": 0.9}
  ]
}

Обработка нескольких фото

// Параллельные запросы к 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.01.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 сканирование/день:

  • 100300 дополнительных Gemini-запросов/день
  • Free tier (1 500/день) остаётся в запасе