Recipe corpus (#108): - Add _MAIN_INGREDIENT_SIGNALS to tag_inferrer.py (Chicken/Beef/Pork/Fish/Pasta/ Vegetables/Eggs/Legumes/Grains/Cheese) — infers main:* tags from ingredient names - Update browser_domains.py main_ingredient categories to use main:* tag queries instead of raw food terms; recipe_browser_fts now has full 3.19M row coverage (was ~1.2K before backfill) Bug fixes: - Fix community posts response shape (#96): add total/page/page_size fields - Fix export endpoint arg types (#92) - Fix household invite store leak (#93) - Fix receipts endpoint issues - Fix saved_recipes endpoint - Add session endpoint (app/api/endpoints/session.py) Shopping list: - Add migration 033_shopping_list.sql - Add shopping schemas (app/models/schemas/shopping.py) - Add ShoppingView.vue, ShoppingItemRow.vue, shopping.ts store Frontend: - InventoryList, RecipesView, RecipeDetailPanel polish - App.vue routing updates for shopping view Docs: - Add user-facing docs under docs/ (getting-started, user-guide, reference) - Add screenshots
91 lines
4.2 KiB
Python
91 lines
4.2 KiB
Python
"""
|
|
Recipe browser domain schemas.
|
|
|
|
Each domain provides a two-level category hierarchy for browsing the recipe corpus.
|
|
Keyword matching is case-insensitive against the recipes.category column and the
|
|
recipes.keywords JSON array. A recipe may appear in multiple categories (correct).
|
|
|
|
These are starter mappings based on the food.com dataset structure. Run:
|
|
|
|
SELECT category, count(*) FROM recipes
|
|
GROUP BY category ORDER BY count(*) DESC LIMIT 50;
|
|
|
|
against the corpus to verify coverage and refine keyword lists before the first
|
|
production deploy.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
DOMAINS: dict[str, dict] = {
|
|
"cuisine": {
|
|
"label": "Cuisine",
|
|
"categories": {
|
|
"Italian": ["italian", "pasta", "pizza", "risotto", "lasagna", "carbonara"],
|
|
"Mexican": ["mexican", "tex-mex", "taco", "enchilada", "burrito", "salsa", "guacamole"],
|
|
"Asian": ["asian", "chinese", "japanese", "thai", "korean", "vietnamese", "stir fry", "stir-fry", "ramen", "sushi"],
|
|
"American": ["american", "southern", "bbq", "barbecue", "comfort food", "cajun", "creole"],
|
|
"Mediterranean": ["mediterranean", "greek", "middle eastern", "turkish", "moroccan", "lebanese"],
|
|
"Indian": ["indian", "curry", "lentil", "dal", "tikka", "masala", "biryani"],
|
|
"European": ["french", "german", "spanish", "british", "irish", "scandinavian"],
|
|
"Latin American": ["latin american", "peruvian", "argentinian", "colombian", "cuban", "caribbean"],
|
|
},
|
|
},
|
|
"meal_type": {
|
|
"label": "Meal Type",
|
|
"categories": {
|
|
"Breakfast": ["breakfast", "brunch", "eggs", "pancakes", "waffles", "oatmeal", "muffin"],
|
|
"Lunch": ["lunch", "sandwich", "wrap", "salad", "soup", "light meal"],
|
|
"Dinner": ["dinner", "main dish", "entree", "main course", "supper"],
|
|
"Snack": ["snack", "appetizer", "finger food", "dip", "bite", "starter"],
|
|
"Dessert": ["dessert", "cake", "cookie", "pie", "sweet", "pudding", "ice cream", "brownie"],
|
|
"Beverage": ["drink", "smoothie", "cocktail", "beverage", "juice", "shake"],
|
|
"Side Dish": ["side dish", "side", "accompaniment", "garnish"],
|
|
},
|
|
},
|
|
"dietary": {
|
|
"label": "Dietary",
|
|
"categories": {
|
|
"Vegetarian": ["vegetarian"],
|
|
"Vegan": ["vegan", "plant-based", "plant based"],
|
|
"Gluten-Free": ["gluten-free", "gluten free", "celiac"],
|
|
"Low-Carb": ["low-carb", "low carb", "keto", "ketogenic"],
|
|
"High-Protein": ["high protein", "high-protein"],
|
|
"Low-Fat": ["low-fat", "low fat", "light"],
|
|
"Dairy-Free": ["dairy-free", "dairy free", "lactose"],
|
|
},
|
|
},
|
|
"main_ingredient": {
|
|
"label": "Main Ingredient",
|
|
"categories": {
|
|
# These values match the inferred_tags written by tag_inferrer._MAIN_INGREDIENT_SIGNALS
|
|
# and indexed into recipe_browser_fts — use exact tag strings.
|
|
"Chicken": ["main:Chicken"],
|
|
"Beef": ["main:Beef"],
|
|
"Pork": ["main:Pork"],
|
|
"Fish": ["main:Fish"],
|
|
"Pasta": ["main:Pasta"],
|
|
"Vegetables": ["main:Vegetables"],
|
|
"Eggs": ["main:Eggs"],
|
|
"Legumes": ["main:Legumes"],
|
|
"Grains": ["main:Grains"],
|
|
"Cheese": ["main:Cheese"],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def get_domain_labels() -> list[dict]:
|
|
"""Return [{id, label}] for all available domains."""
|
|
return [{"id": k, "label": v["label"]} for k, v in DOMAINS.items()]
|
|
|
|
|
|
def get_keywords_for_category(domain: str, category: str) -> list[str]:
|
|
"""Return the keyword list for a domain/category pair, or [] if not found."""
|
|
domain_data = DOMAINS.get(domain, {})
|
|
categories = domain_data.get("categories", {})
|
|
return categories.get(category, [])
|
|
|
|
|
|
def get_category_names(domain: str) -> list[str]:
|
|
"""Return category names for a domain, or [] if domain unknown."""
|
|
domain_data = DOMAINS.get(domain, {})
|
|
return list(domain_data.get("categories", {}).keys())
|