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>
13 KiB
Итерация 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:
// 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.0–1.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.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/день) остаётся в запасе