docs: update README, env example, and design docs
- backend/.env.example: add GEMINI_API_KEY and PEXELS_API_KEY placeholders - backend/Makefile: add test-integration to PHONY targets - backend/README.md: document external API keys, import/translate commands - docs/Design.md, docs/Tech.md: reflect Iteration 1 implementation and future plans Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
119
docs/Design.md
119
docs/Design.md
@@ -953,98 +953,93 @@
|
||||
|
||||
---
|
||||
|
||||
## 9. Каталог рецептов
|
||||
## 9. AI-рекомендации рецептов
|
||||
|
||||
Поиск и просмотр рецептов с фильтрацией и персональными рекомендациями.
|
||||
Персонализированные рецепты, сгенерированные Gemini на основе продуктов пользователя, цели и предпочтений. Статического каталога нет — каждый запрос даёт новую подборку.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Рецепты │
|
||||
│ Рецепты [🔄] │
|
||||
├─────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ 🔍 Найти рецепт... │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Из моих продуктов] [Фильтры ▾] │
|
||||
│ Для вас сегодня │
|
||||
│ На основе ваших продуктов │
|
||||
│ │
|
||||
│ Для вас │
|
||||
│ ┌──────────┐ ┌──────────┐ → │
|
||||
│ │ [фото] │ │ [фото] │ │
|
||||
│ │ Том Ям │ │ Пад Тай │ │
|
||||
│ │ ★4.8 │ │ ★4.6 │ │
|
||||
│ │ 320 ккал │ │ 450 ккал │ │
|
||||
│ │ Есть всё✓│ │ -2 прод. │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ [фото блюда] │ │
|
||||
│ │ │ │
|
||||
│ │ Куриная грудка с овощами │ │
|
||||
│ │ ≈ 420 ккал · 35 мин · Лёгко│ │
|
||||
│ │ Б: 48г Ж: 12г У: 18г │ │
|
||||
│ │ [♡] │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ Готовили недавно │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ [фото блюда] │ │
|
||||
│ │ │ │
|
||||
│ │ Рисовый суп с яйцом │ │
|
||||
│ │ ≈ 310 ккал · 20 мин · Лёгко│ │
|
||||
│ │ Б: 18г Ж: 8г У: 42г │ │
|
||||
│ │ [♡] │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ [фото блюда] │ │
|
||||
│ │ │ │
|
||||
│ │ Морковный суп-пюре │ │
|
||||
│ │ ≈ 180 ккал · 25 мин · Лёгко│ │
|
||||
│ │ Б: 4г Ж: 7г У: 26г │ │
|
||||
│ │ [♡] │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ + ещё 2 рецепта │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ Сохранённые рецепты │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ │
|
||||
│ │[фото] │ │[фото] │ │[фото] │ │
|
||||
│ │Карбон. │ │Борщ │ │Цезарь │ │
|
||||
│ │♡ saved │ │♡ saved │ │♡ saved │ │
|
||||
│ └────────┘ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ Все рецепты │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ [фото] │ │ [фото] │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Ризотто │ │ Борщ │ │
|
||||
│ │ ★4.5 │ │ ★4.9 │ │
|
||||
│ │ 480 ккал │ │ 350 ккал │ │
|
||||
│ │ 50 мин │ │ 90 мин │ │
|
||||
│ │ Сложная │ │ Средняя │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ ... │ │ ... │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
│ [Все сохранённые →] │
|
||||
│ │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Главная] [Продукты] [Меню] [Рецепты] [Профиль] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Панель фильтров (раскрывается по тапу «Фильтры ▾»)
|
||||
### Скелетон при загрузке (2–4 сек)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Фильтры [Сброс]│
|
||||
│ Рецепты │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ Приём пищи │
|
||||
│ [Завтрак] [Обед] [Ужин] [Перекус] │
|
||||
│ Подбираем рецепты... │
|
||||
│ │
|
||||
│ Кухня │
|
||||
│ [Русская] [Азиатская] [Европейская] │
|
||||
│ [Средиземноморская] [Американская] │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
|
||||
│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
|
||||
│ │ ░░░░░░░░ ░░░░░ ░░░░░░░░ │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────┐ │
|
||||
│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
|
||||
│ │ ░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
|
||||
│ │ ░░░░░░░░ ░░░░░ ░░░░░░░░ │ │
|
||||
│ └─────────────────────────────┘ │
|
||||
│ │
|
||||
│ Сложность │
|
||||
│ [Простая] [Средняя] [Сложная] │
|
||||
│ │
|
||||
│ Время приготовления │
|
||||
│ [до 15 мин] [до 30 мин] [до 60 мин]│
|
||||
│ [более 60 мин] │
|
||||
│ │
|
||||
│ Калорийность (на порцию) │
|
||||
│ ○────────────● до 500 ккал │
|
||||
│ │
|
||||
│ Диета │
|
||||
│ [Вегетар.] [Безглютен.] [Низкокал.] │
|
||||
│ [Кето] [Без лактозы] │
|
||||
│ │
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ Показать 24 рецепта │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Элементы и поведение
|
||||
|
||||
- **Строка поиска:** текстовый поиск по названию рецепта и ингредиентам.
|
||||
- **Кнопка «Из моих продуктов»:** toggle-фильтр. При включении показывает только рецепты, которые можно приготовить из имеющихся продуктов (полностью или частично). Рецепты сортируются по доле имеющихся ингредиентов. На каждой карточке — пометка: «Есть всё ✓» или «-N прод.».
|
||||
- **Кнопка «Фильтры»:** раскрывает панель фильтров (bottom sheet). Фильтры — chip-кнопки с множественным выбором. Слайдер для калорийности. Кнопка «Показать N рецептов» — применяет фильтры и закрывает панель. Кнопка «Сброс» — очищает все фильтры.
|
||||
- **Секция «Для вас»:** горизонтальная карусель. Персональные рекомендации на основе: предпочтений кухонь, истории оценок, имеющихся продуктов (особенно с истекающим сроком). Алгоритм: продукты с истекающим сроком > полное совпадение ингредиентов > предпочтения кухни > высокий рейтинг.
|
||||
- **Секция «Готовили недавно»:** горизонтальный ряд из последних 5 приготовленных рецептов. Быстрый доступ для повтора. Не показывается, если нет истории.
|
||||
- **Секция «Все рецепты»:** сетка 2 колонки. Каждая карточка — фото, название, рейтинг (звёзды), калорийность, время, сложность. Тап — переход в карточку рецепта.
|
||||
- **Бесконечный скролл:** подгрузка рецептов по мере прокрутки.
|
||||
- **Кнопка [🔄] «Обновить»:** принудительная перегенерация рекомендаций. Gemini + Pexels вызываются заново. Skeleton показывается на время генерации (2–4 сек).
|
||||
- **Карточка рецепта:** фото (Pexels), название, приблизительное КБЖУ (помечено «≈»), время приготовления, сложность. Тап → карточка рецепта.
|
||||
- **Кнопка [♡]:** сохранить рецепт. Рецепт записывается в `saved_recipes`. Иконка заполняется, haptic feedback.
|
||||
- **Секция «Сохранённые рецепты»:** горизонтальный ряд последних сохранённых. Кнопка «Все сохранённые →» переходит на экран всех сохранённых. Не показывается, если нет сохранённых.
|
||||
- **Автоматическая генерация:** при открытии вкладки (если с последней генерации прошло > 30 мин или изменились продукты).
|
||||
- **КБЖУ «≈»:** пиктограмма «~» перед числами; тап → tooltip «Приблизительно, рассчитано AI».
|
||||
|
||||
---
|
||||
|
||||
|
||||
253
docs/Tech.md
253
docs/Tech.md
@@ -8,10 +8,9 @@
|
||||
| База данных | PostgreSQL | Надёжная реляционная БД, JSONB для гибких структур (нутриенты, шаги рецептов), полнотекстовый поиск, зрелая экосистема |
|
||||
| Клиент | Flutter (Android, iOS, Web) | Единая кодовая база на три платформы, нативная производительность на мобильных, зрелая экосистема виджетов |
|
||||
| Авторизация | Firebase Auth | Бесплатно до 50K MAU, email + Google + Apple из коробки, официальный Go SDK, отличная интеграция с Flutter |
|
||||
| AI (vision) | Google Gemini 2.5 Flash | Лучшее соотношение цена/качество для распознавания еды (~$0.15/1M input tokens), бесплатный tier для разработки, прецедент CalCam |
|
||||
| AI (текст) | Google Gemini 2.5 Flash-Lite | Самый дешёвый вариант для текстовых задач ($0.10/1M input tokens) — генерация рецептов, меню, замены ингредиентов |
|
||||
| База рецептов | Spoonacular API | 365K+ рецептов с нутриентами, фото, ингредиентами, шагами. $29/мес на старте |
|
||||
| Хранение файлов | S3-совместимое (MinIO / Cloud Storage) | Фото блюд от пользователей, фото рецептов |
|
||||
| AI (vision + текст) | Google Gemini 2.5 Flash | Единая модель для распознавания фото (чеки, продукты, блюда), генерации рецептов, меню и замены ингредиентов. Free tier для разработки |
|
||||
| Изображения | Pexels API | Фото к рецептам. Бесплатно до 20K req/мес, коммерческое использование без атрибуции |
|
||||
| Хранение файлов | S3-совместимое (MinIO / Cloud Storage) | Фото блюд от пользователей, загружаемые для распознавания |
|
||||
|
||||
---
|
||||
|
||||
@@ -69,8 +68,8 @@
|
||||
│ - meal_diary │ ▼
|
||||
│ - reviews │ ┌──────────────────┐
|
||||
│ - ai_tasks │ │ │
|
||||
│ │ │ Spoonacular │
|
||||
└──────────────────┘ │ API │
|
||||
│ │ │ Pexels API │
|
||||
└──────────────────┘ │ (фото к рец.) │
|
||||
│ │
|
||||
└──────────────────┘
|
||||
```
|
||||
@@ -218,35 +217,37 @@ AI-провайдер скрыт за интерфейсами. Это позв
|
||||
#### 4.4. Генерация рецептов / подбор меню
|
||||
|
||||
- **Модель:** Gemini 2.5 Flash-Lite (текст) — дешевле, vision не нужен
|
||||
- **Ключевой принцип:** AI НЕ генерирует рецепты с нуля. Вместо этого:
|
||||
- **Ключевой принцип:** Gemini генерирует рецепты с нуля на основе контекста пользователя. Статическая база рецептов не нужна.
|
||||
|
||||
```
|
||||
1. Backend выбирает из БД кандидатов-рецептов (по фильтрам: кухня, сложность, время, ингредиенты)
|
||||
2. AI получает список кандидатов (ID + название + ингредиенты + калории) + контекст юзера
|
||||
3. AI ранжирует, комбинирует в меню, предлагает замены
|
||||
4. Backend возвращает полные рецепты по ID из БД
|
||||
1. Backend собирает контекст: продукты пользователя (с учётом сроков хранения),
|
||||
профиль (цель, КБЖУ), предпочтения кухни, диетические ограничения
|
||||
2. Gemini генерирует полные рецепты: название, ингредиенты с граммовками,
|
||||
шаги, КБЖУ на порцию, image_query для Pexels
|
||||
3. Backend параллельно запрашивает фото из Pexels по image_query
|
||||
4. Результат возвращается клиенту; при сохранении → пишется в saved_recipes
|
||||
```
|
||||
|
||||
- **Промпт для подбора меню:**
|
||||
- **Промпт для рекомендаций:**
|
||||
|
||||
```
|
||||
Контекст пользователя:
|
||||
- Цель: 2100 ккал/день
|
||||
Ты — диетолог-повар. Предложи 5 рецептов на русском языке.
|
||||
|
||||
Профиль:
|
||||
- Цель: похудение, 1800 ккал/день
|
||||
- Ограничения: без орехов
|
||||
- Предпочтения: русская, азиатская кухня
|
||||
- Продукты в наличии: [список с количествами и сроками]
|
||||
|
||||
Доступные рецепты (ID, название, калории, основные ингредиенты):
|
||||
[список из 50–100 кандидатов из БД]
|
||||
Доступные продукты (приоритет — скоро истекают):
|
||||
- Куриное филе 500г (истекает завтра ⚠)
|
||||
- Морковь 3 шт · Рис 400г · Яйца 4 шт
|
||||
|
||||
Задача: составь меню на 7 дней (завтрак, обед, ужин, перекус).
|
||||
Приоритет: использовать продукты с истекающим сроком.
|
||||
Ответ: JSON с recipe_id для каждого слота.
|
||||
Верни ТОЛЬКО валидный JSON без markdown. Для каждого рецепта:
|
||||
title, description, cuisine, difficulty, prep_time_min, cook_time_min,
|
||||
servings, image_query (EN), ingredients, steps, tags, nutrition_per_serving.
|
||||
```
|
||||
|
||||
- **Выход:** массив `{ day, meal_type, recipe_id }` — бэкенд подтягивает полные данные рецептов из БД.
|
||||
|
||||
Это гарантирует согласованность: AI не придумывает рецепты, а выбирает из проверенной базы.
|
||||
- **Выход:** массив рецептов с `image_query` — бэкенд дозапрашивает Pexels и возвращает готовый объект с `image_url`.
|
||||
|
||||
#### 4.5. AI-генерация персональных рецептов
|
||||
|
||||
@@ -255,7 +256,7 @@ AI-провайдер скрыт за интерфейсами. Это позв
|
||||
- **Модель:** Gemini 2.5 Flash-Lite
|
||||
- **Промпт:** «Из продуктов [список] предложи рецепт. Формат: JSON с названием, ингредиентами (с граммовками), шагами, калорийностью, БЖУ.»
|
||||
- **Результат:** сохраняется в БД как `source = 'ai_generated'`, помечается в UI как «AI-рецепт».
|
||||
- **Валидация нутриентов:** бэкенд пересчитывает калории/БЖУ по справочнику (USDA/Spoonacular nutrition data) и корректирует, если AI ошибся более чем на 20%.
|
||||
- **Валидация нутриентов:** значения КБЖУ от Gemini считаются приблизительными и помечаются «≈» в UI. Точная верификация через USDA FoodData Central запланирована в будущем (см. TODO.md).
|
||||
|
||||
#### 4.6. Замена ингредиентов
|
||||
|
||||
@@ -266,15 +267,15 @@ AI-провайдер скрыт за интерфейсами. Это позв
|
||||
|
||||
---
|
||||
|
||||
## 5. Маппинг ингредиентов (связь AI ↔ БД рецептов)
|
||||
## 5. Маппинг ингредиентов (нормализация продуктов пользователя)
|
||||
|
||||
### Проблема
|
||||
|
||||
Gemini оперирует свободным текстом ("куриное филе", "помидоры черри"), а Spoonacular — структурированными ID ингредиентов. Продукты пользователя — тоже свободный текст. Нужен слой, который связывает все три мира.
|
||||
Пользователи вводят продукты свободным текстом: "куриное филе", "куриная грудка", "курица без кости". Это одно и то же. Нужен слой нормализации — чтобы при генерации рекомендаций Gemini понимал, что у пользователя есть, и чтобы не дублировались продукты.
|
||||
|
||||
### Решение: таблица `ingredient_mappings`
|
||||
|
||||
Каноническая таблица ингредиентов с алиасами на разных языках и привязкой к Spoonacular ID.
|
||||
Каноническая таблица с алиасами на русском и английском. Заполняется по мере работы приложения через Gemini (рантайм-дополнение).
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
@@ -282,16 +283,14 @@ Gemini оперирует свободным текстом ("куриное ф
|
||||
│───────────────────────────────────────────────────────│
|
||||
│ id UUID │
|
||||
│ canonical_name "chicken_breast" │
|
||||
│ spoonacular_id 1015062 │
|
||||
│ canonical_name_ru "куриная грудка" │
|
||||
│ aliases (JSONB) ["куриное филе", "куриная грудка", │
|
||||
│ "филе курицы", "chicken breast", │
|
||||
│ "chicken fillet"] │
|
||||
│ "грудка курицы", "chicken breast"]│
|
||||
│ category "meat" │
|
||||
│ default_unit "g" │
|
||||
│ calories_per_100g 165 │
|
||||
│ calories_per_100g 165 (≈ от Gemini) │
|
||||
│ protein_per_100g 31 │
|
||||
│ fat_per_100g 3.6 │
|
||||
│ carbs_per_100g 0 │
|
||||
│ storage_days 3 │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -299,18 +298,15 @@ Gemini оперирует свободным текстом ("куриное ф
|
||||
### Как работает связка
|
||||
|
||||
```
|
||||
Продукт юзера ingredient_mappings Рецепт из Spoonacular
|
||||
────────────── ─────────────────── ─────────────────────
|
||||
Продукт юзера ingredient_mappings Промпт для Gemini
|
||||
────────────── ─────────────────── ──────────────────
|
||||
|
||||
"Куриное филе" │ Ингредиент рецепта:
|
||||
│ │ spoonacular_id: 1015062
|
||||
│ Fuzzy search │ │
|
||||
└────по aliases ───► canonical_name: ◄─── по spoonacular_id
|
||||
"chicken_breast"
|
||||
│
|
||||
MATCH: оба
|
||||
ссылаются на
|
||||
одну сущность
|
||||
"Куриное филе" │ canonical_name_ru:
|
||||
│ │ "куриная грудка"
|
||||
│ Fuzzy search │ │
|
||||
└────по aliases ────► canonical_name ──────────►│
|
||||
"chicken_breast" Gemini понимает
|
||||
что это за продукт
|
||||
```
|
||||
|
||||
### Потоки по сценариям
|
||||
@@ -335,32 +331,32 @@ Gemini оперирует свободным текстом ("куриное ф
|
||||
#### Сценарий 2: Проверка "есть ли ингредиент рецепта в запасах"
|
||||
|
||||
```
|
||||
1. Рецепт "Паста Карбонара" имеет ингредиент: spoonacular_id = 1015062
|
||||
2. ingredient_mappings: spoonacular_id 1015062 → canonical_name "chicken_breast"
|
||||
3. products: mapping_id → ingredient_mappings.id WHERE canonical_name = "chicken_breast"
|
||||
4. Найдено в products → отметка ✅ (есть в запасах)
|
||||
Не найдено → отметка ❌ (нет) или 🔄 (есть замена)
|
||||
1. Рецепт "Паста Карбонара" содержит ингредиент: canonical_name = "spaghetti"
|
||||
2. products: ищем WHERE mapping_id IN (
|
||||
SELECT id FROM ingredient_mappings WHERE canonical_name = 'spaghetti'
|
||||
)
|
||||
3. Найдено в products → отметка ✅ (есть в запасах)
|
||||
Не найдено → отметка ❌ (нет) или 🔄 (есть замена через ingredient_substitutions)
|
||||
```
|
||||
|
||||
#### Сценарий 3: Поиск рецептов "из моих продуктов"
|
||||
#### Сценарий 3: Рекомендации рецептов "из моих продуктов"
|
||||
|
||||
```
|
||||
1. Продукты юзера → через mapping_id → набор canonical_names
|
||||
["chicken_breast", "rice_white", "carrot", "onion"]
|
||||
1. Продукты юзера → через mapping_id → набор canonical_names_ru
|
||||
["куриная грудка", "рис", "морковь", "лук"]
|
||||
|
||||
2. SQL-запрос по рецептам:
|
||||
SELECT r.*,
|
||||
(SELECT count(*) FROM jsonb_array_elements(r.ingredients) i
|
||||
WHERE i->>'mapping_id' IN (выбранные mapping_id)) as matched,
|
||||
jsonb_array_length(r.ingredients) as total
|
||||
FROM recipes r
|
||||
ORDER BY matched::float / total DESC
|
||||
2. Backend формирует промпт для Gemini:
|
||||
- профиль пользователя (цель, КБЖУ-норма, ограничения)
|
||||
- список продуктов с количеством и сроками
|
||||
- приоритет: продукты, истекающие скоро
|
||||
|
||||
3. Если в локальной БД мало результатов — дозапрос Spoonacular:
|
||||
GET /recipes/findByIngredients?ingredients=chicken,rice,carrot,onion
|
||||
Новые рецепты сохраняются в нашу БД
|
||||
3. Gemini генерирует 5 рецептов с нуля, используя имеющиеся продукты.
|
||||
Каждый рецепт содержит image_query (EN) для запроса Pexels.
|
||||
|
||||
4. Gemini НЕ участвует — это чистый SQL + Spoonacular
|
||||
4. Backend параллельно запрашивает Pexels по image_query.
|
||||
|
||||
5. Результат: персонализированные рецепты с фото.
|
||||
Статическая база рецептов не нужна.
|
||||
```
|
||||
|
||||
#### Сценарий 4: Распознавание блюда (фото → калории)
|
||||
@@ -378,24 +374,30 @@ Gemini оперирует свободным текстом ("куриное ф
|
||||
#### Сценарий 5: Генерация меню
|
||||
|
||||
```
|
||||
1. Backend отбирает кандидатов из БД (SQL: фильтры + наличие ингредиентов)
|
||||
2. Формирует промпт с recipe_id:
|
||||
"ID:42 Борщ (550ккал, ингр: свёкла✅ картофель✅ говядина✅ лук✅)
|
||||
ID:87 Том Ям (320ккал, ингр: креветки❌ лемонграсс❌ кокос.молоко❌)"
|
||||
3. Gemini ранжирует и возвращает recipe_id
|
||||
4. Backend подгружает полные данные по ID
|
||||
1. Backend собирает контекст:
|
||||
- профиль (цель, КБЖУ-норма, кухни, ограничения)
|
||||
- продукты пользователя (canonical_names, количество, сроки)
|
||||
- период меню (например, 7 дней × 3 приёма пищи)
|
||||
|
||||
→ Gemini работает ТОЛЬКО с ID из нашей БД
|
||||
→ Никакой рассинхронизации
|
||||
2. Gemini генерирует полное меню с нуля:
|
||||
- 7 дней, 3 приёма пищи в день
|
||||
- каждый слот: полный рецепт (ингредиенты, шаги, КБЖУ, image_query)
|
||||
- баланс КБЖУ в рамках дневной нормы
|
||||
- приоритет продуктам с истекающим сроком
|
||||
|
||||
3. Backend параллельно запрашивает Pexels для каждого рецепта.
|
||||
|
||||
4. Меню сохраняется в menu_plans + menu_items.
|
||||
Рецепты из меню — в saved_recipes (source = 'ai').
|
||||
|
||||
→ Gemini генерирует контент полностью. Никакой статической базы не нужно.
|
||||
```
|
||||
|
||||
### Наполнение таблицы маппингов
|
||||
|
||||
| Этап | Источник | Объём |
|
||||
|------|----------|-------|
|
||||
| Начальный импорт | Spoonacular Ingredient API | ~1 000 базовых ингредиентов |
|
||||
| Ручная локализация | Перевод топ-200 ингредиентов | Aliases на русском |
|
||||
| Batch-перевод | Gemini Flash-Lite (оффлайн) | Остальные aliases |
|
||||
| Начальный seed | Ручное составление топ-200 базовых ингредиентов | Aliases на русском и английском |
|
||||
| Рантайм-дополнение | Gemini (при нераспознанном продукте) | По мере роста юзеров |
|
||||
| Пользовательская обратная связь | Юзер корректирует продукт на экране редактирования | Новые aliases |
|
||||
|
||||
@@ -407,9 +409,9 @@ Gemini оперирует свободным текстом ("куриное ф
|
||||
|--------|---------|---------------------|
|
||||
| OCR чека → продукты | Да (vision) | Fuzzy match по `ingredient_mappings.aliases` |
|
||||
| Фото продуктов → список | Да (vision) | То же |
|
||||
| Фото блюда → калории | Да (vision) | Full-text search по `recipes.title` |
|
||||
| Поиск рецептов из продуктов | **Нет** | SQL по `mapping_id` + Spoonacular `findByIngredients` |
|
||||
| Ранжировка/подбор меню | Да (текст) | Получает `recipe_id` из БД, возвращает `recipe_id` |
|
||||
| Фото блюда → калории | Да (vision) | Результат логируется в `ai_tasks` |
|
||||
| Рекомендации рецептов из продуктов | **Да (текст)** | Продукты передаются в промпт; результат → `saved_recipes` |
|
||||
| Генерация меню на неделю | Да (текст) | Генерирует с нуля; сохраняется в `menu_plans` + `menu_items` |
|
||||
| Замена ингредиентов | Да (текст) | Кеш в `ingredient_substitutions` |
|
||||
| Нераспознанный ингредиент | Да (текст, разово) | Результат сохраняется в `ingredient_mappings` |
|
||||
|
||||
@@ -520,50 +522,42 @@ Gemini API имеет ограничения по RPM (requests per minute) и
|
||||
|
||||
### Источники рецептов
|
||||
|
||||
Статической базы рецептов нет. Все рецепты генерируются Gemini on-demand и сохраняются пользователями.
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ Recipe Service │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
|
||||
│ │ Spoonacular │ │ AI-generated │ │ User │ │
|
||||
│ │ Import │ │ Recipes │ │ Recipes │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ source: │ │ source: │ │ source: │ │
|
||||
│ │ 'spoonacular'│ │ 'ai' │ │ 'user' │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └────┬─────┘ │
|
||||
│ │ │ │ │
|
||||
│ └────────┬────────┘───────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ Единая таблица │ │
|
||||
│ │ recipes в БД │ │
|
||||
│ └────────────────┘ │
|
||||
└────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Recipe Service │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────┐ │
|
||||
│ │ AI-generated │ │ User Recipes │ │
|
||||
│ │ (Gemini) │ │ (будущее) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ source: 'ai' │ │ source: 'user' │ │
|
||||
│ └──────────┬───────────┘ └────────┬─────────┘ │
|
||||
│ │ │ │
|
||||
│ └──────────┬────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ saved_recipes │ │
|
||||
│ │ (per-user) │ │
|
||||
│ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Импорт из Spoonacular
|
||||
### Принцип работы с рецептами
|
||||
|
||||
- **Начальный импорт:** 5 000–10 000 самых популярных рецептов (по рейтингу).
|
||||
- **Данные:** название, описание, ингредиенты с граммовками, шаги приготовления, калории, БЖУ, время, сложность, кухня, фото, теги.
|
||||
- **Хранение:** в нашей БД (PostgreSQL). Не зависим от Spoonacular API при показе рецептов пользователю.
|
||||
- **Синхронизация:** фоновый джоб раз в неделю — обновление данных, добавление новых рецептов из популярных.
|
||||
- **Spoonacular API** используется также для: расширенного поиска (когда в локальной БД нет подходящего), справочника нутриентов, данных по ингредиентам.
|
||||
- **Локализация:** рецепты из Spoonacular на английском. Перевод — через Gemini Flash-Lite (batch, оффлайн). Переведённые рецепты кешируются в БД.
|
||||
- **Генерация:** Gemini создаёт рецепты с нуля на основе контекста пользователя (продукты, профиль, цели).
|
||||
- **Фото:** Pexels API по `image_query` (EN), которую Gemini включает в ответ.
|
||||
- **КБЖУ:** приблизительные значения от Gemini, помечаются «≈» в UI.
|
||||
- **Сохранение:** пользователь тапает ❤ → рецепт сохраняется в `saved_recipes`. До сохранения нигде не хранится.
|
||||
- **Отсутствие глобального каталога:** каждый пользователь видит только свои сохранённые рецепты и свежие рекомендации. Полноценный каталог с поиском — в TODO.md (требует постоянной базы).
|
||||
|
||||
### Согласованность AI и рецептов
|
||||
### Согласованность рецептов
|
||||
|
||||
**Критическое правило:** AI при подборе меню работает ТОЛЬКО с рецептами из нашей БД.
|
||||
|
||||
Поток:
|
||||
1. Backend выбирает кандидатов из БД по фильтрам (SQL-запрос: кухня, сложность, калории, ингредиенты).
|
||||
2. Backend формирует промпт с ID и метаданными кандидатов.
|
||||
3. AI ранжирует и компонует меню, возвращая `recipe_id`.
|
||||
4. Backend подгружает полные данные рецептов по ID.
|
||||
|
||||
Это гарантирует: точные нутриенты, наличие фото, корректные шаги, возможность оставить отзыв.
|
||||
|
||||
**Исключение:** AI-генерация нового рецепта «из того, что есть» — когда в БД нет подходящего. Такой рецепт сохраняется в БД как `source = 'ai'` и доступен другим пользователям после модерации (по рейтингу).
|
||||
- Все данные рецепта (ингредиенты, шаги, КБЖУ, image_url) приходят от Gemini+Pexels в одном запросе.
|
||||
- Сохранённый рецепт хранится целиком в `saved_recipes` как JSONB — нет риска рассинхронизации.
|
||||
- КБЖУ помечены как приблизительные — пользователь понимает точность данных.
|
||||
|
||||
---
|
||||
|
||||
@@ -591,7 +585,6 @@ Gemini API имеет ограничения по RPM (requests per minute) и
|
||||
│ (JSONB) │ │ │──────────────────│
|
||||
│ created_at │ │ │ id │
|
||||
└──────────────┘ │ │ source │
|
||||
│ │ spoonacular_id │
|
||||
│ │ title │
|
||||
│ │ description │
|
||||
│ │ cuisine │
|
||||
@@ -609,7 +602,7 @@ Gemini API имеет ограничения по RPM (requests per minute) и
|
||||
│ │ tags (JSONB) │
|
||||
│ │ avg_rating │
|
||||
│ │ review_count │
|
||||
│ │ created_by │──► users (NULL для spoonacular)
|
||||
│ │ created_by │──► users (NULL для ai-generated)
|
||||
│ └──────────────────┘
|
||||
│
|
||||
┌──────────────────┤ ┌──────────────────┐
|
||||
@@ -678,7 +671,7 @@ Gemini API имеет ограничения по RPM (requests per minute) и
|
||||
│────────────────────────────│
|
||||
│ id │
|
||||
│ canonical_name │
|
||||
│ spoonacular_id (UNIQUE) │
|
||||
│ canonical_name_ru │
|
||||
│ aliases (JSONB) │
|
||||
│ category │
|
||||
│ default_unit │
|
||||
@@ -694,13 +687,13 @@ Gemini API имеет ограничения по RPM (requests per minute) и
|
||||
### Ключевые решения по схеме
|
||||
|
||||
- **`products.mapping_id`** — FK на `ingredient_mappings`. Связывает продукт пользователя с каноническим ингредиентом. Через эту связь определяется наличие ингредиентов рецепта в запасах. Может быть NULL (если продукт не удалось сопоставить).
|
||||
- **`ingredient_mappings`** — каноническая таблица ингредиентов. `aliases` (JSONB) содержит все варианты написания на разных языках. `spoonacular_id` связывает с ингредиентами рецептов из Spoonacular. Нутриенты на 100г используются для пересчёта калорийности AI-генерированных рецептов. `storage_days` — дефолтный срок хранения для категории.
|
||||
- **`ingredient_mappings`** — каноническая таблица ингредиентов. `canonical_name_ru` — русское название. `aliases` (JSONB) содержит все варианты написания на разных языках. Нутриенты на 100г используются для пересчёта калорийности при необходимости. `storage_days` — дефолтный срок хранения для категории. Наполняется вручную (seed) и автоматически через Gemini при нераспознанных продуктах.
|
||||
- **`ingredient_substitutions`** — кеш замен ингредиентов. Ссылается на `ingredient_mappings` по обоим ингредиентам (оригинал и замена). Один раз определённая AI-замена переиспользуется для всех пользователей.
|
||||
- **`products.storage_days`** — период хранения после покупки (не фиксированная дата). `expires_at` вычисляется как `added_at + storage_days`. При отображении: «осталось X дней». Дефолт берётся из `ingredient_mappings.storage_days` при привязке.
|
||||
- **`products.unit`** — ENUM: g, kg, ml, l, pcs, bunch, pack. Фронтенд отображает локализованные названия. Дефолт берётся из `ingredient_mappings.default_unit`.
|
||||
- **`recipes.ingredients`** — JSONB массив: `[{ "name": "...", "amount": 200, "unit": "g", "optional": false }]`. JSONB позволяет гибко хранить без нормализации, при этом поддерживая поиск (`@>` оператор).
|
||||
- **`recipes.steps`** — JSONB массив: `[{ "order": 1, "title": "...", "description": "...", "image_url": "...", "timer_seconds": null }]`. `timer_seconds` не null → на шаге отображается таймер.
|
||||
- **`recipes.source`** — ENUM: spoonacular, ai, user. Определяет происхождение рецепта.
|
||||
- **`recipes.source`** — ENUM: ai, user. Определяет происхождение рецепта.
|
||||
- **`meal_diary.source`** — откуда записано: из меню, через фото, вручную, из рецепта.
|
||||
- **`shopping_lists.items`** — JSONB: `[{ "name": "...", "amount": 1, "unit": "l", "category": "dairy", "checked": false, "manual": false }]`.
|
||||
|
||||
@@ -744,24 +737,20 @@ Gemini API имеет ограничения по RPM (requests per minute) и
|
||||
|
||||
Все `/ai/*` эндпоинты возвращают `{ task_id }` (HTTP 202 Accepted). Клиент получает результат через polling `/ai/tasks/{id}` или через WebSocket (будущее улучшение). Это позволяет обрабатывать запросы асинхронно через очереди.
|
||||
|
||||
### Рецепты
|
||||
### Рекомендации
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|----------|
|
||||
| GET | `/recipes` | Каталог (фильтры, поиск, пагинация) |
|
||||
| GET | `/recipes/{id}` | Карточка рецепта |
|
||||
| GET | `/recipes/recommended` | Персональные рекомендации |
|
||||
| GET | `/recipes/recent` | Недавно приготовленные |
|
||||
| POST | `/recipes` | Создать пользовательский рецепт |
|
||||
| POST | `/recipes/{id}/favorite` | Добавить в избранное |
|
||||
| DELETE | `/recipes/{id}/favorite` | Убрать из избранного |
|
||||
| GET | `/recommendations?count=5` | Персональные рекомендации (Gemini + Pexels) |
|
||||
|
||||
### Отзывы
|
||||
### Сохранённые рецепты
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|-------|------|----------|
|
||||
| GET | `/recipes/{id}/reviews` | Отзывы к рецепту |
|
||||
| POST | `/recipes/{id}/reviews` | Написать отзыв |
|
||||
| GET | `/saved-recipes` | Список сохранённых рецептов |
|
||||
| POST | `/saved-recipes` | Сохранить рецепт |
|
||||
| GET | `/saved-recipes/{id}` | Карточка сохранённого рецепта |
|
||||
| DELETE | `/saved-recipes/{id}` | Удалить из сохранённых |
|
||||
|
||||
### Меню
|
||||
|
||||
@@ -821,11 +810,11 @@ Gemini API имеет ограничения по RPM (requests per minute) и
|
||||
|-----------|--------|---------|---------|
|
||||
| Firebase Auth | $0 | $0 | $0 (лимит 50K) |
|
||||
| Gemini API | $50–150 | $500–1 500 | $2 500–7 500 |
|
||||
| Spoonacular | $29 | $149 | $149 |
|
||||
| Pexels API | $0 | $0 | $0 (Free tier) |
|
||||
| Хостинг (Cloud Run / Fly.io) | $10–20 | $50–100 | $200–500 |
|
||||
| PostgreSQL (managed) | $15 | $50 | $150–300 |
|
||||
| S3 (фото) | $5 | $20 | $50–100 |
|
||||
| **Итого** | **$110–220** | **$770–1 820** | **$3 050–8 550** |
|
||||
| **Итого** | **$80–190** | **$620–1 670** | **$2 900–8 400** |
|
||||
|
||||
### Оптимизация затрат на AI
|
||||
|
||||
|
||||
Reference in New Issue
Block a user